use super::models::{PnL, TriggerSignal};
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
pub struct SimulateParams {
pub signals : Vec<TriggerSignal>,
pub initial_capital : f64,
pub exchange_fee : Option<f64>,
pub min_qty : Option<f64>,
pub min_price : Option<f64>,
pub asset_scale : u32,
pub funds_scale : u32,
}
impl SimulateParams {
pub fn new(signals:Vec<TriggerSignal>) -> Self {
SimulateParams { signals,
initial_capital : 1000.0,
exchange_fee : None,
min_qty : None,
min_price : None,
asset_scale : 8,
funds_scale : 8
}
}
pub fn capital(mut self, capital: f64) -> Self { self.initial_capital = capital;self }
pub fn exchange_fee(mut self, exchange_fee: Option<f64>) -> Self { self.exchange_fee = exchange_fee;self }
pub fn min_qty(mut self, min_qty: Option<f64>) -> Self {self.min_qty = min_qty; self }
pub fn min_price(mut self, min_price: Option<f64>) -> Self {self.min_price = min_price; self }
pub fn asset_scale(mut self, asset_scale: u32) -> Self {self.asset_scale = asset_scale; self }
pub fn funds_scale(mut self, funds_scale: u32) -> Self {self.funds_scale = funds_scale; self }
pub fn get_asset_trade_scale(&self) -> Option<u32> {
let mut scale_result:Option<u32> = None;
if let Some(min_qty) = self.min_qty {
let decimal_val = Decimal::from_f64(min_qty).unwrap();
scale_result = Some(decimal_val.scale());
}
scale_result
}
pub fn get_funds_trade_scale(&self) -> Option<u32> {
let mut scale_result:Option<u32> = None;
if let Some(min_price) = self.min_price {
let decimal_val = Decimal::from_f64(min_price).unwrap();
scale_result = Some(decimal_val.scale());
}
scale_result
}
}
pub fn simulate(sim_params: SimulateParams) -> PnL {
let mut pnl = PnL {
net_profit : 0.0,
gross_profit : 0.0,
gross_loss : 0.0,
buy_and_hold_return : 0.0,
profit_factor : 0.0,
commission_paid : None,
total_closed_trades : 0,
num_winning_trades : 0,
num_losing_trades : 0,
percent_profitable : 0.0,
avg_winning_trade : 0.0,
avg_losing_trade : 0.0,
ratio_avg_win_loss : 0.0,
largest_winning_trade: 0.0,
largest_losing_trade : 0.0,
avg_ticks_in_winning_trades : 0.0,
avg_ticks_in_losing_trades : 0.0,
};
let asset_trade_scale = sim_params.get_asset_trade_scale();
let funds_trade_scale = sim_params.get_funds_trade_scale();
let exchange_fee : Option<Decimal> = sim_params.exchange_fee.map(|v| Decimal::from_f64(v).unwrap());
let mut funds = Decimal::from_f64(sim_params.initial_capital).unwrap();
pnl.buy_and_hold_return = buy_and_hold_return(
&funds,
&exchange_fee,
&Decimal::from_f64(sim_params.signals.get(0).unwrap().price_open).unwrap(),
&Decimal::from_f64(sim_params.signals.last().unwrap().price_close).unwrap(),
&sim_params.asset_scale,
&sim_params.funds_scale,
&funds_trade_scale,
&asset_trade_scale,
);
let mut position_open : bool = false;
let mut simulate_buy : bool = false;
let mut simulate_sell : bool = false;
let mut asset_init_cost = dec!(0.0);
let mut assets:Decimal = dec!(0.0);
let mut commission_paid = dec!(0.0);
let mut tik_at_purchase:u16 = 0;
let mut gross_profit = dec!(0.0);
let mut winning_trades:Vec<Decimal> = vec![];
let mut winning_ticks:Vec<u16> = vec![];
let mut loosing_ticks:Vec<u16> = vec![];
let mut gross_loss = dec!(0.0);
let mut losing_trades:Vec<Decimal> = vec![];
let zero_val = dec!(0.0);
let min_funds = dec!(10.0);
let sim_stop_at = sim_params.signals.len() -1;
for (indx,tick) in sim_params.signals.iter().enumerate() {
if simulate_buy {
let purchase = stage_purchase(
&funds,
&Decimal::from_f64(tick.price_open).unwrap(),
&exchange_fee,
&sim_params.asset_scale,
&sim_params.funds_scale,
&funds_trade_scale,
);
asset_init_cost = purchase.total_fee.map_or(purchase.cost_before_fee, |fee| purchase.cost_before_fee + fee);
funds -= asset_init_cost;
assets += purchase.asset_qty;
if let Some(fee) = purchase.total_fee { commission_paid += fee; }
position_open = true;
simulate_buy = false;
tik_at_purchase = indx as u16;
}
else if simulate_sell || (indx == sim_stop_at && position_open) {
let tik_price_open = Decimal::from_f64(tick.price_open).unwrap();
let sell = stage_sale(
&assets,
&tik_price_open,
&exchange_fee,
&sim_params.asset_scale,
&sim_params.funds_scale,
&asset_trade_scale,
);
funds += sell.sale_before_fee;
assets -= sell.assets_sold;
let mut trade_profit = sell.sale_before_fee - asset_init_cost;
if let Some(fee) = sell.fee_asset_total {
let commission_cost = fee * tik_price_open;
commission_paid += commission_cost;
assets -= fee;
trade_profit -= commission_cost;
}
pnl.total_closed_trades += 1;
#[allow(clippy::comparison_chain)]
if trade_profit > zero_val {
gross_profit += trade_profit;
pnl.num_winning_trades +=1;
winning_trades.push(trade_profit);
winning_ticks.push(indx as u16 - tik_at_purchase);
}
else if trade_profit < zero_val {
gross_loss += trade_profit;
pnl.num_losing_trades +=1;
losing_trades.push(trade_profit);
loosing_ticks.push(indx as u16 - tik_at_purchase);
}
if trade_profit != zero_val {
position_open = false;
simulate_sell = false;
}
if funds < min_funds { break; };
}
if tick.signal_in > tick.signal_out && !position_open {
simulate_buy = true;
} else if tick.signal_in < tick.signal_out && position_open {
simulate_sell = true;
}
}
pnl.net_profit = (gross_profit + gross_loss).to_f64().unwrap();
pnl.commission_paid = commission_paid.to_f64();
pnl.gross_profit = gross_profit.to_f64().unwrap();
pnl.gross_loss = gross_loss.to_f64().unwrap();
let percentage = Decimal::from_i32(pnl.num_winning_trades).unwrap() / Decimal::from_i32(pnl.total_closed_trades).unwrap() * dec!(100.0);
pnl.percent_profitable = percentage.round_dp(2).to_f64().unwrap();
pnl.avg_winning_trade = array_of_decimal_avg(&winning_trades);
pnl.avg_losing_trade = array_of_decimal_avg(&losing_trades);
if pnl.avg_losing_trade != 0.0 {
let avg_winning_trade = Decimal::from_f64(pnl.avg_winning_trade).unwrap();
let avg_losing_trade = Decimal::from_f64(pnl.avg_losing_trade).unwrap().abs();
pnl.ratio_avg_win_loss = (avg_winning_trade / avg_losing_trade).round_dp(3).to_f64().unwrap();
}
if let Some(&max) = winning_trades.iter().max_by(|a, b| a.partial_cmp(b).unwrap()) {
pnl.largest_winning_trade = max.round_dp(2).to_f64().unwrap();
}
if let Some(&min) = losing_trades.iter().min_by(|a, b| a.partial_cmp(b).unwrap()) {
pnl.largest_losing_trade = min.round_dp(2).to_f64().unwrap();
}
let sum_tik_wins:u16 = winning_ticks.iter().sum();
pnl.avg_ticks_in_winning_trades = sum_tik_wins as f64 / (winning_ticks.len() as f64);
let sum_tik_losses:u16 = loosing_ticks.iter().sum();
pnl.avg_ticks_in_losing_trades = sum_tik_losses as f64 / (loosing_ticks.len() as f64);
pnl.profit_factor = profit_factor(&winning_trades, &losing_trades)
.unwrap_or(0.0);
pnl
}
fn array_of_decimal_avg(arr:&Vec<Decimal>) -> f64 {
if arr.is_empty() { 0.0 }
else {
let sum_values = arr.iter().fold(Decimal::from_f64(0.0).unwrap(), |a, b| a + b);
(sum_values / Decimal::from_usize(arr.len()).unwrap()).round_dp(2).to_f64().unwrap()
}
}
#[derive(Debug)]
struct PurchaseInfo {
asset_qty : Decimal,
cost_before_fee : Decimal,
total_fee : Option<Decimal>
}
fn stage_purchase(
funds : &Decimal,
price : &Decimal,
exchange_fee : &Option<Decimal>,
asset_scale : &u32,
funds_scale : &u32,
funds_trade_scale : &Option<u32>,
) -> PurchaseInfo {
let funds_available = exchange_fee
.map_or(*funds, |fee| (funds - (funds * fee)).trunc_with_scale(*funds_scale));
let mut asset_qty = (funds_available / price).trunc_with_scale(*asset_scale);
let mut cost_before_fee = asset_qty * price;
if let Some(scale) = funds_trade_scale {
cost_before_fee = cost_before_fee.trunc_with_scale(*scale);
asset_qty = (cost_before_fee / price).trunc_with_scale(*asset_scale);
}
let total_fee = exchange_fee.map_or(None, |fee| Some(cost_before_fee * fee));
PurchaseInfo { asset_qty, cost_before_fee, total_fee, }
}
#[derive(Debug)]
struct SaleInfo {
assets_sold : Decimal,
sale_before_fee : Decimal,
fee_asset_total : Option<Decimal>,
}
fn stage_sale(
asset_qty : &Decimal,
price : &Decimal,
exchange_fee: &Option<Decimal>,
asset_scale : &u32,
funds_scale : &u32,
asset_trade_scale : &Option<u32>,
) -> SaleInfo {
let mut assets_sold = exchange_fee
.map_or(*asset_qty, |fee| (asset_qty - (asset_qty * fee)).trunc_with_scale(*asset_scale));
if let Some(trade_scale) = asset_trade_scale {
assets_sold = assets_sold.trunc_with_scale(*trade_scale);
}
let sale_before_fee = (assets_sold * price).trunc_with_scale(*funds_scale);
let fee_asset_total = exchange_fee.map_or(None, |fee| Some(assets_sold * fee));
SaleInfo {assets_sold, sale_before_fee, fee_asset_total}
}
#[allow(clippy::too_many_arguments)]
fn buy_and_hold_return(
funds : &Decimal,
exchange_fee: &Option<Decimal>,
price_entry : &Decimal,
price_exit : &Decimal,
asset_scale : &u32,
funds_scale : &u32,
funds_trade_scale : &Option<u32>,
asset_trade_scale : &Option<u32>,
) -> f64 {
let purchase = stage_purchase(
funds,
price_entry,
exchange_fee,
asset_scale,
funds_scale,
funds_trade_scale,
);
let mut position = funds - purchase.cost_before_fee;
if let Some(fee) = purchase.total_fee { position -= fee; }
let sale = stage_sale(
&purchase.asset_qty,
price_exit,
exchange_fee,
asset_scale,
funds_scale,
asset_trade_scale,
);
position += sale.sale_before_fee;
if let Some(fee) = sale.fee_asset_total {
position -= (fee * price_exit).trunc_with_scale(*funds_scale);
}
position += ((purchase.asset_qty - sale.assets_sold) * price_exit).trunc_with_scale(*funds_scale);
(position - funds).round_dp(2).to_f64().unwrap()
}
fn profit_factor(profitable_trades: &[Decimal], losing_trades: &[Decimal]) -> Option<f64> {
let total_profit = profitable_trades.iter().fold(Decimal::from_f64(0.0).unwrap(), |a, b| a + b);
let total_loss = losing_trades.iter().fold(Decimal::from_f64(0.0).unwrap(), |a, b| a + b);
if total_loss.abs() > Decimal::try_from(f64::EPSILON).unwrap() { Some((total_profit / total_loss.abs()).round_dp(3).to_f64().unwrap())
} else {
None
}
}