| | 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(()) |
| | } |
| | } |
| |
|