增强回测demo输出与分区加载

This commit is contained in:
zsb
2026-04-07 21:25:41 -07:00
parent ec425999b0
commit a26049ff15
9 changed files with 211 additions and 63 deletions

View File

@@ -7,3 +7,5 @@ authors.workspace = true
[dependencies]
fidc-core = { path = "../fidc-core" }
serde = { workspace = true }
serde_json = "1"

View File

@@ -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:");