Skip to main content

hyper_playbook/
signal_executor.rs

1use async_trait::async_trait;
2
3use crate::executor::{ExecutionError, OrderExecutor, PlaybookOrderParams};
4
5/// A no-op executor that produces synthetic order/position IDs without
6/// side effects. Used when PlaybookEngine drives signal generation only —
7/// actual execution happens downstream in the Order Pipeline.
8pub struct SignalOnlyExecutor {
9    counter: std::sync::atomic::AtomicU64,
10}
11
12impl SignalOnlyExecutor {
13    pub fn new() -> Self {
14        Self {
15            counter: std::sync::atomic::AtomicU64::new(1),
16        }
17    }
18
19    fn next_id(&self, prefix: &str) -> String {
20        let n = self
21            .counter
22            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
23        format!("{}-{}", prefix, n)
24    }
25}
26
27impl Default for SignalOnlyExecutor {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33#[async_trait]
34impl OrderExecutor for SignalOnlyExecutor {
35    async fn place_order(&self, _params: &PlaybookOrderParams) -> Result<String, ExecutionError> {
36        Ok(self.next_id("sig-order"))
37    }
38
39    async fn cancel_order(&self, _order_id: &str) -> Result<(), ExecutionError> {
40        Ok(())
41    }
42
43    async fn is_filled(&self, _order_id: &str) -> Result<bool, ExecutionError> {
44        Ok(true)
45    }
46
47    async fn close_position(
48        &self,
49        _position_id: &str,
50        _reason: &str,
51    ) -> Result<String, ExecutionError> {
52        Ok(self.next_id("sig-close"))
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::executor::PlaybookOrderParams;
60
61    #[tokio::test]
62    async fn place_order_returns_unique_ids() {
63        let exec = SignalOnlyExecutor::new();
64        let params = PlaybookOrderParams {
65            symbol: "BTC-PERP".into(),
66            side: "buy".into(),
67            size: 0.1,
68            price: Some(60000.0),
69            reduce_only: false,
70        };
71        let id1 = exec.place_order(&params).await.unwrap();
72        let id2 = exec.place_order(&params).await.unwrap();
73        assert_ne!(id1, id2);
74        assert!(id1.starts_with("sig-order-"));
75    }
76
77    #[tokio::test]
78    async fn is_filled_always_true() {
79        let exec = SignalOnlyExecutor::new();
80        assert!(exec.is_filled("any").await.unwrap());
81    }
82
83    #[tokio::test]
84    async fn cancel_order_succeeds() {
85        let exec = SignalOnlyExecutor::new();
86        assert!(exec.cancel_order("order-1").await.is_ok());
87    }
88
89    #[tokio::test]
90    async fn close_position_returns_unique_ids() {
91        let exec = SignalOnlyExecutor::new();
92        let id1 = exec.close_position("pos-1", "stop_loss").await.unwrap();
93        let id2 = exec.close_position("pos-2", "take_profit").await.unwrap();
94        assert_ne!(id1, id2);
95        assert!(id1.starts_with("sig-close-"));
96    }
97
98    #[tokio::test]
99    async fn default_impl() {
100        let exec = SignalOnlyExecutor::default();
101        let params = PlaybookOrderParams {
102            symbol: "ETH-PERP".into(),
103            side: "sell".into(),
104            size: 1.0,
105            price: None,
106            reduce_only: false,
107        };
108        let id = exec.place_order(&params).await.unwrap();
109        assert!(id.starts_with("sig-order-"));
110    }
111}