Add remaining RQAlpha extension helpers
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::broker::{BrokerExecutionReport, BrokerSimulator, MatchingType};
|
||||
use crate::cost::CostModel;
|
||||
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
|
||||
use crate::event_bus::ProcessEventBus;
|
||||
use crate::event_bus::{BacktestProcessMod, ProcessEventBus};
|
||||
use crate::events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
@@ -104,11 +104,41 @@ pub struct AnalyzerPositionRow {
|
||||
pub transaction_cost: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AnalyzerMonthlyReturnRow {
|
||||
pub year: i32,
|
||||
pub month: u32,
|
||||
pub portfolio_return: f64,
|
||||
pub benchmark_return: f64,
|
||||
pub excess_return: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AnalyzerRiskSummary {
|
||||
pub total_return: f64,
|
||||
pub annual_return: f64,
|
||||
pub benchmark_cumulative_return: f64,
|
||||
pub excess_cumulative_return: f64,
|
||||
pub alpha: f64,
|
||||
pub beta: f64,
|
||||
pub sharpe: f64,
|
||||
pub sortino: f64,
|
||||
pub information_ratio: f64,
|
||||
pub tracking_error: f64,
|
||||
pub volatility: f64,
|
||||
pub max_drawdown: f64,
|
||||
pub max_drawdown_duration_days: usize,
|
||||
pub win_rate: f64,
|
||||
pub excess_win_rate: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AnalyzerReport {
|
||||
pub strategy_name: String,
|
||||
pub trades: Vec<AnalyzerTradeRow>,
|
||||
pub positions: Vec<AnalyzerPositionRow>,
|
||||
pub monthly_returns: Vec<AnalyzerMonthlyReturnRow>,
|
||||
pub risk_summary: AnalyzerRiskSummary,
|
||||
pub equity_curve: Vec<DailyEquityPoint>,
|
||||
pub benchmark_series: Vec<BenchmarkSnapshot>,
|
||||
pub metrics: BacktestMetrics,
|
||||
@@ -149,6 +179,8 @@ impl BacktestResult {
|
||||
transaction_cost: holding.transaction_cost,
|
||||
})
|
||||
.collect(),
|
||||
monthly_returns: self.analyzer_monthly_returns(),
|
||||
risk_summary: self.analyzer_risk_summary(),
|
||||
equity_curve: self.equity_curve.clone(),
|
||||
benchmark_series: self.benchmark_series.clone(),
|
||||
metrics: self.metrics.clone(),
|
||||
@@ -158,6 +190,61 @@ impl BacktestResult {
|
||||
pub fn analyzer_report_json(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string_pretty(&self.analyzer_report())
|
||||
}
|
||||
|
||||
pub fn analyzer_monthly_returns(&self) -> Vec<AnalyzerMonthlyReturnRow> {
|
||||
let mut month_points = BTreeMap::<(i32, u32), (f64, f64, f64, f64)>::new();
|
||||
for point in &self.equity_curve {
|
||||
let key = (point.date.year(), point.date.month());
|
||||
month_points
|
||||
.entry(key)
|
||||
.and_modify(|(_, _, end_equity, end_benchmark)| {
|
||||
*end_equity = point.total_equity;
|
||||
*end_benchmark = point.benchmark_close;
|
||||
})
|
||||
.or_insert((
|
||||
point.total_equity,
|
||||
point.benchmark_close,
|
||||
point.total_equity,
|
||||
point.benchmark_close,
|
||||
));
|
||||
}
|
||||
month_points
|
||||
.into_iter()
|
||||
.map(
|
||||
|((year, month), (start_equity, start_benchmark, end_equity, end_benchmark))| {
|
||||
let portfolio_return = analyzer_ratio_change(start_equity, end_equity);
|
||||
let benchmark_return = analyzer_ratio_change(start_benchmark, end_benchmark);
|
||||
AnalyzerMonthlyReturnRow {
|
||||
year,
|
||||
month,
|
||||
portfolio_return,
|
||||
benchmark_return,
|
||||
excess_return: portfolio_return - benchmark_return,
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn analyzer_risk_summary(&self) -> AnalyzerRiskSummary {
|
||||
AnalyzerRiskSummary {
|
||||
total_return: self.metrics.total_return,
|
||||
annual_return: self.metrics.annual_return,
|
||||
benchmark_cumulative_return: self.metrics.benchmark_cumulative_return,
|
||||
excess_cumulative_return: self.metrics.excess_cumulative_return,
|
||||
alpha: self.metrics.alpha,
|
||||
beta: self.metrics.beta,
|
||||
sharpe: self.metrics.sharpe,
|
||||
sortino: self.metrics.sortino,
|
||||
information_ratio: self.metrics.information_ratio,
|
||||
tracking_error: self.metrics.tracking_error,
|
||||
volatility: self.metrics.volatility,
|
||||
max_drawdown: self.metrics.max_drawdown,
|
||||
max_drawdown_duration_days: self.metrics.max_drawdown_duration_days,
|
||||
win_rate: self.metrics.win_rate,
|
||||
excess_win_rate: self.metrics.excess_win_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -307,6 +394,13 @@ impl<S, C, R> BacktestEngine<S, C, R> {
|
||||
{
|
||||
self.process_event_bus.add_any_listener(listener);
|
||||
}
|
||||
|
||||
pub fn install_process_mod<M>(&mut self, module: &mut M)
|
||||
where
|
||||
M: BacktestProcessMod,
|
||||
{
|
||||
self.process_event_bus.install_mod(module);
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, C, R> BacktestEngine<S, C, R>
|
||||
@@ -3042,6 +3136,14 @@ fn merge_futures_execution_report(
|
||||
target.diagnostics.extend(incoming.diagnostics);
|
||||
}
|
||||
|
||||
fn analyzer_ratio_change(start: f64, end: f64) -> f64 {
|
||||
if start.abs() <= f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
end / start - 1.0
|
||||
}
|
||||
}
|
||||
|
||||
fn futures_limit_satisfied(side: OrderSide, price: f64, limit_price: Option<f64>) -> bool {
|
||||
let Some(limit_price) = limit_price else {
|
||||
return price.is_finite() && price > 0.0;
|
||||
|
||||
Reference in New Issue
Block a user