use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::position_manager::{Position, PositionManager};
#[derive(Debug, thiserror::Error)]
pub enum ExecutorError {
#[error("Position error: {0}")]
Position(#[from] crate::position_manager::PositionError),
#[error("Execution error: {0}")]
Execution(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderParams {
pub market: String,
pub side: String,
pub size: f64,
pub price: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderResult {
pub order_id: String,
pub filled_price: f64,
pub filled_size: f64,
pub requested_size: f64,
pub status: String,
}
#[async_trait]
pub trait OrderSubmitter: Send + Sync {
async fn place_order(&self, params: &OrderParams) -> Result<OrderResult, ExecutorError>;
async fn cancel_order(&self, order_id: &str) -> Result<(), ExecutorError>;
}
pub struct PaperExecutor {
position_manager: Arc<PositionManager>,
}
impl PaperExecutor {
pub fn new(position_manager: Arc<PositionManager>) -> Self {
Self { position_manager }
}
}
#[async_trait]
impl OrderSubmitter for PaperExecutor {
async fn place_order(&self, params: &OrderParams) -> Result<OrderResult, ExecutorError> {
let fill_price = params.price.unwrap_or(0.0);
let order_id = uuid::Uuid::new_v4().to_string();
let side = match params.side.as_str() {
"buy" => "long",
"sell" => "short",
other => other,
};
let pos = Position {
id: order_id.clone(),
market: params.market.clone(),
side: side.to_string(),
size: params.size,
entry_price: fill_price,
current_price: Some(fill_price),
status: "open".to_string(),
pnl: Some(0.0),
mode: "paper".to_string(),
strategy: None,
opened_at: chrono::Utc::now().to_rfc3339(),
closed_at: None,
close_reason: None,
};
self.position_manager.open_position(&pos).await?;
Ok(OrderResult {
order_id,
filled_price: fill_price,
filled_size: params.size,
requested_size: params.size,
status: "filled".to_string(),
})
}
async fn cancel_order(&self, _order_id: &str) -> Result<(), ExecutorError> {
Ok(())
}
}
pub struct DryRunExecutor;
#[async_trait]
impl OrderSubmitter for DryRunExecutor {
async fn place_order(&self, params: &OrderParams) -> Result<OrderResult, ExecutorError> {
let order_id = uuid::Uuid::new_v4().to_string();
let fill_price = params.price.unwrap_or(0.0);
tracing::info!(
order_id = %order_id,
market = %params.market,
side = %params.side,
size = %params.size,
price = %fill_price,
"[dry-run] simulated order"
);
Ok(OrderResult {
order_id,
filled_price: fill_price,
filled_size: params.size,
requested_size: params.size,
status: "simulated".to_string(),
})
}
async fn cancel_order(&self, order_id: &str) -> Result<(), ExecutorError> {
tracing::info!(order_id = %order_id, "[dry-run] simulated cancel");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_paper_executor() -> (Arc<PositionManager>, PaperExecutor) {
let pm = Arc::new(PositionManager::in_memory().unwrap());
let exec = PaperExecutor::new(Arc::clone(&pm));
(pm, exec)
}
#[tokio::test]
async fn test_paper_executor_place_buy() {
let (pm, exec) = make_paper_executor();
let result = exec
.place_order(&OrderParams {
market: "BTC-PERP".to_string(),
side: "buy".to_string(),
size: 0.5,
price: Some(60000.0),
})
.await
.unwrap();
assert_eq!(result.filled_price, 60000.0);
assert_eq!(result.filled_size, 0.5);
assert_eq!(result.requested_size, 0.5);
assert_eq!(result.status, "filled");
let pos = pm.get_position(&result.order_id).await.unwrap().unwrap();
assert_eq!(pos.market, "BTC-PERP");
assert_eq!(pos.side, "long");
assert_eq!(pos.size, 0.5);
assert_eq!(pos.entry_price, 60000.0);
assert_eq!(pos.mode, "paper");
assert_eq!(pos.status, "open");
}
#[tokio::test]
async fn test_paper_executor_place_sell() {
let (pm, exec) = make_paper_executor();
let result = exec
.place_order(&OrderParams {
market: "ETH-PERP".to_string(),
side: "sell".to_string(),
size: 10.0,
price: Some(3000.0),
})
.await
.unwrap();
let pos = pm.get_position(&result.order_id).await.unwrap().unwrap();
assert_eq!(pos.side, "short");
assert_eq!(pos.entry_price, 3000.0);
}
#[tokio::test]
async fn test_paper_executor_market_order_zero_price() {
let (_pm, exec) = make_paper_executor();
let result = exec
.place_order(&OrderParams {
market: "SOL-PERP".to_string(),
side: "buy".to_string(),
size: 100.0,
price: None,
})
.await
.unwrap();
assert_eq!(result.filled_price, 0.0);
}
#[tokio::test]
async fn test_paper_executor_cancel_is_noop() {
let (_pm, exec) = make_paper_executor();
exec.cancel_order("any-id").await.unwrap();
}
#[tokio::test]
async fn test_paper_executor_positions_accumulate() {
let (pm, exec) = make_paper_executor();
for _ in 0..3 {
exec.place_order(&OrderParams {
market: "BTC-PERP".to_string(),
side: "buy".to_string(),
size: 1.0,
price: Some(60000.0),
})
.await
.unwrap();
}
assert_eq!(pm.list_open().await.unwrap().len(), 3);
}
#[tokio::test]
async fn test_dry_run_executor_place_order() {
let exec = DryRunExecutor;
let result = exec
.place_order(&OrderParams {
market: "BTC-PERP".to_string(),
side: "buy".to_string(),
size: 1.0,
price: Some(60000.0),
})
.await
.unwrap();
assert_eq!(result.filled_price, 60000.0);
assert_eq!(result.filled_size, 1.0);
assert_eq!(result.requested_size, 1.0);
assert_eq!(result.status, "simulated");
assert!(!result.order_id.is_empty());
}
#[tokio::test]
async fn test_dry_run_executor_cancel() {
let exec = DryRunExecutor;
exec.cancel_order("anything").await.unwrap();
}
#[tokio::test]
async fn test_dry_run_executor_market_order() {
let exec = DryRunExecutor;
let result = exec
.place_order(&OrderParams {
market: "ETH-PERP".to_string(),
side: "sell".to_string(),
size: 5.0,
price: None,
})
.await
.unwrap();
assert_eq!(result.filled_price, 0.0);
assert_eq!(result.status, "simulated");
}
#[test]
fn order_result_requested_size_tracks_original() {
let result = OrderResult {
order_id: "test".into(),
filled_price: 65000.0,
filled_size: 0.005,
requested_size: 0.01,
status: "partial_fill".into(),
};
assert_eq!(result.requested_size, 0.01);
assert_eq!(result.filled_size, 0.005);
assert_eq!(result.status, "partial_fill");
let pct = (result.filled_size / result.requested_size) * 100.0;
assert!((pct - 50.0).abs() < 0.01);
}
#[tokio::test]
async fn paper_executor_always_full_fill() {
let (_pm, exec) = make_paper_executor();
let result = exec
.place_order(&OrderParams {
market: "BTC-PERP".to_string(),
side: "buy".to_string(),
size: 0.01,
price: Some(65000.0),
})
.await
.unwrap();
assert_eq!(result.filled_size, result.requested_size);
assert_eq!(result.status, "filled");
}
}