初始化回测核心引擎骨架

This commit is contained in:
zsb
2026-04-06 23:56:37 -07:00
commit 334864cbc5
25 changed files with 2878 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
[package]
name = "bt-demo"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
[dependencies]
fidc-core = { path = "../fidc-core" }

180
crates/bt-demo/src/main.rs Normal file
View File

@@ -0,0 +1,180 @@
use std::error::Error;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use fidc_core::{
BacktestConfig,
BacktestEngine,
BenchmarkSnapshot,
BrokerSimulator,
ChinaAShareCostModel,
ChinaEquityRuleHooks,
CnSmallCapRotationConfig,
CnSmallCapRotationStrategy,
DataSet,
DailyEquityPoint,
FillEvent,
HoldingSummary,
};
fn main() -> Result<(), Box<dyn Error>> {
let root = workspace_root();
let data_dir = root.join("data/demo");
let output_dir = root.join("output/demo");
fs::create_dir_all(&output_dir)?;
let data = DataSet::from_csv_dir(&data_dir)?;
let strategy = CnSmallCapRotationStrategy::new(CnSmallCapRotationConfig::demo());
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default());
let config = BacktestConfig {
initial_cash: 1_000_000.0,
benchmark_code: data.benchmark_code().to_string(),
};
let mut engine = BacktestEngine::new(data, strategy, broker, config);
let result = engine.run()?;
write_equity_curve_csv(&output_dir.join("equity_curve.csv"), &result.equity_curve)?;
write_trades_csv(&output_dir.join("trades.csv"), &result.fills)?;
write_holdings_csv(&output_dir.join("holdings_summary.csv"), &result.holdings_summary)?;
print_summary(
&result.equity_curve,
&result.fills,
&result.holdings_summary,
result.benchmark_series.last(),
);
println!("Artifacts written under {}", output_dir.display());
Ok(())
}
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("workspace root")
}
fn write_equity_curve_csv(path: &Path, rows: &[DailyEquityPoint]) -> Result<(), Box<dyn Error>> {
let mut file = fs::File::create(path)?;
writeln!(file, "date,cash,market_value,total_equity,benchmark_close,notes")?;
for row in rows {
writeln!(
file,
"{},{:.2},{:.2},{:.2},{:.2},{}",
row.date,
row.cash,
row.market_value,
row.total_equity,
row.benchmark_close,
sanitize_csv_field(&row.notes),
)?;
}
Ok(())
}
fn write_trades_csv(path: &Path, rows: &[FillEvent]) -> Result<(), Box<dyn Error>> {
let mut file = fs::File::create(path)?;
writeln!(
file,
"date,symbol,side,quantity,price,gross_amount,commission,stamp_tax,net_cash_flow,reason"
)?;
for row in rows {
writeln!(
file,
"{},{},{:?},{},{:.2},{:.2},{:.2},{:.2},{:.2},{}",
row.date,
row.symbol,
row.side,
row.quantity,
row.price,
row.gross_amount,
row.commission,
row.stamp_tax,
row.net_cash_flow,
sanitize_csv_field(&row.reason),
)?;
}
Ok(())
}
fn write_holdings_csv(path: &Path, rows: &[HoldingSummary]) -> Result<(), Box<dyn Error>> {
let mut file = fs::File::create(path)?;
writeln!(
file,
"date,symbol,quantity,average_cost,last_price,market_value,unrealized_pnl,realized_pnl"
)?;
for row in rows {
writeln!(
file,
"{},{},{},{:.2},{:.2},{:.2},{:.2},{:.2}",
row.date,
row.symbol,
row.quantity,
row.average_cost,
row.last_price,
row.market_value,
row.unrealized_pnl,
row.realized_pnl,
)?;
}
Ok(())
}
fn sanitize_csv_field(text: &str) -> String {
text.replace(',', ";")
}
fn print_summary(
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;
};
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());
if let Some(benchmark) = benchmark_last {
println!(
"Benchmark last close: {} {:.2}",
benchmark.benchmark, benchmark.close
);
}
println!("Recent equity points:");
for point in equity_curve.iter().rev().take(3).collect::<Vec<_>>().into_iter().rev() {
println!(
" {} equity {:.2} cash {:.2} mv {:.2}",
point.date, point.total_equity, point.cash, point.market_value
);
}
if holdings.is_empty() {
println!("No holdings at the end of the demo run.");
} else {
println!("Ending holdings:");
for holding in holdings {
println!(
" {} qty {} mv {:.2} pnl {:.2}",
holding.symbol, holding.quantity, holding.market_value, holding.unrealized_pnl
);
}
}
}