初始化回测核心引擎骨架
This commit is contained in:
9
crates/bt-demo/Cargo.toml
Normal file
9
crates/bt-demo/Cargo.toml
Normal 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
180
crates/bt-demo/src/main.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user