Skip to main content

trader_bot/strategy/
traits.rs

1//! Strategy traits and types.
2//!
3//! This module defines the core abstractions for trading strategies.
4
5use 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/// An action the strategy wants to take.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum StrategyAction {
16    /// Place a new buy order.
17    Buy {
18        /// The symbol to buy.
19        symbol: String,
20        /// Number of lots to buy.
21        quantity_lots: u32,
22        /// Optional limit price (None for market order).
23        price: Option<Decimal>,
24    },
25
26    /// Place a new sell order.
27    Sell {
28        /// The symbol to sell.
29        symbol: String,
30        /// Number of lots to sell.
31        quantity_lots: u32,
32        /// Optional limit price (None for market order).
33        price: Option<Decimal>,
34    },
35
36    /// Cancel an existing order.
37    Cancel {
38        /// The order ID to cancel.
39        order_id: String,
40    },
41
42    /// Modify an existing order's price.
43    ModifyPrice {
44        /// The order ID to modify.
45        order_id: String,
46        /// The new price.
47        new_price: Decimal,
48    },
49
50    /// Do nothing.
51    Hold,
52}
53
54impl StrategyAction {
55    /// Creates a market buy action.
56    #[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    /// Creates a limit buy action.
66    #[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    /// Creates a market sell action.
76    #[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    /// Creates a limit sell action.
86    #[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    /// Creates a cancel action.
96    #[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    /// Creates a hold action.
104    #[must_use]
105    pub fn hold() -> Self {
106        Self::Hold
107    }
108
109    /// Checks if this is a hold action.
110    #[must_use]
111    pub fn is_hold(&self) -> bool {
112        matches!(self, Self::Hold)
113    }
114
115    /// Returns the symbol for buy/sell actions.
116    #[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/// The decision made by a strategy, containing multiple possible actions.
126#[derive(Debug, Clone, Default)]
127pub struct StrategyDecision {
128    actions: Vec<StrategyAction>,
129    reason: Option<String>,
130}
131
132impl StrategyDecision {
133    /// Creates a new empty decision.
134    #[must_use]
135    pub fn new() -> Self {
136        Self {
137            actions: Vec::new(),
138            reason: None,
139        }
140    }
141
142    /// Creates a decision with a single action.
143    #[must_use]
144    pub fn single(action: StrategyAction) -> Self {
145        Self {
146            actions: vec![action],
147            reason: None,
148        }
149    }
150
151    /// Creates a hold decision.
152    #[must_use]
153    pub fn hold() -> Self {
154        Self::single(StrategyAction::Hold)
155    }
156
157    /// Creates a hold decision with a reason.
158    #[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    /// Adds an action to the decision.
167    pub fn add_action(&mut self, action: StrategyAction) {
168        self.actions.push(action);
169    }
170
171    /// Sets the reason for the decision.
172    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
173        self.reason = Some(reason.into());
174        self
175    }
176
177    /// Returns the actions.
178    #[must_use]
179    pub fn actions(&self) -> &[StrategyAction] {
180        &self.actions
181    }
182
183    /// Returns the reason for the decision.
184    #[must_use]
185    pub fn reason(&self) -> Option<&str> {
186        self.reason.as_deref()
187    }
188
189    /// Checks if this is a hold decision (no actions or only hold actions).
190    #[must_use]
191    pub fn is_hold(&self) -> bool {
192        self.actions.is_empty() || self.actions.iter().all(StrategyAction::is_hold)
193    }
194
195    /// Returns the number of actions.
196    #[must_use]
197    pub fn action_count(&self) -> usize {
198        self.actions.len()
199    }
200}
201
202/// Market state provided to the strategy for decision making.
203#[derive(Debug, Clone)]
204pub struct MarketState {
205    /// The instrument being traded.
206    pub symbol: String,
207    /// Current order book (if available).
208    pub order_book: Option<OrderBook>,
209    /// Current position (if any).
210    pub position: Option<Position>,
211    /// Active orders.
212    pub active_orders: Vec<Order>,
213    /// Available cash for trading.
214    pub available_cash: Decimal,
215    /// Current timestamp.
216    pub timestamp: DateTime<Utc>,
217    /// Last price of the instrument.
218    pub last_price: Option<Decimal>,
219    /// Currency of the instrument.
220    pub currency: String,
221}
222
223impl MarketState {
224    /// Creates a new market state.
225    #[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    /// Sets the order book.
240    pub fn with_order_book(mut self, order_book: OrderBook) -> Self {
241        self.order_book = Some(order_book);
242        self
243    }
244
245    /// Sets the position.
246    pub fn with_position(mut self, position: Position) -> Self {
247        self.position = Some(position);
248        self
249    }
250
251    /// Sets the available cash.
252    pub fn with_cash(mut self, cash: Decimal) -> Self {
253        self.available_cash = cash;
254        self
255    }
256
257    /// Sets the last price.
258    pub fn with_last_price(mut self, price: Decimal) -> Self {
259        self.last_price = Some(price);
260        self
261    }
262
263    /// Returns the best bid price from the order book.
264    #[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    /// Returns the best ask price from the order book.
270    #[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    /// Returns the current position quantity in lots.
276    #[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/// A trading strategy that makes decisions based on market state.
283///
284/// Strategies are composable - a balancer strategy can contain sub-strategies.
285#[async_trait]
286pub trait Strategy: Send + Sync {
287    /// Returns the name of the strategy.
288    fn name(&self) -> &str;
289
290    /// Returns a unique identifier for this strategy instance.
291    fn id(&self) -> &str {
292        self.name()
293    }
294
295    /// Makes a decision based on the current market state.
296    async fn decide(&self, state: &MarketState) -> StrategyDecision;
297
298    /// Called when an order is filled.
299    async fn on_order_filled(&mut self, order: &Order);
300
301    /// Called when an order is cancelled.
302    async fn on_order_cancelled(&mut self, order: &Order);
303
304    /// Resets the strategy state.
305    async fn reset(&mut self);
306
307    /// Returns the symbols this strategy trades.
308    fn symbols(&self) -> Vec<String>;
309
310    /// Returns whether the strategy is enabled.
311    fn is_enabled(&self) -> bool {
312        true
313    }
314}
315
316/// A strategy executor that runs strategies against an exchange.
317#[async_trait]
318pub trait StrategyExecutor: Send + Sync {
319    /// Executes the strategy decision, placing orders on the exchange.
320    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}