File size: 5,290 Bytes
59da845 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | 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<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,
// Internal state cache for symbols
pub current_prices: HashMap<String, (f64, f64)>, // 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<f64>, tp: Option<f64>, time: String) -> Result<u64, String> {
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(())
}
}
|