Skip to main content

bot_engine/
context.rs

1//! Strategy context implementation.
2
3use bot_core::{
4    AssetId, Balance, CancelAll, CancelOrder, ClientOrderId, ExchangeHealth, ExchangeInstance,
5    InstrumentId, InstrumentMeta, LiveOrder, PlaceOrder, Position, Price, Quote, StopStrategy,
6    StrategyContext, StrategyId, TimerId,
7};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Duration;
11
12use crate::inventory::InventoryLedger;
13use crate::order_manager::OrderManager;
14
15static TIMER_COUNTER: AtomicU64 = AtomicU64::new(1);
16
17/// Pending timer registration
18pub struct PendingTimer {
19    /// Engine-assigned timer ID.
20    pub id: TimerId,
21    /// Delay before the timer fires.
22    pub delay: Duration,
23    /// Whether the timer repeats after firing.
24    pub repeating: bool,
25}
26
27/// Concrete implementation of StrategyContext
28pub struct EngineContext<'a> {
29    // Command buffers (collected during event handling, executed after)
30    /// Single-order commands emitted during the current callback.
31    pub place_orders: Vec<PlaceOrder>,
32    /// Batch order commands emitted during the current callback.
33    pub batch_orders: Vec<Vec<PlaceOrder>>,
34    /// Cancel-one commands emitted during the current callback.
35    pub cancel_orders: Vec<CancelOrder>,
36    /// Cancel-all commands emitted during the current callback.
37    pub cancel_alls: Vec<CancelAll>,
38    /// Strategy stop requests emitted during the current callback.
39    pub stop_requests: Vec<StopStrategy>,
40
41    // Timer registrations
42    /// Timer registrations emitted during the current callback.
43    pub timers: Vec<PendingTimer>,
44    /// Timer cancellations emitted during the current callback.
45    pub canceled_timers: Vec<TimerId>,
46
47    // Read-only references to engine state
48    /// Latest quotes by instrument.
49    pub quotes: &'a HashMap<InstrumentId, Quote>,
50    /// Instrument metadata by instrument.
51    pub instruments: &'a HashMap<InstrumentId, InstrumentMeta>,
52    /// Order manager snapshot.
53    pub orders: &'a OrderManager,
54    /// Inventory ledger snapshot.
55    pub inventory: &'a InventoryLedger,
56    /// Current positions by instrument.
57    pub positions: &'a HashMap<InstrumentId, Position>,
58    /// Exchange health by exchange instance.
59    pub exchange_health: &'a HashMap<ExchangeInstance, ExchangeHealth>,
60
61    // Current time
62    /// Engine clock in milliseconds since the Unix epoch.
63    pub now_ms: i64,
64}
65
66impl<'a> EngineContext<'a> {
67    /// Create an engine context for one strategy callback.
68    pub fn new(
69        quotes: &'a HashMap<InstrumentId, Quote>,
70        instruments: &'a HashMap<InstrumentId, InstrumentMeta>,
71        orders: &'a OrderManager,
72        inventory: &'a InventoryLedger,
73        positions: &'a HashMap<InstrumentId, Position>,
74        exchange_health: &'a HashMap<ExchangeInstance, ExchangeHealth>,
75        now_ms: i64,
76    ) -> Self {
77        Self {
78            place_orders: Vec::new(),
79            batch_orders: Vec::new(),
80            cancel_orders: Vec::new(),
81            cancel_alls: Vec::new(),
82            stop_requests: Vec::new(),
83            timers: Vec::new(),
84            canceled_timers: Vec::new(),
85            quotes,
86            instruments,
87            orders,
88            inventory,
89            positions,
90            exchange_health,
91            now_ms,
92        }
93    }
94
95    /// Drain all collected commands
96    pub fn take_commands(
97        &mut self,
98    ) -> (
99        Vec<PlaceOrder>,
100        Vec<Vec<PlaceOrder>>,
101        Vec<CancelOrder>,
102        Vec<CancelAll>,
103        Vec<StopStrategy>,
104    ) {
105        (
106            std::mem::take(&mut self.place_orders),
107            std::mem::take(&mut self.batch_orders),
108            std::mem::take(&mut self.cancel_orders),
109            std::mem::take(&mut self.cancel_alls),
110            std::mem::take(&mut self.stop_requests),
111        )
112    }
113
114    /// Drain all timer registrations
115    pub fn take_timers(&mut self) -> (Vec<PendingTimer>, Vec<TimerId>) {
116        (
117            std::mem::take(&mut self.timers),
118            std::mem::take(&mut self.canceled_timers),
119        )
120    }
121}
122
123impl<'a> StrategyContext for EngineContext<'a> {
124    fn place_order(&mut self, cmd: PlaceOrder) {
125        self.place_orders.push(cmd);
126    }
127
128    fn place_orders(&mut self, cmds: Vec<PlaceOrder>) {
129        if !cmds.is_empty() {
130            self.batch_orders.push(cmds);
131        }
132    }
133
134    fn cancel_order(&mut self, cmd: CancelOrder) {
135        self.cancel_orders.push(cmd);
136    }
137
138    fn cancel_all(&mut self, cmd: CancelAll) {
139        self.cancel_alls.push(cmd);
140    }
141
142    fn stop_strategy(&mut self, strategy_id: StrategyId, reason: &str) {
143        tracing::warn!("Strategy {} requested stop: {}", strategy_id.0, reason);
144        self.stop_requests
145            .push(StopStrategy::new(strategy_id, reason));
146    }
147
148    fn set_timer(&mut self, delay: Duration) -> TimerId {
149        let id = TimerId::new(TIMER_COUNTER.fetch_add(1, Ordering::SeqCst));
150        self.timers.push(PendingTimer {
151            id,
152            delay,
153            repeating: false,
154        });
155        id
156    }
157
158    fn set_interval(&mut self, interval: Duration) -> TimerId {
159        let id = TimerId::new(TIMER_COUNTER.fetch_add(1, Ordering::SeqCst));
160        self.timers.push(PendingTimer {
161            id,
162            delay: interval,
163            repeating: true,
164        });
165        id
166    }
167
168    fn cancel_timer(&mut self, timer_id: TimerId) {
169        self.canceled_timers.push(timer_id);
170    }
171
172    fn mid_price(&self, instrument: &InstrumentId) -> Option<Price> {
173        self.quotes.get(instrument).map(|q| q.mid())
174    }
175
176    fn quote(&self, instrument: &InstrumentId) -> Option<Quote> {
177        self.quotes.get(instrument).cloned()
178    }
179
180    fn instrument_meta(&self, instrument: &InstrumentId) -> Option<&InstrumentMeta> {
181        self.instruments.get(instrument)
182    }
183
184    fn balance(&self, asset: &AssetId) -> Balance {
185        self.inventory.balance(asset)
186    }
187
188    fn position(&self, instrument: &InstrumentId) -> Position {
189        let mut pos = self.positions.get(instrument).cloned().unwrap_or_default();
190
191        // Compute unrealized PnL if we have position and quote
192        if let (Some(avg_entry), Some(quote)) = (pos.avg_entry_px, self.quotes.get(instrument)) {
193            let mid = (quote.bid.0 + quote.ask.0) / rust_decimal::Decimal::TWO;
194            let unrealized = (mid - avg_entry.0) * pos.qty;
195            pos.unrealized_pnl = Some(unrealized);
196        }
197
198        pos
199    }
200
201    fn exchange_health(&self, exchange: &ExchangeInstance) -> ExchangeHealth {
202        self.exchange_health
203            .get(exchange)
204            .copied()
205            .unwrap_or_default()
206    }
207
208    fn order(&self, client_id: &ClientOrderId) -> Option<&LiveOrder> {
209        self.orders.get(client_id)
210    }
211
212    fn now_ms(&self) -> i64 {
213        self.now_ms
214    }
215
216    fn log_info(&self, msg: &str) {
217        tracing::info!("{}", msg);
218    }
219
220    fn log_warn(&self, msg: &str) {
221        tracing::warn!("{}", msg);
222    }
223
224    fn log_error(&self, msg: &str) {
225        tracing::error!("{}", msg);
226    }
227
228    fn log_debug(&self, msg: &str) {
229        tracing::debug!("{}", msg);
230    }
231}