接入独立手工仓位时间线并保留原策略配置
This commit is contained in:
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user