chapaty 0.1.4

A software to backtest trading strategies.
Documentation
use crate::{
    bot::time_interval::timestamp_in_milli_to_string,
    calculator::pnl_report_data_row_calculator::PnLReportDataRow,
    converter::market_decimal_places::MyDecimalPlaces,
    data_frame_operations::io_operations::save_df_as_csv,
    enums::markets::MarketKind,
    enums::{column_names, trade_and_pre_trade::TradeDirectionKind},
    lazy_frame_operations::trait_extensions::{MyLazyFrameOperations, MyLazyFrameVecOperations},
};
use chrono::NaiveDate;
use polars::df;
use polars::prelude::NamedFrom;
use polars::prelude::{DataFrame, IntoLazy};
use std::{collections::HashMap, convert::identity};

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PnLReports {
    pub market: MarketKind,
    pub years: Vec<u32>,
    pub strategy: String,
    pub reports: HashMap<u32, DataFrame>,
}

impl PnLReports {
    pub fn save_as_csv(&self, file_name: &str) {
        self.reports.iter().for_each(|(year, data)| {
            save_df_as_csv(
                &mut data.clone(),
                &format!("{file_name}_{}_{year}_pnl", self.market),
            )
        })
    }

    pub fn agg_year(&self) -> DataFrame {
        let ldfs = self.years.iter().fold(Vec::new(), |mut acc, year| {
            acc.push(self.reports.get(year).unwrap().clone().lazy());
            acc
        });
        ldfs.concatenate_to_lazy_frame()
            .sort_by_date()
            .collect()
            .unwrap()
    }
}

impl PnLReportDataRow {
    fn report_with_trade(self) -> DataFrame {
        let tick_factor = self.get_tick_factor();
        let tick_to_dollar = self.get_tick_to_dollar_conversion_factor();
        let trade_pnl = self.trade_pnl.clone().unwrap();

        let cw = self.time_frame_snapshot.get_calendar_week_as_int();
        let date = self.get_date();
        let strategy = self.strategy_name.to_string().to_uppercase();
        let market = self.market.to_string();
        let trade_direction = self.trade.trade_kind.to_string();
        let entry_price = self.trade.entry_price;
        let take_profit = self.trade.take_profit.map_or_else(|| 0.0, identity);
        let stop_loss = self.trade.stop_loss.map_or_else(|| 0.0, identity);
        let expected_win_tick = self.expected_win_in_tick(tick_factor);
        let expected_loss_tick = self.expected_loss_in_tick(tick_factor);
        let expected_win_dollar = expected_win_tick * tick_to_dollar;
        let expected_loss_dollar = expected_loss_tick * tick_to_dollar;
        let crv = compute_crv(expected_win_tick, expected_loss_tick);
        let entry_ts = self.get_entry_ts();
        let take_profit_ts = self.get_take_profit_ts();
        let stop_loss_ts = self.get_stop_loss_ts();
        let exit_price = match self.trade.trade_kind {
            TradeDirectionKind::None => 0.0,
            _ => trade_pnl.exit_price(),
        };
        let status = match self.trade.trade_kind {
            TradeDirectionKind::None => "No Trade".to_string(),
            _ => trade_pnl.trade_outcome(),
        };
        let pl_tick = match self.trade.trade_kind {
            TradeDirectionKind::None => 0.0,
            _ => trade_pnl.profit() / tick_factor,
        };
        let pl_dollar = pl_tick * tick_to_dollar;

        let n = self.get_decimal_places();

        df!(
            &column_names::PnLReportColumnKind::CalendarWeek.to_string() =>vec![cw],
            &column_names::PnLReportColumnKind::Date.to_string() =>vec![date],
            &column_names::PnLReportColumnKind::Strategy.to_string() =>vec![strategy],
            &column_names::PnLReportColumnKind::Market.to_string() =>vec![market],
            &column_names::PnLReportColumnKind::TradeDirection.to_string() =>vec![trade_direction],
            &column_names::PnLReportColumnKind::Entry.to_string() =>vec![entry_price.round_to_n_decimal_places(n)],
            &column_names::PnLReportColumnKind::TakeProfit.to_string() =>vec![take_profit.round_to_n_decimal_places(n)],
            &column_names::PnLReportColumnKind::StopLoss.to_string() =>vec![stop_loss.round_to_n_decimal_places(n)],
            &column_names::PnLReportColumnKind::ExpectedWinTick.to_string() =>vec![expected_win_tick],
            &column_names::PnLReportColumnKind::ExpectedLossTick.to_string() =>vec![expected_loss_tick],
            &column_names::PnLReportColumnKind::ExpectedWinDollar.to_string() =>vec![expected_win_dollar.round_to_dollar_cents()],
            &column_names::PnLReportColumnKind::ExpectedLossDollar.to_string() =>vec![expected_loss_dollar.round_to_dollar_cents()],
            &column_names::PnLReportColumnKind::Crv.to_string() =>vec![crv.round_to_n_decimal_places(3)],
            &column_names::PnLReportColumnKind::EntryTimestamp.to_string() =>vec![entry_ts],
            &column_names::PnLReportColumnKind::TakeProfitTimestamp.to_string() =>vec![take_profit_ts],
            &column_names::PnLReportColumnKind::StopLossTimestamp.to_string() =>vec![stop_loss_ts],
            &column_names::PnLReportColumnKind::ExitPrice.to_string() =>vec![exit_price.round_to_n_decimal_places(n)],
            &column_names::PnLReportColumnKind::Status.to_string() =>vec![status],
            &column_names::PnLReportColumnKind::PlTick.to_string() =>vec![pl_tick.round_to_n_decimal_places(n)],
            &column_names::PnLReportColumnKind::PlDollar.to_string() =>vec![pl_dollar.round_to_dollar_cents()],
        ).unwrap()
    }

    fn report_without_trade(self) -> DataFrame {
        let tick_factor = self.get_tick_factor();
        let tick_to_dollar = self.get_tick_to_dollar_conversion_factor();

        let cw = self.time_frame_snapshot.get_calendar_week_as_int();
        let date = self.get_date();
        let strategy = self.strategy_name.to_string().to_uppercase();
        let market = self.market.to_string();
        let trade_direction = self.trade.trade_kind.to_string();
        let entry_price = self.trade.entry_price;
        let take_profit = self.trade.take_profit.map_or_else(|| 0.0, identity);
        let stop_loss = self.trade.stop_loss.map_or_else(|| 0.0, identity);
        let expected_win_tick = self.expected_win_in_tick(tick_factor);
        let expected_loss_tick = self.expected_loss_in_tick(tick_factor);
        let expected_win_dollar = expected_win_tick * tick_to_dollar;
        let expected_loss_dollar = expected_loss_tick * tick_to_dollar;
        let crv = (expected_win_tick / expected_loss_tick).abs();

        let n = self.get_decimal_places();

        df!(
            &column_names::PnLReportColumnKind::CalendarWeek.to_string() =>vec![cw],
            &column_names::PnLReportColumnKind::Date.to_string() =>vec![date],
            &column_names::PnLReportColumnKind::Strategy.to_string() =>vec![strategy],
            &column_names::PnLReportColumnKind::Market.to_string() =>vec![market],
            &column_names::PnLReportColumnKind::TradeDirection.to_string() =>vec![trade_direction],
            &column_names::PnLReportColumnKind::Entry.to_string() =>vec![entry_price.round_to_n_decimal_places(n)],
            &column_names::PnLReportColumnKind::TakeProfit.to_string() =>vec![take_profit.round_to_n_decimal_places(n)],
            &column_names::PnLReportColumnKind::StopLoss.to_string() =>vec![stop_loss.round_to_n_decimal_places(n)],
            &column_names::PnLReportColumnKind::ExpectedWinTick.to_string() =>vec![expected_win_tick],
            &column_names::PnLReportColumnKind::ExpectedLossTick.to_string() =>vec![expected_loss_tick],
            &column_names::PnLReportColumnKind::ExpectedWinDollar.to_string() =>vec![expected_win_dollar.round_to_dollar_cents()],
            &column_names::PnLReportColumnKind::ExpectedLossDollar.to_string() =>vec![expected_loss_dollar.round_to_dollar_cents()],
            &column_names::PnLReportColumnKind::Crv.to_string() =>vec![crv.round_to_n_decimal_places(3)],
            &column_names::PnLReportColumnKind::EntryTimestamp.to_string() => &["NoEntry".to_string()],
            &column_names::PnLReportColumnKind::TakeProfitTimestamp.to_string() => &["NoEntry".to_string()],
            &column_names::PnLReportColumnKind::StopLossTimestamp.to_string() => &["NoEntry".to_string()],
            &column_names::PnLReportColumnKind::ExitPrice.to_string() => &[0.0],
            &column_names::PnLReportColumnKind::Status.to_string() => &["NoEntry".to_string()],
            &column_names::PnLReportColumnKind::PlTick.to_string() => &[0.0],
            &column_names::PnLReportColumnKind::PlDollar.to_string() => &[0.0],
        )
        .unwrap()
    }

    fn get_date(&self) -> String {
        let cw = self.time_frame_snapshot.get_calendar_week_as_int();
        let day = self.time_frame_snapshot.get_weekday();
        NaiveDate::from_isoywd_opt(
            i32::try_from(self.year).unwrap(),
            u32::try_from(cw).unwrap(),
            day,
        )
        .unwrap()
        .format("%Y-%m-%d")
        .to_string()
    }

    fn get_decimal_places(&self) -> i32 {
        self.market.decimal_places()
    }

    fn get_tick_factor(&self) -> f64 {
        self.market.tick_step_size().map_or_else(|| 1.0, identity)
    }

    fn get_tick_to_dollar_conversion_factor(&self) -> f64 {
        self.market
            .tik_to_dollar_conversion_factor()
            .map_or_else(|| 1.0, identity)
    }

    fn get_entry_ts(&self) -> String {
        let trade_pnl = self.trade_pnl.clone().unwrap();
        timestamp_in_milli_to_string(trade_pnl.trade_entry_ts)
    }

    fn get_take_profit_ts(&self) -> String {
        let trade_pnl = self.trade_pnl.clone().unwrap();
        trade_pnl.clone().take_profit.map_or_else(
            || "Timeout".to_string(),
            |pnl| timestamp_in_milli_to_string(pnl.ts.unwrap()),
        )
    }

    fn get_stop_loss_ts(&self) -> String {
        let trade_pnl = self.trade_pnl.clone().unwrap();
        trade_pnl.clone().stop_loss.map_or_else(
            || "Timeout".to_string(),
            |pnl| timestamp_in_milli_to_string(pnl.ts.unwrap()),
        )
    }

    fn expected_win_in_tick(&self, tick_factor: f64) -> f64 {
        let profit = self
            .trade
            .profit(self.trade.take_profit.map_or_else(|| 0.0, identity));
        (profit / tick_factor).round()
    }

    fn expected_loss_in_tick(&self, tick_factor: f64) -> f64 {
        let loss = self
            .trade
            .profit(self.trade.stop_loss.map_or_else(|| 0.0, identity));
        (loss / tick_factor).round()
    }
}

fn compute_crv(win: f64, loss: f64) -> f64 {
    if loss == 0.0 {
        f64::INFINITY
    } else {
        (win / loss).abs()
    }
}

impl From<PnLReportDataRow> for DataFrame {
    fn from(value: PnLReportDataRow) -> Self {
        match value.trade_pnl {
            Some(_) => value.report_with_trade(),
            None => value.report_without_trade(),
        }
    }
}

impl FromIterator<PnLReportDataRow> for DataFrame {
    fn from_iter<T: IntoIterator<Item = PnLReportDataRow>>(iter: T) -> Self {
        iter.into_iter()
            .fold(Vec::new(), |mut ldfs, pnl_report_data_row| {
                let df: DataFrame = pnl_report_data_row.into();
                ldfs.push(df.lazy());
                ldfs
            })
            .concatenate_to_lazy_frame()
            .sort_by_date()
            .collect()
            .unwrap()
            .with_row_count("#", Some(1))
            .unwrap()
    }
}

pub struct PnLReport {
    pub market: MarketKind,
    pub year: u32,
    pub strategy: String,
    pub pnl: DataFrame,
}

impl FromIterator<PnLReport> for PnLReports {
    fn from_iter<T: IntoIterator<Item = PnLReport>>(iter: T) -> Self {
        iter.into_iter()
            .fold(PnLReportsBuilder::new(), |builder, i| builder.append(i))
            .build()
    }
}

struct PnLReportsBuilder {
    market: Option<MarketKind>,
    strategy_name: Option<String>,
    years: Vec<u32>,
    reports: HashMap<u32, DataFrame>,
}

impl PnLReportsBuilder {
    pub fn new() -> Self {
        Self {
            market: None,
            strategy_name: None,
            years: Vec::new(),
            reports: HashMap::new(),
        }
    }

    pub fn append(self, pnl_report: PnLReport) -> Self {
        let market = pnl_report.market;
        let strategy_name = pnl_report.strategy.clone();
        let year = pnl_report.year;
        let mut years = self.years;
        years.push(year);

        let mut reports = self.reports;
        reports.insert(year, pnl_report.pnl);

        Self {
            market: Some(market),
            strategy_name: Some(strategy_name),
            years,
            reports,
        }
    }

    pub fn build(self) -> PnLReports {
        PnLReports {
            market: self.market.unwrap(),
            strategy: self.strategy_name.unwrap(),
            years: self.years,
            reports: self.reports,
        }
    }
}