| use crate::data::parser::{Ohlcv, Tick}; |
| use std::collections::HashMap; |
|
|
| #[derive(Debug, Clone, PartialEq)] |
| pub enum OrderType { |
| Buy, |
| Sell, |
| |
| } |
|
|
| #[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<f64>, |
| pub tp: Option<f64>, |
| 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<Position>, |
| pub history: Vec<ClosedTrade>, |
| ticket_counter: u64, |
| |
| |
| pub current_prices: HashMap<String, (f64, f64)>, |
| } |
|
|
| 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) { |
| |
| let simulated_bid = bar.close; |
| let simulated_ask = bar.close + (bar.spread as f64 * 0.00001); |
| self.update_tick(symbol, simulated_bid, simulated_ask); |
| } |
|
|
| fn recalculate_equity(&mut self) { |
| let mut floating_profit = 0.0; |
| let contract_size = 100000.0; |
| |
| 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<f64>, tp: Option<f64>, time: String) -> Result<u64, String> { |
| let &(bid, ask) = self.current_prices.get(symbol).ok_or("No price data for symbol")?; |
| |
| |
| 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; |
| |
| |
| 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(()) |
| } |
| } |
|
|