diff --git a/alpaca-broker/src/lib.rs b/alpaca-broker/src/lib.rs index e0a26d9..e221175 100644 --- a/alpaca-broker/src/lib.rs +++ b/alpaca-broker/src/lib.rs @@ -4,7 +4,8 @@ use std::error::Error; mod cancel_trade; mod close_trade; mod keys; -mod modify_trade; +mod modify_stop; +mod modify_target; mod order_mapper; mod submit_trade; mod sync_trade; @@ -50,7 +51,16 @@ impl Broker for AlpacaBroker { account: &Account, new_stop_price: rust_decimal::Decimal, ) -> Result> { - modify_trade::modify_stop(trade, account, new_stop_price) + modify_stop::modify(trade, account, new_stop_price) + } + + fn modify_target( + &self, + trade: &Trade, + account: &Account, + new_target_price: rust_decimal::Decimal, + ) -> Result> { + modify_target::modify(trade, account, new_target_price) } } diff --git a/alpaca-broker/src/modify_trade.rs b/alpaca-broker/src/modify_stop.rs similarity index 86% rename from alpaca-broker/src/modify_trade.rs rename to alpaca-broker/src/modify_stop.rs index ec86b6b..0245424 100644 --- a/alpaca-broker/src/modify_trade.rs +++ b/alpaca-broker/src/modify_stop.rs @@ -9,7 +9,7 @@ use std::{error::Error, str::FromStr}; use tokio::runtime::Runtime; use uuid::Uuid; -pub fn modify_stop( +pub fn modify( trade: &Trade, account: &Account, price: Decimal, @@ -20,7 +20,7 @@ pub fn modify_stop( let client = Client::new(api_info); // Modify the stop order. - let alpaca_order = Runtime::new().unwrap().block_on(modify_entry( + let alpaca_order = Runtime::new().unwrap().block_on(submit( &client, trade.safety_stop.broker_order_id.unwrap(), price, @@ -36,11 +36,7 @@ pub fn modify_stop( Ok(log) } -async fn modify_entry( - client: &Client, - order_id: Uuid, - price: Decimal, -) -> Result> { +async fn submit(client: &Client, order_id: Uuid, price: Decimal) -> Result> { let request = ChangeReqInit { stop_price: Some(Num::from_str(price.to_string().as_str()).unwrap()), ..Default::default() diff --git a/alpaca-broker/src/modify_target.rs b/alpaca-broker/src/modify_target.rs new file mode 100644 index 0000000..8f7dafa --- /dev/null +++ b/alpaca-broker/src/modify_target.rs @@ -0,0 +1,54 @@ +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( + trade: &Trade, + account: &Account, + price: Decimal, +) -> Result> { + 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(submit( + &client, + trade.target.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 submit(client: &Client, order_id: Uuid, price: Decimal) -> Result> { + let request = ChangeReqInit { + limit_price: Some(Num::from_str(price.to_string().as_str()).unwrap()), + ..Default::default() + } + .init(); + + let result = client.issue::(&(Id(order_id), request)).await; + match result { + Ok(log) => Ok(log), + Err(e) => { + eprintln!("Error modify stop: {:?}", e); + Err(Box::new(e)) + } + } +} diff --git a/cli/src/commands/trade_command.rs b/cli/src/commands/trade_command.rs index c0067cb..487e739 100644 --- a/cli/src/commands/trade_command.rs +++ b/cli/src/commands/trade_command.rs @@ -74,6 +74,13 @@ impl TradeCommandBuilder { self } + pub fn modify_target(mut self) -> Self { + self.subcommands.push( + Command::new("modify-target").about("Modify the target 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")); diff --git a/cli/src/dialogs.rs b/cli/src/dialogs.rs index 52e39ed..4705e46 100644 --- a/cli/src/dialogs.rs +++ b/cli/src/dialogs.rs @@ -1,6 +1,6 @@ mod account_dialog; mod keys_dialog; -mod modify_stop_dialog; +mod modify_dialog; mod rule_dialog; mod trade_cancel_dialog; mod trade_close_dialog; @@ -19,7 +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 modify_dialog::ModifyDialogBuilder; pub use rule_dialog::RuleDialogBuilder; pub use rule_dialog::RuleRemoveDialogBuilder; pub use trade_cancel_dialog::CancelDialogBuilder; diff --git a/cli/src/dialogs/modify_stop_dialog.rs b/cli/src/dialogs/modify_dialog.rs similarity index 67% rename from cli/src/dialogs/modify_stop_dialog.rs rename to cli/src/dialogs/modify_dialog.rs index 0c4076e..c00e75a 100644 --- a/cli/src/dialogs/modify_stop_dialog.rs +++ b/cli/src/dialogs/modify_dialog.rs @@ -6,26 +6,26 @@ use model::{Account, BrokerLog, Status, Trade}; use rust_decimal::Decimal; use std::error::Error; -type ModifyStopDialogBuilderResult = Option>>; +type ModifyDialogBuilderResult = Option>>; -pub struct ModifyStopDialogBuilder { +pub struct ModifyDialogBuilder { account: Option, trade: Option, - new_stop_price: Option, - result: ModifyStopDialogBuilderResult, + new_price: Option, + result: ModifyDialogBuilderResult, } -impl ModifyStopDialogBuilder { +impl ModifyDialogBuilder { pub fn new() -> Self { - ModifyStopDialogBuilder { + ModifyDialogBuilder { account: None, trade: None, - new_stop_price: None, + new_price: None, result: None, } } - pub fn build(mut self, trust: &mut TrustFacade) -> ModifyStopDialogBuilder { + pub fn build_stop(mut self, trust: &mut TrustFacade) -> ModifyDialogBuilder { let trade = self .trade .clone() @@ -36,7 +36,7 @@ impl ModifyStopDialogBuilder { .clone() .expect("No account found, did you forget to call account?"); let stop_price = self - .new_stop_price + .new_price .expect("No stop price found, did you forget to call stop_price?"); match trust.modify_stop(&trade, &account, stop_price) { @@ -46,20 +46,44 @@ impl ModifyStopDialogBuilder { self } + pub fn build_target(mut self, trust: &mut TrustFacade) -> ModifyDialogBuilder { + 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 target_price = self + .new_price + .expect("No target price found, did you forget to call stop_price?"); + + match trust.modify_target(&trade, &account, target_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:"); + println!("Trade updated:"); TradeView::display(&trade, &self.account.unwrap().name); TradeOverviewView::display(&trade.overview); - println!("Stop updated:"); + println!("Stop:"); OrderView::display(trade.safety_stop); + println!("Target:"); + OrderView::display(trade.target); + LogView::display(&log); } Err(error) => println!("Error submitting trade: {:?}", error), @@ -101,12 +125,9 @@ impl ModifyStopDialogBuilder { 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); + pub fn new_price(mut self) -> Self { + let stop_price = Input::new().with_prompt("New price").interact().unwrap(); + self.new_price = Some(stop_price); self } } diff --git a/cli/src/dispatcher.rs b/cli/src/dispatcher.rs index c66d45c..eda9646 100644 --- a/cli/src/dispatcher.rs +++ b/cli/src/dispatcher.rs @@ -1,7 +1,7 @@ use crate::dialogs::{ AccountDialogBuilder, AccountSearchDialog, CancelDialogBuilder, CloseDialogBuilder, ExitDialogBuilder, FillTradeDialogBuilder, FundingDialogBuilder, KeysDeleteDialogBuilder, - KeysReadDialogBuilder, KeysWriteDialogBuilder, ModifyStopDialogBuilder, SubmitDialogBuilder, + KeysReadDialogBuilder, KeysWriteDialogBuilder, ModifyDialogBuilder, SubmitDialogBuilder, SyncTradeDialogBuilder, TradeDialogBuilder, TradeSearchDialogBuilder, TradingVehicleDialogBuilder, TradingVehicleSearchDialogBuilder, TransactionDialogBuilder, }; @@ -79,6 +79,7 @@ impl ArgDispatcher { Some(("sync", _)) => self.create_sync(), Some(("search", _)) => self.search_trade(), Some(("modify-stop", _)) => self.modify_stop(), + Some(("modify-target", _)) => self.modify_target(), _ => unreachable!("No subcommand provided"), }, Some((ext, sub_matches)) => { @@ -269,11 +270,20 @@ impl ArgDispatcher { } fn modify_stop(&mut self) { - ModifyStopDialogBuilder::new() + ModifyDialogBuilder::new() .account(&mut self.trust) .search(&mut self.trust) - .stop_price() - .build(&mut self.trust) + .new_price() + .build_stop(&mut self.trust) + .display(); + } + + fn modify_target(&mut self) { + ModifyDialogBuilder::new() + .account(&mut self.trust) + .search(&mut self.trust) + .new_price() + .build_target(&mut self.trust) .display(); } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 865ab08..5622854 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -59,6 +59,7 @@ fn main() { .manually_target() .manually_close() .modify_stop() + .modify_target() .build(), ) .get_matches(); diff --git a/cli/tests/integration_test_account.rs b/cli/tests/integration_test_account.rs index 8e13c85..66058ae 100644 --- a/cli/tests/integration_test_account.rs +++ b/cli/tests/integration_test_account.rs @@ -237,4 +237,18 @@ impl Broker for MockBroker { new_stop_price ) } + + fn modify_target( + &self, + trade: &Trade, + account: &Account, + new_target_price: rust_decimal::Decimal, + ) -> Result> { + unimplemented!( + "Modify target: {:?} {:?} {:?}", + trade, + account, + new_target_price + ) + } } diff --git a/cli/tests/integration_test_cancel_trade.rs b/cli/tests/integration_test_cancel_trade.rs index 089209f..d045de9 100644 --- a/cli/tests/integration_test_cancel_trade.rs +++ b/cli/tests/integration_test_cancel_trade.rs @@ -142,4 +142,18 @@ impl Broker for MockBroker { new_stop_price ) } + + fn modify_target( + &self, + trade: &Trade, + account: &Account, + new_target_price: rust_decimal::Decimal, + ) -> Result> { + unimplemented!( + "Modify target: {:?} {:?} {:?}", + trade, + account, + new_target_price + ) + } } diff --git a/cli/tests/integration_test_trade.rs b/cli/tests/integration_test_trade.rs index b057c69..f5c6da9 100644 --- a/cli/tests/integration_test_trade.rs +++ b/cli/tests/integration_test_trade.rs @@ -527,6 +527,44 @@ fn test_trade_modify_stop_long() { assert_eq!(log.trade_id, trade.id); } +#[test] +fn test_trade_modify_target() { + 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_target(&trade, &account, dec!(100.1)) + .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.target.unit_price, dec!(100.1)); + assert_eq!(log.trade_id, trade.id); +} + struct BrokerResponse; impl BrokerResponse { @@ -804,4 +842,21 @@ impl Broker for MockBroker { ..Default::default() }) } + + fn modify_target( + &self, + trade: &Trade, + account: &Account, + new_target_price: rust_decimal::Decimal, + ) -> Result> { + assert_eq!(trade.account_id, account.id); + assert_eq!(trade.target.unit_price, dec!(50)); + assert_eq!(new_target_price, dec!(100.1)); + + Ok(BrokerLog { + trade_id: trade.id, + log: "".to_string(), + ..Default::default() + }) + } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 8ab4ffb..ea6f1f0 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -257,6 +257,21 @@ impl TrustFacade { &mut *self.factory, ) } + + pub fn modify_target( + &mut self, + trade: &Trade, + account: &Account, + new_target_price: Decimal, + ) -> Result<(Trade, BrokerLog), Box> { + TradeAction::modify_target( + trade, + account, + new_target_price, + &mut *self.broker, + &mut *self.factory, + ) + } } mod account_calculators; diff --git a/core/src/validators/trade.rs b/core/src/validators/trade.rs index 4b2bcbb..b764700 100644 --- a/core/src/validators/trade.rs +++ b/core/src/validators/trade.rs @@ -78,6 +78,19 @@ pub fn can_modify_stop(trade: &Trade, new_price_stop: Decimal) -> TradeValidatio } } +pub fn can_modify_target(trade: &Trade) -> TradeValidationResult { + match trade.status { + Status::Filled => Ok(()), + _ => Err(Box::new(TradeValidationError { + code: TradeValidationErrorCode::TradeNotFilled, + message: format!( + "Trade with id {} is not filled, cannot be modified", + trade.id + ), + })), + } +} + #[derive(Debug, PartialEq)] pub enum TradeValidationErrorCode { @@ -294,4 +307,24 @@ mod tests { let result = can_modify_stop(&trade, dec!(10)); assert!(result.is_ok()); } + + #[test] + fn test_validate_modify_target() { + let trade = Trade { + status: Status::Filled, + ..Default::default() + }; + let result = can_modify_target(&trade); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_modify_target_not_filled() { + let trade = Trade { + status: Status::Canceled, + ..Default::default() + }; + let result = can_modify_target(&trade); + assert!(result.is_err()); + } } diff --git a/core/src/workers/order_worker.rs b/core/src/workers/order_worker.rs index 0f86b4a..ff632bd 100644 --- a/core/src/workers/order_worker.rs +++ b/core/src/workers/order_worker.rs @@ -108,12 +108,12 @@ impl OrderWorker { read_database.read_trade(trade.id) } - pub fn modify_stop( - stop: &Order, - new_stop_price: Decimal, + pub fn modify( + order: &Order, + new_price: Decimal, write_database: &mut dyn OrderWrite, ) -> Result> { - let stop = write_database.update_price(stop, new_stop_price)?; + let stop = write_database.update_price(order, new_price)?; Ok(stop) } } diff --git a/core/src/workers/trade_action.rs b/core/src/workers/trade_action.rs index f6e7ef5..e86979a 100644 --- a/core/src/workers/trade_action.rs +++ b/core/src/workers/trade_action.rs @@ -290,7 +290,7 @@ impl TradeAction { let log = broker.modify_stop(trade, account, new_stop_price)?; // 3. Modify stop order - OrderWorker::modify_stop( + OrderWorker::modify( &trade.safety_stop, new_stop_price, &mut *database.order_write(), @@ -301,4 +301,26 @@ impl TradeAction { Ok((trade, log)) } + + pub fn modify_target( + trade: &Trade, + account: &Account, + new_price: Decimal, + broker: &mut dyn Broker, + database: &mut dyn DatabaseFactory, + ) -> Result<(Trade, BrokerLog), Box> { + // 1. Verify trade can be modified + crate::validators::trade::can_modify_target(trade)?; + + // 2. Update Trade on the broker + let log = broker.modify_target(trade, account, new_price)?; + + // 3. Modify stop order + OrderWorker::modify(&trade.target, new_price, &mut *database.order_write())?; + + // 4. Refresh Trade + let trade = database.trade_read().read_trade(trade.id)?; + + Ok((trade, log)) + } } diff --git a/model/src/broker.rs b/model/src/broker.rs index 5f9a1a7..c648e37 100644 --- a/model/src/broker.rs +++ b/model/src/broker.rs @@ -70,4 +70,11 @@ pub trait Broker { account: &Account, new_stop_price: Decimal, ) -> Result>; + + fn modify_target( + &self, + trade: &Trade, + account: &Account, + new_price: Decimal, + ) -> Result>; }