增强回测demo输出与分区加载
This commit is contained in:
@@ -7,3 +7,5 @@ authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
fidc-core = { path = "../fidc-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -24,7 +24,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| root.join("data/demo"));
|
||||
let data_layout = std::env::var("FIDC_BT_DATA_LAYOUT").unwrap_or_else(|_| "flat".to_string());
|
||||
let output_dir = root.join("output/demo");
|
||||
let output_dir = std::env::var("FIDC_BT_OUTPUT_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| root.join("output/demo"));
|
||||
let json_output = std::env::var("FIDC_BT_JSON")
|
||||
.map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
@@ -37,6 +42,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
strategy_cfg.base_index_level = 3000.0;
|
||||
strategy_cfg.base_cap_floor = 38.0;
|
||||
strategy_cfg.cap_span = 25.0;
|
||||
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
||||
if !signal_symbol.trim().is_empty() {
|
||||
strategy_cfg.signal_symbol = Some(signal_symbol);
|
||||
}
|
||||
}
|
||||
let strategy = CnSmallCapRotationStrategy::new(strategy_cfg);
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default());
|
||||
let config = BacktestConfig {
|
||||
@@ -51,14 +61,20 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
write_trades_csv(&output_dir.join("trades.csv"), &result.fills)?;
|
||||
write_holdings_csv(&output_dir.join("holdings_summary.csv"), &result.holdings_summary)?;
|
||||
|
||||
print_summary(
|
||||
let summary = build_summary(
|
||||
&result.strategy_name,
|
||||
&result.equity_curve,
|
||||
&result.fills,
|
||||
&result.holdings_summary,
|
||||
result.benchmark_series.last(),
|
||||
&output_dir,
|
||||
);
|
||||
|
||||
print_summary(&summary, &result.equity_curve, &result.holdings_summary);
|
||||
println!("Artifacts written under {}", output_dir.display());
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string(&summary)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -140,34 +156,69 @@ fn sanitize_csv_field(text: &str) -> String {
|
||||
text.replace(',', ";")
|
||||
}
|
||||
|
||||
fn print_summary(
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct RunSummary {
|
||||
strategy: String,
|
||||
start_date: String,
|
||||
end_date: String,
|
||||
start_equity: f64,
|
||||
final_equity: f64,
|
||||
total_return: f64,
|
||||
trade_count: usize,
|
||||
holding_count: usize,
|
||||
benchmark_code: Option<String>,
|
||||
benchmark_last_close: Option<f64>,
|
||||
output_dir: String,
|
||||
}
|
||||
|
||||
fn build_summary(
|
||||
strategy_name: &str,
|
||||
equity_curve: &[DailyEquityPoint],
|
||||
fills: &[FillEvent],
|
||||
holdings: &[HoldingSummary],
|
||||
benchmark_last: Option<&BenchmarkSnapshot>,
|
||||
) {
|
||||
let Some(first) = equity_curve.first() else {
|
||||
println!("No equity curve points generated.");
|
||||
return;
|
||||
};
|
||||
let Some(last) = equity_curve.last() else {
|
||||
println!("No equity curve points generated.");
|
||||
return;
|
||||
output_dir: &Path,
|
||||
) -> RunSummary {
|
||||
let first = equity_curve.first();
|
||||
let last = equity_curve.last();
|
||||
let start_equity = first.map(|row| row.total_equity).unwrap_or_default();
|
||||
let final_equity = last.map(|row| row.total_equity).unwrap_or_default();
|
||||
let total_return = if start_equity.abs() < f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
(final_equity / start_equity) - 1.0
|
||||
};
|
||||
|
||||
let total_return = (last.total_equity / first.total_equity) - 1.0;
|
||||
println!("Strategy: cn-smallcap-rotation");
|
||||
println!("Start equity: {:.2}", first.total_equity);
|
||||
println!("Final equity: {:.2}", last.total_equity);
|
||||
println!("Total return: {:.2}%", total_return * 100.0);
|
||||
println!("Trades: {}", fills.len());
|
||||
println!("Final holdings: {}", holdings.len());
|
||||
RunSummary {
|
||||
strategy: strategy_name.to_string(),
|
||||
start_date: first.map(|row| row.date.to_string()).unwrap_or_default(),
|
||||
end_date: last.map(|row| row.date.to_string()).unwrap_or_default(),
|
||||
start_equity,
|
||||
final_equity,
|
||||
total_return,
|
||||
trade_count: fills.len(),
|
||||
holding_count: holdings.len(),
|
||||
benchmark_code: benchmark_last.map(|row| row.benchmark.clone()),
|
||||
benchmark_last_close: benchmark_last.map(|row| row.close),
|
||||
output_dir: output_dir.display().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(benchmark) = benchmark_last {
|
||||
println!(
|
||||
"Benchmark last close: {} {:.2}",
|
||||
benchmark.benchmark, benchmark.close
|
||||
);
|
||||
fn print_summary(summary: &RunSummary, equity_curve: &[DailyEquityPoint], holdings: &[HoldingSummary]) {
|
||||
if equity_curve.is_empty() {
|
||||
println!("No equity curve points generated.");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Strategy: {}", summary.strategy);
|
||||
println!("Start equity: {:.2}", summary.start_equity);
|
||||
println!("Final equity: {:.2}", summary.final_equity);
|
||||
println!("Total return: {:.2}%", summary.total_return * 100.0);
|
||||
println!("Trades: {}", summary.trade_count);
|
||||
println!("Final holdings: {}", summary.holding_count);
|
||||
|
||||
if let (Some(code), Some(close)) = (&summary.benchmark_code, summary.benchmark_last_close) {
|
||||
println!("Benchmark last close: {} {:.2}", code, close);
|
||||
}
|
||||
|
||||
println!("Recent equity points:");
|
||||
|
||||
Reference in New Issue
Block a user