接入独立手工仓位时间线并保留原策略配置

This commit is contained in:
boris
2026-09-14 15:48:44 +08:00
parent fb8192a286
commit f8955bfb18
9 changed files with 561 additions and 50 deletions
@@ -770,6 +770,150 @@ fn pool_position_adjustments_use_execution_clock_and_restore_original_twenty_per
}
}
#[test]
fn runtime_allocation_is_separate_from_the_frozen_pool_and_restores_its_weights() {
let program = StockPoolProgram {
schema_version: 1,
pool_id: "manual-allocation".into(),
version_id: "v1".into(),
members: contract(day(2), 2, false).members,
exit_signals: vec![],
allocation_policy: serde_json::json!({"target_holding_count":2,"invest_ratio_bps":10000,"portfolio_policy":{"schema_version":1,"membership":"follow_candidates","rebalance_weights":true}}),
timing_policy: serde_json::json!({"pricing_mode":"first_tick"}),
stop_take_policy: serde_json::json!({}),
out_of_pool_policy: "hold".into(),
};
let mut cfg = platform_expr_config_from_value(
"manual-allocation",
"000300.SH",
&serde_json::json!({"stockPool":program,"universe":{"include":[code(1),code(2)]}}),
)
.unwrap();
cfg.market_cap_field = "close".into();
cfg.market_cap_lower_expr = "0".into();
cfg.market_cap_upper_expr = "1e12".into();
cfg.stock_filter_expr = "true".into();
cfg.selection_limit_expr = "2".into();
cfg.selection_candidate_limit_expr = "2".into();
cfg.rank_expr = "0".into();
cfg.matching_type = MatchingType::NextBarOpen;
let mut replay:fidc_core::manual_execution::ManualExecutionReplay=serde_json::from_value(serde_json::json!({
"schema":fidc_core::manual_execution::MANUAL_REPLAY_SCHEMA,"runtimeId":"r","accountId":"a","sourceContractSha256":"a".repeat(64),"contentSha256":"",
"observationCutoff":"2026-01-06T08:00:00Z","actions":[],"positionExposureEvents":[
{"eventId":"weights","sequence":1,"effectiveAt":"2026-01-05T09:30:00+08:00","action":"set","targetExposureBps":8000,"allocationWeightsBps":{"000001.SZ":3000,"000002.SZ":7000}},
{"eventId":"restore","sequence":2,"effectiveAt":"2026-01-06T09:30:00+08:00","action":"restore"}
]})).unwrap();
replay.content_sha256 = replay.content_digest().unwrap();
let result = BacktestEngine::new(
data(false),
PlatformExprStrategy::new(cfg),
broker(false),
BacktestConfig {
initial_cash: 30000.,
benchmark_code: "000300.SH".into(),
start_date: Some(day(2)),
end_date: Some(day(6)),
decision_lag_trading_days: 1,
execution_price_field: PriceField::Open,
},
)
.with_observed_manual_executions(replay)
.unwrap()
.run()
.unwrap();
let quantities = |date| {
result
.daily_holdings
.iter()
.filter(|row| row.date == date)
.map(|row| (row.symbol.clone(), row.quantity))
.collect::<BTreeMap<_, _>>()
};
assert_eq!(
quantities(day(5)),
BTreeMap::from([(code(1), 300), (code(2), 1600)]),
"{:?}",
result.fills
);
assert_eq!(
quantities(day(6)),
BTreeMap::from([(code(1), 700), (code(2), 1500)]),
"{:?}",
result.fills
);
assert!(
result.manual_executions.is_empty(),
"parameter events are not fabricated fills"
);
}
#[test]
fn outside_manual_holding_data_does_not_become_a_pool_candidate() {
let program = StockPoolProgram {
schema_version: 1,
pool_id: "manual-data-scope".into(),
version_id: "v1".into(),
members: vec![contract(day(2), 1, false).members.remove(0)],
exit_signals: vec![],
allocation_policy: serde_json::json!({"target_holding_count":1,"invest_ratio_bps":2000}),
timing_policy: serde_json::json!({"pricing_mode":"first_tick"}),
stop_take_policy: serde_json::json!({}),
out_of_pool_policy: "hold".into(),
};
let mut cfg = platform_expr_config_from_value(
"manual-data-scope",
"000300.SH",
&serde_json::json!({"stockPool":program,"universe":{"include":[code(1)]}}),
)
.unwrap();
cfg.market_cap_field = "close".into();
cfg.market_cap_lower_expr = "0".into();
cfg.market_cap_upper_expr = "1e12".into();
cfg.stock_filter_expr = "true".into();
cfg.selection_limit_expr = "1".into();
cfg.selection_candidate_limit_expr = "2".into();
cfg.rank_expr = "0".into();
cfg.matching_type = MatchingType::NextBarOpen;
let fill = serde_json::json!({"tradeId":"fill","observationEventId":"receipt","observationSequence":1,"tradeDate":"2026-01-05","executedAt":"2026-01-05T01:31:00Z","observedAt":"2026-01-05T01:31:01Z",
"feeObservationEventId":"receipt","feeObservationSequence":1,"feeObservedAt":"2026-01-05T01:31:01Z","timestampPrecision":"second","quantity":100,"price":"10","totalFee":"0"});
let order = serde_json::json!({"orderId":"external-order","sourceAdapter":"paper","symbol":code(2),"side":"Buy","quantity":100,"orderCreatedAt":"2026-01-05T01:30:00Z","terminalObservedAt":"2026-01-05T01:31:01Z","terminalStatus":"filled","fills":[fill]});
let mut replay:fidc_core::manual_execution::ManualExecutionReplay=serde_json::from_value(serde_json::json!({
"schema":fidc_core::manual_execution::MANUAL_REPLAY_SCHEMA,"runtimeId":"r","accountId":"a","sourceContractSha256":"a".repeat(64),"contentSha256":"","observationCutoff":"2026-01-06T08:00:00Z",
"actions":[{"actionId":"manual","source":"manual_security_trade","auditEventIds":["audit"],"confirmedAt":"2026-01-05T01:29:59Z","confirmationObservedAt":"2026-01-05T01:29:59Z","outcome":"orders_terminal","orders":[order]}]})).unwrap();
replay.content_sha256 = replay.content_digest().unwrap();
let result = BacktestEngine::new(
data(false),
PlatformExprStrategy::new(cfg),
broker(false),
BacktestConfig {
initial_cash: 30000.,
benchmark_code: "000300.SH".into(),
start_date: Some(day(2)),
end_date: Some(day(6)),
decision_lag_trading_days: 1,
execution_price_field: PriceField::Open,
},
)
.with_observed_manual_executions(replay)
.unwrap()
.run()
.unwrap();
assert!(
result.fills.iter().all(|fill| fill.symbol != code(2)),
"extra data cannot authorize an extra candidate"
);
assert_eq!(result.manual_executions.len(), 1);
assert_eq!(
result
.holdings_summary
.iter()
.find(|row| row.symbol == code(2))
.unwrap()
.quantity,
100
);
}
#[test]
fn parsed_typed_exit_program_keeps_ordinary_gates_and_independent_risk_targets_separate() {
for (ordinary, risk, quote, sold) in [