use crate::domain::{Order, Position};
use crate::exchange::ExchangeResult;
use crate::exchange::OrderBook;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StrategyAction {
Buy {
symbol: String,
quantity_lots: u32,
price: Option<Decimal>,
},
Sell {
symbol: String,
quantity_lots: u32,
price: Option<Decimal>,
},
Cancel {
order_id: String,
},
ModifyPrice {
order_id: String,
new_price: Decimal,
},
Hold,
}
impl StrategyAction {
#[must_use]
pub fn market_buy(symbol: impl Into<String>, quantity_lots: u32) -> Self {
Self::Buy {
symbol: symbol.into(),
quantity_lots,
price: None,
}
}
#[must_use]
pub fn limit_buy(symbol: impl Into<String>, quantity_lots: u32, price: Decimal) -> Self {
Self::Buy {
symbol: symbol.into(),
quantity_lots,
price: Some(price),
}
}
#[must_use]
pub fn market_sell(symbol: impl Into<String>, quantity_lots: u32) -> Self {
Self::Sell {
symbol: symbol.into(),
quantity_lots,
price: None,
}
}
#[must_use]
pub fn limit_sell(symbol: impl Into<String>, quantity_lots: u32, price: Decimal) -> Self {
Self::Sell {
symbol: symbol.into(),
quantity_lots,
price: Some(price),
}
}
#[must_use]
pub fn cancel(order_id: impl Into<String>) -> Self {
Self::Cancel {
order_id: order_id.into(),
}
}
#[must_use]
pub fn hold() -> Self {
Self::Hold
}
#[must_use]
pub fn is_hold(&self) -> bool {
matches!(self, Self::Hold)
}
#[must_use]
pub fn symbol(&self) -> Option<&str> {
match self {
Self::Buy { symbol, .. } | Self::Sell { symbol, .. } => Some(symbol),
_ => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StrategyDecision {
actions: Vec<StrategyAction>,
reason: Option<String>,
}
impl StrategyDecision {
#[must_use]
pub fn new() -> Self {
Self {
actions: Vec::new(),
reason: None,
}
}
#[must_use]
pub fn single(action: StrategyAction) -> Self {
Self {
actions: vec![action],
reason: None,
}
}
#[must_use]
pub fn hold() -> Self {
Self::single(StrategyAction::Hold)
}
#[must_use]
pub fn hold_with_reason(reason: impl Into<String>) -> Self {
Self {
actions: vec![StrategyAction::Hold],
reason: Some(reason.into()),
}
}
pub fn add_action(&mut self, action: StrategyAction) {
self.actions.push(action);
}
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn actions(&self) -> &[StrategyAction] {
&self.actions
}
#[must_use]
pub fn reason(&self) -> Option<&str> {
self.reason.as_deref()
}
#[must_use]
pub fn is_hold(&self) -> bool {
self.actions.is_empty() || self.actions.iter().all(StrategyAction::is_hold)
}
#[must_use]
pub fn action_count(&self) -> usize {
self.actions.len()
}
}
#[derive(Debug, Clone)]
pub struct MarketState {
pub symbol: String,
pub order_book: Option<OrderBook>,
pub position: Option<Position>,
pub active_orders: Vec<Order>,
pub available_cash: Decimal,
pub timestamp: DateTime<Utc>,
pub last_price: Option<Decimal>,
pub currency: String,
}
impl MarketState {
#[must_use]
pub fn new(symbol: impl Into<String>, currency: impl Into<String>) -> Self {
Self {
symbol: symbol.into(),
order_book: None,
position: None,
active_orders: Vec::new(),
available_cash: Decimal::ZERO,
timestamp: Utc::now(),
last_price: None,
currency: currency.into(),
}
}
pub fn with_order_book(mut self, order_book: OrderBook) -> Self {
self.order_book = Some(order_book);
self
}
pub fn with_position(mut self, position: Position) -> Self {
self.position = Some(position);
self
}
pub fn with_cash(mut self, cash: Decimal) -> Self {
self.available_cash = cash;
self
}
pub fn with_last_price(mut self, price: Decimal) -> Self {
self.last_price = Some(price);
self
}
#[must_use]
pub fn best_bid(&self) -> Option<Decimal> {
self.order_book.as_ref().and_then(|ob| ob.best_bid_price())
}
#[must_use]
pub fn best_ask(&self) -> Option<Decimal> {
self.order_book.as_ref().and_then(|ob| ob.best_ask_price())
}
#[must_use]
pub fn position_lots(&self) -> u32 {
self.position.as_ref().map_or(0, |p| p.lots_held())
}
}
#[async_trait]
pub trait Strategy: Send + Sync {
fn name(&self) -> &str;
fn id(&self) -> &str {
self.name()
}
async fn decide(&self, state: &MarketState) -> StrategyDecision;
async fn on_order_filled(&mut self, order: &Order);
async fn on_order_cancelled(&mut self, order: &Order);
async fn reset(&mut self);
fn symbols(&self) -> Vec<String>;
fn is_enabled(&self) -> bool {
true
}
}
#[async_trait]
pub trait StrategyExecutor: Send + Sync {
async fn execute(
&self,
account_id: &str,
decision: &StrategyDecision,
) -> ExchangeResult<Vec<Order>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strategy_action_creation() {
let buy = StrategyAction::market_buy("AAPL", 10);
assert!(matches!(buy, StrategyAction::Buy { price: None, .. }));
let sell = StrategyAction::market_sell("AAPL", 5);
assert!(matches!(sell, StrategyAction::Sell { price: None, .. }));
}
#[test]
fn test_strategy_decision() {
let mut decision = StrategyDecision::new();
decision.add_action(StrategyAction::market_buy("AAPL", 10));
decision.add_action(StrategyAction::market_sell("GOOGL", 5));
assert_eq!(decision.actions().len(), 2);
assert!(!decision.is_hold());
}
#[test]
fn test_hold_decision() {
let decision = StrategyDecision::hold_with_reason("Market closed");
assert!(decision.is_hold());
assert_eq!(decision.reason(), Some("Market closed"));
}
#[test]
fn test_market_state() {
let state = MarketState::new("AAPL", "USD")
.with_cash(Decimal::from(10000))
.with_last_price(Decimal::from(150));
assert_eq!(state.symbol, "AAPL");
assert_eq!(state.available_cash, Decimal::from(10000));
assert_eq!(state.last_price, Some(Decimal::from(150)));
}
}