1use crate::domain::{Order, Position};
6use crate::exchange::ExchangeResult;
7use crate::exchange::OrderBook;
8use async_trait::async_trait;
9use chrono::{DateTime, Utc};
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum StrategyAction {
16 Buy {
18 symbol: String,
20 quantity_lots: u32,
22 price: Option<Decimal>,
24 },
25
26 Sell {
28 symbol: String,
30 quantity_lots: u32,
32 price: Option<Decimal>,
34 },
35
36 Cancel {
38 order_id: String,
40 },
41
42 ModifyPrice {
44 order_id: String,
46 new_price: Decimal,
48 },
49
50 Hold,
52}
53
54impl StrategyAction {
55 #[must_use]
57 pub fn market_buy(symbol: impl Into<String>, quantity_lots: u32) -> Self {
58 Self::Buy {
59 symbol: symbol.into(),
60 quantity_lots,
61 price: None,
62 }
63 }
64
65 #[must_use]
67 pub fn limit_buy(symbol: impl Into<String>, quantity_lots: u32, price: Decimal) -> Self {
68 Self::Buy {
69 symbol: symbol.into(),
70 quantity_lots,
71 price: Some(price),
72 }
73 }
74
75 #[must_use]
77 pub fn market_sell(symbol: impl Into<String>, quantity_lots: u32) -> Self {
78 Self::Sell {
79 symbol: symbol.into(),
80 quantity_lots,
81 price: None,
82 }
83 }
84
85 #[must_use]
87 pub fn limit_sell(symbol: impl Into<String>, quantity_lots: u32, price: Decimal) -> Self {
88 Self::Sell {
89 symbol: symbol.into(),
90 quantity_lots,
91 price: Some(price),
92 }
93 }
94
95 #[must_use]
97 pub fn cancel(order_id: impl Into<String>) -> Self {
98 Self::Cancel {
99 order_id: order_id.into(),
100 }
101 }
102
103 #[must_use]
105 pub fn hold() -> Self {
106 Self::Hold
107 }
108
109 #[must_use]
111 pub fn is_hold(&self) -> bool {
112 matches!(self, Self::Hold)
113 }
114
115 #[must_use]
117 pub fn symbol(&self) -> Option<&str> {
118 match self {
119 Self::Buy { symbol, .. } | Self::Sell { symbol, .. } => Some(symbol),
120 _ => None,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Default)]
127pub struct StrategyDecision {
128 actions: Vec<StrategyAction>,
129 reason: Option<String>,
130}
131
132impl StrategyDecision {
133 #[must_use]
135 pub fn new() -> Self {
136 Self {
137 actions: Vec::new(),
138 reason: None,
139 }
140 }
141
142 #[must_use]
144 pub fn single(action: StrategyAction) -> Self {
145 Self {
146 actions: vec![action],
147 reason: None,
148 }
149 }
150
151 #[must_use]
153 pub fn hold() -> Self {
154 Self::single(StrategyAction::Hold)
155 }
156
157 #[must_use]
159 pub fn hold_with_reason(reason: impl Into<String>) -> Self {
160 Self {
161 actions: vec![StrategyAction::Hold],
162 reason: Some(reason.into()),
163 }
164 }
165
166 pub fn add_action(&mut self, action: StrategyAction) {
168 self.actions.push(action);
169 }
170
171 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
173 self.reason = Some(reason.into());
174 self
175 }
176
177 #[must_use]
179 pub fn actions(&self) -> &[StrategyAction] {
180 &self.actions
181 }
182
183 #[must_use]
185 pub fn reason(&self) -> Option<&str> {
186 self.reason.as_deref()
187 }
188
189 #[must_use]
191 pub fn is_hold(&self) -> bool {
192 self.actions.is_empty() || self.actions.iter().all(StrategyAction::is_hold)
193 }
194
195 #[must_use]
197 pub fn action_count(&self) -> usize {
198 self.actions.len()
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct MarketState {
205 pub symbol: String,
207 pub order_book: Option<OrderBook>,
209 pub position: Option<Position>,
211 pub active_orders: Vec<Order>,
213 pub available_cash: Decimal,
215 pub timestamp: DateTime<Utc>,
217 pub last_price: Option<Decimal>,
219 pub currency: String,
221}
222
223impl MarketState {
224 #[must_use]
226 pub fn new(symbol: impl Into<String>, currency: impl Into<String>) -> Self {
227 Self {
228 symbol: symbol.into(),
229 order_book: None,
230 position: None,
231 active_orders: Vec::new(),
232 available_cash: Decimal::ZERO,
233 timestamp: Utc::now(),
234 last_price: None,
235 currency: currency.into(),
236 }
237 }
238
239 pub fn with_order_book(mut self, order_book: OrderBook) -> Self {
241 self.order_book = Some(order_book);
242 self
243 }
244
245 pub fn with_position(mut self, position: Position) -> Self {
247 self.position = Some(position);
248 self
249 }
250
251 pub fn with_cash(mut self, cash: Decimal) -> Self {
253 self.available_cash = cash;
254 self
255 }
256
257 pub fn with_last_price(mut self, price: Decimal) -> Self {
259 self.last_price = Some(price);
260 self
261 }
262
263 #[must_use]
265 pub fn best_bid(&self) -> Option<Decimal> {
266 self.order_book.as_ref().and_then(|ob| ob.best_bid_price())
267 }
268
269 #[must_use]
271 pub fn best_ask(&self) -> Option<Decimal> {
272 self.order_book.as_ref().and_then(|ob| ob.best_ask_price())
273 }
274
275 #[must_use]
277 pub fn position_lots(&self) -> u32 {
278 self.position.as_ref().map_or(0, |p| p.lots_held())
279 }
280}
281
282#[async_trait]
286pub trait Strategy: Send + Sync {
287 fn name(&self) -> &str;
289
290 fn id(&self) -> &str {
292 self.name()
293 }
294
295 async fn decide(&self, state: &MarketState) -> StrategyDecision;
297
298 async fn on_order_filled(&mut self, order: &Order);
300
301 async fn on_order_cancelled(&mut self, order: &Order);
303
304 async fn reset(&mut self);
306
307 fn symbols(&self) -> Vec<String>;
309
310 fn is_enabled(&self) -> bool {
312 true
313 }
314}
315
316#[async_trait]
318pub trait StrategyExecutor: Send + Sync {
319 async fn execute(
321 &self,
322 account_id: &str,
323 decision: &StrategyDecision,
324 ) -> ExchangeResult<Vec<Order>>;
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_strategy_action_creation() {
333 let buy = StrategyAction::market_buy("AAPL", 10);
334 assert!(matches!(buy, StrategyAction::Buy { price: None, .. }));
335
336 let sell = StrategyAction::market_sell("AAPL", 5);
337 assert!(matches!(sell, StrategyAction::Sell { price: None, .. }));
338 }
339
340 #[test]
341 fn test_strategy_decision() {
342 let mut decision = StrategyDecision::new();
343 decision.add_action(StrategyAction::market_buy("AAPL", 10));
344 decision.add_action(StrategyAction::market_sell("GOOGL", 5));
345
346 assert_eq!(decision.actions().len(), 2);
347 assert!(!decision.is_hold());
348 }
349
350 #[test]
351 fn test_hold_decision() {
352 let decision = StrategyDecision::hold_with_reason("Market closed");
353 assert!(decision.is_hold());
354 assert_eq!(decision.reason(), Some("Market closed"));
355 }
356
357 #[test]
358 fn test_market_state() {
359 let state = MarketState::new("AAPL", "USD")
360 .with_cash(Decimal::from(10000))
361 .with_last_price(Decimal::from(150));
362
363 assert_eq!(state.symbol, "AAPL");
364 assert_eq!(state.available_cash, Decimal::from(10000));
365 assert_eq!(state.last_price, Some(Decimal::from(150)));
366 }
367}