Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: modify stop #32

Merged
merged 10 commits into from
Aug 5, 2023
10 changes: 10 additions & 0 deletions alpaca-broker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::error::Error;
mod cancel_trade;
mod close_trade;
mod keys;
mod modify_trade;
mod order_mapper;
mod submit_trade;
mod sync_trade;
Expand Down Expand Up @@ -42,6 +43,15 @@ impl Broker for AlpacaBroker {
println!("Canceling trade: {:?}", trade);
cancel_trade::cancel(trade, account)
}

fn modify_stop(
&self,
trade: &Trade,
account: &Account,
new_stop_price: rust_decimal::Decimal,
) -> Result<BrokerLog, Box<dyn Error>> {
modify_trade::modify_stop(trade, account, new_stop_price)
}
}

/// Alpaca-specific Broker API
Expand Down
58 changes: 58 additions & 0 deletions alpaca-broker/src/modify_trade.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::keys;
use apca::api::v2::order::{ChangeReqInit, Id, Order, Patch};
use apca::Client;
use model::BrokerLog;
use model::{Account, Trade};
use num_decimal::Num;
use rust_decimal::Decimal;
use std::{error::Error, str::FromStr};
use tokio::runtime::Runtime;
use uuid::Uuid;

pub fn modify_stop(
trade: &Trade,
account: &Account,
price: Decimal,
) -> Result<BrokerLog, Box<dyn Error>> {
assert!(trade.account_id == account.id); // Verify that the trade is for the account

let api_info = keys::read_api_key(&account.environment, account)?;
let client = Client::new(api_info);

// Modify the stop order.
let alpaca_order = Runtime::new().unwrap().block_on(modify_entry(
&client,
trade.safety_stop.broker_order_id.unwrap(),
price,
))?;

// 3. Log the Alpaca order.
let log = BrokerLog {
trade_id: trade.id,
log: serde_json::to_string(&alpaca_order)?,
..Default::default()
};

Ok(log)
}

async fn modify_entry(
client: &Client,
order_id: Uuid,
price: Decimal,
) -> Result<Order, Box<dyn Error>> {
let request = ChangeReqInit {
stop_price: Some(Num::from_str(price.to_string().as_str()).unwrap()),
..Default::default()
}
.init();

let result = client.issue::<Patch>(&(Id(order_id), request)).await;
match result {
Ok(log) => Ok(log),
Err(e) => {
eprintln!("Error modify stop: {:?}", e);
Err(Box::new(e))
}
}
}
7 changes: 7 additions & 0 deletions cli/src/commands/trade_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ impl TradeCommandBuilder {
self
}

pub fn modify_stop(mut self) -> Self {
self.subcommands.push(
Command::new("modify-stop").about("Modify the stop loss order of a filled trade."),
);
self
}

pub fn manually_target(mut self) -> Self {
self.subcommands
.push(Command::new("manually-target").about("Execute manually the target of a trade"));
Expand Down
2 changes: 2 additions & 0 deletions cli/src/dialogs.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod account_dialog;
mod keys_dialog;
mod modify_stop_dialog;
mod rule_dialog;
mod trade_cancel_dialog;
mod trade_close_dialog;
Expand All @@ -18,6 +19,7 @@ pub use account_dialog::AccountSearchDialog;
pub use keys_dialog::KeysDeleteDialogBuilder;
pub use keys_dialog::KeysReadDialogBuilder;
pub use keys_dialog::KeysWriteDialogBuilder;
pub use modify_stop_dialog::ModifyStopDialogBuilder;
pub use rule_dialog::RuleDialogBuilder;
pub use rule_dialog::RuleRemoveDialogBuilder;
pub use trade_cancel_dialog::CancelDialogBuilder;
Expand Down
112 changes: 112 additions & 0 deletions cli/src/dialogs/modify_stop_dialog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use crate::dialogs::AccountSearchDialog;
use crate::views::{LogView, OrderView, TradeOverviewView, TradeView};
use core::TrustFacade;
use dialoguer::{theme::ColorfulTheme, FuzzySelect, Input};
use model::{Account, BrokerLog, Status, Trade};
use rust_decimal::Decimal;
use std::error::Error;

type ModifyStopDialogBuilderResult = Option<Result<(Trade, BrokerLog), Box<dyn Error>>>;

pub struct ModifyStopDialogBuilder {
account: Option<Account>,
trade: Option<Trade>,
new_stop_price: Option<Decimal>,
result: ModifyStopDialogBuilderResult,
}

impl ModifyStopDialogBuilder {
pub fn new() -> Self {
ModifyStopDialogBuilder {
account: None,
trade: None,
new_stop_price: None,
result: None,
}
}

pub fn build(mut self, trust: &mut TrustFacade) -> ModifyStopDialogBuilder {
let trade = self
.trade
.clone()
.expect("No trade found, did you forget to call search?");

let account = self
.account
.clone()
.expect("No account found, did you forget to call account?");
let stop_price = self
.new_stop_price
.expect("No stop price found, did you forget to call stop_price?");

match trust.modify_stop(&trade, &account, stop_price) {
Ok((trade, log)) => self.result = Some(Ok((trade, log))),
Err(error) => self.result = Some(Err(error)),
}
self
}

pub fn display(self) {
match self
.result
.expect("No result found, did you forget to call search?")
{
Ok((trade, log)) => {
println!("Trade stop updated:");
TradeView::display(&trade, &self.account.unwrap().name);

TradeOverviewView::display(&trade.overview);

println!("Stop updated:");
OrderView::display(trade.safety_stop);

LogView::display(&log);
}
Err(error) => println!("Error submitting trade: {:?}", error),
}
}

pub fn account(mut self, trust: &mut TrustFacade) -> Self {
let account = AccountSearchDialog::new().search(trust).build();
match account {
Ok(account) => self.account = Some(account),
Err(error) => println!("Error searching account: {:?}", error),
}
self
}

pub fn search(mut self, trust: &mut TrustFacade) -> Self {
let trades = trust.search_trades(self.account.clone().unwrap().id, Status::Filled);
match trades {
Ok(trades) => {
if trades.is_empty() {
panic!("No trade found with the status filled, did you forget to submit one?")
}
let trade = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("Trade:")
.items(&trades[..])
.default(0)
.interact_opt()
.unwrap()
.map(|index| trades.get(index).unwrap())
.unwrap();

println!("Trade selected:");
TradeView::display(trade, &self.account.clone().unwrap().name);
self.trade = Some(trade.to_owned());
}
Err(error) => self.result = Some(Err(error)),
}

self
}

pub fn stop_price(mut self) -> Self {
let stop_price = Input::new()
.with_prompt("New stop price")
.interact()
.unwrap();
self.new_stop_price = Some(stop_price);
self
}
}
16 changes: 13 additions & 3 deletions cli/src/dispatcher.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::dialogs::{
AccountDialogBuilder, AccountSearchDialog, CancelDialogBuilder, CloseDialogBuilder,
ExitDialogBuilder, FillTradeDialogBuilder, FundingDialogBuilder, KeysDeleteDialogBuilder,
KeysReadDialogBuilder, KeysWriteDialogBuilder, SubmitDialogBuilder, SyncTradeDialogBuilder,
TradeDialogBuilder, TradeSearchDialogBuilder, TradingVehicleDialogBuilder,
TradingVehicleSearchDialogBuilder, TransactionDialogBuilder,
KeysReadDialogBuilder, KeysWriteDialogBuilder, ModifyStopDialogBuilder, SubmitDialogBuilder,
SyncTradeDialogBuilder, TradeDialogBuilder, TradeSearchDialogBuilder,
TradingVehicleDialogBuilder, TradingVehicleSearchDialogBuilder, TransactionDialogBuilder,
};
use crate::dialogs::{RuleDialogBuilder, RuleRemoveDialogBuilder};
use alpaca_broker::AlpacaBroker;
Expand Down Expand Up @@ -78,6 +78,7 @@ impl ArgDispatcher {
Some(("manually-close", _)) => self.close(),
Some(("sync", _)) => self.create_sync(),
Some(("search", _)) => self.search_trade(),
Some(("modify-stop", _)) => self.modify_stop(),
_ => unreachable!("No subcommand provided"),
},
Some((ext, sub_matches)) => {
Expand Down Expand Up @@ -266,6 +267,15 @@ impl ArgDispatcher {
.build(&mut self.trust)
.display();
}

fn modify_stop(&mut self) {
ModifyStopDialogBuilder::new()
.account(&mut self.trust)
.search(&mut self.trust)
.stop_price()
.build(&mut self.trust)
.display();
}
}

impl ArgDispatcher {
Expand Down
1 change: 1 addition & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fn main() {
.manually_stop()
.manually_target()
.manually_close()
.modify_stop()
.build(),
)
.get_matches();
Expand Down
15 changes: 15 additions & 0 deletions cli/tests/integration_test_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use model::{
Account, BrokerLog, Currency, Order, OrderIds, RuleLevel, RuleName, Status, Trade,
TransactionCategory,
};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::error::Error;

Expand Down Expand Up @@ -222,4 +223,18 @@ impl Broker for MockBroker {
fn cancel_trade(&self, trade: &Trade, account: &Account) -> Result<(), Box<dyn Error>> {
unimplemented!("Cancel trade: {:?} {:?}", trade, account)
}

fn modify_stop(
&self,
trade: &Trade,
account: &Account,
new_stop_price: Decimal,
) -> Result<BrokerLog, Box<dyn Error>> {
unimplemented!(
"Modify stop: {:?} {:?} {:?}",
trade,
account,
new_stop_price
)
}
}
15 changes: 15 additions & 0 deletions cli/tests/integration_test_cancel_trade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use model::{
Account, BrokerLog, Currency, DraftTrade, Order, OrderIds, Status, Trade, TradeCategory,
TradingVehicleCategory, TransactionCategory,
};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::error::Error;

Expand Down Expand Up @@ -127,4 +128,18 @@ impl Broker for MockBroker {
fn cancel_trade(&self, _trade: &Trade, _account: &Account) -> Result<(), Box<dyn Error>> {
unimplemented!("Cancel trade not implemented")
}

fn modify_stop(
&self,
trade: &Trade,
account: &Account,
new_stop_price: Decimal,
) -> Result<BrokerLog, Box<dyn Error>> {
unimplemented!(
"Modify stop: {:?} {:?} {:?}",
trade,
account,
new_stop_price
)
}
}
56 changes: 56 additions & 0 deletions cli/tests/integration_test_trade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use model::{
Trade, TradeCategory, TradingVehicleCategory, TransactionCategory,
};
use model::{Broker, DraftTrade, OrderStatus};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::error::Error;
use uuid::Uuid;
Expand Down Expand Up @@ -488,6 +489,44 @@ fn test_trade_close() {
assert_eq!(trade.target.status, OrderStatus::PendingNew);
}

#[test]
fn test_trade_modify_stop_long() {
let (trust, account, trade) = create_trade(
BrokerResponse::orders_entry_filled,
Some(BrokerResponse::closed_order),
);
let mut trust = trust;

// 1. Sync trade with the Broker - Entry is filled
trust
.sync_trade(&trade, &account)
.expect("Failed to sync trade with broker when entry is filled");

let trade = trust
.search_trades(account.id, Status::Filled)
.expect("Failed to find trade with status submitted 2")
.first()
.unwrap()
.clone();

// 7. Modify stop
let (_, log) = trust
.modify_stop(&trade, &account, dec!(39))
.expect("Failed to modify stop");

let trade = trust
.search_trades(account.id, Status::Filled)
.expect("Failed to find trade with status filled")
.first()
.unwrap()
.clone();

// Assert Trade Overview
assert_eq!(trade.status, Status::Filled); // The trade is still filled, but the stop was changed
assert_eq!(trade.safety_stop.unit_price, dec!(39));
assert_eq!(log.trade_id, trade.id);
}

struct BrokerResponse;

impl BrokerResponse {
Expand Down Expand Up @@ -748,4 +787,21 @@ impl Broker for MockBroker {
fn cancel_trade(&self, _trade: &Trade, _account: &Account) -> Result<(), Box<dyn Error>> {
Ok(())
}

fn modify_stop(
&self,
trade: &Trade,
account: &Account,
new_stop_price: Decimal,
) -> Result<BrokerLog, Box<dyn Error>> {
assert_eq!(trade.account_id, account.id);
assert_eq!(trade.safety_stop.unit_price, dec!(38));
assert_eq!(new_stop_price, dec!(39));

Ok(BrokerLog {
trade_id: trade.id,
log: "".to_string(),
..Default::default()
})
}
}
Loading