增加通用期货策略动作并修正组合净值

This commit is contained in:
boris
2026-08-25 17:10:17 +08:00
parent 90da7f8a21
commit c284cc191e
5 changed files with 408 additions and 15 deletions
+75 -5
View File
@@ -9,11 +9,12 @@ use fidc_core::{
BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, ExecutionQuoteRequest,
FuturesAccountState, FuturesCommissionType, FuturesContractSpec, FuturesDirection,
FuturesOrderIntent, FuturesTradingParameter, FuturesValidationConfig, Instrument,
IntradayExecutionQuote, IntradayOrderBookDepthLevel, MatchingType, OpenOrderView, OrderIntent,
OrderSide, OrderStatus, PlatformExprStrategy, PlatformExprStrategyConfig, PortfolioState,
PriceField, ProcessEvent, ProcessEventBus, ProcessEventKind, ScheduleRule, ScheduleStage,
ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision,
FuturesOrderIntent, FuturesPositionEffect, FuturesTradingParameter, FuturesValidationConfig,
Instrument, IntradayExecutionQuote, IntradayOrderBookDepthLevel, MatchingType, OpenOrderView,
OrderIntent, OrderSide, OrderStatus, PlatformExprStrategy, PlatformExprStrategyConfig,
PlatformTradeAction, PortfolioState, PriceField, ProcessEvent, ProcessEventBus,
ProcessEventKind, ScheduleRule, ScheduleStage, ScheduleTimeRule, Strategy, StrategyContext,
StrategyDecision,
};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -1475,6 +1476,73 @@ fn engine_executes_futures_order_intents_against_future_account() {
assert!((futures_account.cash() - 355_988.0).abs() < 1e-6);
}
#[test]
fn platform_runtime_actions_execute_generic_futures_open_and_close() {
let mut cfg = PlatformExprStrategyConfig::generic();
cfg.signal_symbol = "000001.SZ".to_string();
cfg.benchmark_symbol = "000300.SH".to_string();
cfg.rotation_enabled = false;
cfg.benchmark_short_ma_days = 1;
cfg.benchmark_long_ma_days = 1;
cfg.explicit_actions = vec![
PlatformTradeAction::Futures {
symbol: "IF2501".to_string(),
direction: FuturesDirection::Long,
effect: FuturesPositionEffect::Open,
quantity_expr: "1".to_string(),
limit_price_expr: None,
transaction_cost_expr: None,
when_expr: Some("decision_date == \"2025-01-02\"".to_string()),
reason: "generic futures open".to_string(),
},
PlatformTradeAction::Futures {
symbol: "IF2501".to_string(),
direction: FuturesDirection::Long,
effect: FuturesPositionEffect::Close,
quantity_expr: "1".to_string(),
limit_price_expr: None,
transaction_cost_expr: None,
when_expr: Some("decision_date == \"2025-01-03\"".to_string()),
reason: "generic futures close".to_string(),
},
];
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
let mut engine = BacktestEngine::new(
two_day_futures_data(),
PlatformExprStrategy::new(cfg),
broker,
BacktestConfig {
initial_cash: 100_000.0,
benchmark_code: "000300.SH".to_string(),
start_date: Some(d(2025, 1, 2)),
end_date: Some(d(2025, 1, 3)),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Open,
},
)
.with_futures_initial_cash(500_000.0);
let result = engine.run().expect("generic futures actions execute");
let futures_fills = result
.fills
.iter()
.filter(|fill| fill.symbol == "IF2501")
.collect::<Vec<_>>();
assert_eq!(futures_fills.len(), 2);
assert!((futures_fills[0].price - 4000.0).abs() < 1e-12);
assert!((futures_fills[0].commission - 2.5).abs() < 1e-12);
assert!((futures_fills[1].price - 3988.0).abs() < 1e-12);
assert!((futures_fills[1].commission - 2.0).abs() < 1e-12);
let futures_account = engine.futures_account().expect("future account");
assert!(futures_account.positions().is_empty());
assert!((futures_account.total_cash() - 496_395.5).abs() < 1e-12);
}
#[test]
fn engine_settles_configured_futures_expiration_at_settlement() {
let date = d(2025, 1, 2);
@@ -1550,7 +1618,9 @@ fn engine_aggregates_futures_account_into_nav_and_metrics() {
assert_eq!(result.metrics.initial_cash, 600_000.0);
assert!((result.equity_curve[0].total_equity - 599_988.0).abs() < 1e-6);
assert!((result.equity_curve[0].unit_nav - 0.99998).abs() < 1e-12);
assert!((result.metrics.total_assets - 599_988.0).abs() < 1e-6);
assert!((result.metrics.total_return + 0.00002).abs() < 1e-12);
assert_eq!(result.analyzer_report().trades.len(), result.fills.len());
assert_eq!(result.analyzer_report().monthly_returns.len(), 1);
assert_eq!(