hyper_playbook/
signal_executor.rs1use async_trait::async_trait;
2
3use crate::executor::{ExecutionError, OrderExecutor, PlaybookOrderParams};
4
5pub 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(¶ms).await.unwrap();
72 let id2 = exec.place_order(¶ms).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(¶ms).await.unwrap();
109 assert!(id.starts_with("sig-order-"));
110 }
111}