补验锁定期间手工卖出与送转后的自动退出

This commit is contained in:
boris
2026-09-14 19:17:59 +08:00
parent e9c9ecbd48
commit b4c68be29b
2 changed files with 17 additions and 8 deletions
@@ -108,22 +108,28 @@ fn action(quantity: &str, when: &str) -> PlatformTradeAction {
#[test]
fn observed_manual_trades_then_split_keep_real_fill_protection_and_lock_dates() {
for sell_during_lock in [false, true] {
let sale = if sell_during_lock {
("manual-sell", "Sell", "2026-09-16T01:31:00Z", "2026-09-16T01:31:01Z", "5", "0.5", 200)
} else {
("manual-sell", "Sell", "2026-09-14T01:31:00Z", "2026-09-14T01:31:01Z", "10", "0.5", 100)
};
let actions = [
("new-buy", "Buy", "2026-09-14T01:30:00Z", "2026-09-14T01:30:01Z", "10", "0.25"),
("late-buy", "Buy", "2026-09-11T06:00:00Z", "2026-09-14T01:30:02Z", "10", "0.75"),
("manual-sell", "Sell", "2026-09-14T01:31:00Z", "2026-09-14T01:31:01Z", "10", "0.5"),
].into_iter().enumerate().map(|(index, (id, side, executed, observed, price, fee))| {
("new-buy", "Buy", "2026-09-14T01:30:00Z", "2026-09-14T01:30:01Z", "10", "0.25", 100),
("late-buy", "Buy", "2026-09-11T06:00:00Z", "2026-09-14T01:30:02Z", "10", "0.75", 100),
sale,
].into_iter().enumerate().map(|(index, (id, side, executed, observed, price, fee, quantity))| {
let executed: chrono::DateTime<chrono::Utc> = executed.parse().unwrap();
let observed: chrono::DateTime<chrono::Utc> = observed.parse().unwrap();
let created = executed - chrono::Duration::seconds(1);
serde_json::json!({"actionId":id,"source":"manual_security_trade","auditEventIds":[format!("audit-{id}")],
"confirmedAt":created,"confirmationObservedAt":created,"outcome":"orders_terminal","orders":[{
"orderId":id,"brokerOrderId":id,"sourceAdapter":"paper","symbol":"000001.SZ","side":side,"quantity":100,
"orderId":id,"brokerOrderId":id,"sourceAdapter":"paper","symbol":"000001.SZ","side":side,"quantity":quantity,
"orderCreatedAt":created,"terminalObservedAt":observed,"terminalStatus":"filled","fills":[{
"tradeId":id,"observationEventId":id,"observationSequence":index+1,
"tradeDate":executed.date_naive(),"executedAt":executed,"observedAt":observed,
"feeObservationEventId":id,"feeObservationSequence":index+1,"feeObservedAt":observed,
"timestampPrecision":"second","quantity":100,"price":price,"totalFee":fee
"timestampPrecision":"second","quantity":quantity,"price":price,"totalFee":fee
}]
}]})
}).collect::<Vec<_>>();
@@ -212,7 +218,7 @@ fn observed_manual_trades_then_split_keep_real_fill_protection_and_lock_dates()
.run()
.unwrap();
assert_eq!(result.manual_executions.len(), 3);
assert_eq!(result.manual_executions[2].quantity_after, 100);
assert_eq!(result.manual_executions[2].quantity_after, if sell_during_lock { 200 } else { 100 });
assert_eq!(result.fills.len(), 1, "{:?}", result.fills);
assert_eq!(
(
@@ -226,6 +232,7 @@ fn observed_manual_trades_then_split_keep_real_fill_protection_and_lock_dates()
assert!(result.fills[0].reason.contains("max_holding_days_exit"));
for day in [14, 15] {
for rule in ["buy_fill_protection", "sell_fill_cooldown"] {
if rule == "sell_fill_cooldown" && sell_during_lock { continue; }
assert!(result.risk_decisions.iter().any(|audit| audit.date == d(day)
&& audit.symbol == "000001.SZ" && audit.rule_code == rule && !audit.accepted), "day={day} rule={rule}");
}
@@ -238,7 +245,7 @@ fn observed_manual_trades_then_split_keep_real_fill_protection_and_lock_dates()
result
.daily_holdings
.iter()
.any(|row| row.date == d(15) && row.quantity == 200)
.any(|row| row.date == d(15) && row.quantity == if sell_during_lock { 400 } else { 200 })
);
assert!(result.holdings_summary.is_empty());
assert!(
@@ -247,6 +254,7 @@ fn observed_manual_trades_then_split_keep_real_fill_protection_and_lock_dates()
.iter()
.all(|point| point.external_cash_flow == 0.)
);
}
}
fn run(policy: AutomaticTradeProtection) -> fidc_core::BacktestResult {