Skip to main content

stock_trek/examples/
cost_averaging.rs

1use crate::prelude::*;
2use std::cmp::Ordering;
3
4pub struct CostAveraging {
5    key_cex: SignalKey<CexId>,
6    key_market_exists: SignalKey<bool>,
7    key_satoshi_price: SignalKey<f64>,
8    key_satoshi_quantity: SignalKey<f64>,
9}
10
11impl Default for CostAveraging {
12    fn default() -> Self {
13        Self {
14            key_cex: SignalKey::new_required("CEX"),
15            key_market_exists: SignalKey::new_optional("MARKET_EXISTS", false),
16            key_satoshi_price: SignalKey::new_required("SATOSHI_PRICE"),
17            key_satoshi_quantity: SignalKey::new_required("SATOSHI_QUANTITY"),
18        }
19    }
20}
21
22#[register_algorithm(default)]
23impl Algorithm for CostAveraging {
24    fn preferences(&self) -> Preferences {
25        Preferences {
26            cex: CexPreferences {
27                max_network_delay_millis: 5000,
28                rounding: Rounding {
29                    activation_price_triggered_above: RoundingStrategy::AwayFromZero,
30                    activation_price_triggered_below: RoundingStrategy::ToZero,
31                    price: RoundingStrategy::ToZero,
32                    quantity: RoundingStrategy::ToZero,
33                    callback_rate_bps: RoundingStrategy::ToZero,
34                },
35            },
36        }
37    }
38    fn signals(&self, c: &SignalContext) -> Signals {
39        let mut signals = Signals::new();
40        let one_millionth = 1.0 / 1_000_000.0;
41        signals.write(&self.key_satoshi_quantity, one_millionth);
42        let iter = c.cex_markets_for(AssetId::bitcoin(), AssetId::tether_usd());
43        let min_by_last_ask = iter.min_by(|(_a_exch, a_market), (_b_exch, b_market)| {
44            let a_last_ask = a_market.ticks.ticks[0].ask.price;
45            let b_last_ask = b_market.ticks.ticks[0].ask.price;
46            a_last_ask.partial_cmp(&b_last_ask).unwrap()
47        });
48        if let Some((cheapest_cex_name, market)) = min_by_last_ask {
49            signals.write(&self.key_cex, cheapest_cex_name);
50            signals.write(&self.key_market_exists, true);
51            let satoshi_price = market.ticks.ticks[0].ask.price / 1_000_000.0;
52            signals.write(&self.key_satoshi_price, satoshi_price);
53        }
54        signals
55    }
56    fn strategy(&self, c: &StrategyContext) -> Command {
57        let cex = c.signals.cex_id(&self.key_cex);
58        let btc = c.literals.asset_id(AssetId::bitcoin());
59        let usdt = c.literals.asset_id(AssetId::tether_usd());
60        let satoshi_price = c.signals.number(&self.key_satoshi_price);
61        let quantity = c.signals.number(&self.key_satoshi_quantity);
62        c.commands.if_else(
63            c.conditions.signal(&self.key_market_exists),
64            c.commands.if_else(
65                c.conditions.compare(
66                    c.portfolio.asset_in_cex(cex.clone(), usdt.clone()),
67                    Ordering::Greater,
68                    satoshi_price,
69                ),
70                c.commands.plan(vec![c.actions.send_order_request(
71                    cex.clone(),
72                    c.orders.single(
73                        btc,
74                        usdt.clone(),
75                        OrderSide::Buy,
76                        OrderActivation::Immediate,
77                        OrderPricing::Market,
78                        OrderQuantity::OfQuote(quantity),
79                        vec![OrderConstraint::FillPolicy {
80                            allow_partial: true,
81                        }],
82                        OrderTag::new("CostAveraging"),
83                    ),
84                    RecoveryPolicy::with_default(ErrorResponse::Stop).on_error(
85                        ErrorCause::TemporaryCexRejection,
86                        ErrorResponse::Retry { max_retries: 3 },
87                    ),
88                )]),
89                c.commands.no_op(),
90            ),
91            c.commands.no_op(),
92        )
93    }
94}