Skip to content

Commit 8b10e76

Browse files
Merge pull request #32 from integer256/feature/update-stop
feat: modify stop
2 parents e75f846 + 380d93b commit 8b10e76

File tree

18 files changed

+509
-5
lines changed

18 files changed

+509
-5
lines changed

alpaca-broker/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::error::Error;
44
mod cancel_trade;
55
mod close_trade;
66
mod keys;
7+
mod modify_trade;
78
mod order_mapper;
89
mod submit_trade;
910
mod sync_trade;
@@ -42,6 +43,15 @@ impl Broker for AlpacaBroker {
4243
println!("Canceling trade: {:?}", trade);
4344
cancel_trade::cancel(trade, account)
4445
}
46+
47+
fn modify_stop(
48+
&self,
49+
trade: &Trade,
50+
account: &Account,
51+
new_stop_price: rust_decimal::Decimal,
52+
) -> Result<BrokerLog, Box<dyn Error>> {
53+
modify_trade::modify_stop(trade, account, new_stop_price)
54+
}
4555
}
4656

4757
/// Alpaca-specific Broker API

alpaca-broker/src/modify_trade.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use crate::keys;
2+
use apca::api::v2::order::{ChangeReqInit, Id, Order, Patch};
3+
use apca::Client;
4+
use model::BrokerLog;
5+
use model::{Account, Trade};
6+
use num_decimal::Num;
7+
use rust_decimal::Decimal;
8+
use std::{error::Error, str::FromStr};
9+
use tokio::runtime::Runtime;
10+
use uuid::Uuid;
11+
12+
pub fn modify_stop(
13+
trade: &Trade,
14+
account: &Account,
15+
price: Decimal,
16+
) -> Result<BrokerLog, Box<dyn Error>> {
17+
assert!(trade.account_id == account.id); // Verify that the trade is for the account
18+
19+
let api_info = keys::read_api_key(&account.environment, account)?;
20+
let client = Client::new(api_info);
21+
22+
// Modify the stop order.
23+
let alpaca_order = Runtime::new().unwrap().block_on(modify_entry(
24+
&client,
25+
trade.safety_stop.broker_order_id.unwrap(),
26+
price,
27+
))?;
28+
29+
// 3. Log the Alpaca order.
30+
let log = BrokerLog {
31+
trade_id: trade.id,
32+
log: serde_json::to_string(&alpaca_order)?,
33+
..Default::default()
34+
};
35+
36+
Ok(log)
37+
}
38+
39+
async fn modify_entry(
40+
client: &Client,
41+
order_id: Uuid,
42+
price: Decimal,
43+
) -> Result<Order, Box<dyn Error>> {
44+
let request = ChangeReqInit {
45+
stop_price: Some(Num::from_str(price.to_string().as_str()).unwrap()),
46+
..Default::default()
47+
}
48+
.init();
49+
50+
let result = client.issue::<Patch>(&(Id(order_id), request)).await;
51+
match result {
52+
Ok(log) => Ok(log),
53+
Err(e) => {
54+
eprintln!("Error modify stop: {:?}", e);
55+
Err(Box::new(e))
56+
}
57+
}
58+
}

cli/src/commands/trade_command.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ impl TradeCommandBuilder {
6767
self
6868
}
6969

70+
pub fn modify_stop(mut self) -> Self {
71+
self.subcommands.push(
72+
Command::new("modify-stop").about("Modify the stop loss order of a filled trade."),
73+
);
74+
self
75+
}
76+
7077
pub fn manually_target(mut self) -> Self {
7178
self.subcommands
7279
.push(Command::new("manually-target").about("Execute manually the target of a trade"));

cli/src/dialogs.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod account_dialog;
22
mod keys_dialog;
3+
mod modify_stop_dialog;
34
mod rule_dialog;
45
mod trade_cancel_dialog;
56
mod trade_close_dialog;
@@ -18,6 +19,7 @@ pub use account_dialog::AccountSearchDialog;
1819
pub use keys_dialog::KeysDeleteDialogBuilder;
1920
pub use keys_dialog::KeysReadDialogBuilder;
2021
pub use keys_dialog::KeysWriteDialogBuilder;
22+
pub use modify_stop_dialog::ModifyStopDialogBuilder;
2123
pub use rule_dialog::RuleDialogBuilder;
2224
pub use rule_dialog::RuleRemoveDialogBuilder;
2325
pub use trade_cancel_dialog::CancelDialogBuilder;

cli/src/dialogs/modify_stop_dialog.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use crate::dialogs::AccountSearchDialog;
2+
use crate::views::{LogView, OrderView, TradeOverviewView, TradeView};
3+
use core::TrustFacade;
4+
use dialoguer::{theme::ColorfulTheme, FuzzySelect, Input};
5+
use model::{Account, BrokerLog, Status, Trade};
6+
use rust_decimal::Decimal;
7+
use std::error::Error;
8+
9+
type ModifyStopDialogBuilderResult = Option<Result<(Trade, BrokerLog), Box<dyn Error>>>;
10+
11+
pub struct ModifyStopDialogBuilder {
12+
account: Option<Account>,
13+
trade: Option<Trade>,
14+
new_stop_price: Option<Decimal>,
15+
result: ModifyStopDialogBuilderResult,
16+
}
17+
18+
impl ModifyStopDialogBuilder {
19+
pub fn new() -> Self {
20+
ModifyStopDialogBuilder {
21+
account: None,
22+
trade: None,
23+
new_stop_price: None,
24+
result: None,
25+
}
26+
}
27+
28+
pub fn build(mut self, trust: &mut TrustFacade) -> ModifyStopDialogBuilder {
29+
let trade = self
30+
.trade
31+
.clone()
32+
.expect("No trade found, did you forget to call search?");
33+
34+
let account = self
35+
.account
36+
.clone()
37+
.expect("No account found, did you forget to call account?");
38+
let stop_price = self
39+
.new_stop_price
40+
.expect("No stop price found, did you forget to call stop_price?");
41+
42+
match trust.modify_stop(&trade, &account, stop_price) {
43+
Ok((trade, log)) => self.result = Some(Ok((trade, log))),
44+
Err(error) => self.result = Some(Err(error)),
45+
}
46+
self
47+
}
48+
49+
pub fn display(self) {
50+
match self
51+
.result
52+
.expect("No result found, did you forget to call search?")
53+
{
54+
Ok((trade, log)) => {
55+
println!("Trade stop updated:");
56+
TradeView::display(&trade, &self.account.unwrap().name);
57+
58+
TradeOverviewView::display(&trade.overview);
59+
60+
println!("Stop updated:");
61+
OrderView::display(trade.safety_stop);
62+
63+
LogView::display(&log);
64+
}
65+
Err(error) => println!("Error submitting trade: {:?}", error),
66+
}
67+
}
68+
69+
pub fn account(mut self, trust: &mut TrustFacade) -> Self {
70+
let account = AccountSearchDialog::new().search(trust).build();
71+
match account {
72+
Ok(account) => self.account = Some(account),
73+
Err(error) => println!("Error searching account: {:?}", error),
74+
}
75+
self
76+
}
77+
78+
pub fn search(mut self, trust: &mut TrustFacade) -> Self {
79+
let trades = trust.search_trades(self.account.clone().unwrap().id, Status::Filled);
80+
match trades {
81+
Ok(trades) => {
82+
if trades.is_empty() {
83+
panic!("No trade found with the status filled, did you forget to submit one?")
84+
}
85+
let trade = FuzzySelect::with_theme(&ColorfulTheme::default())
86+
.with_prompt("Trade:")
87+
.items(&trades[..])
88+
.default(0)
89+
.interact_opt()
90+
.unwrap()
91+
.map(|index| trades.get(index).unwrap())
92+
.unwrap();
93+
94+
println!("Trade selected:");
95+
TradeView::display(trade, &self.account.clone().unwrap().name);
96+
self.trade = Some(trade.to_owned());
97+
}
98+
Err(error) => self.result = Some(Err(error)),
99+
}
100+
101+
self
102+
}
103+
104+
pub fn stop_price(mut self) -> Self {
105+
let stop_price = Input::new()
106+
.with_prompt("New stop price")
107+
.interact()
108+
.unwrap();
109+
self.new_stop_price = Some(stop_price);
110+
self
111+
}
112+
}

cli/src/dispatcher.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use crate::dialogs::{
22
AccountDialogBuilder, AccountSearchDialog, CancelDialogBuilder, CloseDialogBuilder,
33
ExitDialogBuilder, FillTradeDialogBuilder, FundingDialogBuilder, KeysDeleteDialogBuilder,
4-
KeysReadDialogBuilder, KeysWriteDialogBuilder, SubmitDialogBuilder, SyncTradeDialogBuilder,
5-
TradeDialogBuilder, TradeSearchDialogBuilder, TradingVehicleDialogBuilder,
6-
TradingVehicleSearchDialogBuilder, TransactionDialogBuilder,
4+
KeysReadDialogBuilder, KeysWriteDialogBuilder, ModifyStopDialogBuilder, SubmitDialogBuilder,
5+
SyncTradeDialogBuilder, TradeDialogBuilder, TradeSearchDialogBuilder,
6+
TradingVehicleDialogBuilder, TradingVehicleSearchDialogBuilder, TransactionDialogBuilder,
77
};
88
use crate::dialogs::{RuleDialogBuilder, RuleRemoveDialogBuilder};
99
use alpaca_broker::AlpacaBroker;
@@ -78,6 +78,7 @@ impl ArgDispatcher {
7878
Some(("manually-close", _)) => self.close(),
7979
Some(("sync", _)) => self.create_sync(),
8080
Some(("search", _)) => self.search_trade(),
81+
Some(("modify-stop", _)) => self.modify_stop(),
8182
_ => unreachable!("No subcommand provided"),
8283
},
8384
Some((ext, sub_matches)) => {
@@ -266,6 +267,15 @@ impl ArgDispatcher {
266267
.build(&mut self.trust)
267268
.display();
268269
}
270+
271+
fn modify_stop(&mut self) {
272+
ModifyStopDialogBuilder::new()
273+
.account(&mut self.trust)
274+
.search(&mut self.trust)
275+
.stop_price()
276+
.build(&mut self.trust)
277+
.display();
278+
}
269279
}
270280

271281
impl ArgDispatcher {

cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ fn main() {
5858
.manually_stop()
5959
.manually_target()
6060
.manually_close()
61+
.modify_stop()
6162
.build(),
6263
)
6364
.get_matches();

cli/tests/integration_test_account.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use model::{
55
Account, BrokerLog, Currency, Order, OrderIds, RuleLevel, RuleName, Status, Trade,
66
TransactionCategory,
77
};
8+
use rust_decimal::Decimal;
89
use rust_decimal_macros::dec;
910
use std::error::Error;
1011

@@ -222,4 +223,18 @@ impl Broker for MockBroker {
222223
fn cancel_trade(&self, trade: &Trade, account: &Account) -> Result<(), Box<dyn Error>> {
223224
unimplemented!("Cancel trade: {:?} {:?}", trade, account)
224225
}
226+
227+
fn modify_stop(
228+
&self,
229+
trade: &Trade,
230+
account: &Account,
231+
new_stop_price: Decimal,
232+
) -> Result<BrokerLog, Box<dyn Error>> {
233+
unimplemented!(
234+
"Modify stop: {:?} {:?} {:?}",
235+
trade,
236+
account,
237+
new_stop_price
238+
)
239+
}
225240
}

cli/tests/integration_test_cancel_trade.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use model::{
55
Account, BrokerLog, Currency, DraftTrade, Order, OrderIds, Status, Trade, TradeCategory,
66
TradingVehicleCategory, TransactionCategory,
77
};
8+
use rust_decimal::Decimal;
89
use rust_decimal_macros::dec;
910
use std::error::Error;
1011

@@ -127,4 +128,18 @@ impl Broker for MockBroker {
127128
fn cancel_trade(&self, _trade: &Trade, _account: &Account) -> Result<(), Box<dyn Error>> {
128129
unimplemented!("Cancel trade not implemented")
129130
}
131+
132+
fn modify_stop(
133+
&self,
134+
trade: &Trade,
135+
account: &Account,
136+
new_stop_price: Decimal,
137+
) -> Result<BrokerLog, Box<dyn Error>> {
138+
unimplemented!(
139+
"Modify stop: {:?} {:?} {:?}",
140+
trade,
141+
account,
142+
new_stop_price
143+
)
144+
}
130145
}

cli/tests/integration_test_trade.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use model::{
66
Trade, TradeCategory, TradingVehicleCategory, TransactionCategory,
77
};
88
use model::{Broker, DraftTrade, OrderStatus};
9+
use rust_decimal::Decimal;
910
use rust_decimal_macros::dec;
1011
use std::error::Error;
1112
use uuid::Uuid;
@@ -488,6 +489,44 @@ fn test_trade_close() {
488489
assert_eq!(trade.target.status, OrderStatus::PendingNew);
489490
}
490491

492+
#[test]
493+
fn test_trade_modify_stop_long() {
494+
let (trust, account, trade) = create_trade(
495+
BrokerResponse::orders_entry_filled,
496+
Some(BrokerResponse::closed_order),
497+
);
498+
let mut trust = trust;
499+
500+
// 1. Sync trade with the Broker - Entry is filled
501+
trust
502+
.sync_trade(&trade, &account)
503+
.expect("Failed to sync trade with broker when entry is filled");
504+
505+
let trade = trust
506+
.search_trades(account.id, Status::Filled)
507+
.expect("Failed to find trade with status submitted 2")
508+
.first()
509+
.unwrap()
510+
.clone();
511+
512+
// 7. Modify stop
513+
let (_, log) = trust
514+
.modify_stop(&trade, &account, dec!(39))
515+
.expect("Failed to modify stop");
516+
517+
let trade = trust
518+
.search_trades(account.id, Status::Filled)
519+
.expect("Failed to find trade with status filled")
520+
.first()
521+
.unwrap()
522+
.clone();
523+
524+
// Assert Trade Overview
525+
assert_eq!(trade.status, Status::Filled); // The trade is still filled, but the stop was changed
526+
assert_eq!(trade.safety_stop.unit_price, dec!(39));
527+
assert_eq!(log.trade_id, trade.id);
528+
}
529+
491530
struct BrokerResponse;
492531

493532
impl BrokerResponse {
@@ -748,4 +787,21 @@ impl Broker for MockBroker {
748787
fn cancel_trade(&self, _trade: &Trade, _account: &Account) -> Result<(), Box<dyn Error>> {
749788
Ok(())
750789
}
790+
791+
fn modify_stop(
792+
&self,
793+
trade: &Trade,
794+
account: &Account,
795+
new_stop_price: Decimal,
796+
) -> Result<BrokerLog, Box<dyn Error>> {
797+
assert_eq!(trade.account_id, account.id);
798+
assert_eq!(trade.safety_stop.unit_price, dec!(38));
799+
assert_eq!(new_stop_price, dec!(39));
800+
801+
Ok(BrokerLog {
802+
trade_id: trade.id,
803+
log: "".to_string(),
804+
..Default::default()
805+
})
806+
}
751807
}

0 commit comments

Comments
 (0)