1use 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
17pub struct PendingTimer {
19 pub id: TimerId,
21 pub delay: Duration,
23 pub repeating: bool,
25}
26
27pub struct EngineContext<'a> {
29 pub place_orders: Vec<PlaceOrder>,
32 pub batch_orders: Vec<Vec<PlaceOrder>>,
34 pub cancel_orders: Vec<CancelOrder>,
36 pub cancel_alls: Vec<CancelAll>,
38 pub stop_requests: Vec<StopStrategy>,
40
41 pub timers: Vec<PendingTimer>,
44 pub canceled_timers: Vec<TimerId>,
46
47 pub quotes: &'a HashMap<InstrumentId, Quote>,
50 pub instruments: &'a HashMap<InstrumentId, InstrumentMeta>,
52 pub orders: &'a OrderManager,
54 pub inventory: &'a InventoryLedger,
56 pub positions: &'a HashMap<InstrumentId, Position>,
58 pub exchange_health: &'a HashMap<ExchangeInstance, ExchangeHealth>,
60
61 pub now_ms: i64,
64}
65
66impl<'a> EngineContext<'a> {
67 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 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 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 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}