use crate::data::parser::{Ohlcv, Tick}; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq)] pub enum OrderType { Buy, Sell, // Add pending later } #[derive(Debug, Clone)] pub struct Position { pub ticket: u64, pub symbol: String, pub order_type: OrderType, pub volume: f64, pub open_price: f64, pub sl: Option, pub tp: Option, pub open_time: String, } #[derive(Debug, Clone)] pub struct ClosedTrade { pub ticket: u64, pub symbol: String, pub order_type: OrderType, pub volume: f64, pub open_price: f64, pub close_price: f64, pub profit: f64, pub open_time: String, pub close_time: String, } #[derive(Debug, Clone)] pub struct BacktestConfig { pub initial_deposit: f64, pub leverage: f64, pub spread_modifier: u32, } pub struct Backtester { pub config: BacktestConfig, pub balance: f64, pub equity: f64, pub free_margin: f64, pub margin: f64, pub open_positions: Vec, pub history: Vec, ticket_counter: u64, // Internal state cache for symbols pub current_prices: HashMap, // Bid, Ask } impl Backtester { pub fn new(config: BacktestConfig) -> Self { Self { balance: config.initial_deposit, equity: config.initial_deposit, free_margin: config.initial_deposit, margin: 0.0, config, open_positions: Vec::new(), history: Vec::new(), ticket_counter: 1, current_prices: HashMap::new(), } } pub fn update_tick(&mut self, symbol: &str, bid: f64, ask: f64) { self.current_prices.insert(symbol.to_string(), (bid, ask)); self.recalculate_equity(); } pub fn update_ohlcv(&mut self, symbol: &str, bar: &Ohlcv) { // Simple approximation for bar execution (using Close price for equity calculation) let simulated_bid = bar.close; let simulated_ask = bar.close + (bar.spread as f64 * 0.00001); // Assumes generic 5-digit broker formatting self.update_tick(symbol, simulated_bid, simulated_ask); } fn recalculate_equity(&mut self) { let mut floating_profit = 0.0; let contract_size = 100000.0; // Assume standard Forex lots for MVP for pos in &self.open_positions { if let Some(&(bid, ask)) = self.current_prices.get(&pos.symbol) { if pos.order_type == OrderType::Buy { floating_profit += (bid - pos.open_price) * contract_size * pos.volume; } else if pos.order_type == OrderType::Sell { floating_profit += (pos.open_price - ask) * contract_size * pos.volume; } } } self.equity = self.balance + floating_profit; self.free_margin = self.equity - self.margin; } pub fn market_order(&mut self, symbol: &str, order_type: OrderType, volume: f64, sl: Option, tp: Option, time: String) -> Result { let &(bid, ask) = self.current_prices.get(symbol).ok_or("No price data for symbol")?; // Ensure Margin let required_margin = (volume * 100000.0) / self.config.leverage; if self.free_margin < required_margin { return Err("Not enough free margin".to_string()); } let price = match order_type { OrderType::Buy => ask, OrderType::Sell => bid, }; let ticket = self.ticket_counter; self.ticket_counter += 1; let pos = Position { ticket, symbol: symbol.to_string(), order_type, volume, open_price: price, sl, tp, open_time: time, }; self.margin += required_margin; self.open_positions.push(pos); self.recalculate_equity(); Ok(ticket) } pub fn close_position(&mut self, ticket: u64, time: String) -> Result<(), String> { let pos_index = self.open_positions.iter().position(|p| p.ticket == ticket) .ok_or("Position ticket not found")?; let pos = self.open_positions.remove(pos_index); let &(bid, ask) = self.current_prices.get(&pos.symbol).ok_or("No price data for symbol")?; let contract_size = 100000.0; let (close_price, profit) = match pos.order_type { OrderType::Buy => (bid, (bid - pos.open_price) * contract_size * pos.volume), OrderType::Sell => (ask, (pos.open_price - ask) * contract_size * pos.volume), }; self.balance += profit; // Free margin back let required_margin = (pos.volume * 100000.0) / self.config.leverage; self.margin -= required_margin; self.history.push(ClosedTrade { ticket: pos.ticket, symbol: pos.symbol.clone(), order_type: pos.order_type, volume: pos.volume, open_price: pos.open_price, close_price, profit, open_time: pos.open_time, close_time: time, }); self.recalculate_equity(); Ok(()) } }