Skip to main content

engine/strategies/
external.rs

1// cargo test -p market_engine -- external
2use super::Strategy;
3use crate::types::{Observation, OrderRequest};
4
5/// A strategy whose decisions are supplied externally (e.g. from Python).
6///
7/// Each tick it stores the latest `Observation` and drains any order requests
8/// that were previously injected via `set_pending_requests`.  This lets a
9/// foreign caller:
10///
11///   1. Call `engine.step()` which internally calls `on_tick` -- the strategy
12///      captures the observation and pushes any pending requests.
13///   2. Read `last_observation()` to see what the agent observed.
14///   3. Decide on orders, call `set_pending_requests(...)`.
15///   4. Call `engine.step()` again.
16pub struct ExternalStrategy {
17    last_observation: Option<Observation>,
18    pending_requests: Vec<OrderRequest>,
19}
20
21impl Default for ExternalStrategy {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl ExternalStrategy {
28    pub fn new() -> Self {
29        Self {
30            last_observation: None,
31            pending_requests: Vec::new(),
32        }
33    }
34
35    /// Snapshot of the most recent observation delivered to this strategy.
36    pub fn last_observation(&self) -> Option<&Observation> {
37        self.last_observation.as_ref()
38    }
39
40    /// Queue order requests that will be submitted on the **next** `on_tick`.
41    pub fn set_pending_requests(&mut self, requests: Vec<OrderRequest>) {
42        self.pending_requests = requests;
43    }
44}
45
46impl Strategy for ExternalStrategy {
47    fn on_tick(&mut self, obs: &Observation, requests: &mut Vec<OrderRequest>) {
48        self.last_observation = Some(obs.clone());
49        requests.append(&mut self.pending_requests);
50    }
51
52    fn as_any(&self) -> &dyn std::any::Any {
53        self
54    }
55    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
56        self
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::types::{Order, PortfolioSnapshot, Side};
64
65    fn sample_obs() -> Observation {
66        Observation {
67            timestamp: 0.0,
68            best_bid: 99.0,
69            best_ask: 101.0,
70            last_price: Some(100.0),
71            portfolio: PortfolioSnapshot {
72                cash: 10000.0,
73                position: 0.0,
74            },
75            volatility: Some(0.2),
76            drift: None,
77            parameters: None,
78        }
79    }
80
81    #[test]
82    fn test_captures_observation() {
83        let mut strat = ExternalStrategy::new();
84        assert!(strat.last_observation().is_none());
85
86        let obs = sample_obs();
87        let mut buf = Vec::new();
88        strat.on_tick(&obs, &mut buf);
89
90        assert!(strat.last_observation().is_some());
91        assert!(buf.is_empty());
92    }
93
94    #[test]
95    fn test_drains_pending_requests() {
96        let mut strat = ExternalStrategy::new();
97        strat.set_pending_requests(vec![
98            OrderRequest::CancelAll,
99            OrderRequest::New(Order::new(1, Side::Buy, 99.5, 1.0)),
100        ]);
101
102        let obs = sample_obs();
103        let mut buf = Vec::new();
104        strat.on_tick(&obs, &mut buf);
105
106        assert_eq!(buf.len(), 2);
107        // After drain, no more pending
108        let mut buf2 = Vec::new();
109        strat.on_tick(&obs, &mut buf2);
110        assert!(buf2.is_empty());
111    }
112}