1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6use crate::position_manager::{Position, PositionManager};
7
8#[derive(Debug, thiserror::Error)]
10pub enum ExecutorError {
11 #[error("Position error: {0}")]
12 Position(#[from] crate::position_manager::PositionError),
13 #[error("Execution error: {0}")]
14 Execution(String),
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct OrderParams {
20 pub market: String,
22 pub side: String,
24 pub size: f64,
26 pub price: Option<f64>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct OrderResult {
33 pub order_id: String,
34 pub filled_price: f64,
35 pub filled_size: f64,
36 pub requested_size: f64,
38 pub status: String,
40}
41
42#[async_trait]
47pub trait OrderSubmitter: Send + Sync {
48 async fn place_order(&self, params: &OrderParams) -> Result<OrderResult, ExecutorError>;
49 async fn cancel_order(&self, order_id: &str) -> Result<(), ExecutorError>;
50}
51
52pub struct PaperExecutor {
61 position_manager: Arc<PositionManager>,
62}
63
64impl PaperExecutor {
65 pub fn new(position_manager: Arc<PositionManager>) -> Self {
66 Self { position_manager }
67 }
68}
69
70#[async_trait]
71impl OrderSubmitter for PaperExecutor {
72 async fn place_order(&self, params: &OrderParams) -> Result<OrderResult, ExecutorError> {
73 let fill_price = params.price.unwrap_or(0.0);
74 let order_id = uuid::Uuid::new_v4().to_string();
75
76 let side = match params.side.as_str() {
77 "buy" => "long",
78 "sell" => "short",
79 other => other,
80 };
81
82 let pos = Position {
83 id: order_id.clone(),
84 market: params.market.clone(),
85 side: side.to_string(),
86 size: params.size,
87 entry_price: fill_price,
88 current_price: Some(fill_price),
89 status: "open".to_string(),
90 pnl: Some(0.0),
91 mode: "paper".to_string(),
92 strategy: None,
93 opened_at: chrono::Utc::now().to_rfc3339(),
94 closed_at: None,
95 close_reason: None,
96 };
97
98 self.position_manager.open_position(&pos).await?;
99
100 Ok(OrderResult {
101 order_id,
102 filled_price: fill_price,
103 filled_size: params.size,
104 requested_size: params.size,
105 status: "filled".to_string(),
106 })
107 }
108
109 async fn cancel_order(&self, _order_id: &str) -> Result<(), ExecutorError> {
110 Ok(())
112 }
113}
114
115pub struct DryRunExecutor;
123
124#[async_trait]
125impl OrderSubmitter for DryRunExecutor {
126 async fn place_order(&self, params: &OrderParams) -> Result<OrderResult, ExecutorError> {
127 let order_id = uuid::Uuid::new_v4().to_string();
128 let fill_price = params.price.unwrap_or(0.0);
129
130 tracing::info!(
131 order_id = %order_id,
132 market = %params.market,
133 side = %params.side,
134 size = %params.size,
135 price = %fill_price,
136 "[dry-run] simulated order"
137 );
138
139 Ok(OrderResult {
140 order_id,
141 filled_price: fill_price,
142 filled_size: params.size,
143 requested_size: params.size,
144 status: "simulated".to_string(),
145 })
146 }
147
148 async fn cancel_order(&self, order_id: &str) -> Result<(), ExecutorError> {
149 tracing::info!(order_id = %order_id, "[dry-run] simulated cancel");
150 Ok(())
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 fn make_paper_executor() -> (Arc<PositionManager>, PaperExecutor) {
159 let pm = Arc::new(PositionManager::in_memory().unwrap());
160 let exec = PaperExecutor::new(Arc::clone(&pm));
161 (pm, exec)
162 }
163
164 #[tokio::test]
165 async fn test_paper_executor_place_buy() {
166 let (pm, exec) = make_paper_executor();
167
168 let result = exec
169 .place_order(&OrderParams {
170 market: "BTC-PERP".to_string(),
171 side: "buy".to_string(),
172 size: 0.5,
173 price: Some(60000.0),
174 })
175 .await
176 .unwrap();
177
178 assert_eq!(result.filled_price, 60000.0);
179 assert_eq!(result.filled_size, 0.5);
180 assert_eq!(result.requested_size, 0.5);
181 assert_eq!(result.status, "filled");
182
183 let pos = pm.get_position(&result.order_id).await.unwrap().unwrap();
185 assert_eq!(pos.market, "BTC-PERP");
186 assert_eq!(pos.side, "long");
187 assert_eq!(pos.size, 0.5);
188 assert_eq!(pos.entry_price, 60000.0);
189 assert_eq!(pos.mode, "paper");
190 assert_eq!(pos.status, "open");
191 }
192
193 #[tokio::test]
194 async fn test_paper_executor_place_sell() {
195 let (pm, exec) = make_paper_executor();
196
197 let result = exec
198 .place_order(&OrderParams {
199 market: "ETH-PERP".to_string(),
200 side: "sell".to_string(),
201 size: 10.0,
202 price: Some(3000.0),
203 })
204 .await
205 .unwrap();
206
207 let pos = pm.get_position(&result.order_id).await.unwrap().unwrap();
208 assert_eq!(pos.side, "short");
209 assert_eq!(pos.entry_price, 3000.0);
210 }
211
212 #[tokio::test]
213 async fn test_paper_executor_market_order_zero_price() {
214 let (_pm, exec) = make_paper_executor();
215
216 let result = exec
217 .place_order(&OrderParams {
218 market: "SOL-PERP".to_string(),
219 side: "buy".to_string(),
220 size: 100.0,
221 price: None,
222 })
223 .await
224 .unwrap();
225
226 assert_eq!(result.filled_price, 0.0);
227 }
228
229 #[tokio::test]
230 async fn test_paper_executor_cancel_is_noop() {
231 let (_pm, exec) = make_paper_executor();
232 exec.cancel_order("any-id").await.unwrap();
233 }
234
235 #[tokio::test]
236 async fn test_paper_executor_positions_accumulate() {
237 let (pm, exec) = make_paper_executor();
238
239 for _ in 0..3 {
240 exec.place_order(&OrderParams {
241 market: "BTC-PERP".to_string(),
242 side: "buy".to_string(),
243 size: 1.0,
244 price: Some(60000.0),
245 })
246 .await
247 .unwrap();
248 }
249
250 assert_eq!(pm.list_open().await.unwrap().len(), 3);
251 }
252
253 #[tokio::test]
254 async fn test_dry_run_executor_place_order() {
255 let exec = DryRunExecutor;
256
257 let result = exec
258 .place_order(&OrderParams {
259 market: "BTC-PERP".to_string(),
260 side: "buy".to_string(),
261 size: 1.0,
262 price: Some(60000.0),
263 })
264 .await
265 .unwrap();
266
267 assert_eq!(result.filled_price, 60000.0);
268 assert_eq!(result.filled_size, 1.0);
269 assert_eq!(result.requested_size, 1.0);
270 assert_eq!(result.status, "simulated");
271 assert!(!result.order_id.is_empty());
272 }
273
274 #[tokio::test]
275 async fn test_dry_run_executor_cancel() {
276 let exec = DryRunExecutor;
277 exec.cancel_order("anything").await.unwrap();
278 }
279
280 #[tokio::test]
281 async fn test_dry_run_executor_market_order() {
282 let exec = DryRunExecutor;
283
284 let result = exec
285 .place_order(&OrderParams {
286 market: "ETH-PERP".to_string(),
287 side: "sell".to_string(),
288 size: 5.0,
289 price: None,
290 })
291 .await
292 .unwrap();
293
294 assert_eq!(result.filled_price, 0.0);
295 assert_eq!(result.status, "simulated");
296 }
297
298 #[test]
299 fn order_result_requested_size_tracks_original() {
300 let result = OrderResult {
301 order_id: "test".into(),
302 filled_price: 65000.0,
303 filled_size: 0.005,
304 requested_size: 0.01,
305 status: "partial_fill".into(),
306 };
307 assert_eq!(result.requested_size, 0.01);
308 assert_eq!(result.filled_size, 0.005);
309 assert_eq!(result.status, "partial_fill");
310 let pct = (result.filled_size / result.requested_size) * 100.0;
312 assert!((pct - 50.0).abs() < 0.01);
313 }
314
315 #[tokio::test]
316 async fn paper_executor_always_full_fill() {
317 let (_pm, exec) = make_paper_executor();
318 let result = exec
319 .place_order(&OrderParams {
320 market: "BTC-PERP".to_string(),
321 side: "buy".to_string(),
322 size: 0.01,
323 price: Some(65000.0),
324 })
325 .await
326 .unwrap();
327 assert_eq!(result.filled_size, result.requested_size);
329 assert_eq!(result.status, "filled");
330 }
331}