Compare commits

...

116 Commits

Author SHA1 Message Date
boris f789866d33 追加待交付权益风险更正与最终验证回执 2026-10-05 06:28:10 +08:00
boris 1092cd0b80 声明退市应收回归的独立合成基准 2026-10-05 06:20:22 +08:00
boris 4a41057ec1 保护待交付股份的持仓上限与退市计价 2026-10-05 06:17:11 +08:00
boris 4a829fc0b9 归档登记股份交付修复与Linux完整验证证据 2026-10-05 06:13:11 +08:00
boris ce46c13f37 通过实际登记链路构造待交付股份回归 2026-10-05 05:57:07 +08:00
boris 8d90c983ec 验证待交付股份的目标仓位与证券转换保护 2026-10-05 05:56:31 +08:00
boris 7d7cce8b91 拒绝缺少应收股份承接证据的复合证券转换 2026-10-05 05:48:39 +08:00
boris a267052f7b 验证股份应收不能作为已交付持仓卖出 2026-10-05 05:13:17 +08:00
boris 05315f05d7 按账户汇总分配登记权益并保持显式模拟舍入合同 2026-10-05 04:48:22 +08:00
boris 4eb4c17724 使用真实标记日期区分交付计价并更新同日交付回归 2026-10-05 04:31:50 +08:00
boris 0caac44d52 补齐股份交付溢出原子性与既有失败样本日期合同 2026-10-05 04:15:20 +08:00
boris 0bbeed4ad0 建立登记股份应收与独立交付账簿候选实现 2026-10-05 04:04:38 +08:00
boris 371c573454 归档应收股息权益计价与完整引擎回归 2026-10-05 03:19:01 +08:00
boris d3f49a17ef 保留极小非零股息并执行相同权益日期门禁 2026-10-05 02:55:17 +08:00
boris af655de0c1 将已确认应收股息计入权益并验证支付资产守恒 2026-10-05 02:30:38 +08:00
boris cd82492a89 统一在持仓与退役账簿中的已发行批次身份 2026-10-03 21:45:24 +08:00
boris a435154108 在账簿覆盖错误中标明差异字段且保留完整校验 2026-10-03 21:34:20 +08:00
boris b331d41c31 仅在持仓退役时归档批次序号并保留无持仓现金权益 2026-10-03 21:23:50 +08:00
boris 591a71bf43 用实际执行日历确定全现金账户起点 2026-10-03 21:11:57 +08:00
boris b3491da6ef 按登记日收盘持仓计算现金权益并保留批次血缘 2026-10-03 21:02:26 +08:00
boris dbc2ccbfef 补齐继承转换回归样本的明确现金付款日 2026-10-03 16:37:39 +08:00
boris 6d531e3d96 按真实付款日结算分红并移除日期推定 2026-10-03 16:22:54 +08:00
boris 2bc033b8cc 增加逐决策动态模型投影并隔离共享数据与首日预热 2026-10-03 12:29:11 +08:00
boris 7485d4ceb9 修复回测未定义风险比率被补成零 2026-09-27 18:44:39 +08:00
boris 4a349ca3c7 refactor: share dated risk-adjusted return statistics with trading 2026-09-27 14:52:51 +08:00
boris 98732bbc46 修复次日开盘观测被日线价格分支跳过行情加载 2026-09-19 22:47:03 +08:00
boris 0ac07ac0f1 保持未升级精度状态的旧合同序列化身份 2026-09-19 21:56:16 +08:00
boris 689f93f1f1 限制开盘报价路径仅用于显式逐笔容量合同 2026-09-19 21:33:25 +08:00
boris 4d731f9c14 修复补减仓时对原持仓重复计入交易滑点 2026-09-19 21:29:52 +08:00
boris 175b319d47 补齐精度回归样例并允许次日开盘使用真实执行报价 2026-09-19 21:22:12 +08:00
boris 5924e43f3d 分离股票池精确预算比例与展示基点并持久化状态 2026-09-19 21:09:27 +08:00
boris 3c70a9273b 记录模拟委托异常恢复的Linux配套验收 2026-09-15 01:39:10 +08:00
boris 695fdee4b8 保留模拟器失败调用前的委托与执行状态 2026-09-15 01:28:38 +08:00
boris 4c6147e2ee 记录人工零仓位再投修复的Linux验收 2026-09-15 00:36:09 +08:00
boris 818552bc96 让人工零仓位和零权重约束红利再投 2026-09-15 00:23:09 +08:00
boris 534ab42906 记录换股保护继承的配套Linux验收 2026-09-14 23:48:27 +08:00
boris ba4b77fd74 让实际换股持仓继承原策略保护期限 2026-09-14 23:38:43 +08:00
boris ad76bdb6ae 记录换股契约与批次回滚Linux验收 2026-09-14 23:06:41 +08:00
boris 59a0c95aae 严格校验换股证据并原子提交公司行为批次 2026-09-14 22:57:53 +08:00
boris d4e7cdd5b5 记录账务再投来源与流式完整性Linux验收 2026-09-14 22:39:21 +08:00
boris 984f9d308d 明确红利账务再投来源并修复流式遗漏与校正时钟 2026-09-14 22:30:05 +08:00
boris fbc4233dcd 记录手工公司行为校正Linux配套验收 2026-09-14 21:24:46 +08:00
boris 05f1cbbe00 修复迟到成交跨公司行为的经济账本校正 2026-09-14 21:14:20 +08:00
boris ef9cc39882 记录迟到成交与持有保护Linux验收 2026-09-14 19:27:10 +08:00
boris b4c68be29b 补验锁定期间手工卖出与送转后的自动退出 2026-09-14 19:17:59 +08:00
boris e9c9ecbd48 修复迟到成交和换股批次的FIFO与持有期 2026-09-14 19:15:01 +08:00
boris b2eaaa0d26 记录开盘和跨日时钟Linux联合回归证据 2026-09-14 18:53:29 +08:00
boris 13c89e8d59 修复开盘与跨日ETF执行时钟及资金阻断原因 2026-09-14 18:42:25 +08:00
boris 232e9ae154 保留手工逐日进度并在交付校验失败时终止 2026-09-14 17:30:00 +08:00
boris f8955bfb18 接入独立手工仓位时间线并保留原策略配置 2026-09-14 15:48:44 +08:00
boris fb8192a286 修正盘后意图保留及手工观察的阶段时序 2026-09-14 12:27:30 +08:00
boris 7f0c6a008a 将手工观察接入执行时钟并保留来源与账本语义 2026-09-14 11:41:15 +08:00
boris 93de28d369 docs(perf): record combined risk and series storage Backtest release 2026-09-14 11:14:59 +08:00
boris 665653c3fe 完善手工回放的最终费用和真实观察时间合同 2026-09-14 10:15:03 +08:00
boris a29c434be9 docs(perf): reconcile storage acceptance with current business main 2026-09-14 10:04:00 +08:00
boris 4c96d0c31f Merge remote-tracking branch 'origin/main' 2026-09-14 09:15:58 +08:00
boris 5e11f3da22 建立手工成交观察合同与原子回放游标 2026-09-14 07:30:19 +08:00
boris 8e7ae69b0b 归档回报与最新目标的配套发布及生产回测证据 2026-09-14 06:25:35 +08:00
boris 81acc54228 修复回报上下文与盘前意图并在提交前采用最新完整目标 2026-09-14 06:06:57 +08:00
boris 600808b171 归档日内时钟配套发布与九次历史执行验收 2026-09-14 04:26:14 +08:00
boris d2f1b64af1 记录时钟候选验证与磁盘保护后的正式缓存清理 2026-09-14 04:06:18 +08:00
boris 9a54156df9 docs(data): record DayOpen correctness and exact-column memory acceptance 2026-09-14 04:00:41 +08:00
boris 237ee15a51 修复日内时钟提前记账并按原订单续执行算法单 2026-09-14 03:52:52 +08:00
boris 996b909608 perf(data): retain exact repeated and reference-matched series columns 2026-09-14 03:31:28 +08:00
boris c62ae1206f fix(data): use day-open values for day-open rolling averages 2026-09-14 03:18:56 +08:00
boris 3a3091a2cf docs(perf): record small selection CPU savings and full shared-input parity 2026-09-14 03:05:55 +08:00
boris d2aa16a2f0 perf(risk): avoid per-symbol selection checks when the frozen policy has none 2026-09-14 02:18:24 +08:00
boris 0576cf9b6d 记录日内晚窗口提前影响早间持仓的隔离反例 2026-09-14 02:08:20 +08:00
boris 636e0dfd05 记录卖出回报续买修复的真实回放与配套发布 2026-09-14 00:10:58 +08:00
boris c98bcc3eb2 修复股票池卖单回报后未继续执行买入阶段 2026-09-13 23:43:26 +08:00
boris 53af3a6a85 perf(data): support exact reservation for known numeric field additions 2026-09-13 22:12:40 +08:00
boris 70c6f7e90b fix(data): expose actual snapshot row counts without cloning data 2026-09-13 17:11:06 +08:00
boris 0ed6752a73 perf(engine): deduplicate daily factor names before allocating sorted output 2026-09-13 16:47:16 +08:00
boris 3e8cc63b9a Revert "perf(engine): share immutable daily factor schemas and numeric buffers"
This reverts commit 5d0823c060bfd2a42a3f86a381e874004ab7f6af.
2026-09-13 14:16:07 +08:00
boris be171683c9 Revert "test(engine): retain static schema names across shared factor rows"
This reverts commit ce0dc0a106f0a98230bb9c428537ec086b968273.
2026-09-13 14:16:07 +08:00
boris 0a6fab9038 Revert "perf(engine): keep empty numeric maps on a direct lookup path"
This reverts commit a63dd94045f3a4b95dbfc917d5d8afa5c22f1897.
2026-09-13 14:16:07 +08:00
boris e8abf43cd4 perf(engine): keep empty numeric maps on a direct lookup path 2026-09-13 13:49:20 +08:00
boris 2286bfa757 test(engine): retain static schema names across shared factor rows 2026-09-13 13:07:52 +08:00
boris 93809eea1b perf(engine): share immutable daily factor schemas and numeric buffers 2026-09-13 13:02:17 +08:00
boris f7f0ff2951 Merge remote-tracking branch 'origin/main' 2026-09-13 11:37:38 +08:00
boris effa0c6456 test(engine): validate quote demand across different account capital 2026-09-13 11:21:09 +08:00
boris b1ca2dfada fix(engine): resolve decision quote scope from the actual run context 2026-09-13 11:18:39 +08:00
boris d15abc18ae test(engine): reproduce account-sensitive quote scope bypass 2026-09-13 11:07:57 +08:00
boris 9370dfe6e9 Merge remote-tracking branch 'origin/main' 2026-09-13 11:06:22 +08:00
boris b19108558f 统一仓位调整时点并保留策略择时与退出意图 2026-09-13 11:05:12 +08:00
boris fe05384f80 test(data): compare compact names by value and allocation class 2026-09-13 09:57:47 +08:00
boris 0ff90c4329 refactor(data): use explicit string views for compact numeric names 2026-09-13 09:55:16 +08:00
boris c85daae608 perf(data): inline numeric factor keys and preserve borrowed static names 2026-09-13 09:53:06 +08:00
boris f73513e2d4 Revert "perf(data): sort compact market-cap keys without revisiting wide snapshots"
This reverts commit 4e953b6e98.
2026-09-13 07:33:05 +08:00
boris 4e953b6e98 perf(data): sort compact market-cap keys without revisiting wide snapshots 2026-09-13 07:12:42 +08:00
boris b232847e40 docs(strategy): record feature-date gap reproduction and replay acceptance 2026-09-13 06:34:22 +08:00
boris e3b3929578 test(strategy): supply declared historical prices in next-open fixture 2026-09-13 06:09:49 +08:00
boris 20e73d567b fix(strategy): require exact feature-date market snapshots 2026-09-13 06:06:18 +08:00
boris cf4498668b test(strategy): qualify the expected missing-market error 2026-09-13 06:03:54 +08:00
boris 3f39943ee4 test(strategy): reject execution-day fallback for missing decision-day market 2026-09-13 05:59:40 +08:00
boris 5c65e65c6f perf(data): parallelize bounded daily symbol indices 2026-09-13 03:25:37 +08:00
boris f3c70ea566 docs(perf): record expression context CPU and real HTTP parity evidence 2026-09-13 01:13:44 +08:00
boris 07b7b181b6 perf(strategy): retain compact factor values in expression contexts 2026-09-13 00:50:07 +08:00
boris fe7243bbc3 perf(data): normalize owned daily bundles in parallel with stable errors 2026-09-12 19:30:28 +08:00
boris 875e31f71f fix(stock-pool): separate cumulative condition facts from quote capacity 2026-09-12 18:50:12 +08:00
boris 61bd14d001 fix(backtest): inspect the authoritative strategy inside runner bundles 2026-09-12 16:55:16 +08:00
boris 531df39911 chore(test): remove obsolete tree-map import after compact snapshot merge 2026-09-12 16:29:56 +08:00
boris e912e2a6f5 Merge remote-tracking branch 'origin/main' 2026-09-12 16:26:06 +08:00
boris b1f2fcb85c fix(stock-pool): scan decoded native conditions instead of serialized source echoes 2026-09-12 16:23:18 +08:00
boris 828690a51e perf(data): compact immutable snapshots with current execution rules 2026-09-12 16:02:24 +08:00
boris ffd23b9920 fix(stock-pool): preserve exit roles and freeze relative reduction bases 2026-09-12 15:54:33 +08:00
boris 4ac9ee5058 fix(stock-pool): keep full stops stronger than simultaneous reductions 2026-09-12 14:21:33 +08:00
boris 848c1a514a fix(stock-pool): separate exit ownership and ordinary quote dependencies 2026-09-12 12:18:26 +08:00
boris 099759ae67 fix(stock-pool): execute verified ETF daily fallbacks with frozen next-open targets 2026-09-12 10:47:47 +08:00
boris d646ca455d merge: integrate causal capacity model with current order clocks and intent planning 2026-09-12 08:52:37 +08:00
boris 3b2a97fa84 fix(backtest): keep next-open limit remainders active on the execution day 2026-09-12 08:02:41 +08:00
boris 7ac87a90c4 test: keep snapshot price-risk fixtures in their explicit daily model 2026-09-12 06:51:33 +08:00
boris 5949d4cc69 refactor: remove retired daily quota parameters from target diagnostics 2026-09-12 06:48:07 +08:00
boris 24cb4805a7 fix: keep decision estimates independent and use timed capacity in fill fixtures 2026-09-12 06:46:49 +08:00
boris 053f880e34 test: declare historical audit assumptions in non-liquidity fixtures 2026-09-12 06:39:34 +08:00
boris 4d3a9e0e5b fix: separate historical session capacity audits from execution sizing 2026-09-12 06:32:10 +08:00
139 changed files with 45456 additions and 3003 deletions
Generated
+30
View File
@@ -145,6 +145,15 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.2.59"
@@ -192,6 +201,20 @@ dependencies = [
"windows-link",
]
[[package]]
name = "compact_str"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"serde",
"static_assertions",
"zmij",
]
[[package]]
name = "const-random"
version = "0.1.18"
@@ -316,6 +339,7 @@ version = "0.1.0"
dependencies = [
"ahash 0.8.12",
"chrono",
"compact_str",
"indexmap",
"rayon",
"rhai",
@@ -2044,3 +2068,9 @@ dependencies = [
"quote",
"syn 3.0.5",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+1
View File
@@ -6,6 +6,7 @@ license.workspace = true
authors.workspace = true
[dependencies]
compact_str = { version = "=0.10.0", features = ["serde"] }
rust_decimal.workspace = true
ahash.workspace = true
chrono.workspace = true
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,778 @@
use super::*;
fn time(minute: u32) -> NaiveTime {
NaiveTime::from_hms_opt(10, minute, 0).unwrap()
}
fn data(quotes: &[(u32, f64, u32)]) -> DataSet {
data_with_snapshot(quotes, limit_test_snapshot())
}
fn data_with_snapshot(quotes: &[(u32, f64, u32)], snapshot: DailyMarketSnapshot) -> DataSet {
DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![snapshot],
vec![],
vec![limit_test_candidate(true, true)],
vec![limit_test_benchmark()],
vec![],
quotes
.iter()
.map(|&(minute, price, volume)| {
let mut quote = limit_test_quote(price, price, price);
quote.timestamp = quote.date.and_time(time(minute));
quote.volume_delta = u64::from(volume);
quote.amount_delta = price * f64::from(volume);
quote.bid1_volume = u64::from(volume / 100);
quote.ask1_volume = u64::from(volume / 100);
quote
})
.collect(),
)
.unwrap()
}
fn broker() -> BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks> {
BrokerSimulator::new(
ChinaAShareCostModel::default()
.with_commission_rate(0.0003)
.with_minimum_commission(5.),
ChinaEquityRuleHooks,
)
.with_matching_type(MatchingType::MinuteLast)
.with_execution_price_field(PriceField::Last)
.with_intraday_execution_start_time(time(0))
.with_volume_limit(true)
.with_volume_percent(0.25)
.with_liquidity_limit(false)
.with_inactive_limit(false)
.with_strict_value_budget(true)
}
fn intent(style: AlgoOrderStyle, value: f64) -> StrategyDecision {
StrategyDecision {
order_intents: vec![OrderIntent::AlgoValue {
symbol: "000001.SZ".into(),
value,
style,
start_time: Some(time(0)),
end_time: Some(time(10)),
reason: "clock-algorithm".into(),
}],
..Default::default()
}
}
fn step(
broker: &BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks>,
portfolio: &mut PortfolioState,
data: &DataSet,
minute: u32,
decision: &StrategyDecision,
) -> BrokerExecutionReport {
broker
.execute_between(
limit_test_snapshot().date,
portfolio,
data,
decision,
Some(time(minute)),
Some(time(minute)),
)
.unwrap()
}
#[test]
fn twap_clock_preserves_quantity_prices_fees_budget_and_parent_order() {
let data = data(&[
(0, 10., 4_000),
(2, 10.1, 4_000),
(5, 10.2, 4_000),
(10, 10.3, 4_000),
]);
let decision = intent(AlgoOrderStyle::Twap, 10_000.);
let mut synchronous_account = PortfolioState::new(20_000.);
let reference = broker()
.execute(
limit_test_snapshot().date,
&mut synchronous_account,
&data,
&decision,
)
.unwrap();
let broker = broker();
let mut account = PortfolioState::new(20_000.);
let mut fills = Vec::new();
let mut events = Vec::new();
let empty = StrategyDecision::default();
for minute in [0, 2, 5, 10] {
let batch = step(
&broker,
&mut account,
&data,
minute,
if minute == 0 { &decision } else { &empty },
);
assert!(
batch
.fill_events
.iter()
.all(|fill| fill.execution_timestamp.unwrap().time() <= time(minute))
);
fills.extend(batch.fill_events);
events.extend(batch.order_events);
}
let canonical = |rows: &[crate::events::FillEvent]| {
rows.iter()
.map(|fill| {
(
fill.quantity,
fill.price.to_bits(),
fill.commission.to_bits(),
fill.stamp_tax.to_bits(),
fill.transfer_fee.to_bits(),
fill.execution_timestamp,
fill.order_id,
)
})
.collect::<Vec<_>>()
};
assert_eq!(canonical(&fills), canonical(&reference.fill_events));
assert_eq!(account.cash(), synchronous_account.cash());
assert_eq!(fills.iter().map(|fill| fill.quantity).sum::<u32>(), 900);
assert_eq!(fills.iter().map(|fill| fill.commission).sum::<f64>(), 5.);
assert!(fills.iter().map(|fill| -fill.net_cash_flow).sum::<f64>() <= 10_000.);
assert!(events.iter().all(|event| event.order_id == Some(1)));
assert_eq!(events.last().unwrap().status, OrderStatus::Filled);
assert!(broker.open_order_views().is_empty());
}
#[test]
fn partial_algorithm_cancel_releases_reservation_and_never_executes_the_remainder() {
let data = data(&[
(0, 10., 4_000),
(2, 10., 4_000),
(5, 10., 4_000),
(10, 10., 4_000),
]);
let broker = broker();
let mut account = PortfolioState::new(20_000.);
step(
&broker,
&mut account,
&data,
0,
&intent(AlgoOrderStyle::Twap, 10_000.),
);
assert_eq!(broker.open_order_views()[0].reserved_cash, Some(10_000.));
let partial = step(
&broker,
&mut account,
&data,
2,
&StrategyDecision::default(),
);
assert_eq!(
partial
.fill_events
.iter()
.map(|fill| fill.quantity)
.sum::<u32>(),
100
);
let working = broker.open_order_views();
assert_eq!(working[0].order_id, 1);
assert_eq!(working[0].filled_quantity, 100);
assert_eq!(
working[0].reserved_cash,
Some(10_000. + partial.fill_events[0].net_cash_flow)
);
let cancel = step(
&broker,
&mut account,
&data,
3,
&StrategyDecision {
order_intents: vec![OrderIntent::CancelAll {
reason: "explicit-user-cancel".into(),
}],
..Default::default()
},
);
assert!(cancel.fill_events.is_empty());
assert_eq!(
cancel.order_events.last().unwrap().status,
OrderStatus::Canceled
);
assert_eq!(cancel.order_events.last().unwrap().filled_quantity, 100);
assert!(broker.open_order_views().is_empty());
assert!(
step(
&broker,
&mut account,
&data,
10,
&StrategyDecision::default()
)
.fill_events
.is_empty()
);
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
}
#[test]
fn algorithm_expiry_without_a_quote_does_not_reuse_old_liquidity() {
let data = data(&[(0, 10., 4_000), (2, 10., 4_000)]);
let broker = broker();
let mut account = PortfolioState::new(20_000.);
step(
&broker,
&mut account,
&data,
0,
&intent(AlgoOrderStyle::Twap, 10_000.),
);
step(
&broker,
&mut account,
&data,
2,
&StrategyDecision::default(),
);
assert_eq!(
broker.next_day_order_expiry(limit_test_snapshot().date),
Some(time(10))
);
let terminal = step(
&broker,
&mut account,
&data,
10,
&StrategyDecision::default(),
);
assert!(terminal.fill_events.is_empty());
assert_eq!(
terminal.order_events.last().unwrap().status,
OrderStatus::Expired
);
assert_eq!(terminal.order_events.last().unwrap().filled_quantity, 100);
assert!(
terminal
.process_events
.iter()
.any(|event| event.detail.contains("Expired")),
"{:?}",
terminal.process_events
);
assert!(broker.open_order_views().is_empty());
}
#[test]
fn separate_buy_cannot_spend_the_working_algorithms_cash_budget() {
let data = data(&[
(0, 10., 4_000),
(1, 10., 4_000),
(2, 10., 4_000),
(10, 10., 4_000),
]);
let broker = broker();
let mut account = PortfolioState::new(11_000.);
step(
&broker,
&mut account,
&data,
0,
&intent(AlgoOrderStyle::Twap, 10_000.),
);
let other = step(
&broker,
&mut account,
&data,
1,
&StrategyDecision {
order_intents: vec![OrderIntent::Shares {
symbol: "000001.SZ".into(),
quantity: 1_000,
reason: "separate-buy".into(),
}],
..Default::default()
},
);
assert!(
other.fill_events.is_empty(),
"cash reserved for order 1 was spent: {:?}",
other.fill_events
);
let final_batch = step(
&broker,
&mut account,
&data,
10,
&StrategyDecision::default(),
);
assert!(
final_batch
.fill_events
.iter()
.all(|fill| fill.order_id == Some(1))
);
assert_eq!(account.position("000001.SZ").unwrap().quantity, 900);
assert!(account.cash() >= 1_000.);
}
#[test]
fn changing_the_later_daily_close_does_not_resize_an_algorithm_submitted_now() {
let quotes = [(0, 10., 4_000), (2, 10.1, 4_000), (10, 10.2, 4_000)];
let mut changed = limit_test_snapshot();
changed.close = 100.;
changed.last_price = 100.;
let run = |data: DataSet| {
let broker = broker();
let mut account = PortfolioState::new(20_000.);
let initial = step(
&broker,
&mut account,
&data,
0,
&intent(AlgoOrderStyle::Twap, 10_000.),
);
assert!(initial.fill_events.is_empty());
let quantity = broker.open_order_views()[0].requested_quantity;
let final_batch = step(
&broker,
&mut account,
&data,
10,
&StrategyDecision::default(),
);
(
quantity,
final_batch
.fill_events
.iter()
.map(|fill| {
(
fill.quantity,
fill.price.to_bits(),
fill.net_cash_flow.to_bits(),
)
})
.collect::<Vec<_>>(),
)
};
assert_eq!(
run(data(&quotes)),
run(data_with_snapshot(&quotes, changed))
);
}
#[test]
fn vwap_clock_preserves_cash_costs_and_does_not_spend_future_volume() {
let data = data(&[
(0, 10., 400),
(2, 10., 800),
(5, 10., 1_200),
(10, 10., 4_000),
]);
let decision = intent(AlgoOrderStyle::Vwap, 10_000.);
let mut synchronous_account = PortfolioState::new(20_000.);
let reference = broker()
.execute(
limit_test_snapshot().date,
&mut synchronous_account,
&data,
&decision,
)
.unwrap();
let broker = broker();
let mut account = PortfolioState::new(20_000.);
let empty = StrategyDecision::default();
let mut filled = 0;
let mut commission = 0.;
for (minute, expected) in [(0, 100), (2, 300), (5, 600), (10, 900)] {
let batch = step(
&broker,
&mut account,
&data,
minute,
if minute == 0 { &decision } else { &empty },
);
filled += batch
.fill_events
.iter()
.map(|fill| fill.quantity)
.sum::<u32>();
commission += batch
.fill_events
.iter()
.map(|fill| fill.commission)
.sum::<f64>();
assert_eq!(filled, expected);
assert!(batch.fill_events.iter().all(|fill| fill.order_id == Some(1)
&& fill.execution_timestamp.unwrap().time() <= time(minute)));
}
assert_eq!(account.cash(), synchronous_account.cash());
assert_eq!(
commission,
reference
.fill_events
.iter()
.map(|fill| fill.commission)
.sum::<f64>()
);
assert!(broker.open_order_views().is_empty());
}
#[test]
fn global_vwap_matching_keeps_the_same_working_order_between_clock_ticks() {
let data = data(&[(0, 10., 400), (2, 10., 400), (10, 10., 4_000)]);
let broker = broker().with_matching_type(MatchingType::Vwap);
let mut account = PortfolioState::new(20_000.);
let first = step(
&broker,
&mut account,
&data,
0,
&StrategyDecision {
order_intents: vec![OrderIntent::Shares {
symbol: "000001.SZ".into(),
quantity: 900,
reason: "configured-vwap".into(),
}],
..Default::default()
},
);
assert_eq!(
first
.fill_events
.iter()
.map(|fill| fill.quantity)
.sum::<u32>(),
100
);
assert_eq!(
broker.open_order_views().len(),
1,
"{:?}",
first.order_events
);
let second = step(
&broker,
&mut account,
&data,
2,
&StrategyDecision::default(),
);
assert_eq!(second.fill_events[0].quantity, 100);
assert_eq!(second.fill_events[0].order_id, Some(1));
let final_batch = step(
&broker,
&mut account,
&data,
10,
&StrategyDecision::default(),
);
assert_eq!(final_batch.fill_events[0].quantity, 700);
assert_eq!(final_batch.fill_events[0].order_id, Some(1));
assert!(broker.open_order_views().is_empty());
}
#[test]
fn algorithm_sell_honors_t_plus_one_and_keeps_original_quantity_after_partial_fills() {
let data = data(&[(0, 10., 400), (2, 10., 800), (10, 10., 4_000)]);
let date = limit_test_snapshot().date;
for acquired_today in [false, true] {
let broker = broker();
let mut account = PortfolioState::new(20_000.);
account.position_mut("000001.SZ").buy(
if acquired_today {
date
} else {
date.pred_opt().unwrap()
},
1_000,
10.,
);
let decision = intent(AlgoOrderStyle::Vwap, -10_000.);
let mut fills = Vec::new();
let mut events = Vec::new();
let empty = StrategyDecision::default();
for minute in [0, 2, 10] {
let batch = step(
&broker,
&mut account,
&data,
minute,
if minute == 0 { &decision } else { &empty },
);
fills.extend(batch.fill_events);
events.extend(batch.order_events);
}
assert_eq!(
fills.iter().map(|fill| fill.quantity).sum::<u32>(),
if acquired_today { 0 } else { 1_000 }
);
assert!(events.iter().all(|event| event.order_id == Some(1)));
if !acquired_today {
assert_eq!(events.last().unwrap().status, OrderStatus::Filled);
assert_eq!(events.last().unwrap().requested_quantity, 1_000);
assert_eq!(events.last().unwrap().filled_quantity, 1_000);
}
assert!(broker.open_order_views().is_empty());
}
}
#[test]
fn an_explicit_ioc_or_fok_does_not_become_a_persistent_algorithm() {
let data = data(&[(0, 10., 400), (2, 10., 4_000), (10, 10., 4_000)]);
for tif in [
OrderTimeInForce::Ioc,
OrderTimeInForce::Fok,
OrderTimeInForce::Day,
OrderTimeInForce::Gtc,
] {
let broker = broker();
let mut account = PortfolioState::new(20_000.);
let mut decision = intent(AlgoOrderStyle::Vwap, 10_000.);
if !decision.order_intents[0].supports_time_in_force(tif) {
decision.order_intents = decision
.order_intents
.into_iter()
.map(|intent| intent.with_time_in_force(tif))
.collect();
let error = broker
.execute_between(
limit_test_snapshot().date,
&mut account,
&data,
&decision,
Some(time(0)),
Some(time(0)),
)
.unwrap_err();
assert!(
error
.to_string()
.contains("is not supported for this order intent")
);
assert_eq!(account.cash(), 20_000.);
assert!(broker.open_order_views().is_empty());
continue;
}
decision.order_intents = decision
.order_intents
.into_iter()
.map(|intent| intent.with_time_in_force(tif))
.collect();
let first = step(&broker, &mut account, &data, 0, &decision);
let persists = matches!(tif, OrderTimeInForce::Day | OrderTimeInForce::Gtc);
assert_eq!(
!broker.open_order_views().is_empty(),
persists,
"{tif:?}: {:?}",
first.order_events
);
if !persists {
assert!(
step(
&broker,
&mut account,
&data,
10,
&StrategyDecision::default()
)
.fill_events
.is_empty()
);
}
}
}
#[test]
fn two_working_algorithms_reserve_only_real_cash_without_starving_the_first() {
let data = data(&[(0, 10., 40_000), (10, 10., 40_000)]);
let broker = broker();
let mut account = PortfolioState::new(15_000.);
let mut decision = intent(AlgoOrderStyle::Twap, 10_000.);
decision
.order_intents
.extend(intent(AlgoOrderStyle::Twap, 10_000.).order_intents);
step(&broker, &mut account, &data, 0, &decision);
assert_eq!(
broker
.open_order_views()
.iter()
.map(|order| order.reserved_cash.unwrap())
.collect::<Vec<_>>(),
vec![10_000., 5_000.]
);
let report = step(
&broker,
&mut account,
&data,
10,
&StrategyDecision::default(),
);
assert_eq!(
report
.fill_events
.iter()
.map(|fill| (fill.order_id, fill.quantity))
.collect::<Vec<_>>(),
vec![(Some(1), 900), (Some(2), 500)]
);
assert!(account.cash() >= 0.);
assert!(broker.open_order_views().is_empty());
}
#[test]
fn a_clock_slice_does_not_turn_window_twap_into_an_unlimited_instant_order() {
let data = data(&[(0, 10., 100), (2, 10., 100), (10, 10.1, 100)]);
let broker = broker()
.with_volume_limit(false)
.with_liquidity_limit(false);
let mut account = PortfolioState::new(20_000.);
step(
&broker,
&mut account,
&data,
0,
&intent(AlgoOrderStyle::Twap, 10_000.),
);
let first = step(
&broker,
&mut account,
&data,
2,
&StrategyDecision::default(),
);
let last = step(
&broker,
&mut account,
&data,
10,
&StrategyDecision::default(),
);
assert_eq!(
first
.fill_events
.iter()
.map(|fill| fill.quantity)
.sum::<u32>(),
100
);
assert_eq!(
last.fill_events
.iter()
.map(|fill| fill.quantity)
.sum::<u32>(),
100
);
assert_eq!(
last.order_events.last().unwrap().status,
OrderStatus::Expired
);
assert_eq!(last.order_events.last().unwrap().filled_quantity, 200);
assert!(broker.open_order_views().is_empty());
}
#[test]
fn non_matching_controls_amend_or_cancel_without_filling_a_crossing_quote() {
let data = data(&[(0, 10., 4_000), (2, 9.4, 4_000)]);
let broker = broker();
let mut account = PortfolioState::new(20_000.);
step(
&broker,
&mut account,
&data,
0,
&StrategyDecision {
order_intents: vec![
OrderIntent::LimitShares {
symbol: "000001.SZ".into(),
quantity: 100,
limit_price: 9.5,
reason: "resting".into(),
}
.with_time_in_force(OrderTimeInForce::Gtc),
],
..Default::default()
},
);
assert_eq!(broker.open_order_views().len(), 1);
let modify = broker
.execute_controls_without_matching(
limit_test_snapshot().date,
limit_test_snapshot().date,
&mut account,
&data,
&StrategyDecision {
order_intents: vec![OrderIntent::ModifyOrder {
order_id: 1,
new_total_quantity: Some(200),
new_limit_price: Some(9.3),
reason: "pre-open-amend".into(),
}],
..Default::default()
},
Some(time(2)),
)
.unwrap();
assert!(modify.fill_events.is_empty());
assert_eq!(broker.open_order_views()[0].limit_price, 9.3);
assert_eq!(broker.open_order_views()[0].requested_quantity, 200);
let cancel = broker
.execute_controls_without_matching(
limit_test_snapshot().date,
limit_test_snapshot().date,
&mut account,
&data,
&StrategyDecision {
order_intents: vec![OrderIntent::CancelAll {
reason: "pre-open-cancel".into(),
}],
..Default::default()
},
Some(time(2)),
)
.unwrap();
assert!(cancel.fill_events.is_empty());
assert_eq!(
cancel.order_events.last().unwrap().status,
OrderStatus::Canceled
);
assert_eq!(account.cash(), 20_000.);
assert!(broker.open_order_views().is_empty());
}
#[test]
fn control_only_phase_cannot_be_used_to_submit_an_order_or_leave_matching_disabled() {
let data = data(&[(0, 10., 4_000)]);
let broker = broker();
let mut account = PortfolioState::new(20_000.);
let submit = StrategyDecision {
order_intents: vec![OrderIntent::Shares {
symbol: "000001.SZ".into(),
quantity: 100,
reason: "normal-order".into(),
}],
..Default::default()
};
assert!(
broker
.execute_controls_without_matching(
limit_test_snapshot().date,
limit_test_snapshot().date,
&mut account,
&data,
&submit,
Some(time(0))
)
.is_err()
);
assert_eq!(account.cash(), 20_000.);
assert_eq!(
step(&broker, &mut account, &data, 0, &submit).fill_events[0].quantity,
100
);
}
@@ -0,0 +1,516 @@
// Kept inside broker::tests to inspect internal accepted-order identity as
// well as the public report. These are simulator states, never GT requests.
fn recovery_test_data(missing_previous: Option<usize>, intraday: bool) -> DataSet {
let previous = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
let mut instruments = Vec::new();
let mut rows = Vec::new();
let mut candidates = Vec::new();
let mut quotes = Vec::new();
for index in 1..=2 {
let symbol = format!("{index:06}.SZ");
let mut instrument = limit_test_instrument();
instrument.symbol = symbol.clone();
instruments.push(instrument);
for day in [previous, date] {
if day == previous && missing_previous == Some(index) {
continue;
}
let mut row = dated_limit_test_snapshot(day);
row.symbol = symbol.clone().into();
rows.push(row);
let mut candidate = dated_limit_test_candidate(day, false, false, true, true);
candidate.symbol = symbol.clone().into();
candidates.push(candidate);
}
if intraday {
let mut quote = limit_test_quote(10., 10., 10.);
quote.symbol = symbol;
quote.date = date;
quote.timestamp = date.and_hms_opt(9, 33, 0).unwrap();
quotes.push(quote);
}
}
DataSet::from_components_with_actions_and_quotes(
instruments,
rows,
vec![],
candidates,
vec![
dated_limit_test_benchmark(previous),
dated_limit_test_benchmark(date),
],
vec![],
quotes,
)
.unwrap()
}
fn recovery_test_broker(
intraday: bool,
first_side: OrderSide,
) -> (
BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks>,
PortfolioState,
) {
let mut broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_matching_type(if intraday {
MatchingType::MinuteLast
} else {
MatchingType::CurrentBarClose
})
.with_volume_limit(false)
.with_liquidity_limit(false)
.with_slippage_model(SlippageModel::HistoricalVolumeVolatility(
super::DynamicSlippageConfig::new(0., 0., 0.1),
));
if intraday {
broker =
broker.with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 33, 0).unwrap());
}
let mut first = test_open_order(1);
first.filled_quantity = 100;
first.remaining_quantity = 100;
first.commission_remaining = Some(0.);
first.side = first_side;
let mut second = test_open_order(2);
second.symbol = "000002.SZ".into();
broker.upsert_open_order(first);
broker.upsert_open_order(second);
broker.next_order_id.set(3);
let mut account = PortfolioState::new(9000.);
account.position_mut("000001.SZ").buy(
chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
if first_side == OrderSide::Buy {
100
} else {
200
},
10.,
);
account.begin_trading_day();
(broker, account)
}
fn recovery_report_value(report: &BrokerExecutionReport) -> serde_json::Value {
serde_json::json!({"orders":report.order_events,"fills":report.fill_events,
"positions":report.position_events,"accounts":report.account_events,
"events":report.process_events,"diagnostics":report.diagnostics})
}
#[test]
fn failed_resting_order_batch_keeps_accepted_orders_and_unpublished_financial_state() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
for intraday in [false, true] {
for first_side in [OrderSide::Buy, OrderSide::Sell] {
for missing in [1, 2] {
let (broker, mut account) = recovery_test_broker(intraday, first_side);
let orders = format!("{:?}", broker.open_orders.borrow());
let ledger = account.financial_replay_identity();
let error = broker
.execute(
date,
&mut account,
&recovery_test_data(Some(missing), intraday),
&StrategyDecision::default(),
)
.unwrap_err();
assert!(
error
.to_string()
.contains("historical_slippage_calibration_missing")
);
assert_eq!(
format!("{:?}", broker.open_orders.borrow()),
orders,
"intraday={intraday} first={first_side:?} missing={missing}"
);
assert_eq!(account.financial_replay_identity(), ledger);
assert!(broker.same_day_sold_symbols.borrow().is_empty());
let recovered = broker
.execute(
date,
&mut account,
&recovery_test_data(None, intraday),
&StrategyDecision::default(),
)
.unwrap();
let (clean, mut clean_account) = recovery_test_broker(intraday, first_side);
let reference = clean
.execute(
date,
&mut clean_account,
&recovery_test_data(None, intraday),
&StrategyDecision::default(),
)
.unwrap();
assert_eq!(
recovery_report_value(&recovered),
recovery_report_value(&reference)
);
assert_eq!(
account.financial_replay_identity(),
clean_account.financial_replay_identity()
);
assert!(broker.open_order_views().is_empty());
assert_eq!(recovered.fill_events.len(), 2);
}
}
}
}
#[test]
fn failed_new_batch_does_not_erase_prior_success_or_double_charge_on_retry() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
let (broker, mut account) = recovery_test_broker(true, OrderSide::Buy);
let good = recovery_test_data(None, true);
let prior = broker
.execute(date, &mut account, &good, &StrategyDecision::default())
.unwrap();
assert_eq!(prior.fill_events.len(), 2);
let initial = account.financial_replay_identity();
let id = broker.next_order_id.get();
let decision = StrategyDecision {
order_intents: vec![
OrderIntent::Shares {
symbol: "000001.SZ".into(),
quantity: 100,
reason: "next-batch-a".into(),
},
OrderIntent::Shares {
symbol: "000002.SZ".into(),
quantity: 100,
reason: "next-batch-b".into(),
},
],
..Default::default()
};
// A later quote lets this batch execute independently of the prior fills.
let mut parts = good.snapshot_components();
for quote in &mut parts.execution_quotes {
quote.timestamp += chrono::Duration::minutes(1);
}
broker
.runtime_execution_clock
.set(Some(NaiveTime::from_hms_opt(9, 34, 0).unwrap()));
let restored = DataSet::from_components_with_actions_and_quotes(
parts.instruments.clone(),
parts.market.clone(),
parts.factors.clone(),
parts.candidates.clone(),
parts.benchmarks.clone(),
vec![],
parts.execution_quotes.clone(),
)
.unwrap();
parts
.market
.retain(|row| !(row.symbol.as_str() == "000002.SZ" && row.date < date));
let broken = DataSet::from_components_with_actions_and_quotes(
parts.instruments,
parts.market,
parts.factors,
parts.candidates,
parts.benchmarks,
vec![],
parts.execution_quotes,
)
.unwrap();
assert!(
broker
.execute(date, &mut account, &broken, &decision)
.is_err()
);
assert_eq!(account.financial_replay_identity(), initial);
assert_eq!(broker.next_order_id.get(), id);
assert!(broker.open_orders.borrow().is_empty());
let result = broker
.execute(date, &mut account, &restored, &decision)
.unwrap();
assert_eq!(result.fill_events.len(), 2);
assert_eq!(result.fill_events[0].order_id, Some(id));
assert_eq!(result.fill_events[1].order_id, Some(id + 1));
assert_eq!(account.position("000001.SZ").unwrap().quantity, 300);
assert_eq!(account.position("000002.SZ").unwrap().quantity, 300);
assert_eq!(
prior.fill_events.len(),
2,
"previously returned report remains intact"
);
}
#[test]
fn unwinding_an_unpublished_simulator_transaction_restores_its_state() {
let (broker, mut account) = recovery_test_broker(false, OrderSide::Sell);
let initial = account.financial_replay_identity();
let orders = format!("{:?}", broker.open_orders.borrow());
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = broker.execution_transaction(&mut account, true, None, |account| {
account.apply_cash_delta(500.).unwrap();
broker.open_orders.borrow_mut().clear();
panic!("isolated simulator callback unwind");
});
}));
assert!(result.is_err());
assert_eq!(account.financial_replay_identity(), initial);
assert_eq!(format!("{:?}", broker.open_orders.borrow()), orders);
assert_eq!(broker.execution_transaction_depth.get(), 0);
}
#[test]
fn deferred_etf_batch_failure_keeps_both_targets_and_prior_generation_progress() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
let previous = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let mut parts = recovery_test_data(None, false).snapshot_components();
for instrument in &mut parts.instruments {
instrument.board = "ETF".into();
}
let good = DataSet::from_components_with_actions_and_quotes(
parts.instruments.clone(),
parts.market.clone(),
parts.factors.clone(),
parts.candidates.clone(),
parts.benchmarks.clone(),
vec![],
vec![],
)
.unwrap();
parts
.market
.retain(|row| !(row.date == date && row.symbol.as_str() == "000002.SZ"));
let bad = DataSet::from_components_with_actions_and_quotes(
parts.instruments,
parts.market,
parts.factors,
parts.candidates,
parts.benchmarks,
vec![],
vec![],
)
.unwrap();
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_limit(false)
.with_liquidity_limit(false);
let members = std::sync::Arc::new(
(1..=2)
.map(|index| crate::stock_pool_execution::StockPoolMemberSpec {
symbol: format!("{index:06}.SZ"),
requested_order: index,
recommendation_reason: String::new(),
target_weight_bps: None,
stop_loss: None,
take_profit: None,
})
.collect::<Vec<_>>(),
);
broker
.deferred_etf_targets
.borrow_mut()
.replace_generation("pool", "latest");
for index in 1..=2 {
broker
.deferred_etf_targets
.borrow_mut()
.upsert(crate::etf_execution::DeferredEtfTarget {
pool_id: "pool".into(),
generation: "latest".into(),
symbol: format!("{index:06}.SZ"),
signal_date: previous,
signal_at: previous.and_hms_opt(13, 0, 0).unwrap(),
execute_on: Some(date),
target_value: 1000.into(),
target_weight_bps: 5000,
target_weight_ratio: None,
side: crate::stock_pool_execution::OrderSide::Buy,
max_positions: 2,
rule: Default::default(),
members: std::sync::Arc::clone(&members),
reason: "deferred recovery fixture".into(),
});
}
let queue = format!("{:?}", broker.deferred_etf_targets.borrow());
let mut account = PortfolioState::new(10000.);
let state = account.stock_pool_execution_state("pool");
assert!(
broker
.execute_deferred_etf_targets(date, &mut account, &bad)
.is_err()
);
assert_eq!(account.cash(), 10000.);
assert!(account.positions().is_empty());
assert_eq!(account.stock_pool_execution_state("pool"), state);
assert_eq!(format!("{:?}", broker.deferred_etf_targets.borrow()), queue);
assert_eq!(broker.next_order_id.get(), 1);
assert_eq!(broker.execution_transaction_depth.get(), 0);
let result = broker
.execute_deferred_etf_targets(date, &mut account, &good)
.unwrap();
assert_eq!(result.fill_events.len(), 2, "{result:?}");
assert_eq!(broker.pending_etf_target_count(), 0);
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
assert_eq!(account.position("000002.SZ").unwrap().quantity, 100);
}
#[test]
fn public_callback_unwind_does_not_leak_order_context_or_authoritative_prior_state() {
struct PanicRules;
impl crate::rules::EquityRuleHooks for PanicRules {
fn can_buy(
&self,
_: chrono::NaiveDate,
_: &DailyMarketSnapshot,
_: &CandidateEligibility,
_: PriceField,
) -> crate::rules::RuleCheck {
panic!("isolated rule callback panic")
}
fn can_sell(
&self,
_: chrono::NaiveDate,
_: &DailyMarketSnapshot,
_: &CandidateEligibility,
_: &crate::portfolio::Position,
_: PriceField,
) -> crate::rules::RuleCheck {
unreachable!()
}
}
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
let prior = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), PanicRules)
.with_volume_limit(false)
.with_liquidity_limit(false);
broker.runtime_decision_date.set(Some(prior));
broker
.runtime_buy_denials
.borrow_mut()
.insert("unrelated".into(), "prior".into());
let mut account = PortfolioState::new(10000.);
let decision = StrategyDecision {
buy_denials: BTreeMap::from([("another".into(), "temporary".into())]),
order_intents: vec![
OrderIntent::LimitShares {
symbol: "000001.SZ".into(),
quantity: 100,
limit_price: 10.,
reason: "panic fixture".into(),
}
.with_time_in_force(OrderTimeInForce::Gtc),
],
..Default::default()
};
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = broker.execute(
date,
&mut account,
&recovery_test_data(None, false),
&decision,
);
}))
.is_err()
);
assert_eq!(account.cash(), 10000.);
assert!(account.positions().is_empty());
assert!(broker.open_order_views().is_empty());
assert_eq!(broker.runtime_decision_date.get(), Some(prior));
assert_eq!(
*broker.runtime_buy_denials.borrow(),
BTreeMap::from([("unrelated".into(), "prior".into())])
);
assert_eq!(broker.runtime_time_in_force.get(), None);
assert_eq!(broker.runtime_target_position_limit.get(), None);
assert_eq!(broker.execution_transaction_depth.get(), 0);
}
#[test]
fn simulator_transaction_profile_preserves_successful_output() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
let previous = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let mut instruments = Vec::new();
let mut market = Vec::new();
let mut candidates = Vec::new();
for index in 1..=30 {
let symbol = format!("{index:06}.SZ");
let mut instrument = limit_test_instrument();
instrument.symbol = symbol.clone();
instruments.push(instrument);
for day in [previous, date] {
let mut row = dated_limit_test_snapshot(day);
row.symbol = symbol.clone().into();
market.push(row);
let mut row = dated_limit_test_candidate(day, false, false, true, true);
row.symbol = symbol.clone().into();
candidates.push(row);
}
}
let data = DataSet::from_components(
instruments,
market,
vec![],
candidates,
vec![
dated_limit_test_benchmark(previous),
dated_limit_test_benchmark(date),
],
)
.unwrap();
let mut reference = None;
let mut samples = Vec::new();
for protected in [false, true, true, false] {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_matching_type(MatchingType::CurrentBarClose)
.with_volume_limit(false)
.with_liquidity_limit(false);
// Private comparison only: no runtime option can disable protection.
if !protected {
broker.execution_transaction_depth.set(1);
}
let mut account = PortfolioState::new(10_000_000.);
for index in 1..=30 {
for _ in 0..20 {
account
.position_mut(&format!("{index:06}.SZ"))
.buy(previous, 100, 10.);
}
}
account.begin_trading_day();
let mut orders = Vec::new();
let mut fills = Vec::new();
let start = std::time::Instant::now();
for index in 0..500 {
let report = broker
.execute(
date,
&mut account,
&data,
&StrategyDecision {
order_intents: vec![OrderIntent::Shares {
symbol: format!("{:06}.SZ", index % 30 + 1),
quantity: 100,
reason: "transaction profile".into(),
}],
..Default::default()
},
)
.unwrap();
orders.extend(report.order_events);
fills.extend(report.fill_events);
}
samples.push(
serde_json::json!({"protected":protected,"microseconds":start.elapsed().as_micros()}),
);
assert_eq!(fills.len(), 500);
let outcome = serde_json::json!({"orders":orders,"fills":fills,"ledger":account.financial_replay_identity()});
if let Some(reference) = &reference {
assert_eq!(&outcome, reference);
} else {
reference = Some(outcome);
}
}
println!(
"simulator_transaction_profile={}",
serde_json::json!({"securities":30,"initial_lots_per_security":20,"calls":500,"samples":samples,
"scope":"isolated broker only; not Source or full backtest throughput"})
);
}
+423 -46
View File
@@ -3,6 +3,35 @@ use super::*;
use crate::holding_policy::HoldingLifecycleEvidence;
use crate::stock_pool_execution as pool;
use rust_decimal::{Decimal, prelude::ToPrimitive};
use chrono::Timelike;
#[derive(Debug, Clone)]
pub(super) struct DeferredStockPoolExecution {
date: NaiveDate,
contract: Box<pool::FrozenStockPoolIntent>,
buy_only: bool,
symbols: BTreeSet<String>,
initial_holdings: BTreeSet<String>,
}
impl<C, R> BrokerSimulator<C, R> {
pub(crate) fn pending_stock_pool_symbols(&self) -> BTreeSet<String> {
self.deferred_stock_pools.borrow().values().flat_map(|pending| pending.symbols.iter().cloned()).collect()
}
pub(crate) fn has_pending_stock_pool_execution(&self) -> bool {
!self.deferred_stock_pools.borrow().is_empty()
}
pub(crate) fn finish_stock_pool_session(&self, date: NaiveDate, report: &mut BrokerExecutionReport) {
self.deferred_stock_pools.borrow_mut().retain(|_, pending| {
if pending.date <= date {
report.diagnostics.push(format!("stock_pool_unsubmitted_phase_expired generation={} date={date} no_buy_order_created=true",pending.contract.generation));
false
} else { true }
});
}
}
fn decimal(value: f64, label: &str) -> Result<Decimal, BacktestError> {
if !value.is_finite() {
@@ -16,6 +45,11 @@ fn decimal(value: f64, label: &str) -> Result<Decimal, BacktestError> {
.map_err(|_| BacktestError::Execution(format!("stock_pool_decimal_range_{label}")))
}
fn etf_activity(report:&mut BrokerExecutionReport,date:NaiveDate,symbol:&str,side:pool::OrderSide,detail:String) {
report.process_events.push(ProcessEvent {date,kind:ProcessEventKind::EtfExecutionFallback,order_id:None,
symbol:Some(symbol.into()),side:Some(if side==pool::OrderSide::Buy {OrderSide::Buy} else {OrderSide::Sell}),detail});
}
fn pool_positions(
portfolio: &PortfolioState,
date: NaiveDate,
@@ -35,15 +69,141 @@ fn pool_positions(
.collect()
}
#[cfg(test)]
mod successor_protection_tests {
use super::*;
use crate::holding_policy::{AutomaticTradeLock, AutomaticTradeProtection};
fn day(n: u32) -> NaiveDate { NaiveDate::from_ymd_opt(2026, 9, n).unwrap() }
#[test]
fn deferred_etf_open_rechecks_inherited_locks_and_cooldown_before_any_order() {
let old = "159915.SZ";
let new = "159999.SZ";
let data = DataSet::from_components(
[old, new].into_iter().map(|symbol| crate::Instrument {
symbol: symbol.into(), name: "isolated ETF fixture".into(), board: "ETF".into(), round_lot: 100,
listed_at: Some(day(1)), delisted_at: None, status: "active".into(),
}).collect(), vec![crate::DailyMarketSnapshot {
date: day(15), symbol: new.into(), timestamp: None, day_open: 5., open: 5., high: 5., low: 5.,
close: 5., last_price: 5., bid1: 5., ask1: 5., prev_close: 5., volume: 100000,
minute_volume: 0, bid1_volume: 100000, ask1_volume: 100000, trading_phase: None,
paused: false, upper_limit: 5.5, lower_limit: 4.5, price_tick: 0.001,
}], vec![], vec![crate::CandidateEligibility {
date: day(15), symbol: new.into(), is_st: false, is_star_st: false, is_new_listing: false,
is_paused: false, allow_buy: true, allow_sell: true, is_kcb: false, is_one_yuan: false,
risk_level_code: None,
}], [11,14,15].into_iter().map(|n| crate::BenchmarkSnapshot {
date: day(n), benchmark: "000300.SH".into(), open: 100., close: 100., prev_close: 100., volume: 10000,
}).collect()).unwrap();
for mode in ["lock", "cooldown", "expired"] {
let broker = BrokerSimulator::new(crate::ChinaAShareCostModel::default(), crate::ChinaEquityRuleHooks)
.with_volume_limit(false).with_liquidity_limit(false);
let mut portfolio = PortfolioState::new(10000.);
portfolio.position_mut(old).buy(day(11), 200, 10.);
portfolio.position_mut(old).sell(100, 10.).unwrap();
broker.mark_same_day_sold(day(11), old);
portfolio.apply_successor_conversion(old, new, 2., 0.).unwrap();
let policy = AutomaticTradeProtection {
sell_cooldown_days: if mode == "cooldown" { 3 } else { 0 },
locks: if mode != "cooldown" { vec![AutomaticTradeLock {
symbol: old.into(), start_date: day(11), end_date: Some(day(if mode == "expired" {14} else {15})),
}] } else { vec![] }, ..Default::default()
};
let rule = pool::StockPoolExecutionRule { automatic_trade_protection: policy, ..Default::default() };
broker.deferred_etf_targets.borrow_mut().replace_generation("pool", "latest");
broker.deferred_etf_targets.borrow_mut().upsert(crate::etf_execution::DeferredEtfTarget {
pool_id: "pool".into(), generation: "latest".into(), symbol: new.into(),
signal_date: day(14), signal_at: day(14).and_hms_opt(13,0,0).unwrap(), execute_on: Some(day(15)),
target_value: 5000.into(), target_weight_bps: 10000, target_weight_ratio:None, side: pool::OrderSide::Buy, max_positions: 1,
rule: std::sync::Arc::new(rule), members: std::sync::Arc::new(vec![pool::StockPoolMemberSpec {
symbol: new.into(), requested_order: 0, recommendation_reason: String::new(),
target_weight_bps: None, stop_loss: None, take_profit: None,
}]), reason: "isolated deferred ETF target".into(),
});
let report = broker.execute_deferred_etf_targets(day(15), &mut portfolio, &data).unwrap();
if mode == "expired" {
assert_eq!(report.fill_events.len(), 1, "{report:?}");
assert_eq!(portfolio.position(new).unwrap().quantity, 1000);
} else {
assert!(report.order_events.is_empty(), "{mode}: {report:?}");
assert!(report.fill_events.is_empty());
assert_eq!(portfolio.position(new).unwrap().quantity, 200);
assert!(report.diagnostics.iter().any(|text| text.contains(if mode == "lock" {"automatic_trade_locked"} else {"sell_fill_cooldown"})));
}
}
}
}
impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
fn pool_automatic_permission(&self, symbol: &str, date: NaiveDate,
policy: &crate::holding_policy::AutomaticTradeProtection,
portfolio: &PortfolioState, data: &DataSet)
-> Result<crate::holding_policy::AutomaticTradePermission, BacktestError> {
let position = portfolio.position(symbol).filter(|position| position.quantity > 0);
let sold = self.same_day_sold_symbols.borrow().iter().rev()
.find(|(day, symbols)| **day <= date && (symbols.contains(symbol)
|| portfolio.corporate_predecessors(symbol).any(|previous| symbols.contains(previous))))
.map(|(day, _)| *day);
let evidence = HoldingLifecycleEvidence {
has_position: position.is_some(), opened_date: position.and_then(|position| position.opened_date()),
last_buy_date: position.and_then(|position| position.last_buy_date()), last_sell_date: sold,
};
policy.evaluate_with_predecessors(symbol, date, &evidence, data.calendar(),
portfolio.corporate_predecessors(symbol)).map_err(BacktestError::Execution)
}
pub(super) fn resume_stock_pool_executions(&self, date: NaiveDate, portfolio: &mut PortfolioState, data: &DataSet,
session: &mut BrokerExecutionSession, report: &mut BrokerExecutionReport) -> Result<(), BacktestError> {
let clock = self.runtime_intraday_start_time.get().or(self.intraday_execution_start_time);
let mut expired = Vec::new();
for (id, pending) in self.deferred_stock_pools.borrow().iter() {
let end = NaiveTime::parse_from_str(&pending.contract.rule.window_end, "%H:%M")
.map_err(|_| BacktestError::Execution("stock_pool_execution_window_invalid".into()))?;
if pending.date != date || clock.is_some_and(|clock| clock >= end) { expired.push(id.clone()); }
}
for id in expired {
if let Some(pending) = self.deferred_stock_pools.borrow_mut().remove(&id) {
report.diagnostics.push(format!("stock_pool_unsubmitted_phase_expired generation={} date={date} no_buy_order_created=true",pending.contract.generation));
}
}
if self.has_open_orders() || clock.is_none() { return Ok(()); }
let pending = std::mem::take(&mut *self.deferred_stock_pools.borrow_mut());
for (id, pending) in pending {
let now = clock.expect("clock checked above");
let start = NaiveTime::parse_from_str(&pending.contract.rule.window_start, "%H:%M")
.map_err(|_| BacktestError::Execution("stock_pool_execution_window_invalid".into()))?;
if now < start || !pool::stock_pool_is_trading_minute(now.hour() * 60 + now.minute()) {
self.deferred_stock_pools.borrow_mut().insert(id, pending);
continue;
}
let prior_followup = self.runtime_stock_pool_followup.replace(true);
let prior_decision = self.runtime_decision_date.replace(Some(pending.contract.signal_date));
let prior_created = self.runtime_order_created_date.replace(Some(date));
let order_start = report.order_events.len();
let fill_start = report.fill_events.len();
report.diagnostics.push(format!("stock_pool_resume_after_order_reports generation={} clock={} cash={}",pending.contract.generation,clock.unwrap(),portfolio.cash()));
let result = self.process_stock_pool_contract_phase(date, portfolio, data, &pending.contract,
&mut session.intraday_turnover, &mut session.execution_cursors, &mut session.global_execution_cursor,
&mut session.commission_state, report, pending.buy_only, Some(&pending.initial_holdings));
self.runtime_stock_pool_followup.set(prior_followup);
self.runtime_decision_date.set(prior_decision);
self.runtime_order_created_date.set(prior_created);
result?;
Self::annotate_report_range(report, order_start, fill_start, pending.contract.signal_date, date, date);
}
Ok(())
}
fn pool_quote_inputs(
&self,
date: NaiveDate,
data: &DataSet,
symbols: &BTreeSet<String>,
execution_clock: Option<NaiveDateTime>,
) -> Result<Vec<pool::MarketSnapshot>, BacktestError> {
symbols
cumulative_conditions: bool,
) -> Result<(Vec<pool::MarketSnapshot>, Vec<String>), BacktestError> {
let mut unavailable = Vec::new();
let quotes = symbols
.iter()
.map(|symbol| {
let snapshot = data.market(date, symbol).ok_or_else(|| {
@@ -54,7 +214,13 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
let instrument = data.instruments().get(symbol).ok_or_else(|| {
BacktestError::Execution(format!("stock_pool_instrument_missing:{symbol}"))
})?;
let (price, prev, volume, amount, bid, ask, buy_price, sell_price) = if self
let fallback = self.pool_etf_fallback_reference(date, data, symbol, execution_clock)?;
let (price, prev, volume, amount, bid, ask, buy_price, sell_price) = if let Some(reference) = fallback {
let calibration = self.slippage_calibration(data, snapshot)?;
(reference.price, snapshot.prev_close, None, None, None, None,
self.quote_execution_price(snapshot, OrderSide::Buy, reference.price, None, calibration.as_ref())?,
self.quote_execution_price(snapshot, OrderSide::Sell, reference.price, None, calibration.as_ref())?)
} else if self
.matching_type_uses_intraday_quotes()
{
let time = self
@@ -89,7 +255,7 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
snapshot,
quote,
OrderSide::Buy,
self.matching_type,
self.matching_type_for_algo_request(None),
)
.ok_or_else(|| {
BacktestError::Execution(format!(
@@ -101,7 +267,7 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
snapshot,
quote,
OrderSide::Sell,
self.matching_type,
self.matching_type_for_algo_request(None),
)
.ok_or_else(|| {
BacktestError::Execution(format!(
@@ -123,11 +289,17 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
None,
calibration.as_ref(),
)?;
let totals = if cumulative_conditions {
match data.execution_session_totals(symbol, clock) {
Ok(totals) => Some(totals),
Err(reason) => { unavailable.push(reason); None }
}
} else { None };
(
quote.last_price,
snapshot.prev_close,
Some(quote.volume_delta as f64),
Some(quote.amount_delta),
totals.map(|total| total.0),
totals.map(|total| total.1),
Some(quote.bid1),
Some(quote.ask1),
buy,
@@ -142,13 +314,24 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
}
// A daily open does not reveal the session's volume/turnover.
let completed = self.effective_execution_price_field(date) == PriceField::Close;
let totals = if cumulative_conditions && !completed {
let at = execution_clock.unwrap_or_else(|| date.and_hms_opt(9,30,0).unwrap());
match data.execution_session_totals(symbol, at) {
Ok(totals) => Some(totals),
Err(reason) => { unavailable.push(reason); None }
}
} else { None };
let amount = if completed && cumulative_conditions {
data.factor(date, symbol).and_then(|row| row.extra_factors.get("amount")).copied()
.map(|value| decimal(value, "amount")).transpose()?
} else { totals.map(|total| total.1) };
(
price,
snapshot.prev_close,
completed.then_some(snapshot.volume as f64),
if completed { Some(Decimal::from(snapshot.volume)) } else { totals.map(|total| total.0) },
amount,
None,
None,
Some(price),
Some(price),
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?,
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, None)?,
)
@@ -157,8 +340,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
symbol: symbol.clone(),
last_price: decimal(price, "price")?,
prev_close: Some(decimal(prev, "prev_close")?),
volume: volume.map(|v| decimal(v, "volume")).transpose()?,
turnover: amount.map(|v| decimal(v, "amount")).transpose()?,
volume,
turnover: amount,
bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?,
ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?,
is_kcb: Some(instrument.board.eq_ignore_ascii_case("KSH")),
@@ -171,7 +354,18 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
sell_sizing_price: Some(decimal(sell_price, "sell_price")?),
})
})
.collect()
.collect::<Result<Vec<_>, BacktestError>>()?;
Ok((quotes, unavailable))
}
fn pool_etf_fallback_reference(&self, date: NaiveDate, data: &DataSet, symbol: &str, clock: Option<NaiveDateTime>) -> Result<Option<crate::etf_execution::EtfFallbackReference>, BacktestError> {
if !self.matching_type_uses_intraday_quotes() || !self.has_verified_etf_minute_absence(date, symbol) {
return Ok(None);
}
let time = self.runtime_intraday_start_time.get().or(self.intraday_execution_start_time)
.ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: execution clock missing".into()))?;
let at = clock.unwrap_or(date.and_time(time)).max(date.and_time(time));
crate::etf_execution::reference(data, symbol, at).map(Some)
}
pub(super) fn process_stock_pool_contract(
@@ -185,6 +379,17 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
global_execution_cursor: &mut Option<NaiveDateTime>,
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
self.process_stock_pool_contract_phase(date, portfolio, data, contract, intraday_turnover,
execution_cursors, global_execution_cursor, commission_state, report, false, None)
}
fn process_stock_pool_contract_phase(
&self, date: NaiveDate, portfolio: &mut PortfolioState, data: &DataSet,
contract: &pool::FrozenStockPoolIntent, intraday_turnover: &mut BTreeMap<String, u32>,
execution_cursors: &mut IntradayExecutionLedger, global_execution_cursor: &mut Option<NaiveDateTime>,
commission_state: &mut BTreeMap<u64, f64>, report: &mut BrokerExecutionReport, buy_only: bool,
initial_holdings: Option<&BTreeSet<String>>,
) -> Result<(), BacktestError> {
if contract.signal_date > date
|| contract.frozen_equity < Decimal::ZERO
@@ -225,6 +430,7 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
.cloned()
.collect::<BTreeSet<_>>();
scope.extend(portfolio.positions().keys().cloned());
let before_positions = initial_holdings.cloned().unwrap_or_else(|| portfolio.positions().keys().cloned().collect());
let official_dates = data.calendar().iter().collect::<Vec<_>>();
let initial_positions = pool_positions(portfolio, date)?;
let state = portfolio
@@ -240,7 +446,12 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
portfolio
.set_stock_pool_execution_state(&contract.pool_id, state)
.map_err(BacktestError::Execution)?;
let superseded = self.deferred_etf_targets.borrow_mut().replace_generation(&contract.pool_id, &contract.generation);
if superseded > 0 { report.diagnostics.push(format!("etf_daily_open_fallback:superseded pool={} generation={} targets={superseded}", contract.pool_id, contract.generation)); }
if self.has_open_orders() {
self.deferred_stock_pools.borrow_mut().insert(contract.pool_id.clone(), DeferredStockPoolExecution {
date, contract: Box::new(contract.clone()), buy_only, symbols: scope, initial_holdings: before_positions,
});
report
.diagnostics
.push("stock_pool_waiting_for_active_orders no_new_intent=true".into());
@@ -286,14 +497,31 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
.push("paused".into());
}
}
let before_positions = portfolio
.positions()
.keys()
.cloned()
.collect::<BTreeSet<_>>();
// All delayed symbols in a generation share immutable configuration.
// Do not duplicate an N-member pool N times in a large mixed pool.
let mut deferred_configuration = None;
for side in [pool::OrderSide::Sell, pool::OrderSide::Buy] {
let quotes =
self.pool_quote_inputs(date, data, &quote_scope, *global_execution_cursor)?;
if buy_only && side == pool::OrderSide::Sell { continue; }
if side == pool::OrderSide::Buy && self.has_open_orders()
&& self.effective_rebalance_cash_mode() == RebalanceCashMode::SellThenBuy {
self.deferred_stock_pools.borrow_mut().insert(contract.pool_id.clone(), DeferredStockPoolExecution {
date, contract: Box::new(contract.clone()), buy_only: true, symbols: quote_scope.clone(), initial_holdings: before_positions.clone(),
});
report.diagnostics.push(format!("stock_pool_waiting_for_sell_reports generation={} no_buy_order_created=true",contract.generation));
break;
}
let mut fallback_references = BTreeMap::new();
for symbol in &quote_scope {
if let Some(reference) = self.pool_etf_fallback_reference(date, data, symbol, *global_execution_cursor)? {
let condition = if side == pool::OrderSide::Buy { &contract.rule.buy_condition } else { &contract.rule.sell_condition };
if !condition.trim().is_empty() {
return Err(BacktestError::Execution(format!("etf_daily_open_fallback: intraday condition evidence unavailable symbol={symbol} side={side:?}; daily reference is not a minute or tick signal")));
}
fallback_references.insert(symbol.clone(), reference);
}
}
let (quotes, unavailable) = self.pool_quote_inputs(date, data, &quote_scope, *global_execution_cursor,
crate::stock_pool_quote_facts::requires_session_totals(&contract.rule))?;
let positions = pool_positions(portfolio, date)?;
let execution_state = portfolio
.stock_pool_execution_state(&contract.pool_id)
@@ -307,6 +535,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
.map_err(BacktestError::Execution)?;
constraints.pending_entry_symbols = execution_state.pending_symbols();
constraints.prior_target_weights = execution_state.last_target_weights.clone();
constraints.prior_target_weight_ratios = execution_state.last_target_weight_ratios.clone();
constraints.position_action_bases = execution_state.position_action_bases_for(&contract.generation);
constraints.next_day_outside_exit_symbols = execution_state.next_day_exit_symbols(date);
let account = pool::AccountSnapshot {
total_equity: contract.frozen_equity,
@@ -316,30 +546,24 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
constraints.automatic_permissions.clear();
if contract.rule.automatic_trade_protection.enabled() {
for symbol in &scope {
let position = portfolio.position(symbol).filter(|p| p.quantity > 0);
let sold = self
.same_day_sold_symbols
.borrow()
.iter()
.rev()
.find(|(day, symbols)| **day <= date && symbols.contains(symbol))
.map(|(day, _)| *day);
let evidence = HoldingLifecycleEvidence {
has_position: position.is_some(),
opened_date: position.and_then(|p| p.opened_date()),
last_buy_date: position.and_then(|p| p.last_buy_date()),
last_sell_date: sold,
};
let permission = contract
.rule
.automatic_trade_protection
.evaluate(symbol, date, &evidence, data.calendar())
.map_err(BacktestError::Execution)?;
let permission = self.pool_automatic_permission(symbol, date,
&contract.rule.automatic_trade_protection, portfolio, data)?;
constraints
.automatic_permissions
.insert(symbol.clone(), permission);
}
}
if side == pool::OrderSide::Buy {
for (symbol, reference) in &fallback_references {
if !reference.immediate {
// The sell leg was queued, not filled. Keep its real
// holdings/slots and do not finance buys with proceeds
// from the following session.
constraints.automatic_permissions.entry(symbol.clone()).or_default()
.sell_denial.get_or_insert("etf_daily_open_deferred");
}
}
}
if self
.risk_config
.static_rules
@@ -400,10 +624,20 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
Decimal::ZERO,
Some(&fee),
)
.map_err(BacktestError::Execution)?;
let updated = execution_state
.map_err(|error| BacktestError::Execution(if !unavailable.is_empty()
&& (error.contains("requires volume") || error.contains("requires amount")) {
format!("{error}; {}", unavailable.join("; "))
} else { error }))?;
report.diagnostics.extend(unavailable.into_iter().map(|reason| format!("stock_pool_quote_fact_unavailable {reason}")));
let mut updated = execution_state
.record_plan(contract.signal_date, &contract.generation, &plan)
.map_err(BacktestError::Execution)?;
for (symbol, reference) in &fallback_references {
if !reference.immediate && let Some(entry) = updated.entries.get_mut(symbol) {
// The signal only fixes money, not shares at a stale close.
entry.completion_quantity = None;
}
}
portfolio
.set_stock_pool_execution_state(&contract.pool_id, updated)
.map_err(BacktestError::Execution)?;
@@ -425,6 +659,26 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
if row.side != Some(side) {
continue;
}
if let Some(reference) = fallback_references.get(&row.symbol) {
let time = self.runtime_intraday_start_time.get().or(self.intraday_execution_start_time).expect("fallback clock validated");
let at = global_execution_cursor.unwrap_or(date.and_time(time)).max(date.and_time(time));
if !reference.immediate {
report.diagnostics.push(format!("etf_daily_open_fallback:deferred symbol={} signal_at={at} reference_date={} reference_price={} target_value={} execute_on={:?}", row.symbol, reference.reference_date, reference.price, row.target_value, reference.execute_on));
let deferred = deferred_configuration.get_or_insert_with(|| (
std::sync::Arc::new(contract.rule.clone()), std::sync::Arc::new(members.clone()),
));
let opening_date=reference.execute_on.map(|day|day.to_string()).unwrap_or_else(||"回测区间外(后续日历未加载)".into());
etf_activity(report,date,&row.symbol,side,format!("ETF 顺延执行:信号 {at},参考 {} 收盘 {},目标金额 {},下一正式开盘日 {opening_date};未生成成交。",reference.reference_date,reference.price,row.target_value));
self.deferred_etf_targets.borrow_mut().upsert(crate::etf_execution::DeferredEtfTarget {
pool_id:contract.pool_id.clone(), generation:contract.generation.clone(), symbol:row.symbol.clone(),
signal_date:contract.signal_date, signal_at:at, execute_on:reference.execute_on,
target_value:row.target_value, target_weight_bps:row.target_weight_bps, target_weight_ratio:plan.target_weight_ratios.get(&row.symbol).copied(), side,
max_positions, rule:std::sync::Arc::clone(&deferred.0), members:std::sync::Arc::clone(&deferred.1),
reason:row.source_intent.clone().unwrap_or_else(||"stock_pool_target".into()),
});
continue;
}
}
if side == pool::OrderSide::Buy
&& portfolio
.position(&row.symbol)
@@ -440,8 +694,14 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
let target = row.target_quantity.to_i32().ok_or_else(|| {
BacktestError::Execution("stock_pool_target_quantity_out_of_range".into())
})?;
let reason = row.source_intent.as_deref().unwrap_or("stock_pool_target");
if let Some(price) = row.limit_price {
let fallback_reason = fallback_references.contains_key(&row.symbol).then(|| format!("{}: etf_daily_open_fallback signal_date={} execution_date={date}", row.source_intent.as_deref().unwrap_or("stock_pool_target"), contract.signal_date));
let reason = fallback_reason.as_deref().unwrap_or_else(|| row.source_intent.as_deref().unwrap_or("stock_pool_target"));
let first_fill = report.fill_events.len();
if fallback_references.contains_key(&row.symbol) {
report.diagnostics.push(format!("etf_daily_open_fallback:opening symbol={} signal_date={} execution_date={date}", row.symbol, contract.signal_date));
etf_activity(report,date,&row.symbol,side,format!("ETF 日线开盘回退:信号日 {},执行日 {date},使用正式日线开盘价;不是分钟成交行情。",contract.signal_date));
}
let mut execute = || if let Some(price) = row.limit_price {
self.process_limit_target_shares(
date,
portfolio,
@@ -457,7 +717,7 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
global_execution_cursor,
commission_state,
report,
)?;
)
} else {
self.process_target_shares(
date,
@@ -471,10 +731,127 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
global_execution_cursor,
commission_state,
report,
)?;
}
)
};
if fallback_references.contains_key(&row.symbol) {
self.with_etf_daily_open(execute)?;
for fill in &mut report.fill_events[first_fill..] {
fill.execution_start_timestamp.get_or_insert(date.and_time(crate::etf_execution::opening_time()));
fill.execution_timestamp.get_or_insert(date.and_time(crate::etf_execution::opening_time()));
}
} else { execute()?; }
}
}
Ok(())
}
pub(crate) fn pending_etf_target_count(&self) -> usize {
self.deferred_etf_targets.borrow().len()
}
/// Called at the opening clock, after settlement/corporate actions and
/// auction callbacks. It never sends a stock order or replays a strategy.
pub(crate) fn execute_deferred_etf_targets(&self, date: NaiveDate, portfolio: &mut PortfolioState, data: &DataSet) -> Result<BrokerExecutionReport, BacktestError> {
self.execution_transaction(portfolio, !self.has_open_orders() && self.pending_etf_target_count() > 0, None,
|portfolio| self.execute_deferred_etf_targets_inner(date, portfolio, data))
}
fn execute_deferred_etf_targets_inner(&self, date: NaiveDate, portfolio: &mut PortfolioState, data: &DataSet) -> Result<BrokerExecutionReport, BacktestError> {
let mut report = BrokerExecutionReport::default();
if self.has_open_orders() {
if self.pending_etf_target_count() > 0 {
report.diagnostics.push("etf_daily_open_fallback:waiting_for_active_orders".into());
}
return Ok(report);
}
let due = self.deferred_etf_targets.borrow_mut().take_due(date);
let dates = data.calendar().iter().collect::<Vec<_>>();
for target in due {
let instrument = data.instrument(&target.symbol).ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: instrument identity missing at execution".into()))?;
if !instrument.is_exchange_traded_fund() { return Err(BacktestError::Execution("etf_daily_open_fallback: instrument identity changed".into())); }
if let Some(reason) = instrument.dated_market_absence_reason(date) {
report.diagnostics.push(format!("etf_daily_open_fallback:blocked symbol={} date={date} reason={reason}", target.symbol));
continue;
}
let snapshot = data.market(date, &target.symbol).ok_or_else(|| BacktestError::Execution(format!("etf_daily_open_fallback: daily_open_missing symbol={} date={date}", target.symbol)))?;
if !snapshot.open.is_finite() || snapshot.open <= 0.0 {
return Err(BacktestError::Execution(format!("etf_daily_open_fallback: daily_open_invalid symbol={} date={date}", target.symbol)));
}
let position = portfolio.position(&target.symbol).filter(|p| p.quantity > 0);
let before_quantity = position.map_or(0, |p| p.quantity);
let permission = self.pool_automatic_permission(&target.symbol, date,
&target.rule.automatic_trade_protection, portfolio, data)?;
let denial = if target.side == pool::OrderSide::Buy {
permission.buy_denial.or(permission.max_holding_exit.then_some("max_holding_exit_pending"))
} else { permission.sell_denial };
if let Some(denial) = denial {
report.diagnostics.push(format!("etf_daily_open_fallback:protected symbol={} date={date} reason={denial}", target.symbol));
etf_activity(&mut report,date,&target.symbol,target.side,format!("ETF 顺延目标受持有保护限制:{denial};未提交委托。"));
continue;
}
if target.side == pool::OrderSide::Buy && before_quantity == 0 && Self::positive_position_count(portfolio) >= target.max_positions {
report.diagnostics.push(format!("etf_daily_open_fallback:blocked symbol={} reason=occupied_position_slots", target.symbol));
continue;
}
let value = target.target_value.to_f64().ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: target value out of range".into()))?;
let current_value = snapshot.open * f64::from(before_quantity);
let satisfied = (target.side == pool::OrderSide::Buy && value <= current_value)
|| (target.side == pool::OrderSide::Sell && value >= current_value);
let reason = format!("{}: etf_daily_open_fallback signal_at={} execution_at={} target_value={}", target.reason, target.signal_at, date.and_time(crate::etf_execution::opening_time()), target.target_value);
let mut sub = BrokerExecutionReport::default();
if !satisfied {
let (_, limit) = pool::resolve_stock_pool_order_price(&target.rule, &target.symbol, decimal(snapshot.open, "etf_open")?, target.side, decimal(snapshot.price_tick, "etf_tick")?).map_err(BacktestError::Execution)?;
let intent = match limit {
Some(limit) => OrderIntent::LimitTargetValue { symbol:target.symbol.clone(), target_value:value, limit_price:limit.to_f64().ok_or_else(|| BacktestError::Execution("ETF limit out of range".into()))?, reason:reason.clone() },
None => OrderIntent::TargetValue { symbol:target.symbol.clone(), target_value:value, reason:reason.clone() },
};
let old_time = self.runtime_intraday_start_time.replace(Some(crate::etf_execution::opening_time()));
let old_origin = self.runtime_resting_order_origin.replace(Some(RestingOrderOrigin { created_date:Some(target.signal_at.date()), submission_time:Some(target.signal_at.time()), accepted_date:date }));
let outcome = self.with_etf_daily_open(|| self.execute_with_event_dates(date, target.signal_date, target.signal_at.date(), portfolio, data, &StrategyDecision {
order_intents:vec![OrderIntent::WithTimeInForce { intent:Box::new(intent), time_in_force:OrderTimeInForce::Day }], ..Default::default()
}));
self.runtime_intraday_start_time.set(old_time);
self.runtime_resting_order_origin.set(old_origin);
sub = outcome?;
}
// The actual open determines the full requested shares. A clipped
// or rejected execution must not be recorded as completed entry.
let order = sub.order_events.iter().rev().find(|order| order.symbol == target.symbol);
let goal_quantity = order.map_or(before_quantity, |order| match order.side {
OrderSide::Buy => before_quantity.saturating_add(order.requested_quantity),
OrderSide::Sell => before_quantity.saturating_sub(order.requested_quantity),
});
let status = if satisfied || (order.is_none() && !self.has_open_orders()) { "BELOW_MINIMUM_TRADE_UNIT_ALREADY_SATISFIED" } else { "READY" };
let positions = pool_positions(portfolio, date)?;
let state = portfolio.stock_pool_execution_state(&target.pool_id)
.observe(target.signal_date, date, &dates, &target.members, &positions).map_err(BacktestError::Execution)?
.record_targets(target.signal_date, &target.generation, [crate::stock_pool_state::StockPoolGoalObservation {
symbol:&target.symbol, target_weight_bps:target.target_weight_bps, target_weight_ratio:target.target_weight_ratio, target_value:target.target_value,
current_quantity:before_quantity.into(), target_quantity:goal_quantity.into(), status,
}]).map_err(BacktestError::Execution)?
.observe(target.signal_date, date, &dates, &target.members, &positions).map_err(BacktestError::Execution)?;
portfolio.set_stock_pool_execution_state(&target.pool_id, state).map_err(BacktestError::Execution)?;
for fill in &mut sub.fill_events {
fill.decision_date.get_or_insert(target.signal_date);
fill.order_created_date.get_or_insert(target.signal_at.date());
fill.execution_date.get_or_insert(date);
fill.execution_start_timestamp.get_or_insert(date.and_time(crate::etf_execution::opening_time()));
fill.execution_timestamp.get_or_insert(date.and_time(crate::etf_execution::opening_time()));
}
for order in &mut sub.order_events {
order.decision_date.get_or_insert(target.signal_date);
order.order_created_date.get_or_insert(target.signal_at.date());
order.execution_date.get_or_insert(date);
}
report.diagnostics.push(reason);
etf_activity(&mut report,date,&target.symbol,target.side,format!("ETF 顺延目标开盘处理:原信号 {},本次 {date} 09:30,冻结目标金额 {},持仓 {before_quantity} → {};按本日开盘价、资金与风控重新定量。",target.signal_at,target.target_value,portfolio.position(&target.symbol).map_or(0,|position|position.quantity)));
report.order_events.extend(sub.order_events);
report.fill_events.extend(sub.fill_events);
report.position_events.extend(sub.position_events);
report.account_events.extend(sub.account_events);
report.process_events.extend(sub.process_events);
report.diagnostics.extend(sub.diagnostics);
}
Ok(report)
}
}
@@ -0,0 +1,684 @@
fn pool_batch_data() -> DataSet {
pool_batch_data_with(|_| true)
}
fn pool_batch_data_with(change: impl Fn(&mut IntradayExecutionQuote) -> bool) -> DataSet {
let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"];
let instruments = symbols
.iter()
.map(|symbol| Instrument {
symbol: (*symbol).into(),
..limit_test_instrument()
})
.collect();
let snapshots = symbols
.iter()
.map(|symbol| DailyMarketSnapshot {
symbol: (*symbol).into(),
..limit_test_snapshot()
})
.collect();
let candidates = symbols
.iter()
.map(|symbol| CandidateEligibility {
symbol: (*symbol).into(),
..limit_test_candidate(true, true)
})
.collect();
let mut quotes = Vec::new();
for minute in [30, 31, 32, 33, 34, 36] {
for symbol in symbols {
let price = if symbol == "000001.SZ" && minute > 30 {
10.5
} else {
10.0
};
let mut quote = limit_test_quote(price, price, price);
quote.symbol = symbol.into();
quote.timestamp = quote.date.and_hms_opt(9, minute, 0).unwrap();
quote.volume_delta = 200;
quote.bid1_volume = 200;
quote.ask1_volume = 200;
quote.amount_delta = price * 200.0;
if change(&mut quote) {
quotes.push(quote);
}
}
}
DataSet::from_components_with_actions_and_quotes(
instruments,
snapshots,
Vec::new(),
candidates,
vec![limit_test_benchmark()],
Vec::new(),
quotes,
)
.unwrap()
.with_additional_trading_dates([chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()])
}
fn pool_batch_decision(symbol: &str, generation: &str, end: &str) -> StrategyDecision {
use crate::stock_pool_execution as pool;
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let symbols = vec![symbol.to_owned()];
let rule = pool::StockPoolExecutionRule {
pricing_mode: pool::POOL_PRICE_FIXED_LIMIT.into(),
fixed_prices: [
("000001.SZ".into(), rust_decimal::Decimal::new(104, 1)),
("000002.SZ".into(), 10.into()),
("000003.SZ".into(), 10.into()),
]
.into(),
window_end: end.into(),
..Default::default()
};
StrategyDecision {
order_intents: vec![OrderIntent::StockPool {
contract: Box::new(pool::FrozenStockPoolIntent {
pool_id: "batch-test".into(),
signal_date: signal,
frozen_equity: 2000.into(),
selection: pool::StockPoolSelection {
trade_date: signal,
requested_symbols: symbols.clone(),
normal_trading_symbols: symbols.clone(),
risk_eligible_symbols: symbols.clone(),
final_symbols: symbols,
exclusion_reasons: Default::default(),
inherited_from_generation: None,
explicit_empty: false,
generation: Some(generation.into()),
},
members: vec![pool::StockPoolMemberSpec {
symbol: symbol.into(),
recommendation_reason: String::new(),
requested_order: 0,
target_weight_bps: None,
stop_loss: None,
take_profit: None,
}],
rule,
constraints: pool::StockPoolDecisionConstraints {
target_holding_count: Some(1),
..Default::default()
},
invest_ratio_bps: 10000,
reserve_cash: 0.into(),
out_of_pool_policy: "reduce_to_zero_when_sellable".into(),
generation: generation.into(),
}),
}],
..Default::default()
}
}
fn pool_batch_broker(partial: bool) -> BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks> {
let cost = ChinaAShareCostModel::from_trading_constraints(
crate::risk_control::TradingConstraintConfig {
commission_rate: 0.0,
minimum_commission: 0.0,
stamp_tax_rate_before_change: 0.0,
stamp_tax_rate_after_change: 0.0,
transfer_fee_rate: 0.0,
..Default::default()
},
);
let broker =
BrokerSimulator::new_with_execution_price(cost, ChinaEquityRuleHooks, PriceField::Open)
.with_matching_type(if partial {
MatchingType::MinuteLast
} else {
MatchingType::NextBarOpen
})
.with_volume_limit(partial)
.with_volume_percent(0.5)
.with_liquidity_limit(false)
.with_inactive_limit(false);
if partial {
broker
.with_intraday_execution_start_time(chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap())
} else {
broker
}
}
fn pool_batch_account() -> PortfolioState {
let mut account = PortfolioState::new(0.0);
account.position_mut("000001.SZ").buy(
chrono::NaiveDate::from_ymd_opt(2024, 12, 30).unwrap(),
200,
10.0,
);
account
}
fn pool_batch_tick(
broker: &BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks>,
account: &mut PortfolioState,
data: &DataSet,
minute: u32,
decision: &StrategyDecision,
) -> BrokerExecutionReport {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
broker
.runtime_intraday_start_time
.set(Some(chrono::NaiveTime::from_hms_opt(9, minute, 0).unwrap()));
broker
.runtime_intraday_end_time
.set(Some(chrono::NaiveTime::from_hms_opt(9, minute, 0).unwrap()));
broker.execute(date, account, data, decision).unwrap()
}
#[test]
fn stock_pool_pending_sell_continues_buy_after_actual_fill_without_strategy_rerun() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let data = pool_batch_data();
let broker = pool_batch_broker(false);
let mut account = pool_batch_account();
let initial = broker
.execute_with_event_dates(
date,
signal,
signal,
&mut account,
&data,
&pool_batch_decision("000002.SZ", "first", "09:35"),
)
.unwrap();
assert!(initial.fill_events.is_empty());
assert_eq!(broker.open_order_views().len(), 1);
assert_eq!(broker.open_order_views()[0].side, OrderSide::Sell);
let done = pool_batch_tick(
&broker,
&mut account,
&data,
31,
&StrategyDecision::default(),
);
assert!(
done.fill_events
.iter()
.any(|fill| fill.symbol == "000001.SZ" && fill.side == OrderSide::Sell)
);
assert_eq!(
account.position("000002.SZ").map(|p| p.quantity),
Some(200),
"sell proceeds must trigger the retained buy phase: {:?}",
done.diagnostics
);
assert!(
account
.position("000001.SZ")
.is_none_or(|p| p.quantity == 0)
);
let repeated = pool_batch_tick(
&broker,
&mut account,
&data,
32,
&StrategyDecision::default(),
);
assert!(repeated.order_events.is_empty() && repeated.fill_events.is_empty());
}
#[test]
fn stock_pool_partial_sell_waits_for_the_whole_batch_and_never_reissues_buys() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let data = pool_batch_data();
let broker = pool_batch_broker(true);
let mut account = pool_batch_account();
broker
.execute_with_event_dates(
date,
signal,
signal,
&mut account,
&data,
&pool_batch_decision("000002.SZ", "partial", "09:35"),
)
.unwrap();
let first = pool_batch_tick(
&broker,
&mut account,
&data,
31,
&StrategyDecision::default(),
);
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
assert!(account.position("000002.SZ").is_none());
assert!(
first
.order_events
.iter()
.all(|event| event.side == OrderSide::Sell)
);
let second = pool_batch_tick(
&broker,
&mut account,
&data,
32,
&StrategyDecision::default(),
);
let third = pool_batch_tick(
&broker,
&mut account,
&data,
33,
&StrategyDecision::default(),
);
assert_eq!(account.position("000002.SZ").unwrap().quantity, 200);
let ids = second
.order_events
.iter()
.chain(&third.order_events)
.filter(|event| event.side == OrderSide::Buy)
.filter_map(|event| event.order_id)
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
ids.len(),
1,
"one buy intention; partial reports must keep its ID"
);
assert!(
pool_batch_tick(
&broker,
&mut account,
&data,
34,
&StrategyDecision::default()
)
.order_events
.is_empty()
);
}
#[test]
fn stock_pool_delayed_sell_does_not_start_buys_after_the_configured_window() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let data = pool_batch_data();
let broker = pool_batch_broker(true);
let mut account = pool_batch_account();
broker
.execute_with_event_dates(
date,
signal,
signal,
&mut account,
&data,
&pool_batch_decision("000002.SZ", "expired", "09:32"),
)
.unwrap();
pool_batch_tick(
&broker,
&mut account,
&data,
31,
&StrategyDecision::default(),
);
let last = pool_batch_tick(
&broker,
&mut account,
&data,
32,
&StrategyDecision::default(),
);
assert!(account.position("000002.SZ").is_none());
assert!(
last.order_events
.iter()
.all(|event| event.side == OrderSide::Sell)
);
assert!(
last.diagnostics
.iter()
.any(|event| event.contains("unsubmitted_phase_expired"))
);
assert!(!broker.has_pending_stock_pool_execution());
}
#[test]
fn stock_pool_new_signal_supersedes_the_unsubmitted_buy_phase() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let data = pool_batch_data();
let broker = pool_batch_broker(true);
let mut account = pool_batch_account();
broker
.execute_with_event_dates(
date,
signal,
signal,
&mut account,
&data,
&pool_batch_decision("000002.SZ", "old", "09:35"),
)
.unwrap();
pool_batch_tick(
&broker,
&mut account,
&data,
31,
&pool_batch_decision("000003.SZ", "new", "09:35"),
);
pool_batch_tick(
&broker,
&mut account,
&data,
32,
&StrategyDecision::default(),
);
pool_batch_tick(
&broker,
&mut account,
&data,
33,
&StrategyDecision::default(),
);
assert!(account.position("000002.SZ").is_none());
assert_eq!(account.position("000003.SZ").unwrap().quantity, 200);
assert!(!broker.has_pending_stock_pool_execution());
}
#[test]
fn stock_pool_after_sell_uses_fresh_quotes_and_actual_submission_clock() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let data = pool_batch_data_with(|quote| {
if quote.symbol == "000002.SZ" {
quote.last_price = 10.2;
quote.bid1 = 10.2;
quote.ask1 = 10.2;
quote.amount_delta = 2040.0;
}
true
});
let broker = pool_batch_broker(false);
let mut account = pool_batch_account();
let mut decision = pool_batch_decision("000002.SZ", "fresh", "09:35");
if let OrderIntent::StockPool { contract } = &mut decision.order_intents[0] {
contract.rule.pricing_mode = crate::stock_pool_execution::POOL_PRICE_FORMULA_LIMIT.into();
contract.rule.sell_offset_bps = 400;
}
broker
.execute_with_event_dates(date, signal, signal, &mut account, &data, &decision)
.unwrap();
let result = pool_batch_tick(
&broker,
&mut account,
&data,
31,
&StrategyDecision::default(),
);
assert_eq!(
account.position("000002.SZ").unwrap().quantity,
100,
"2000/10.2 rounds to one 100-share lot, not 200 at stale open"
);
let fill = result
.fill_events
.iter()
.find(|fill| fill.symbol == "000002.SZ")
.unwrap();
assert_eq!(fill.price, 10.2);
assert_eq!(
fill.execution_start_timestamp,
Some(date.and_hms_opt(9, 31, 0).unwrap())
);
let event = result
.order_events
.iter()
.find(|event| event.side == OrderSide::Buy)
.unwrap();
assert_eq!(event.decision_date, Some(signal));
assert_eq!(event.order_created_date, Some(date));
}
#[test]
fn stock_pool_after_sell_rejects_missing_quote_instead_of_reusing_daily_open() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let data = pool_batch_data_with(|quote| quote.symbol != "000002.SZ");
let broker = pool_batch_broker(false);
let mut account = pool_batch_account();
broker
.execute_with_event_dates(
date,
signal,
signal,
&mut account,
&data,
&pool_batch_decision("000002.SZ", "missing", "09:35"),
)
.unwrap();
broker
.runtime_intraday_start_time
.set(Some(chrono::NaiveTime::from_hms_opt(9, 31, 0).unwrap()));
broker
.runtime_intraday_end_time
.set(Some(chrono::NaiveTime::from_hms_opt(9, 31, 0).unwrap()));
let error = broker
.execute(date, &mut account, &data, &StrategyDecision::default())
.unwrap_err();
assert!(
error
.to_string()
.contains("stock_pool_execution_quote_missing:000002.SZ"),
"{error}"
);
assert!(account.position("000002.SZ").is_none());
}
#[test]
fn stock_pool_delayed_take_profit_does_not_rebuy_the_same_generation_exit() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let data = pool_batch_data();
let broker = pool_batch_broker(false);
let mut account = PortfolioState::new(0.0);
account.position_mut("000001.SZ").buy(
chrono::NaiveDate::from_ymd_opt(2024, 12, 30).unwrap(),
200,
9.0,
);
let mut decision = pool_batch_decision("000002.SZ", "take-profit", "09:35");
if let OrderIntent::StockPool { contract } = &mut decision.order_intents[0] {
let symbols = vec!["000001.SZ".to_owned(), "000002.SZ".to_owned()];
contract.selection.requested_symbols = symbols.clone();
contract.selection.normal_trading_symbols = symbols.clone();
contract.selection.risk_eligible_symbols = symbols.clone();
contract.selection.final_symbols = symbols;
contract.constraints.target_holding_count = Some(2);
contract.members.insert(
0,
crate::stock_pool_execution::StockPoolMemberSpec {
symbol: "000001.SZ".into(),
recommendation_reason: String::new(),
requested_order: 0,
target_weight_bps: None,
stop_loss: None,
take_profit: Some(rust_decimal::Decimal::new(5, 2)),
},
);
contract.members[1].requested_order = 1;
}
broker
.execute_with_event_dates(date, signal, signal, &mut account, &data, &decision)
.unwrap();
let result = pool_batch_tick(
&broker,
&mut account,
&data,
31,
&StrategyDecision::default(),
);
assert!(
account
.position("000001.SZ")
.is_none_or(|p| p.quantity == 0)
);
assert_eq!(account.position("000002.SZ").unwrap().quantity, 200);
assert!(
!result
.order_events
.iter()
.any(|event| event.symbol == "000001.SZ" && event.side == OrderSide::Buy)
);
}
#[test]
fn stock_pool_pending_phase_cannot_cross_the_execution_session() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let data = pool_batch_data();
let broker = pool_batch_broker(false);
let mut account = pool_batch_account();
let mut report = broker
.execute_with_event_dates(
date,
signal,
signal,
&mut account,
&data,
&pool_batch_decision("000002.SZ", "end", "09:35"),
)
.unwrap();
assert!(broker.has_pending_stock_pool_execution());
broker.finish_stock_pool_session(date, &mut report);
assert!(!broker.has_pending_stock_pool_execution());
assert!(
report
.diagnostics
.iter()
.any(|event| event.contains("unsubmitted_phase_expired"))
);
assert_eq!(
broker.open_order_views().len(),
1,
"session cleanup preserves broker order history and remainder"
);
}
#[test]
fn stock_pool_engine_drives_the_pending_buy_without_a_minute_strategy_callback() {
use crate::{BacktestConfig, BacktestEngine, BacktestError, Strategy, StrategyContext};
struct DailyPool;
impl Strategy for DailyPool {
fn name(&self) -> &str {
"daily-pool-batch"
}
fn requires_minute_callbacks(&self) -> bool {
false
}
fn schedule_rules(&self) -> Vec<crate::ScheduleRule> {
vec![
crate::ScheduleRule::daily("open", crate::ScheduleStage::OnDay)
.with_time_rule(crate::ScheduleTimeRule::physical_time(9, 30)),
]
}
fn on_scheduled(
&mut self,
ctx: &StrategyContext<'_>,
_: &crate::ScheduleRule,
) -> Result<StrategyDecision, BacktestError> {
if ctx.execution_date.day() == 2 {
Ok(StrategyDecision {
order_intents: vec![OrderIntent::LimitTargetShares {
symbol: "000001.SZ".into(),
target_quantity: 200,
limit_price: 10.0,
reason: "initial-entry".into(),
}],
..Default::default()
})
} else {
Ok(pool_batch_decision("000002.SZ", "rotation", "09:35"))
}
}
fn on_minute(
&mut self,
_: &StrategyContext<'_>,
_: &IntradayExecutionQuote,
) -> Result<StrategyDecision, BacktestError> {
panic!("this daily strategy must not be rerun to continue a pending batch")
}
}
use chrono::Datelike;
let first = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let last = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
let base = pool_batch_data();
let mut market = Vec::new();
let mut factors = Vec::new();
let mut candidates = Vec::new();
let mut benchmarks = Vec::new();
let mut quotes = Vec::new();
for date in [first, last] {
for symbol in ["000001.SZ", "000002.SZ", "000003.SZ"] {
let mut row = base.market(first, symbol).unwrap().clone();
row.date = date;
market.push(row);
let mut row = base.candidate(first, symbol).unwrap().clone();
row.date = date;
candidates.push(row);
factors.push(crate::data::DailyFactorSnapshot {
date,
symbol: symbol.into(),
market_cap_bn: 10.,
free_float_cap_bn: 10.,
pe_ttm: 10.,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: Some(1.),
extra_factors: Default::default(),
});
for original in base.execution_quotes_on(first, symbol) {
let mut quote = original.clone();
quote.date = date;
quote.timestamp = date.and_time(original.timestamp.time());
quotes.push(quote);
}
}
let mut row = limit_test_benchmark();
row.date = date;
benchmarks.push(row);
}
let data = DataSet::from_components_with_actions_and_quotes(
base.instruments().values().cloned().collect(),
market,
factors,
candidates,
benchmarks,
Vec::new(),
quotes,
)
.unwrap()
.with_additional_trading_dates([chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()]);
let config = BacktestConfig {
initial_cash: 2000.0,
benchmark_code: "000852.SH".into(),
start_date: Some(first),
end_date: Some(last),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Open,
};
let result = BacktestEngine::new(data, DailyPool, pool_batch_broker(false), config)
.run()
.unwrap();
assert_eq!(
result.fills.len(),
3,
"initial buy, delayed sell, resumed buy: orders={:?} equity={:?}",
result.order_events,
result.equity_curve
);
assert_eq!(result.fills[2].symbol, "000002.SZ");
assert_eq!(result.fills[2].quantity, 200);
assert_eq!(
result.fills[2].execution_timestamp,
Some(last.and_hms_opt(9, 31, 0).unwrap())
);
assert_eq!(result.holdings_summary.len(), 1);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+272
View File
@@ -0,0 +1,272 @@
//! Run-local, receipt-bound model projections for a completed decision session.
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Component, Path};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::engine::BacktestError;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct DecisionProjectionContract {
pub contract_sha256: String,
pub fields: BTreeSet<String>,
pub input_cutoff_time: NaiveTime,
}
impl DecisionProjectionContract {
pub fn validate(&self) -> Result<(), BacktestError> {
require_sha256(&self.contract_sha256)?;
if self.fields.is_empty() || self.fields.iter().any(|field| !valid_projection_field(field)) {
return Err(error("projection fields must be explicit canonical numeric identifiers, not market or typed factors"));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct DecisionProjectionRequest {
pub contract_sha256: String,
pub decision_date: NaiveDate,
pub execution_date: NaiveDate,
pub as_of: NaiveDateTime,
pub symbols: BTreeSet<String>,
pub fields: BTreeSet<String>,
}
impl DecisionProjectionRequest {
pub fn sha256(&self) -> Result<String, BacktestError> {
json_sha256(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct DecisionProjectionRow {
pub symbol: String,
pub values: BTreeMap<String, f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct DecisionProjectionExclusion {
pub symbol: String,
pub classification: String,
pub reason: String,
pub source_sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct DecisionProjectionReference {
pub path: String,
pub sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct DecisionProjectionBatch {
pub contract_sha256: String,
pub decision_date: NaiveDate,
pub execution_date: NaiveDate,
pub input_available_at: NaiveDateTime,
pub rows: Vec<DecisionProjectionRow>,
pub exclusions: Vec<DecisionProjectionExclusion>,
pub inference_reference: DecisionProjectionReference,
pub provider_request_sha256: String,
pub provider_response_sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct DecisionProjectionReceipt {
pub schema_version: String,
pub request: DecisionProjectionRequest,
pub request_sha256: String,
pub input_available_at: NaiveDateTime,
pub inference_reference: DecisionProjectionReference,
pub provider_request_sha256: String,
pub provider_response_sha256: String,
pub values_sha256: String,
pub row_count: usize,
pub excluded_count: usize,
}
impl DecisionProjectionBatch {
pub(crate) fn validate(
&self,
request: &DecisionProjectionRequest,
) -> Result<DecisionProjectionReceipt, BacktestError> {
if self.contract_sha256 != request.contract_sha256
|| self.decision_date != request.decision_date
|| self.execution_date != request.execution_date
|| request.as_of.date() != request.decision_date
|| request.decision_date >= request.execution_date
|| self.input_available_at > request.as_of
{
return Err(error("projection identity or causal availability differs from the decision request"));
}
require_sha256(&self.inference_reference.sha256)?;
require_sha256(&self.provider_request_sha256)?;
require_sha256(&self.provider_response_sha256)?;
let path = Path::new(&self.inference_reference.path);
if !path.is_absolute() || path.components().any(|part| matches!(part, Component::ParentDir)) {
return Err(error("projection inference receipt requires an absolute immutable artifact reference"));
}
if request.symbols.is_empty() || request.fields.is_empty() {
return Err(error("projection request population and fields must be explicit"));
}
let mut covered = BTreeSet::new();
for row in &self.rows {
if !request.symbols.contains(&row.symbol) || !covered.insert(row.symbol.clone()) {
return Err(error("projection returned an unexpected or duplicate symbol"));
}
if row.values.keys().collect::<BTreeSet<_>>() != request.fields.iter().collect()
|| row.values.values().any(|value| !value.is_finite())
{
return Err(error("projection numeric field coverage is incomplete or non-finite"));
}
}
for exclusion in &self.exclusions {
if !request.symbols.contains(&exclusion.symbol) || !covered.insert(exclusion.symbol.clone()) {
return Err(error("projection exclusion overlaps a value or repeats an unexpected symbol"));
}
if exclusion.classification != "valid_market_absence" || exclusion.reason.trim().is_empty() {
return Err(error("projection missing data is unclassified; no order skip is permitted"));
}
require_sha256(&exclusion.source_sha256)?;
}
if covered != request.symbols {
return Err(error("projection did not explain every requested symbol"));
}
// Digest the actual normalized values consumed by this engine, not a
// provider's summary flag. Stable ordering preserves same-input replay.
let rows = self.rows.iter().map(|row| (&row.symbol, &row.values)).collect::<BTreeMap<_, _>>();
let exclusions = self.exclusions.iter().map(|item| (&item.symbol, item)).collect::<BTreeMap<_, _>>();
Ok(DecisionProjectionReceipt {
schema_version: "fidc.decision-projection-receipt/v1".into(),
request: request.clone(),
request_sha256: request.sha256()?,
input_available_at: self.input_available_at,
inference_reference: self.inference_reference.clone(),
provider_request_sha256: self.provider_request_sha256.clone(),
provider_response_sha256: self.provider_response_sha256.clone(),
values_sha256: json_sha256(&(rows, exclusions))?,
row_count: self.rows.len(),
excluded_count: self.exclusions.len(),
})
}
}
fn valid_projection_field(field: &str) -> bool {
let mut bytes = field.bytes();
let identifier = bytes.next().is_some_and(|c| c.is_ascii_lowercase() || c == b'_')
&& bytes.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_');
identifier && !matches!(field,
"market_cap" | "market_cap_bn" | "free_float_cap" | "free_float_cap_bn"
| "pe" | "pe_ttm" | "turnover_ratio" | "effective_turnover_ratio"
| "adjustment_factor_backward1" | "open" | "day_open" | "close"
| "high" | "low" | "last" | "last_price" | "volume" | "amount"
| "prev_close" | "bid1" | "ask1" | "upper_limit" | "lower_limit")
}
fn require_sha256(value: &str) -> Result<(), BacktestError> {
if value.len() != 64 || !value.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)) {
return Err(error("projection requires canonical SHA256 identities"));
}
Ok(())
}
fn json_sha256(value: &impl Serialize) -> Result<String, BacktestError> {
let bytes = serde_json::to_vec(value).map_err(|_| error("projection receipt could not be serialized"))?;
Ok(format!("{:x}", Sha256::digest(bytes)))
}
fn error(message: &str) -> BacktestError {
BacktestError::Execution(message.into())
}
#[cfg(test)]
mod tests {
use super::*;
fn request() -> DecisionProjectionRequest {
let decision_date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
DecisionProjectionRequest {
contract_sha256: "1".repeat(64), decision_date,
execution_date: NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(),
as_of: decision_date.and_hms_opt(16, 0, 0).unwrap(),
symbols: BTreeSet::from(["000001.SZ".into(), "000002.SZ".into()]),
fields: BTreeSet::from(["model_score_a".into()]),
}
}
fn batch() -> DecisionProjectionBatch {
let request = request();
DecisionProjectionBatch {
contract_sha256: request.contract_sha256,
decision_date: request.decision_date, execution_date: request.execution_date,
input_available_at: request.as_of,
rows: vec![DecisionProjectionRow { symbol: "000001.SZ".into(), values: BTreeMap::from([("model_score_a".into(), 0.5)]) }],
exclusions: vec![DecisionProjectionExclusion { symbol: "000002.SZ".into(), classification: "valid_market_absence".into(), reason: "dated suspension evidence".into(), source_sha256: "2".repeat(64) }],
inference_reference: DecisionProjectionReference { path: "/private/inference.json".into(), sha256: "3".repeat(64) },
provider_request_sha256: "4".repeat(64), provider_response_sha256: "5".repeat(64),
}
}
#[test]
fn exact_projection_coverage_and_classified_absence_are_receipted() {
let receipt = batch().validate(&request()).unwrap();
assert_eq!((receipt.row_count, receipt.excluded_count), (1, 1));
assert_eq!(receipt.request_sha256, request().sha256().unwrap());
let mut changed = batch(); changed.rows[0].values.insert("model_score_a".into(), 0.6);
assert_ne!(changed.validate(&request()).unwrap().values_sha256, receipt.values_sha256);
}
#[test]
fn projection_contract_does_not_shadow_core_market_or_typed_fields() {
for field in ["", "Score", "model-score", "close", "market_cap_bn", "adjustment_factor_backward1"] {
let contract = DecisionProjectionContract { contract_sha256: "1".repeat(64), fields: BTreeSet::from([field.into()]), input_cutoff_time: NaiveTime::from_hms_opt(16, 0, 0).unwrap() };
assert!(contract.validate().is_err(), "{field}");
}
}
#[test]
fn projection_rejects_incomplete_duplicate_extra_and_overlapping_population() {
let mut examples = vec![];
let mut item = batch(); item.exclusions.clear(); examples.push(item);
let mut item = batch(); item.rows.push(item.rows[0].clone()); examples.push(item);
let mut item = batch(); item.rows[0].symbol = "999999.SZ".into(); examples.push(item);
let mut item = batch(); item.exclusions[0].symbol = item.rows[0].symbol.clone(); examples.push(item);
for item in examples { assert!(item.validate(&request()).is_err()); }
}
#[test]
fn projection_rejects_nonfinite_wrong_fields_and_unproven_absence() {
let mut examples = vec![];
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let mut item = batch(); item.rows[0].values.insert("model_score_a".into(), value); examples.push(item);
}
let mut item = batch(); item.rows[0].values.clear(); examples.push(item);
let mut item = batch(); item.rows[0].values.insert("extra".into(), 1.0); examples.push(item);
let mut item = batch(); item.exclusions[0].classification = "unknown".into(); examples.push(item);
let mut item = batch(); item.exclusions[0].reason.clear(); examples.push(item);
let mut item = batch(); item.exclusions[0].source_sha256.clear(); examples.push(item);
for item in examples { assert!(item.validate(&request()).is_err()); }
}
#[test]
fn projection_rejects_future_drift_or_unbound_receipts() {
let mut examples = vec![];
let mut item = batch(); item.input_available_at += chrono::Duration::seconds(1); examples.push(item);
let mut item = batch(); item.decision_date = item.execution_date; examples.push(item);
let mut item = batch(); item.contract_sha256 = "0".repeat(64); examples.push(item);
let mut item = batch(); item.inference_reference.path = "/private/../inference.json".into(); examples.push(item);
let mut item = batch(); item.provider_response_sha256 = "A".repeat(64); examples.push(item);
for item in examples { assert!(item.validate(&request()).is_err()); }
}
}
File diff suppressed because it is too large Load Diff
+129
View File
@@ -0,0 +1,129 @@
//! Historical ETF execution fallback. Never manufactures an intraday bar.
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use rust_decimal::Decimal;
use crate::{BacktestError, DataSet};
pub(crate) fn opening_time() -> NaiveTime {
NaiveTime::from_hms_opt(9, 30, 0).expect("valid exchange opening time")
}
#[derive(Debug, Clone)]
pub(crate) struct EtfFallbackReference {
pub price: f64,
pub reference_date: NaiveDate,
/// None means the next official session is outside the loaded calendar.
/// No natural-day guess or price from beyond the requested run is used.
pub execute_on: Option<NaiveDate>,
pub immediate: bool,
}
pub(crate) fn reference(data: &DataSet, symbol: &str, at: NaiveDateTime) -> Result<EtfFallbackReference, BacktestError> {
let fail = |reason: &str| BacktestError::Execution(format!(
"etf_daily_open_fallback:{reason} symbol={symbol} signal_at={at}"
));
let instrument = data.instrument(symbol).ok_or_else(|| fail("instrument_identity_missing"))?;
if !instrument.is_exchange_traded_fund() || instrument.listed_at.is_none() {
return Err(fail("verified_etf_identity_required"));
}
if instrument.dated_market_absence_reason(at.date()).is_some() {
return Err(fail("outside_instrument_lifecycle"));
}
if at.time() == opening_time() {
let row = data.market(at.date(), symbol).ok_or_else(|| fail("daily_open_missing"))?;
if !row.open.is_finite() || row.open <= 0.0 { return Err(fail("daily_open_invalid")); }
return Ok(EtfFallbackReference { price: row.open, reference_date: at.date(), execute_on: Some(at.date()), immediate: true });
}
let previous = data.previous_trading_date(at.date(), 1).ok_or_else(|| fail("previous_official_session_missing"))?;
let close = data.market(previous, symbol).map(|row| row.close).ok_or_else(|| fail("previous_completed_close_missing"))?;
if !close.is_finite() || close <= 0.0 { return Err(fail("previous_completed_close_invalid")); }
Ok(EtfFallbackReference {
price: close, reference_date: previous, immediate: false,
execute_on: if at.time() < opening_time() { Some(at.date()) } else { data.next_trading_date(at.date(), 1) },
})
}
#[derive(Debug, Clone)]
pub(crate) struct DeferredEtfTarget {
pub pool_id: String,
pub generation: String,
pub symbol: String,
pub signal_date: NaiveDate,
pub signal_at: NaiveDateTime,
pub execute_on: Option<NaiveDate>,
pub target_value: Decimal,
pub target_weight_bps: i32,
pub target_weight_ratio: Option<Decimal>,
pub side: crate::stock_pool_execution::OrderSide,
pub max_positions: usize,
pub rule: std::sync::Arc<crate::stock_pool_execution::StockPoolExecutionRule>,
pub members: std::sync::Arc<Vec<crate::stock_pool_execution::StockPoolMemberSpec>>,
pub reason: String,
}
/// Owned by one broker/run. Replacing a full pool generation supersedes older
/// queued targets; order of the latest candidate list is retained.
#[derive(Debug, Default, Clone)]
pub(crate) struct DeferredEtfTargets {
generations: std::collections::BTreeMap<String, String>,
rows: Vec<DeferredEtfTarget>,
}
impl DeferredEtfTargets {
pub fn replace_generation(&mut self, pool_id: &str, generation: &str) -> usize {
if self.generations.get(pool_id).is_some_and(|old| old == generation) { return 0; }
self.generations.insert(pool_id.into(), generation.into());
let before = self.rows.len();
self.rows.retain(|row| row.pool_id != pool_id);
before - self.rows.len()
}
pub fn upsert(&mut self, row: DeferredEtfTarget) {
if let Some(existing) = self.rows.iter_mut().find(|v| v.pool_id == row.pool_id && v.symbol == row.symbol) {
*existing = row;
} else { self.rows.push(row); }
}
pub fn take_due(&mut self, date: NaiveDate) -> Vec<DeferredEtfTarget> {
let mut due = Vec::new();
self.rows.retain(|row| {
if row.execute_on.is_some_and(|day| day <= date) { due.push(row.clone()); false } else { true }
});
due.sort_by_key(|row| match row.side { crate::stock_pool_execution::OrderSide::Sell => 0, crate::stock_pool_execution::OrderSide::Buy => 1 });
due
}
pub fn len(&self) -> usize { self.rows.len() }
}
#[cfg(test)]
mod tests {
use super::*;
fn target(symbol:&str,side:crate::stock_pool_execution::OrderSide,generation:&str)->DeferredEtfTarget {
let date=NaiveDate::from_ymd_opt(2026,1,2).unwrap();
DeferredEtfTarget {pool_id:"pool".into(),generation:generation.into(),symbol:symbol.into(),signal_date:date,signal_at:date.and_hms_opt(13,0,0).unwrap(),execute_on:NaiveDate::from_ymd_opt(2026,1,5),target_value:1000.into(),target_weight_bps:5000,target_weight_ratio:None,side,max_positions:2,rule:Default::default(),members:std::sync::Arc::new(vec![]),reason:"fixture".into()}
}
#[test]
fn latest_generation_overwrites_pending_targets_and_preserves_candidate_order() {
use crate::stock_pool_execution::OrderSide::{Buy,Sell};
let mut queue=DeferredEtfTargets::default();
queue.replace_generation("pool","v1");
queue.upsert(target("510300.SH",Buy,"v1"));
queue.upsert(target("159915.SZ",Buy,"v1"));
assert_eq!(queue.replace_generation("pool","v1"),0);
assert_eq!(queue.replace_generation("pool","v2"),2);
queue.upsert(target("560450.SH",Buy,"v2"));
queue.upsert(target("159915.SZ",Sell,"v2"));
queue.upsert(target("510300.SH",Buy,"v2"));
assert!(queue.take_due(NaiveDate::from_ymd_opt(2026,1,2).unwrap()).is_empty());
let due=queue.take_due(NaiveDate::from_ymd_opt(2026,1,5).unwrap());
assert_eq!(due.iter().map(|v|v.symbol.as_str()).collect::<Vec<_>>(),vec!["159915.SZ","560450.SH","510300.SH"]);
assert!(due.iter().all(|v|v.generation=="v2"));
assert_eq!(queue.len(),0);
}
#[test]
fn no_loaded_next_session_is_not_guessed_from_natural_days() {
let mut queue=DeferredEtfTargets::default();
let mut item=target("510300.SH",crate::stock_pool_execution::OrderSide::Buy,"v1");
item.execute_on=None;
queue.upsert(item);
assert!(queue.take_due(NaiveDate::from_ymd_opt(2026,2,1).unwrap()).is_empty());
assert_eq!(queue.len(),1);
}
}
+54 -1
View File
@@ -181,8 +181,22 @@ impl OrderEvent {
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FillOrigin {
#[default]
MarketExecution,
DividendReinvestment,
}
impl FillOrigin {
pub fn is_market_execution(&self) -> bool { *self == Self::MarketExecution }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FillEvent {
#[serde(default, skip_serializing_if = "FillOrigin::is_market_execution")]
pub origin: FillOrigin,
#[serde(with = "date_format")]
pub date: NaiveDate,
#[serde(default, with = "optional_date_format")]
@@ -219,6 +233,14 @@ pub struct FillEvent {
impl FillEvent {
pub fn validate(&self) -> Result<(), String> {
if self.origin == FillOrigin::DividendReinvestment && (
self.order_id.is_some() || self.side != OrderSide::Buy
|| self.commission != 0. || self.stamp_tax != 0. || self.transfer_fee != 0.
|| self.execution_timestamp != self.date.and_hms_opt(0, 0, 0)
|| self.execution_start_timestamp != self.execution_timestamp
) {
return Err("dividend accounting allocation cannot carry an exchange order, fees, or a market clock".into());
}
if self.symbol.trim().is_empty()
|| self.quantity == 0
|| !self.price.is_finite()
@@ -311,12 +333,15 @@ pub enum ProcessEventKind {
OrderUpdateReject,
OrderUnsolicitedUpdate,
Trade,
ManualExecutionObserved,
UniverseUpdated,
UniverseSubscribed,
UniverseUnsubscribed,
AccountDepositWithdraw,
AccountFinanceRepay,
AccountManagementFee,
SessionCapacityAudit,
EtfExecutionFallback,
}
impl ProcessEventKind {
@@ -356,12 +381,15 @@ impl ProcessEventKind {
Self::OrderUpdateReject => "order_update_reject",
Self::OrderUnsolicitedUpdate => "order_unsolicited_update",
Self::Trade => "trade",
Self::ManualExecutionObserved => "manual_execution_observed",
Self::UniverseUpdated => "universe_updated",
Self::UniverseSubscribed => "universe_subscribed",
Self::UniverseUnsubscribed => "universe_unsubscribed",
Self::AccountDepositWithdraw => "account_deposit_withdraw",
Self::AccountFinanceRepay => "account_finance_repay",
Self::AccountManagementFee => "account_management_fee",
Self::SessionCapacityAudit => "session_capacity_audit",
Self::EtfExecutionFallback => "etf_execution_fallback",
}
}
@@ -387,12 +415,15 @@ impl ProcessEventKind {
| Self::OrderUpdateReject
| Self::OrderUnsolicitedUpdate
| Self::Trade
| Self::ManualExecutionObserved
| Self::UniverseUpdated
| Self::UniverseSubscribed
| Self::UniverseUnsubscribed
| Self::AccountDepositWithdraw
| Self::AccountFinanceRepay
| Self::AccountManagementFee
| Self::SessionCapacityAudit
| Self::EtfExecutionFallback
| Self::Settlement
)
}
@@ -416,7 +447,7 @@ pub struct ProcessEvent {
mod tests {
use chrono::{NaiveDate, NaiveDateTime};
use super::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEventKind};
use super::{FillEvent, FillOrigin, OrderEvent, OrderSide, OrderStatus, ProcessEventKind};
fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
OrderEvent {
@@ -460,6 +491,7 @@ mod tests {
fn fill_event(start: Option<NaiveDateTime>, end: Option<NaiveDateTime>) -> FillEvent {
FillEvent {
origin: crate::events::FillOrigin::MarketExecution,
date: NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
decision_date: None,
order_created_date: None,
@@ -480,6 +512,27 @@ mod tests {
}
}
#[test]
fn accounting_origin_cannot_disguise_an_exchange_order_or_fee() {
let at = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap().and_hms_opt(0, 0, 0).unwrap();
let mut fill = fill_event(Some(at), Some(at));
fill.origin = FillOrigin::DividendReinvestment;
fill.order_id = None;
fill.commission = 0.;
fill.net_cash_flow = -1000.;
assert!(fill.validate().is_ok());
for kind in 0..3 {
let mut invalid = fill.clone();
match kind {
0 => invalid.order_id = Some(1),
1 => invalid.commission = 1.,
_ => { invalid.execution_timestamp = Some(at + chrono::Duration::hours(9)); invalid.execution_start_timestamp = invalid.execution_timestamp; }
}
assert!(invalid.validate().is_err());
}
assert!(serde_json::to_value(fill_event(None, None)).unwrap().get("origin").is_none());
}
#[test]
fn fill_execution_timestamp_range_is_explicit_and_backward_compatible() {
let start = NaiveDate::from_ymd_opt(2025, 1, 2)
@@ -12,6 +12,19 @@ pub enum VolumeCapacityMode {
SessionCapacityAudit,
}
impl VolumeCapacityMode {
pub fn validate(self, enabled: bool, has_execution_observations: bool) -> Result<(), CapacityError> {
if !enabled { return Ok(()); }
match self {
Self::ExecutionObservation if !has_execution_observations => Err(CapacityError::MissingObservation),
Self::CompletedBar => Err(CapacityError::MissingCompletedBar),
_ => Ok(()),
}
}
pub fn limits_execution_quantity(self) -> bool { self != Self::SessionCapacityAudit }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum CapacityError {
#[error("execution capacity ratio must be finite and in (0, 1]")]
@@ -26,6 +39,28 @@ pub enum CapacityError {
WrongSession,
#[error("execution-time capacity is missing; daily session volume cannot size an earlier fill")]
MissingObservation,
#[error("completed_bar capacity requires declared bar end and availability; an undated daily total is not a completed observation")]
MissingCompletedBar,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapacityAuditSummary {
pub mode: VolumeCapacityMode,
pub enabled: bool,
pub participation_rate: f64,
pub audited_symbol_sessions: usize,
pub failed_symbol_sessions: usize,
pub audit_passed: Option<bool>,
pub execution_time_capacity_proven: bool,
}
impl CapacityAuditSummary {
pub fn observe(&mut self, audit: &SessionCapacityAudit) {
self.audited_symbol_sessions += 1;
self.failed_symbol_sessions += usize::from(!audit.passed);
self.audit_passed = Some(self.failed_symbol_sessions == 0);
}
}
/// Decimal semantics of the frozen JSON rate, evaluated without a float product.
@@ -0,0 +1,196 @@
//! Check typed pending intent numbers before JSON could replace NaN/Inf with null.
//! This traverses the original Serialize representation without materializing it.
use serde::{Serialize, Serializer, ser};
#[derive(Clone, Copy)]
struct Finite;
pub(crate) fn validate(value: &impl Serialize) -> Result<(), serde_json::Error> {
value.serialize(Finite)
}
macro_rules! scalar {
($($method:ident: $ty:ty),* $(,)?) => {$(
fn $method(self, _: $ty) -> Result<(), Self::Error> { Ok(()) }
)*};
}
impl Serializer for Finite {
type Ok = ();
type Error = serde_json::Error;
type SerializeSeq = Self;
type SerializeTuple = Self;
type SerializeTupleStruct = Self;
type SerializeTupleVariant = Self;
type SerializeMap = Self;
type SerializeStruct = Self;
type SerializeStructVariant = Self;
scalar!(serialize_bool: bool, serialize_i8: i8, serialize_i16: i16,
serialize_i32: i32, serialize_i64: i64, serialize_i128: i128,
serialize_u8: u8, serialize_u16: u16, serialize_u32: u32,
serialize_u64: u64, serialize_u128: u128, serialize_char: char,
serialize_str: &str, serialize_bytes: &[u8]);
fn serialize_f32(self, value: f32) -> Result<(), Self::Error> {
self.serialize_f64(f64::from(value))
}
fn serialize_f64(self, value: f64) -> Result<(), Self::Error> {
if value.is_finite() {
Ok(())
} else {
Err(ser::Error::custom(
"pending strategy intent contains a non-finite number",
))
}
}
fn serialize_none(self) -> Result<(), Self::Error> {
Ok(())
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<(), Self::Error> {
value.serialize(self)
}
fn serialize_unit(self) -> Result<(), Self::Error> {
Ok(())
}
fn serialize_unit_struct(self, _: &'static str) -> Result<(), Self::Error> {
Ok(())
}
fn serialize_unit_variant(
self,
_: &'static str,
_: u32,
_: &'static str,
) -> Result<(), Self::Error> {
Ok(())
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_: &'static str,
value: &T,
) -> Result<(), Self::Error> {
value.serialize(self)
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_: &'static str,
_: u32,
_: &'static str,
value: &T,
) -> Result<(), Self::Error> {
value.serialize(self)
}
fn serialize_seq(self, _: Option<usize>) -> Result<Self, Self::Error> {
Ok(self)
}
fn serialize_tuple(self, _: usize) -> Result<Self, Self::Error> {
Ok(self)
}
fn serialize_tuple_struct(self, _: &'static str, _: usize) -> Result<Self, Self::Error> {
Ok(self)
}
fn serialize_tuple_variant(
self,
_: &'static str,
_: u32,
_: &'static str,
_: usize,
) -> Result<Self, Self::Error> {
Ok(self)
}
fn serialize_map(self, _: Option<usize>) -> Result<Self, Self::Error> {
Ok(self)
}
fn serialize_struct(self, _: &'static str, _: usize) -> Result<Self, Self::Error> {
Ok(self)
}
fn serialize_struct_variant(
self,
_: &'static str,
_: u32,
_: &'static str,
_: usize,
) -> Result<Self, Self::Error> {
Ok(self)
}
}
macro_rules! sequence {
($trait:ident, $method:ident) => {
impl ser::$trait for Finite {
type Ok = ();
type Error = serde_json::Error;
fn $method<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
value.serialize(*self)
}
fn end(self) -> Result<(), Self::Error> {
Ok(())
}
}
};
}
sequence!(SerializeSeq, serialize_element);
sequence!(SerializeTuple, serialize_element);
sequence!(SerializeTupleStruct, serialize_field);
sequence!(SerializeTupleVariant, serialize_field);
impl ser::SerializeMap for Finite {
type Ok = ();
type Error = serde_json::Error;
fn serialize_key<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
value.serialize(*self)
}
fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
value.serialize(*self)
}
fn end(self) -> Result<(), Self::Error> {
Ok(())
}
}
macro_rules! structure {
($trait:ident) => {
impl ser::$trait for Finite {
type Ok = ();
type Error = serde_json::Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
_: &'static str,
value: &T,
) -> Result<(), Self::Error> {
value.serialize(*self)
}
fn end(self) -> Result<(), Self::Error> {
Ok(())
}
}
};
}
structure!(SerializeStruct);
structure!(SerializeStructVariant);
#[cfg(test)]
mod tests {
use super::*;
use crate::strategy::{OrderIntent, StrategyDecision};
#[test]
fn pending_numbers_cannot_be_silently_serialized_as_optional_nulls() {
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let decision = StrategyDecision {
order_intents: vec![
OrderIntent::LimitTargetPercent {
symbol: "000001.SZ".into(),
target_percent: 0.5,
limit_price: value,
reason: "test".into(),
}
.with_time_in_force(crate::strategy::OrderTimeInForce::Day),
],
..Default::default()
};
assert!(validate(&decision).is_err());
assert!(validate(&vec![Some(value)]).is_err());
}
assert!(validate(&(None::<f64>, vec![0., -0., 0.123456789], "NaN")).is_ok());
}
}
+11
View File
@@ -28,6 +28,17 @@ impl FixedMoney {
self.0
}
pub fn to_decimal_string(self) -> String {
let magnitude = self.0.unsigned_abs();
let scale = MONEY_SCALE as u128;
let sign = if self.0 < 0 { "-" } else { "" };
let width = MONEY_SCALE.ilog10() as usize;
format!("{sign}{}.{:0width$}", magnitude / scale, magnitude % scale)
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}
pub fn from_decimal_str(value: &str) -> Result<Self, String> {
let value = value.trim();
if value.is_empty() {
+5 -4
View File
@@ -26,7 +26,7 @@ fn sum_futures_money(values: impl IntoIterator<Item = FixedMoney>, label: &str)
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub enum FuturesDirection {
Long,
Short,
@@ -62,7 +62,7 @@ impl FuturesDirection {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum FuturesPositionEffect {
Open,
Close,
@@ -81,7 +81,7 @@ impl FuturesPositionEffect {
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, Serialize)]
pub struct FuturesContractSpec {
pub contract_multiplier: f64,
pub long_margin_rate: f64,
@@ -190,7 +190,7 @@ impl FuturesTransactionCostModel {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct FuturesOrderIntent {
pub symbol: String,
pub direction: FuturesDirection,
@@ -1048,6 +1048,7 @@ impl FuturesAccountState {
)
.to_f64();
report.fill_events.push(FillEvent {
origin: crate::events::FillOrigin::MarketExecution,
date,
decision_date: None,
order_created_date: None,
+17 -2
View File
@@ -63,7 +63,7 @@ pub struct HoldingLifecycleEvidence {
pub last_sell_date: Option<NaiveDate>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct AutomaticTradePermission {
pub buy_denial: Option<&'static str>,
pub sell_denial: Option<&'static str>,
@@ -120,10 +120,25 @@ impl AutomaticTradeProtection {
execution_date: NaiveDate,
evidence: &HoldingLifecycleEvidence,
calendar: &TradingCalendar,
) -> Result<AutomaticTradePermission, String> {
self.evaluate_with_predecessors(symbol, execution_date, evidence, calendar, std::iter::empty())
}
/// Only accept predecessors from validated, actually applied holding
/// conversions. Catalog aliases or requested strategy symbols are not
/// evidence that a configured lock covers another security.
pub fn evaluate_with_predecessors<'a>(
&self,
symbol: &str,
execution_date: NaiveDate,
evidence: &HoldingLifecycleEvidence,
calendar: &TradingCalendar,
verified_predecessors: impl IntoIterator<Item = &'a str>,
) -> Result<AutomaticTradePermission, String> {
self.validate()?;
let predecessors = verified_predecessors.into_iter().collect::<std::collections::BTreeSet<_>>();
if self.locks.iter().any(|lock| {
lock.symbol == symbol
(lock.symbol == symbol || predecessors.contains(lock.symbol.as_str()))
&& lock.start_date <= execution_date
&& lock.end_date.is_none_or(|end| execution_date <= end)
}) {
+18 -3
View File
@@ -1,12 +1,16 @@
pub mod broker;
pub mod calendar;
pub mod cost;
mod corporate_book;
pub mod data;
pub mod decision_projection;
mod numeric_factors;
pub mod daily_patterns;
pub mod pattern_context;
pub mod session_events;
pub mod factor_events;
pub mod execution_capacity;
mod etf_execution;
mod execution_schedule;
mod factor_event_catalog;
pub mod factor_cross_section;
@@ -15,15 +19,19 @@ pub mod engine;
pub mod event_bus;
pub mod events;
pub mod fixed_point;
mod finite_serialization;
pub mod futures;
pub mod instrument;
pub mod metrics;
pub mod manual_execution;
mod manual_corporate_replay;
mod numeric_expr_vm;
pub mod platform_expr_strategy;
pub mod platform_runtime_schema;
pub mod platform_strategy_spec;
pub mod portfolio;
pub mod portfolio_loss;
pub mod position_exposure;
pub mod risk_control;
pub mod rules;
pub mod scheduler;
@@ -35,6 +43,7 @@ pub mod stock_pool_execution;
pub mod stock_pool_index_policy;
pub mod stock_pool_market_cap;
pub mod stock_pool_state;
pub mod stock_pool_quote_facts;
pub mod signal_contract;
pub mod strategy_ai;
pub mod universe;
@@ -52,16 +61,21 @@ pub use data::{
IntradayMarketSnapshotOverlay, IntradayOrderBookDepthLevel, NumericFactorMap, PriceBar,
PriceField, SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
};
pub use decision_projection::{
DecisionProjectionBatch, DecisionProjectionContract, DecisionProjectionExclusion,
DecisionProjectionReceipt, DecisionProjectionReference, DecisionProjectionRequest,
DecisionProjectionRow,
};
pub use engine::{
AnalyzerMonthlyReturnRow, AnalyzerPositionRow, AnalyzerReport, AnalyzerRiskSummary,
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
BacktestResult, BacktestTerminalAssetClass, BacktestTerminalAudit, BacktestTerminalOpenOrder,
BacktestTerminalStatus, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
ProcessEventRetention, backtest_execution_dates,
ProcessEventRetention, backtest_execution_dates, backtest_execution_dates_with_rules,
};
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
pub use events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
AccountEvent, FillEvent, FillOrigin, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
};
pub use fixed_point::{
@@ -75,7 +89,8 @@ pub use futures::{
};
pub use instrument::Instrument;
pub use metrics::{
BacktestMetrics, RiskFreeRateContract, RiskFreeRateObservation, compute_backtest_metrics,
BacktestMetrics, RiskFreeRateContract, RiskFreeRateObservation, RiskAdjustedStatistics,
compute_backtest_metrics, risk_adjusted_statistics,
};
pub use platform_expr_strategy::{
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
@@ -0,0 +1,444 @@
use std::cell::{Cell, RefCell};
use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, Utc};
use sha2::{Digest, Sha256};
use crate::manual_execution::{
AppliedManualFill, ManualCorporateActionReference, ManualCorporateAdjustment,
ManualCorporatePositionChange, ManualExecutionReplay, ManualFillObservation,
};
use crate::{DataSet, FillEvent, FixedMoney, MatchingType, OrderSide, PortfolioState, PriceField};
#[derive(Debug, Clone)]
struct CashEffect {
at: NaiveDateTime,
amount: FixedMoney,
}
/// Replays issued financial facts, never strategy callbacks or hypothetical orders.
/// The ordinary book remains observable until an actual receipt is delivered.
pub(crate) struct ManualCorporateReplay {
first_date: NaiveDate,
sessions: RefCell<BTreeSet<NaiveDate>>,
closed: RefCell<BTreeSet<NaiveDate>>,
cash: RefCell<Vec<CashEffect>>,
reconciled_count: Cell<usize>,
cash_dividends: bool,
adjust_cost: bool,
reinvest: bool,
matching: MatchingType,
daily_price: PriceField,
same_day_mark_at_fill: bool,
}
impl ManualCorporateReplay {
pub(crate) fn new(
first_date: NaiveDate,
cash_dividends: bool,
adjust_cost: bool,
reinvest: bool,
matching: MatchingType,
daily_price: PriceField,
same_day_mark_at_fill: bool,
) -> Self {
Self {
first_date,
sessions: RefCell::new(BTreeSet::new()),
closed: RefCell::new(BTreeSet::new()),
cash: RefCell::new(Vec::new()),
reconciled_count: Cell::new(0),
cash_dividends,
adjust_cost,
reinvest,
matching,
daily_price,
same_day_mark_at_fill,
}
}
pub(crate) fn record_session(&self, date: NaiveDate) {
self.sessions.borrow_mut().insert(date);
}
pub(crate) fn record_close(&self, date: NaiveDate) {
self.closed.borrow_mut().insert(date);
}
pub(crate) fn committed(&self, count: usize) {
self.reconciled_count.set(count);
}
pub(crate) fn record_cash(
&self,
at: Option<NaiveDateTime>,
before: FixedMoney,
after: FixedMoney,
) -> Result<(), String> {
let amount = after
.checked_sub(before)
.ok_or("manual corporate cash observation overflow")?;
if amount != FixedMoney::ZERO {
self.cash.borrow_mut().push(CashEffect {
at: at.ok_or("manual corporate cash observation has no execution clock")?,
amount,
});
}
Ok(())
}
fn action_references(
&self,
observation: ManualFillObservation<'_>,
data: &DataSet,
) -> Result<Vec<ManualCorporateActionReference>, String> {
let mut symbols = BTreeSet::from([observation.order.symbol.clone()]);
let observed_date = local(observation.fill.observed_at).date();
let mut actions = Vec::new();
for date in self.sessions.borrow().range((
std::ops::Bound::Excluded(observation.fill.trade_date),
std::ops::Bound::Included(observed_date),
)) {
for action in data.corporate_actions_on(*date) {
if !symbols.contains(&action.symbol) {
continue;
}
let successor_terms = crate::corporate_book::validate_action(action, data)?;
let effective = (action.split_ratio() - 1.).abs() > f64::EPSILON
|| action.has_successor_conversion()
|| (self.cash_dividends && action.share_cash.abs() > f64::EPSILON);
if !effective {
continue;
}
if let Some((successor, _, _)) = successor_terms {
symbols.insert(successor.to_owned());
}
actions.push(ManualCorporateActionReference {
date: *date,
symbol: action.symbol.clone(),
successor_symbol: action.successor_symbol.clone(),
share_cash: action.share_cash.to_string(),
split_ratio: action.split_ratio().to_string(),
successor_ratio: action.successor_ratio.map(|value| value.to_string()),
successor_cash: action.successor_cash.map(|value| value.to_string()),
sha256: digest(
&serde_json::to_value(action).map_err(|error| error.to_string())?,
)?,
});
}
}
Ok(actions)
}
pub(crate) fn required(
&self,
observation: ManualFillObservation<'_>,
data: &DataSet,
) -> Result<bool, String> {
Ok(!self.action_references(observation, data)?.is_empty())
}
pub(crate) fn project(
&self,
source: &ManualExecutionReplay,
applied_count: usize,
observation: ManualFillObservation<'_>,
current: &mut PortfolioState,
data: &DataSet,
fills: &[FillEvent],
has_pending: bool,
) -> Result<(AppliedManualFill, ManualCorporateAdjustment), String> {
if has_pending {
return Err("manual observation conflicts with pending shadow orders".into());
}
let all = source.observations()?;
if all.get(applied_count).is_none_or(|next| {
next.fill.observation_event_id != observation.fill.observation_event_id
}) {
return Err(
"manual corporate observation prefix differs from the immutable trace".into(),
);
}
let actions = self.action_references(observation, data)?;
if actions.is_empty() {
return Err("manual corporate projection has no processed corporate action".into());
}
let at = local(observation.fill.observed_at);
let reference = self.replay(
source,
current.initial_cash_fixed(),
&all[..applied_count],
self.reconciled_count.get(),
data,
fills,
at,
)?;
let expected = current.financial_replay_identity();
let reference_identity = reference.financial_replay_identity();
if reference_identity != expected {
let differing = expected.as_object().into_iter().flat_map(|value| value.keys())
.chain(reference_identity.as_object().into_iter().flat_map(|value| value.keys()))
.collect::<BTreeSet<_>>().into_iter().filter(|key| expected.get(*key)!=reference_identity.get(*key))
.cloned().collect::<Vec<_>>().join(",");
return Err(format!(
"manual corporate ledger coverage mismatch: symbol={} observed_at={} expected={} replayed={} differing_fields={}",
observation.order.symbol,
observation.fill.observed_at,
digest(&expected)?,
digest(&reference_identity)?, differing
));
}
let replayed = self.replay(
source,
current.initial_cash_fixed(),
&all[..=applied_count],
applied_count + 1,
data,
fills,
at,
)?;
let replayed_identity = replayed.financial_replay_identity();
let gross = FixedMoney::from_decimal_str(&observation.fill.gross_amount()?.to_string())?;
let fees = FixedMoney::from_decimal_str(&observation.fill.total_fees()?.to_string())?;
let cash_delta = match observation.order.side {
OrderSide::Buy => gross.checked_add(fees).and_then(FixedMoney::checked_neg),
OrderSide::Sell => gross.checked_sub(fees),
}
.ok_or("manual corporate trade cash overflow")?;
let before = current.cash_fixed();
let after = replayed.cash_fixed();
let corporate_cash = after
.checked_sub(before)
.and_then(|delta| delta.checked_sub(cash_delta))
.ok_or("manual corporate adjustment overflow")?;
let symbols = current
.positions()
.keys()
.chain(replayed.positions().keys())
.cloned()
.chain(std::iter::once(observation.order.symbol.clone()))
.collect::<BTreeSet<_>>();
let mut positions = BTreeMap::new();
for symbol in symbols {
let change = ManualCorporatePositionChange {
quantity_before: current
.position(&symbol)
.map_or(0, |position| position.quantity),
quantity_after: replayed
.position(&symbol)
.map_or(0, |position| position.quantity),
cost_basis_before: current
.financial_position_basis(&symbol)
.to_decimal_string(),
cost_basis_after: replayed
.financial_position_basis(&symbol)
.to_decimal_string(),
};
if change.quantity_before != change.quantity_after
|| change.cost_basis_before != change.cost_basis_after
{
positions.insert(symbol, change);
}
}
let quantity_after = replayed
.position(&observation.order.symbol)
.map_or(0, |position| position.quantity);
let adjustment = ManualCorporateAdjustment {
schema: "fidc.manual-corporate-adjustment/v1".into(),
observed_at: observation.fill.observed_at,
cash_dividends_enabled: self.cash_dividends,
dividend_cost_basis_adjustment: self.adjust_cost,
dividend_reinvestment: self.reinvest,
actions,
cash_before: before.to_decimal_string(),
cash_after: after.to_decimal_string(),
corporate_cash_delta: corporate_cash.to_decimal_string(),
positions,
reference_sha256: digest(&reference_identity)?,
replayed_sha256: digest(&replayed_identity)?,
};
current.replace_replayed_financial_book(replayed)?;
Ok((
AppliedManualFill {
gross,
fees,
cash_delta,
quantity_after,
},
adjustment,
))
}
fn replay(
&self,
runtime_input: &ManualExecutionReplay,
initial_cash: FixedMoney,
manual: &[ManualFillObservation<'_>],
economic_count: usize,
data: &DataSet,
fills: &[FillEvent],
at: NaiveDateTime,
) -> Result<PortfolioState, String> {
enum Event<'a> {
Session,
Cash(&'a CashEffect),
Corporate(NaiveDate),
Settle(NaiveDate),
Manual(ManualFillObservation<'a>),
Simulated(&'a FillEvent),
Close(NaiveDate),
}
let mut events = Vec::new();
let sessions = self.sessions.borrow();
let closed = self.closed.borrow();
let cash = self.cash.borrow();
for date in &*sessions {
let clock = date.and_hms_opt(0, 0, 0).unwrap();
events.push((clock, 0, 0, Event::Session));
events.push((clock, 2, 0, Event::Corporate(*date)));
events.push((clock, 3, 0, Event::Settle(*date)));
}
for (index, effect) in cash.iter().enumerate() {
events.push((effect.at, 1, index, Event::Cash(effect)));
}
for (index, observation) in manual.iter().enumerate() {
let clock = if index < economic_count {
local(observation.fill.executed_at)
} else {
local(observation.fill.observed_at)
};
if clock.date() < self.first_date {
return Err("manual corporate execution precedes the represented initial ledger; opening facts are required".into());
}
events.push((clock, 4, fills.len() + index, Event::Manual(*observation)));
}
for (index, fill) in fills.iter().enumerate() {
fill.validate()?;
if fill.origin == crate::events::FillOrigin::DividendReinvestment {
// The declared accounting model is recalculated from the
// corrected entitlements; this was never a submitted order.
continue;
}
let date = fill.execution_date.unwrap_or(fill.date);
// This is the frozen daily matching model, not a broker timestamp.
// Intraday contracts must supply their actual execution clock.
let clock = match fill.execution_timestamp {
Some(clock) => clock,
None if matches!(
self.matching,
MatchingType::OpenAuction | MatchingType::NextBarOpen
) =>
{
date.and_hms_opt(9, 30, 0).unwrap()
}
None if self.matching == MatchingType::CurrentBarClose
&& self.daily_price == PriceField::Close =>
{
date.and_hms_opt(15, 0, 0).unwrap()
}
None => {
return Err(
"manual corporate replay lacks an intraday fill execution clock".into(),
);
}
};
events.push((clock, 4, index, Event::Simulated(fill)));
}
for date in &*closed {
events.push((
date.and_hms_nano_opt(23, 59, 59, 999_999_999)
.unwrap()
.min(at),
5,
0,
Event::Close(*date),
));
}
events.sort_by_key(|(clock, priority, sequence, _)| (*clock, *priority, *sequence));
let mut book = PortfolioState::from_fixed_initial_cash(initial_cash);
book.record_empty_simulation_origin(self.first_date)?;
for (clock, _, _, event) in events {
if clock > at {
return Err("manual corporate replay contains a future financial fact".into());
}
match event {
Event::Session => book.begin_trading_day_at(clock.date()),
Event::Cash(effect) => {
book.apply_cash_delta_fixed(effect.amount)?;
if book.cash_fixed() < FixedMoney::ZERO {
return Err(
"manual corporate replay conflicts with prior cash facts".into()
);
}
}
Event::Corporate(date) => {
crate::corporate_book::apply(
date,
data,
&mut book,
&mut Vec::new(),
self.cash_dividends,
self.adjust_cost,
)
.map_err(|error| error.to_string())?;
}
Event::Settle(date) => {
crate::corporate_book::settle_shares(date, &mut book, &mut Vec::new())
.map_err(|error| error.to_string())?;
crate::corporate_book::settle_receivables(date, data, &mut book, &mut Vec::new(), self.reinvest, Some(runtime_input))
.map_err(|error| error.to_string())?;
}
Event::Manual(observation) => {
observation.apply(&mut book, data, false)?;
}
Event::Simulated(fill) => {
let gross = FixedMoney::from_f64(fill.gross_amount)
.ok_or("invalid simulated gross amount")?;
let fees = FixedMoney::checked_sum_f64([
fill.commission,
fill.stamp_tax,
fill.transfer_fee,
])
.ok_or("invalid simulated fee amount")?;
book.apply_observed_manual_fill(
fill.execution_date.unwrap_or(fill.date),
&fill.symbol,
fill.side,
fill.quantity,
fill.price,
fill.price,
gross,
fees,
)?;
book.prune_flat_positions();
}
Event::Close(date) => {
book.update_prices_with_options(
date,
data,
PriceField::Close,
self.same_day_mark_at_fill,
)
.map_err(|error| error.to_string())?;
if date < at.date() || at.time() == chrono::NaiveTime::from_hms_nano_opt(23,59,59,999_999_999).unwrap() {
book.record_closing_holdings(date)?;
}
}
}
if book.cash_fixed() < FixedMoney::ZERO {
return Err("manual corporate replay would borrow unobserved cash".into());
}
}
Ok(book)
}
}
fn local(value: DateTime<Utc>) -> NaiveDateTime {
value
.with_timezone(&FixedOffset::east_opt(8 * 3600).unwrap())
.naive_local()
}
fn digest(value: &serde_json::Value) -> Result<String, String> {
Ok(format!(
"{:x}",
Sha256::digest(serde_json::to_vec(value).map_err(|error| error.to_string())?)
))
}
+748
View File
@@ -0,0 +1,748 @@
//! Confirmed manual fills are external observations, not simulated broker fills.
//! The producer must bind these records to the runtime's durable order/audit facts.
use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, FixedOffset, NaiveDate, Timelike, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::events::OrderSide;
use crate::{DataSet, FixedMoney, PortfolioState};
use rust_decimal::prelude::ToPrimitive;
pub const MANUAL_REPLAY_SCHEMA: &str = "fidc.observed-manual-executions/v3";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ManualExecutionReplay {
pub schema: String,
pub runtime_id: String,
pub account_id: String,
pub source_contract_sha256: String,
pub content_sha256: String,
pub observation_cutoff: DateTime<Utc>,
pub actions: Vec<ManualExecutionAction>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub position_exposure_events: Vec<crate::position_exposure::PositionExposureEvent>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub legacy_position_exposure_bps: BTreeMap<NaiveDate, i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ManualExecutionAction {
pub action_id: String,
pub source: ManualExecutionSource,
pub audit_event_ids: Vec<String>,
pub confirmed_at: DateTime<Utc>,
pub confirmation_observed_at: DateTime<Utc>,
pub outcome: ManualActionOutcome,
pub orders: Vec<ManualExecutionOrder>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ManualActionOutcome {
NoOrdersNeeded,
NotExecuted,
OrdersTerminal,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ManualExecutionSource {
ManualSecurityTrade,
ManualPositionAction,
ManualRebalance,
StockPoolAllocation,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ManualExecutionOrder {
pub order_id: String,
pub broker_order_id: Option<String>,
pub source_adapter: Option<String>,
pub symbol: String,
pub side: OrderSide,
pub quantity: u32,
pub order_created_at: DateTime<Utc>,
pub terminal_observed_at: DateTime<Utc>,
pub terminal_status: ManualOrderTerminalStatus,
pub fills: Vec<ManualExecutionFill>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ManualOrderTerminalStatus {
Filled,
Cancelled,
Rejected,
Expired,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ManualExecutionFill {
pub trade_id: String,
pub observation_event_id: String,
pub observation_sequence: u64,
pub fee_observation_event_id: String,
pub fee_observation_sequence: u64,
pub fee_observed_at: DateTime<Utc>,
pub trade_date: NaiveDate,
pub executed_at: DateTime<Utc>,
pub observed_at: DateTime<Utc>,
pub timestamp_precision: ManualTimestampPrecision,
pub quantity: u32,
#[serde(with = "rust_decimal::serde::str")]
pub price: Decimal,
#[serde(default, with = "rust_decimal::serde::str_option")]
pub commission: Option<Decimal>,
#[serde(default, with = "rust_decimal::serde::str_option")]
pub stamp_tax: Option<Decimal>,
#[serde(default, with = "rust_decimal::serde::str_option")]
pub transfer_fee: Option<Decimal>,
/// Full observed charge, including any venue fees not itemized above.
#[serde(with = "rust_decimal::serde::str")]
pub total_fee: Decimal,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ManualTimestampPrecision {
Second,
Millisecond,
Microsecond,
Nanosecond,
}
impl ManualTimestampPrecision {
fn nanoseconds(self) -> i64 {
match self {
Self::Second => 1_000_000_000,
Self::Millisecond => 1_000_000,
Self::Microsecond => 1_000,
Self::Nanosecond => 1,
}
}
}
impl ManualExecutionFill {
pub fn gross_amount(&self) -> Result<Decimal, String> {
self.price
.checked_mul(Decimal::from(self.quantity))
.ok_or_else(|| "manual fill gross amount overflow".into())
}
pub fn total_fees(&self) -> Result<Decimal, String> {
let known = [self.commission, self.stamp_tax, self.transfer_fee]
.into_iter()
.flatten()
.try_fold(Decimal::ZERO, |sum, fee| {
if fee < Decimal::ZERO {
return Err("manual fill fee component is negative");
}
sum.checked_add(fee).ok_or("manual fill fees overflow")
})?;
if self.total_fee < known {
return Err("manual total fee is below its known components".into());
}
Ok(self.total_fee)
}
}
fn identifier(value: &str) -> Result<(), String> {
if value.is_empty()
|| value.trim() != value
|| value.len() > 256
|| value.chars().any(char::is_control)
{
return Err("manual execution identity is empty, untrimmed or invalid".into());
}
Ok(())
}
impl ManualExecutionReplay {
/// Market/indicator data is needed for securities whose observed fills
/// change the portfolio. A rejected, never-filled order is not data demand.
pub fn required_data_symbols(&self) -> Result<BTreeSet<String>, String> {
self.validate()?;
Ok(self
.actions
.iter()
.flat_map(|action| &action.orders)
.filter(|order| !order.fills.is_empty())
.map(|order| order.symbol.clone())
.collect())
}
pub fn observations(&self) -> Result<Vec<ManualFillObservation<'_>>, String> {
self.validate()?;
let mut observations = Vec::new();
for action in &self.actions {
for order in &action.orders {
for fill in &order.fills {
observations.push(ManualFillObservation {
action,
order,
fill,
});
}
}
}
observations.sort_by_key(|entry| (entry.fill.observed_at, entry.fill.observation_sequence));
Ok(observations)
}
pub fn content_digest(&self) -> Result<String, String> {
let mut value = serde_json::to_value(self).map_err(|error| error.to_string())?;
value
.as_object_mut()
.ok_or("manual replay is not an object")?
.remove("contentSha256");
let bytes = serde_json::to_vec(&value).map_err(|error| error.to_string())?;
Ok(format!("{:x}", Sha256::digest(bytes)))
}
pub fn validate(&self) -> Result<(), String> {
if self.schema != MANUAL_REPLAY_SCHEMA
&& self.schema != "fidc.observed-manual-executions/v2"
{
return Err("unsupported manual replay schema".into());
}
if self.schema == "fidc.observed-manual-executions/v2"
&& (!self.position_exposure_events.is_empty()
|| !self.legacy_position_exposure_bps.is_empty())
{
return Err("runtime configuration requires manual replay v3".into());
}
crate::position_exposure::PositionExposureTimeline::from_events(
&self.position_exposure_events,
)?;
if self.position_exposure_events.iter().any(|event| event.effective_at > self.observation_cutoff) {
return Err("observed runtime position event is after the evidence cutoff".into());
}
if self
.legacy_position_exposure_bps
.values()
.any(|value| !(0..=10000).contains(value))
{
return Err("legacy manual exposure is outside 0..10000 bps".into());
}
identifier(&self.runtime_id)?;
identifier(&self.account_id)?;
if self.source_contract_sha256.len() != 64
|| !self
.source_contract_sha256
.bytes()
.all(|v| v.is_ascii_hexdigit())
{
return Err("manual replay source contract hash is invalid".into());
}
if self.content_digest()? != self.content_sha256 {
return Err("manual replay content digest mismatch".into());
}
if self.actions.len() > 100_000 {
return Err("manual replay action limit exceeded; trace was not truncated".into());
}
let shanghai = FixedOffset::east_opt(8 * 3600).unwrap();
let mut actions = BTreeSet::new();
let mut audits = BTreeSet::new();
let mut orders = BTreeSet::new();
let mut broker_orders = BTreeSet::new();
let mut trades = BTreeSet::new();
let mut observation_events = BTreeSet::new();
let mut observation_sequences = BTreeSet::new();
let mut fee_observations = BTreeSet::new();
let mut receipt_ids = BTreeMap::new();
let mut receipt_sequences = BTreeMap::new();
for action in &self.actions {
identifier(&action.action_id)?;
if !actions.insert(action.action_id.as_str())
|| action.confirmed_at > self.observation_cutoff
|| action.confirmation_observed_at < action.confirmed_at
|| action.confirmation_observed_at > self.observation_cutoff
{
return Err("duplicate manual action or confirmation after cutoff".into());
}
if action.audit_event_ids.is_empty() {
return Err("manual action has no immutable audit binding".into());
}
if (action.outcome != ManualActionOutcome::OrdersTerminal) != action.orders.is_empty() {
return Err("manual action outcome does not prove its order coverage".into());
}
for id in &action.audit_event_ids {
identifier(id)?;
if !audits.insert(id.as_str()) {
return Err("manual audit event is bound more than once".into());
}
}
for order in &action.orders {
identifier(&order.order_id)?;
if let Some(adapter) = &order.source_adapter {
identifier(adapter)?;
}
identifier(&order.symbol)?;
if let Some(id) = &order.broker_order_id {
identifier(id)?;
if !broker_orders.insert((
order
.source_adapter
.as_deref()
.ok_or("broker identity requires its source adapter")?,
order.order_created_at.with_timezone(&shanghai).date_naive(),
id.as_str(),
)) {
return Err("manual local orders share one broker order identity".into());
}
}
if !order.fills.is_empty() && order.source_adapter.is_none() {
return Err("manual fills require a known source adapter".into());
}
if !order.fills.is_empty()
&& order.source_adapter.as_deref() != Some("paper")
&& order.broker_order_id.is_none()
{
return Err(
"manual broker fills require their original broker order identity".into(),
);
}
if !orders.insert(order.order_id.as_str())
|| order.quantity == 0
|| order.quantity > i32::MAX as u32
{
return Err("duplicate manual order or invalid quantity".into());
}
if order.order_created_at < action.confirmed_at
|| order.terminal_observed_at < order.order_created_at
|| order.terminal_observed_at > self.observation_cutoff
{
return Err(
"manual order confirmation/submission/terminal time is inconsistent".into(),
);
}
let mut filled = 0_u32;
for fill in &order.fills {
identifier(&fill.trade_id)?;
identifier(&fill.observation_event_id)?;
identifier(&fill.fee_observation_event_id)?;
if fill.observation_sequence == 0
|| fill.observation_sequence > i64::MAX as u64
|| !observation_events.insert(fill.observation_event_id.as_str())
|| !observation_sequences.insert(fill.observation_sequence)
{
return Err(
"manual fill requires a unique durable observation event and sequence"
.into(),
);
}
if fill.fee_observation_sequence == 0
|| fill.fee_observation_sequence > i64::MAX as u64
|| fill.fee_observed_at < fill.observed_at
|| fill.fee_observed_at > self.observation_cutoff
|| !fee_observations.insert((
fill.fee_observation_event_id.as_str(),
fill.fee_observation_sequence,
))
{
return Err("manual finalized fees require their own unique observation within the cutoff".into());
}
if (fill.fee_observation_event_id == fill.observation_event_id)
!= (fill.fee_observation_sequence == fill.observation_sequence)
|| (fill.fee_observation_event_id == fill.observation_event_id
&& fill.fee_observed_at != fill.observed_at)
{
return Err("manual fill and fee observation identities disagree".into());
}
if !trades.insert((fill.trade_date, fill.trade_id.as_str()))
|| fill.quantity == 0
{
return Err("duplicate manual trade or zero fill quantity".into());
}
for (id, sequence) in [
(&fill.observation_event_id, fill.observation_sequence),
(
&fill.fee_observation_event_id,
fill.fee_observation_sequence,
),
] {
if receipt_ids
.insert(id, (&fill.trade_id, sequence))
.is_some_and(|owner| owner != (&fill.trade_id, sequence))
|| receipt_sequences
.insert(sequence, (&fill.trade_id, id))
.is_some_and(|owner| owner != (&fill.trade_id, id))
{
return Err("manual observation identity is reused by a different trade or sequence".into());
}
}
if fill.executed_at.with_timezone(&shanghai).date_naive() != fill.trade_date
|| fill.observed_at > self.observation_cutoff
|| fill.observed_at < order.order_created_at
|| fill.observed_at < action.confirmation_observed_at
|| fill.observed_at < fill.executed_at
|| fill.executed_at > order.terminal_observed_at
{
return Err("manual fill execution/observation time is inconsistent".into());
}
if i64::from(fill.executed_at.nanosecond())
% fill.timestamp_precision.nanoseconds()
!= 0
{
return Err(
"broker timestamp contains digits finer than its declared precision"
.into(),
);
}
let upper = fill
.executed_at
.checked_add_signed(chrono::Duration::nanoseconds(
fill.timestamp_precision.nanoseconds(),
))
.ok_or("manual execution timestamp overflow")?;
let earliest = order.order_created_at.max(action.confirmation_observed_at);
if fill.executed_at < earliest && earliest >= upper {
return Err("manual fill predates its order or durable confirmation".into());
}
if fill.price <= Decimal::ZERO {
return Err("manual fill requires a positive price".into());
}
fill.gross_amount()?
.checked_add(fill.total_fees()?)
.ok_or("manual fill cash amount overflow")?;
filled = filled
.checked_add(fill.quantity)
.ok_or("manual cumulative fill quantity overflow")?;
}
if filled > order.quantity
|| (order.terminal_status == ManualOrderTerminalStatus::Filled
&& filled != order.quantity)
|| (order.terminal_status == ManualOrderTerminalStatus::Rejected && filled != 0)
|| (matches!(
order.terminal_status,
ManualOrderTerminalStatus::Cancelled | ManualOrderTerminalStatus::Expired
) && filled == order.quantity)
{
return Err("manual terminal status disagrees with cumulative fills".into());
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct ManualFillObservation<'a> {
pub action: &'a ManualExecutionAction,
pub order: &'a ManualExecutionOrder,
pub fill: &'a ManualExecutionFill,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AppliedManualFill {
pub gross: FixedMoney,
pub fees: FixedMoney,
pub cash_delta: FixedMoney,
pub quantity_after: u32,
}
/// One replay owns its immutable trace and progress. Advancing is atomic even
/// if a later receipt in the same step disagrees with the shadow account.
pub struct ManualReplayCursor {
replay: std::sync::Arc<ManualExecutionReplay>,
indices: Vec<(usize, usize, usize)>,
cursor: usize,
clock: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ManualReplayApplication {
pub action_id: String,
pub order_id: String,
pub trade_id: String,
pub observation_event_id: String,
pub observation_sequence: u64,
pub observed_at: DateTime<Utc>,
pub fee_observation_event_id: String,
pub fee_observed_at: DateTime<Utc>,
pub executed_at: DateTime<Utc>,
pub symbol: String,
pub side: OrderSide,
pub quantity: u32,
pub quantity_after: u32,
pub price: String,
pub commission: Option<String>,
pub stamp_tax: Option<String>,
pub transfer_fee: Option<String>,
pub source_total_fee: String,
pub source_gross_amount: String,
pub ledger_gross_amount: String,
pub ledger_fees: String,
pub cash_delta: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub corporate_adjustment: Option<ManualCorporateAdjustment>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ManualCorporateAdjustment {
pub schema: String,
pub observed_at: DateTime<Utc>,
pub cash_dividends_enabled: bool,
pub dividend_cost_basis_adjustment: bool,
#[serde(default, skip_serializing_if = "disabled_flag")]
pub dividend_reinvestment: bool,
pub actions: Vec<ManualCorporateActionReference>,
pub cash_before: String,
pub cash_after: String,
pub corporate_cash_delta: String,
pub positions: BTreeMap<String, ManualCorporatePositionChange>,
pub reference_sha256: String,
pub replayed_sha256: String,
}
fn disabled_flag(value: &bool) -> bool { !value }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ManualCorporateActionReference {
pub date: NaiveDate,
pub symbol: String,
pub successor_symbol: Option<String>,
pub share_cash: String,
pub split_ratio: String,
pub successor_ratio: Option<String>,
pub successor_cash: Option<String>,
pub sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ManualCorporatePositionChange {
pub quantity_before: u32,
pub quantity_after: u32,
pub cost_basis_before: String,
pub cost_basis_after: String,
}
impl ManualReplayCursor {
pub(crate) fn frozen_source(&self) -> std::sync::Arc<ManualExecutionReplay> {
self.replay.clone()
}
pub(crate) fn next_observation(&self) -> Option<ManualFillObservation<'_>> {
self.indices.get(self.cursor).map(|&(a, o, f)| ManualFillObservation {
action: &self.replay.actions[a], order: &self.replay.actions[a].orders[o],
fill: &self.replay.actions[a].orders[o].fills[f],
})
}
pub(crate) fn advance_next_projected<F>(
&mut self, portfolio: &mut PortfolioState, project: F,
) -> Result<Option<ManualReplayApplication>, String>
where F: FnOnce(ManualFillObservation<'_>, &mut PortfolioState) -> Result<(AppliedManualFill, ManualCorporateAdjustment), String> {
let Some(observation) = self.next_observation() else { return Ok(None); };
let at = observation.fill.observed_at;
if at > self.replay.observation_cutoff || self.clock.is_some_and(|clock| at < clock) {
return Err("manual projected observation clock violates the frozen trace".into());
}
let mut next = portfolio.clone();
let (applied, adjustment) = project(observation, &mut next)?;
let mut application = observation.application(applied)?;
application.corporate_adjustment = Some(adjustment);
crate::finite_serialization::validate(&application).map_err(|error| error.to_string())?;
*portfolio = next;
self.cursor += 1;
self.clock = Some(at);
Ok(Some(application))
}
pub fn new(replay: ManualExecutionReplay) -> Result<Self, String> {
Self::from_shared(std::sync::Arc::new(replay))
}
pub fn from_shared(replay: std::sync::Arc<ManualExecutionReplay>) -> Result<Self, String> {
replay.validate()?;
let mut indices = Vec::new();
for (a, action) in replay.actions.iter().enumerate() {
for (o, order) in action.orders.iter().enumerate() {
for f in 0..order.fills.len() {
indices.push((a, o, f));
}
}
}
indices.sort_by_key(|&(a, o, f)| {
let fill = &replay.actions[a].orders[o].fills[f];
(fill.observed_at, fill.observation_sequence)
});
Ok(Self {
replay,
indices,
cursor: 0,
clock: None,
})
}
pub fn next_observation_at(&self) -> Option<DateTime<Utc>> {
self.indices
.get(self.cursor)
.map(|&(a, o, f)| self.replay.actions[a].orders[o].fills[f].observed_at)
}
pub fn applied_count(&self) -> usize {
self.cursor
}
pub fn advance(
&mut self,
at: DateTime<Utc>,
portfolio: &mut PortfolioState,
data: &DataSet,
has_pending_orders: bool,
) -> Result<Vec<ManualReplayApplication>, String> {
let end = self.cursor
+ self.indices[self.cursor..].iter().take_while(|&&(a, o, f)| {
self.replay.actions[a].orders[o].fills[f].observed_at <= at
}).count();
self.advance_through(at, end, portfolio, data, has_pending_orders)
}
/// One receipt at a time lets callbacks observe the intermediate state
/// when multiple fills share a timestamp but have distinct durable sequences.
pub fn advance_next(
&mut self, portfolio: &mut PortfolioState, data: &DataSet, has_pending_orders: bool,
) -> Result<Option<ManualReplayApplication>, String> {
let Some(at) = self.next_observation_at() else { return Ok(None); };
let mut applications = self.advance_through(at, self.cursor + 1, portfolio, data, has_pending_orders)?;
Ok(applications.pop())
}
fn advance_through(
&mut self, at: DateTime<Utc>, end: usize, portfolio: &mut PortfolioState,
data: &DataSet, has_pending_orders: bool,
) -> Result<Vec<ManualReplayApplication>, String> {
if at > self.replay.observation_cutoff {
return Err("manual observation clock exceeds the frozen evidence cutoff".into());
}
if self.clock.is_some_and(|clock| at < clock) {
return Err("manual observation clock moved backwards".into());
}
if end == self.cursor {
self.clock = Some(at);
return Ok(vec![]);
}
let mut next = portfolio.clone();
let mut applications = Vec::with_capacity(end - self.cursor);
for &(a, o, f) in &self.indices[self.cursor..end] {
let action = &self.replay.actions[a];
let order = &action.orders[o];
let fill = &order.fills[f];
let applied = ManualFillObservation {
action,
order,
fill,
}
.apply(&mut next, data, has_pending_orders)?;
applications.push(ManualReplayApplication {
action_id: action.action_id.clone(),
order_id: order.order_id.clone(),
trade_id: fill.trade_id.clone(),
observation_event_id: fill.observation_event_id.clone(),
observation_sequence: fill.observation_sequence,
observed_at: fill.observed_at,
fee_observation_event_id: fill.fee_observation_event_id.clone(),
fee_observed_at: fill.fee_observed_at,
executed_at: fill.executed_at,
symbol: order.symbol.clone(),
side: order.side,
quantity: fill.quantity,
quantity_after: applied.quantity_after,
price: fill.price.to_string(),
commission: fill.commission.map(|fee| fee.to_string()),
stamp_tax: fill.stamp_tax.map(|fee| fee.to_string()),
transfer_fee: fill.transfer_fee.map(|fee| fee.to_string()),
source_total_fee: fill.total_fee.to_string(),
source_gross_amount: fill.gross_amount()?.to_string(),
ledger_gross_amount: applied.gross.to_decimal_string(),
ledger_fees: applied.fees.to_decimal_string(),
cash_delta: applied.cash_delta.to_decimal_string(),
corporate_adjustment: None,
});
}
*portfolio = next;
self.cursor = end;
self.clock = Some(at);
Ok(applications)
}
}
impl ManualFillObservation<'_> {
fn application(&self, applied: AppliedManualFill) -> Result<ManualReplayApplication, String> {
Ok(ManualReplayApplication {
action_id: self.action.action_id.clone(), order_id: self.order.order_id.clone(),
trade_id: self.fill.trade_id.clone(), observation_event_id: self.fill.observation_event_id.clone(),
observation_sequence: self.fill.observation_sequence, observed_at: self.fill.observed_at,
fee_observation_event_id: self.fill.fee_observation_event_id.clone(), fee_observed_at: self.fill.fee_observed_at,
executed_at: self.fill.executed_at, symbol: self.order.symbol.clone(), side: self.order.side,
quantity: self.fill.quantity, quantity_after: applied.quantity_after, price: self.fill.price.to_string(),
commission: self.fill.commission.map(|fee| fee.to_string()), stamp_tax: self.fill.stamp_tax.map(|fee| fee.to_string()),
transfer_fee: self.fill.transfer_fee.map(|fee| fee.to_string()), source_total_fee: self.fill.total_fee.to_string(),
source_gross_amount: self.fill.gross_amount()?.to_string(), ledger_gross_amount: applied.gross.to_decimal_string(),
ledger_fees: applied.fees.to_decimal_string(), cash_delta: applied.cash_delta.to_decimal_string(), corporate_adjustment: None,
})
}
pub(crate) fn apply(
&self,
portfolio: &mut PortfolioState,
data: &DataSet,
has_pending_orders: bool,
) -> Result<AppliedManualFill, String> {
if has_pending_orders {
return Err("manual observation conflicts with pending shadow orders".into());
}
let instrument = data
.instrument(&self.order.symbol)
.ok_or("manual observation instrument is absent from frozen source data")?;
if instrument
.dated_market_absence_reason(self.fill.trade_date)
.is_some()
{
return Err("manual execution contradicts the frozen instrument lifecycle".into());
}
let gross = FixedMoney::from_decimal_str(&self.fill.gross_amount()?.to_string())?;
let fees = FixedMoney::from_decimal_str(&self.fill.total_fees()?.to_string())?;
let price = self
.fill
.price
.to_f64()
.filter(|price| price.is_finite() && *price > 0.)
.ok_or("manual execution price cannot be represented for valuation")?;
// This is the real observed trade price, not a fabricated quote. The
// normal market clock remains responsible for subsequent marks.
let cash_delta = portfolio.apply_observed_manual_fill(
self.fill.trade_date,
&self.order.symbol,
self.order.side,
self.fill.quantity,
price,
price,
gross,
fees,
)?;
Ok(AppliedManualFill {
gross,
fees,
cash_delta,
quantity_after: portfolio
.position(&self.order.symbol)
.map_or(0, |position| position.quantity),
})
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,761 @@
use super::*;
use serde_json::{Value, json};
fn sample() -> ManualExecutionReplay {
let fill = json!({"tradeId":"trade-1","observationEventId":"received-1","observationSequence":1,"tradeDate":"2026-09-14","executedAt":"2026-09-14T01:30:00Z",
"feeObservationEventId":"received-1","feeObservationSequence":1,"feeObservedAt":"2026-09-14T01:30:01Z",
"observedAt":"2026-09-14T01:30:01Z","timestampPrecision":"second","quantity":100,
"price":"10.1234567891","commission":"0.1000001","stampTax":"0","transferFee":"0.02","totalFee":"0.1200001"});
let mut input:ManualExecutionReplay=serde_json::from_value(json!({
"schema":MANUAL_REPLAY_SCHEMA,"runtimeId":"runtime-1","accountId":"account-1",
"sourceContractSha256":"a".repeat(64),"contentSha256":"", "observationCutoff":"2026-09-14T08:00:00Z",
"actions":[{"actionId":"action-1","source":"manual_security_trade","auditEventIds":["audit-1"],
"confirmedAt":"2026-09-14T01:30:00.500Z","confirmationObservedAt":"2026-09-14T01:30:00.550Z","outcome":"orders_terminal","orders":[{
"orderId":"order-1","brokerOrderId":"broker-1","sourceAdapter":"gt-api","symbol":"000001.SZ","side":"Buy","quantity":100,
"orderCreatedAt":"2026-09-14T01:30:00.600Z","terminalObservedAt":"2026-09-14T01:30:00.900Z","terminalStatus":"filled",
"fills":[fill]
}]}]
})).unwrap();
reseal(&mut input);
input
}
fn reseal(input: &mut ManualExecutionReplay) {
input.content_sha256 = input.content_digest().unwrap();
}
#[test]
fn a_failed_corporate_projection_does_not_change_the_book_or_receipt_cursor() {
let mut cursor = ManualReplayCursor::new(sample()).unwrap();
let mut portfolio = PortfolioState::new(10000.);
let before = portfolio.financial_replay_identity();
let error = cursor.advance_next_projected(&mut portfolio, |_, next| {
next.apply_cash_delta(-50.)?;
next.position_mut("000001.SZ").buy(NaiveDate::from_ymd_opt(2026, 9, 11).unwrap(), 100, 10.);
Err("financial coverage mismatch".into())
}).unwrap_err();
assert_eq!(error, "financial coverage mismatch");
assert_eq!(portfolio.financial_replay_identity(), before);
assert_eq!(cursor.applied_count(), 0);
}
fn delayed_buy_replay() -> ManualExecutionReplay {
let mut input = sample();
let template = input.actions[0].clone();
input.actions.clear();
for (index, side, executed, observed, price, fee) in [
(
0,
OrderSide::Buy,
"2026-09-14T01:30:00Z",
"2026-09-14T01:30:01Z",
"20",
"0.25",
),
(
1,
OrderSide::Buy,
"2026-09-11T06:00:00Z",
"2026-09-14T01:30:02Z",
"10",
"0.75",
),
(
2,
OrderSide::Sell,
"2026-09-14T01:31:00Z",
"2026-09-14T01:31:01Z",
"10",
"0.5",
),
(
3,
OrderSide::Sell,
"2026-09-14T01:32:00Z",
"2026-09-14T01:32:01Z",
"10",
"0.5",
),
] {
let executed: DateTime<Utc> = executed.parse().unwrap();
let observed: DateTime<Utc> = observed.parse().unwrap();
let mut action = template.clone();
action.action_id = format!("action-{index}");
action.audit_event_ids = vec![format!("audit-{index}")];
action.confirmed_at = executed - chrono::Duration::seconds(2);
action.confirmation_observed_at = action.confirmed_at;
let order = &mut action.orders[0];
order.order_id = format!("order-{index}");
order.broker_order_id = Some(format!("broker-{index}"));
order.side = side;
order.order_created_at = executed - chrono::Duration::seconds(1);
order.terminal_observed_at = observed;
let fill = &mut order.fills[0];
fill.trade_id = format!("trade-{index}");
fill.observation_event_id = format!("receipt-{index}");
fill.observation_sequence = index + 1;
fill.fee_observation_event_id = fill.observation_event_id.clone();
fill.fee_observation_sequence = fill.observation_sequence;
fill.trade_date = executed
.with_timezone(&FixedOffset::east_opt(8 * 3600).unwrap())
.date_naive();
fill.executed_at = executed;
fill.observed_at = observed;
fill.fee_observed_at = observed;
fill.price = price.parse().unwrap();
fill.commission = None;
fill.stamp_tax = None;
fill.transfer_fee = None;
fill.total_fee = fee.parse().unwrap();
input.actions.push(action);
}
reseal(&mut input);
input.validate().unwrap();
input
}
#[test]
fn late_buy_retains_the_earliest_opening_and_latest_buy_dates() {
let mut cursor = ManualReplayCursor::new(delayed_buy_replay()).unwrap();
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let mut portfolio = PortfolioState::new(10000.);
let applications = cursor
.advance(
"2026-09-14T01:30:02Z".parse().unwrap(),
&mut portfolio,
&data,
false,
)
.unwrap();
assert_eq!(
applications
.iter()
.map(|row| row.trade_id.as_str())
.collect::<Vec<_>>(),
["trade-0", "trade-1"]
);
let position = portfolio.position("000001.SZ").unwrap();
assert_eq!(position.opened_date(), NaiveDate::from_ymd_opt(2026, 9, 11));
assert_eq!(
position.last_buy_date(),
NaiveDate::from_ymd_opt(2026, 9, 14)
);
assert_eq!(position.quantity, 200);
let calendar = crate::TradingCalendar::new(
[11, 14, 15, 16, 17, 18]
.map(|day| NaiveDate::from_ymd_opt(2026, 9, day).unwrap())
.into(),
);
let evidence = crate::holding_policy::HoldingLifecycleEvidence {
has_position: true,
opened_date: position.opened_date(),
last_buy_date: position.last_buy_date(),
last_sell_date: None,
};
let mut policy = crate::holding_policy::AutomaticTradeProtection {
max_holding_days: 1,
..Default::default()
};
assert!(
policy
.evaluate(
"000001.SZ",
NaiveDate::from_ymd_opt(2026, 9, 14).unwrap(),
&evidence,
&calendar
)
.unwrap()
.max_holding_exit
);
policy.buy_protection_days = 3;
for day in [14, 15, 16, 17] {
let permission = policy
.evaluate(
"000001.SZ",
NaiveDate::from_ymd_opt(2026, 9, day).unwrap(),
&evidence,
&calendar,
)
.unwrap();
assert_eq!(permission.sell_denial, Some("buy_fill_protection"));
assert!(!permission.max_holding_exit);
}
assert!(
policy
.evaluate(
"000001.SZ",
NaiveDate::from_ymd_opt(2026, 9, 18).unwrap(),
&evidence,
&calendar
)
.unwrap()
.max_holding_exit
);
}
#[test]
fn late_buy_fifo_depletion_preserves_costs_and_cannot_unlock_today_lots() {
let mut cursor = ManualReplayCursor::new(delayed_buy_replay()).unwrap();
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let mut portfolio = PortfolioState::new(10000.);
let applications = cursor
.advance(
"2026-09-14T01:31:01Z".parse().unwrap(),
&mut portfolio,
&data,
false,
)
.unwrap();
assert_eq!(applications.len(), 3);
let position = portfolio.position("000001.SZ").unwrap();
assert_eq!(position.quantity, 100);
assert_eq!(position.unrealized_pnl(), -1000.25);
assert_eq!(
position.sellable_qty(NaiveDate::from_ymd_opt(2026, 9, 14).unwrap()),
0
);
assert_eq!(position.realized_pnl(), -0.75);
assert_eq!(portfolio.cash(), 7998.5);
assert_eq!(portfolio.external_cash_flow_total(), 0.);
assert!(
cursor
.advance(
"2026-09-14T01:32:01Z".parse().unwrap(),
&mut portfolio,
&data,
false
)
.unwrap_err()
.contains("T+1")
);
assert_eq!(cursor.applied_count(), 3);
assert_eq!(portfolio.cash(), 7998.5);
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 100);
}
fn semantic_result(input: &ManualExecutionReplay) -> Result<(), String> {
let mut input = input.clone();
reseal(&mut input);
input.validate()
}
#[test]
fn complete_exact_decimal_evidence_allows_later_observation_and_retains_source_digits() {
let input = sample();
input.validate().unwrap();
let fill = &input.actions[0].orders[0].fills[0];
assert_eq!(fill.gross_amount().unwrap().to_string(), "1012.3456789100");
assert_eq!(fill.total_fees().unwrap().to_string(), "0.1200001");
assert_eq!(
serde_json::to_value(&input).unwrap()["actions"][0]["orders"][0]["fills"][0]["price"],
"10.1234567891"
);
}
#[test]
fn data_scope_only_contains_actual_filled_securities_and_validates_the_source() {
let mut input = sample();
let mut rejected = input.actions[0].orders[0].clone();
rejected.order_id = "rejected-order".into();
rejected.broker_order_id = None;
rejected.source_adapter = None;
rejected.symbol = "510300.SH".into();
rejected.terminal_status = ManualOrderTerminalStatus::Rejected;
rejected.fills.clear();
input.actions[0].orders.push(rejected);
reseal(&mut input);
assert_eq!(
input.required_data_symbols().unwrap(),
BTreeSet::from(["000001.SZ".into()])
);
input.actions[0].orders[0].symbol = "600000.SH".into();
assert!(input.required_data_symbols().is_err());
}
#[test]
fn v2_facts_keep_their_encoding_but_cannot_silently_carry_new_runtime_settings() {
let mut input = sample();
input.schema = "fidc.observed-manual-executions/v2".into();
reseal(&mut input);
input.validate().unwrap();
let old = serde_json::to_value(&input).unwrap();
assert!(old.get("positionExposureEvents").is_none());
assert!(old.get("legacyPositionExposureBps").is_none());
input
.legacy_position_exposure_bps
.insert(NaiveDate::from_ymd_opt(2026, 9, 14).unwrap(), 5000);
reseal(&mut input);
assert!(input.validate().is_err());
input.schema = MANUAL_REPLAY_SCHEMA.into();
reseal(&mut input);
input.validate().unwrap();
}
#[test]
fn runtime_position_events_cannot_claim_observations_after_the_source_cutoff() {
let mut input = sample();
input.position_exposure_events.push(serde_json::from_value(json!({
"eventId": "position-event", "sequence": 1, "effectiveAt": input.observation_cutoff,
"action": "scale", "requestedBps": 5000
})).unwrap());
semantic_result(&input).unwrap();
input.position_exposure_events[0].effective_at += chrono::Duration::nanoseconds(1);
assert!(semantic_result(&input).unwrap_err().contains("after the evidence cutoff"));
}
#[test]
fn all_required_money_and_binding_fields_reject_missing_or_wrong_values() {
let original = serde_json::to_value(sample()).unwrap();
for field in ["price", "totalFee"] {
let mut missing = original.clone();
missing["actions"][0]["orders"][0]["fills"][0]
.as_object_mut()
.unwrap()
.remove(field);
assert!(
serde_json::from_value::<ManualExecutionReplay>(missing).is_err(),
"{field}"
);
let mut numeric = original.clone();
numeric["actions"][0]["orders"][0]["fills"][0][field] = json!(1.1);
assert!(
serde_json::from_value::<ManualExecutionReplay>(numeric).is_err(),
"numeric {field}"
);
}
for mutate in [
("schema", json!("unknown")),
("sourceContractSha256", json!("broken")),
("accountId", json!(" ")),
] {
let mut value = original.clone();
value[mutate.0] = mutate.1;
assert!(
semantic_result(&serde_json::from_value::<ManualExecutionReplay>(value).unwrap())
.is_err()
);
}
}
#[test]
fn inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected() {
let original = sample();
let mut invalid = original.clone();
invalid.actions[0].orders[0].quantity = 200;
assert!(semantic_result(&invalid).is_err());
let mut invalid = original.clone();
invalid.actions[0].orders[0].terminal_status = ManualOrderTerminalStatus::Rejected;
assert!(semantic_result(&invalid).is_err());
let mut invalid = original.clone();
invalid.actions[0].audit_event_ids.clear();
assert!(semantic_result(&invalid).is_err());
let mut invalid = original.clone();
invalid.actions.push(invalid.actions[0].clone());
assert!(semantic_result(&invalid).is_err());
let mut invalid = original.clone();
let duplicate = invalid.actions[0].orders[0].fills[0].clone();
invalid.actions[0].orders[0].fills.push(duplicate);
assert!(semantic_result(&invalid).is_err());
let mut invalid = original.clone();
invalid.actions[0].orders[0].broker_order_id = None;
assert!(semantic_result(&invalid).is_err());
invalid.actions[0].orders[0].source_adapter = Some("paper".into());
reseal(&mut invalid);
invalid.validate().unwrap();
}
#[test]
fn source_time_precision_is_not_invented_and_submitted_time_must_fit_the_interval() {
let mut input = sample();
input.actions[0].orders[0].order_created_at = "2026-09-14T01:30:00.999999Z".parse().unwrap();
input.actions[0].orders[0].terminal_observed_at = "2026-09-14T01:30:01.500Z".parse().unwrap();
input.actions[0].orders[0].fills[0].observed_at = "2026-09-14T01:30:02Z".parse().unwrap();
input.actions[0].orders[0].fills[0].fee_observed_at =
input.actions[0].orders[0].fills[0].observed_at;
reseal(&mut input);
input.validate().unwrap();
input.actions[0].orders[0].order_created_at = "2026-09-14T01:30:01Z".parse().unwrap();
assert!(semantic_result(&input).is_err());
let mut input = sample();
input.actions[0].orders[0].fills[0].executed_at = "2026-09-14T01:30:00.800Z".parse().unwrap();
assert!(semantic_result(&input).is_err());
input.actions[0].orders[0].fills[0].timestamp_precision = ManualTimestampPrecision::Millisecond;
reseal(&mut input);
input.validate().unwrap();
input.actions[0].orders[0].fills[0].executed_at =
"2026-09-14T01:30:00.800001Z".parse().unwrap();
assert!(semantic_result(&input).is_err());
}
#[test]
fn confirmed_no_order_outcome_is_distinct_from_unconfirmed_or_unknown_work() {
let mut input = sample();
input.actions[0].orders.clear();
assert!(semantic_result(&input).is_err());
input.actions[0].outcome = ManualActionOutcome::NoOrdersNeeded;
reseal(&mut input);
input.validate().unwrap();
input.actions[0].outcome = ManualActionOutcome::NotExecuted;
reseal(&mut input);
input.validate().unwrap();
let mut value = serde_json::to_value(input).unwrap();
value["actions"][0]["outcome"] = json!("result_unknown");
assert!(serde_json::from_value::<ManualExecutionReplay>(value).is_err());
}
#[test]
fn raw_timezone_and_cutoff_are_required() {
let mut value = serde_json::to_value(sample()).unwrap();
value["actions"][0]["orders"][0]["fills"][0]["executedAt"] = json!("2026-09-14T09:30:00");
assert!(serde_json::from_value::<ManualExecutionReplay>(value).is_err());
let mut input = sample();
input.observation_cutoff = "2026-09-14T01:30:00.700Z".parse().unwrap();
assert!(semantic_result(&input).is_err());
let mut value = serde_json::to_value(sample()).unwrap();
value["actions"][0]["orders"][0]["fills"][0]["totalFee"] = Value::Null;
assert!(serde_json::from_value::<ManualExecutionReplay>(value).is_err());
}
#[test]
fn authoritative_total_fee_does_not_require_inventing_unknown_components() {
let mut input = sample();
let fill = &mut input.actions[0].orders[0].fills[0];
fill.commission = None;
fill.stamp_tax = None;
fill.transfer_fee = None;
assert_eq!(
fill.total_fees().unwrap(),
"0.1200001".parse::<Decimal>().unwrap()
);
assert!(semantic_result(&input).is_ok());
let value = serde_json::to_value(&input).unwrap();
assert!(value["actions"][0]["orders"][0]["fills"][0]["commission"].is_null());
assert_eq!(
value["actions"][0]["orders"][0]["fills"][0]["totalFee"],
"0.1200001"
);
for field in ["commission", "stampTax", "transferFee"] {
let mut numeric = value.clone();
numeric["actions"][0]["orders"][0]["fills"][0][field] = json!(0.1);
assert!(serde_json::from_value::<ManualExecutionReplay>(numeric).is_err());
}
}
#[test]
fn manual_fee_total_includes_extra_charges_and_rejects_inconsistent_components() {
let mut input = sample();
input.actions[0].orders[0].fills[0].total_fee = "0.15".parse().unwrap();
assert!(semantic_result(&input).is_ok());
assert_eq!(
input.actions[0].orders[0].fills[0]
.total_fees()
.unwrap()
.to_string(),
"0.15"
);
input.actions[0].orders[0].fills[0].total_fee = "0.1".parse().unwrap();
assert!(semantic_result(&input).is_err());
input.actions[0].orders[0].fills[0].total_fee = "0.15".parse().unwrap();
input.actions[0].orders[0].fills[0].commission = Some(Decimal::NEGATIVE_ONE);
assert!(semantic_result(&input).is_err());
}
#[test]
fn late_fee_evidence_keeps_the_original_fill_observation_clock() {
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let mut input = sample();
let fill = &mut input.actions[0].orders[0].fills[0];
let original = fill.observed_at;
fill.fee_observation_event_id = "fee-receipt-1".into();
fill.fee_observation_sequence = 2;
fill.fee_observed_at = original + chrono::Duration::hours(1);
let fee_time = fill.fee_observed_at;
reseal(&mut input);
let mut cursor = ManualReplayCursor::new(input).unwrap();
assert_eq!(cursor.next_observation_at(), Some(original));
let mut portfolio = PortfolioState::new(10_000.);
let result = cursor
.advance(original, &mut portfolio, &data, false)
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].observed_at, original);
assert_eq!(result[0].fee_observed_at, fee_time);
assert_eq!(result[0].source_total_fee, "0.1200001");
assert!(
cursor
.advance(fee_time, &mut portfolio, &data, false)
.unwrap()
.is_empty()
);
}
#[test]
fn changing_any_external_price_or_identity_invalidates_the_frozen_trace() {
let input = sample();
let original = input.content_sha256.clone();
let mut changed = input.clone();
changed.actions[0].orders[0].fills[0].price += Decimal::ONE;
assert_ne!(changed.content_digest().unwrap(), original);
assert_eq!(
changed.validate().unwrap_err(),
"manual replay content digest mismatch"
);
let mut changed = input;
changed.account_id = "another-account".into();
assert_ne!(changed.content_digest().unwrap(), original);
assert!(changed.validate().is_err());
}
fn identity_data(listed: NaiveDate) -> DataSet {
DataSet::from_components(
vec![crate::Instrument {
symbol: "000001.SZ".into(),
name: "test".into(),
board: "SZ".into(),
round_lot: 100,
listed_at: Some(listed),
delisted_at: None,
status: "active".into(),
}],
vec![],
vec![],
vec![],
vec![crate::BenchmarkSnapshot {
date: listed,
benchmark: "000300.SH".into(),
open: 100.,
close: 100.,
prev_close: 100.,
volume: 0,
}],
)
.unwrap()
}
#[test]
fn confirmed_manual_fill_changes_cash_and_lots_but_not_external_cash_flow_units() {
let input = sample();
let observations = input.observations().unwrap();
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let mut account = PortfolioState::new(10_000.);
let applied = observations[0].apply(&mut account, &data, false).unwrap();
assert_eq!(
applied.gross,
FixedMoney::from_decimal_str("1012.345679").unwrap()
);
assert_eq!(applied.fees, FixedMoney::from_decimal_str("0.12").unwrap());
assert_eq!(account.cash(), 8987.534321);
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
assert_eq!(
account
.position("000001.SZ")
.unwrap()
.sellable_qty(input.actions[0].orders[0].fills[0].trade_date),
0
);
assert_eq!(account.external_cash_flow_total(), 0.);
assert_eq!(account.starting_cash(), 10_000.);
}
#[test]
fn manual_mismatches_are_atomic_and_do_not_borrow_shares_cash_or_override_pending_orders() {
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let input = sample();
let observations = input.observations().unwrap();
let mut poor = PortfolioState::new(10.);
assert!(observations[0].apply(&mut poor, &data, false).is_err());
assert_eq!(poor.cash(), 10.);
assert!(poor.positions().is_empty());
let mut account = PortfolioState::new(10_000.);
assert!(observations[0].apply(&mut account, &data, true).is_err());
assert_eq!(account.cash(), 10_000.);
assert!(account.positions().is_empty());
observations[0].apply(&mut account, &data, false).unwrap();
let before = account.cash();
let mut sell = input.clone();
sell.actions[0].orders[0].side = OrderSide::Sell;
reseal(&mut sell);
assert!(
sell.observations().unwrap()[0]
.apply(&mut account, &data, false)
.unwrap_err()
.contains("T+1")
);
assert_eq!(account.cash(), before);
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
let unlisted = identity_data(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap());
assert!(
observations[0]
.apply(&mut account, &unlisted, false)
.unwrap_err()
.contains("lifecycle")
);
assert_eq!(account.cash(), before);
}
#[test]
fn the_next_day_manual_sale_keeps_the_actual_quantity_and_fee_contract() {
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let input = sample();
let mut account = PortfolioState::new(10_000.);
input.observations().unwrap()[0]
.apply(&mut account, &data, false)
.unwrap();
let mut sell = input.clone();
let order = &mut sell.actions[0].orders[0];
order.side = OrderSide::Sell;
order.order_created_at += chrono::Duration::days(1);
order.terminal_observed_at += chrono::Duration::days(1);
order.fills[0].trade_date = order.fills[0].trade_date.succ_opt().unwrap();
order.fills[0].executed_at += chrono::Duration::days(1);
order.fills[0].observed_at += chrono::Duration::days(1);
order.fills[0].fee_observed_at += chrono::Duration::days(1);
sell.observation_cutoff += chrono::Duration::days(1);
reseal(&mut sell);
let applied = sell.observations().unwrap()[0]
.apply(&mut account, &data, false)
.unwrap();
assert_eq!(applied.quantity_after, 0);
assert_eq!(account.cash(), 9999.76);
assert_eq!(account.external_cash_flow_total(), 0.);
}
#[test]
fn observations_follow_durable_receipt_order_and_not_input_array_order() {
let mut input = sample();
let mut second = input.actions[0].orders[0].fills[0].clone();
second.trade_id = "trade-2".into();
second.observation_event_id = "received-2".into();
second.observation_sequence = 2;
second.fee_observation_event_id = "received-2".into();
second.fee_observation_sequence = 2;
input.actions[0].orders[0].quantity = 200;
input.actions[0].orders[0].fills.insert(0, second);
reseal(&mut input);
assert_eq!(
input
.observations()
.unwrap()
.iter()
.map(|row| row.fill.observation_sequence)
.collect::<Vec<_>>(),
vec![1, 2]
);
let mut invalid = input.clone();
invalid.actions[0].orders[0].fills[0].observation_sequence = 1;
assert!(
semantic_result(&invalid)
.unwrap_err()
.contains("observation")
);
let mut invalid = input;
invalid.actions[0].orders[0].fills[0].observation_event_id = "received-1".into();
assert!(
semantic_result(&invalid)
.unwrap_err()
.contains("observation")
);
}
#[test]
fn partial_cancel_is_valid_but_full_fill_cannot_be_reported_as_cancelled() {
let mut input = sample();
input.actions[0].orders[0].quantity = 200;
input.actions[0].orders[0].terminal_status = ManualOrderTerminalStatus::Cancelled;
semantic_result(&input).unwrap();
input.actions[0].orders[0].quantity = 100;
assert!(
semantic_result(&input)
.unwrap_err()
.contains("terminal status")
);
}
#[test]
fn cursor_waits_for_observation_and_never_reapplies_or_rewinds() {
let input = sample();
let at = input.actions[0].orders[0].fills[0].observed_at;
let mut replay = ManualReplayCursor::new(input).unwrap();
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let mut account = PortfolioState::new(10_000.);
assert_eq!(replay.next_observation_at(), Some(at));
assert!(
replay
.advance(
at - chrono::Duration::milliseconds(1),
&mut account,
&data,
false
)
.unwrap()
.is_empty()
);
assert_eq!(account.cash(), 10_000.);
let records = replay.advance(at, &mut account, &data, false).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].cash_delta, "-1012.465679");
assert_eq!(replay.applied_count(), 1);
assert_eq!(replay.next_observation_at(), None);
let cash = account.cash();
assert!(
replay
.advance(at, &mut account, &data, false)
.unwrap()
.is_empty()
);
assert_eq!(account.cash(), cash);
assert!(
replay
.advance(
at - chrono::Duration::seconds(1),
&mut account,
&data,
false
)
.unwrap_err()
.contains("backwards")
);
}
#[test]
fn failed_multi_receipt_advance_keeps_both_progress_and_portfolio_unchanged() {
let mut input = sample();
let mut next = input.actions[0].orders[0].fills[0].clone();
next.trade_id = "trade-2".into();
next.observation_event_id = "received-2".into();
next.observation_sequence = 2;
next.fee_observation_event_id = "received-2".into();
next.fee_observation_sequence = 2;
input.actions[0].orders[0].quantity = 200;
input.actions[0].orders[0].fills.push(next);
reseal(&mut input);
let at = input.actions[0].orders[0].fills[0].observed_at;
let mut replay = ManualReplayCursor::new(input).unwrap();
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let mut account = PortfolioState::new(1_500.);
assert!(replay.advance(at, &mut account, &data, false).is_err());
assert_eq!(account.cash(), 1_500.);
assert!(account.positions().is_empty());
assert_eq!(replay.applied_count(), 0);
assert_eq!(replay.next_observation_at(), Some(at));
}
#[test]
fn fixed_money_decimal_text_preserves_micro_units_without_float_conversion() {
for text in [
"0",
"100",
"-100",
"0.000001",
"-0.000001",
"12345678901234567890123456.123456",
] {
assert_eq!(
FixedMoney::from_decimal_str(text)
.unwrap()
.to_decimal_string(),
text
);
}
let min = FixedMoney::from_raw(i128::MIN);
assert!(min.to_decimal_string().starts_with('-'));
}
+189 -100
View File
@@ -9,6 +9,75 @@ use crate::portfolio::HoldingSummary;
const TRADING_DAYS_PER_YEAR: f64 = 252.0;
const MONTHS_PER_YEAR: f64 = 12.0;
pub const RISK_STATISTICS_VERSION: &str = "fidc-risk-statistics/v2";
/// Shared by historical backtests and observed paper/live account returns.
/// Undefined ratios remain None; callers must not invent a risk-free rate.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct RiskAdjustedStatistics {
pub sharpe: Option<f64>,
pub sortino: Option<f64>,
pub downside_volatility: Option<f64>,
}
pub fn risk_adjusted_statistics(
returns: &[f64], rates: &[f64], periods_per_year: f64,
) -> Result<RiskAdjustedStatistics, &'static str> {
if returns.len() != rates.len() || !periods_per_year.is_finite() || periods_per_year <= 0.0
|| returns.iter().chain(rates).any(|value| !value.is_finite()) {
return Err("risk-adjusted statistics require finite aligned returns and rates");
}
if returns.is_empty() { return Ok(RiskAdjustedStatistics::default()); }
let adjusted: Vec<_> = returns.iter().zip(rates).map(|(value, rate)| value-rate).collect();
if adjusted.iter().any(|value| !value.is_finite()) { return Err("risk-adjusted return overflow"); }
let mean_return = mean(&adjusted);
let deviation = std_dev(&adjusted);
let downside = (adjusted.iter().map(|value| value.min(0.0).powi(2)).sum::<f64>() / adjusted.len() as f64).sqrt();
let annual = periods_per_year.sqrt();
Ok(RiskAdjustedStatistics {
sharpe: (adjusted.len() > 1 && deviation > f64::EPSILON).then_some(mean_return/deviation*annual).filter(|value|value.is_finite()),
sortino: (downside > f64::EPSILON).then_some(mean_return/downside*annual).filter(|value|value.is_finite()),
downside_volatility: Some(downside*annual).filter(|value|value.is_finite()),
})
}
#[cfg(test)]
mod risk_adjusted_contract_tests {
use super::*;
#[test]
fn changing_daily_rates_adjusts_each_return_before_variance_and_downside() {
let returns = [0.02, -0.01, 0.005];
let rates = [0.0001, 0.0002, 0.0003];
let values: Vec<f64> = returns.iter().zip(rates).map(|(r,f)| r-f).collect();
let stats = risk_adjusted_statistics(&returns,&rates,252.0).unwrap();
let average = values.iter().sum::<f64>()/3.0;
let deviation = (values.iter().map(|r|(r-average).powi(2)).sum::<f64>()/2.0).sqrt();
let downside = (values.iter().map(|r|r.min(0.0).powi(2)).sum::<f64>()/3.0).sqrt();
assert!((stats.sharpe.unwrap()-average/deviation*252.0_f64.sqrt()).abs()<1e-12);
assert!((stats.sortino.unwrap()-average/downside*252.0_f64.sqrt()).abs()<1e-12);
assert_ne!(stats.sharpe, risk_adjusted_statistics(&returns,&[0.0;3],252.0).unwrap().sharpe);
}
#[test]
fn incomplete_or_invalid_rates_are_not_zero_rate_observations() {
for rates in [vec![],vec![0.0],vec![0.0,f64::NAN],vec![0.0,f64::INFINITY]] {
assert!(risk_adjusted_statistics(&[0.01,-0.01],&rates,252.0).is_err());
}
assert!(risk_adjusted_statistics(&[f64::NAN],&[0.0],252.0).is_err());
assert!(risk_adjusted_statistics(&[0.0],&[0.0],0.0).is_err());
}
#[test]
fn zero_deviation_and_single_sample_ratios_remain_undefined() {
let flat=risk_adjusted_statistics(&[0.001,0.001],&[0.001,0.001],252.0).unwrap();
assert!(flat.sharpe.is_none() && flat.sortino.is_none());
assert_eq!(flat.downside_volatility,Some(0.0));
let one=risk_adjusted_statistics(&[-0.01],&[0.001],252.0).unwrap();
assert_eq!(one.sharpe,None);
assert!((one.sortino.unwrap()+252.0_f64.sqrt()).abs()<1e-12);
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -34,13 +103,15 @@ pub struct RiskFreeRateContract {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BacktestMetrics {
#[serde(default)]
pub risk_statistics_version: String,
pub total_return: f64,
pub annual_return: f64,
pub sharpe: f64,
pub sharpe: Option<f64>,
pub max_drawdown: f64,
pub win_rate: f64,
pub alpha: f64,
pub beta: f64,
pub alpha: Option<f64>,
pub beta: Option<f64>,
pub benchmark_cumulative_return: f64,
pub benchmark_net_value: f64,
pub risk_free_rate: f64,
@@ -49,14 +120,14 @@ pub struct BacktestMetrics {
pub excess_annual_return: f64,
pub max_drawdown_duration_days: usize,
pub total_trade_days: usize,
pub sortino: f64,
pub downside_risk: f64,
pub information_ratio: f64,
pub tracking_error: f64,
pub volatility: f64,
pub sortino: Option<f64>,
pub downside_risk: Option<f64>,
pub information_ratio: Option<f64>,
pub tracking_error: Option<f64>,
pub volatility: Option<f64>,
pub excess_return: f64,
pub excess_sharpe: f64,
pub excess_volatility: f64,
pub excess_sharpe: Option<f64>,
pub excess_volatility: Option<f64>,
pub excess_max_drawdown: f64,
pub holding_count: usize,
pub average_weight: f64,
@@ -75,8 +146,8 @@ pub struct BacktestMetrics {
#[serde(default)]
pub external_cash_flow_total: f64,
pub excess_win_rate: f64,
pub monthly_sharpe: f64,
pub monthly_volatility: f64,
pub monthly_sharpe: Option<f64>,
pub monthly_volatility: Option<f64>,
pub risk_free_rate_contract_version: String,
pub risk_free_rate_source: String,
pub risk_free_rate_tenor: String,
@@ -93,15 +164,26 @@ pub fn compute_backtest_metrics(
account_events: &[AccountEvent],
initial_cash: f64,
risk_free_contract: Option<&RiskFreeRateContract>,
) -> Result<BacktestMetrics, String> {
compute_backtest_metrics_with_manual(equity_curve, fills, &[], daily_holdings, account_events, initial_cash, risk_free_contract)
}
pub fn compute_backtest_metrics_with_manual(
equity_curve: &[DailyEquityPoint], fills: &[FillEvent],
manual_executions: &[crate::manual_execution::ManualReplayApplication],
daily_holdings: &[HoldingSummary], account_events: &[AccountEvent], initial_cash: f64,
risk_free_contract: Option<&RiskFreeRateContract>,
) -> Result<BacktestMetrics, String> {
let Some(first_point) = equity_curve.first() else {
return Ok(BacktestMetrics {
risk_statistics_version: RISK_STATISTICS_VERSION.into(),
initial_cash,
..BacktestMetrics::default()
});
};
let Some(last_point) = equity_curve.last() else {
return Ok(BacktestMetrics {
risk_statistics_version: RISK_STATISTICS_VERSION.into(),
initial_cash,
..BacktestMetrics::default()
});
@@ -124,6 +206,10 @@ pub fn compute_backtest_metrics(
flow_neutral_nav_series(equity_curve, account_events, initial_cash)
};
let mut returns = Vec::with_capacity(portfolio_nav.len());
// A zero terminal NAV is a real -100% return. A later return starting
// from zero has no denominator and must not become another zero return.
let risk_periods_valid = portfolio_nav.iter().all(|nav|nav.is_finite() && *nav >= 0.0)
&& portfolio_nav.windows(2).all(|pair| pair[0] > 0.0);
if let Some(first_nav) = portfolio_nav.first().copied() {
returns.push(pct_change(1.0, first_nav));
}
@@ -167,24 +253,23 @@ pub fn compute_backtest_metrics(
aligned_daily_risk_free_rates(equity_curve, risk_free_contract)?;
let risk_free_rate =
effective_annual_risk_free_rate(&daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
let sharpe = annualized_sharpe(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
let sortino = annualized_sortino(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
let downside_risk =
annualized_downside_risk(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
let information_ratio = annualized_sharpe(
let risk_stats = if risk_periods_valid {
risk_adjusted_statistics(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR)?
} else { RiskAdjustedStatistics::default() };
let sharpe = risk_stats.sharpe;
let sortino = risk_stats.sortino;
let downside_risk = risk_stats.downside_volatility;
let excess_stats = if risk_periods_valid { risk_adjusted_statistics(
&excess_returns,
&zero_risk_free_rates,
TRADING_DAYS_PER_YEAR,
);
let tracking_error = annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR);
let volatility = annualized_std(&returns, TRADING_DAYS_PER_YEAR);
let excess_volatility = annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR);
let excess_sharpe = annualized_sharpe(
&excess_returns,
&zero_risk_free_rates,
TRADING_DAYS_PER_YEAR,
);
let (alpha, beta) = alpha_beta(&returns, &benchmark_returns, &daily_risk_free_rates);
)? } else { RiskAdjustedStatistics::default() };
let information_ratio = excess_stats.sharpe;
let tracking_error = risk_periods_valid.then(||annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR)).flatten();
let volatility = risk_periods_valid.then(||annualized_std(&returns, TRADING_DAYS_PER_YEAR)).flatten();
let excess_volatility = tracking_error;
let excess_sharpe = excess_stats.sharpe;
let (alpha, beta) = if risk_periods_valid { alpha_beta(&returns, &benchmark_returns, &daily_risk_free_rates) } else { (None,None) };
let equity_nav = portfolio_nav;
let benchmark_nav_series = equity_curve
@@ -222,19 +307,27 @@ pub fn compute_backtest_metrics(
.count(),
monthly_excess_returns.len(),
);
let monthly_sharpe = annualized_sharpe(
let monthly_sharpe = if risk_periods_valid { risk_adjusted_statistics(
&monthly_portfolio_returns,
&monthly_risk_free_returns,
MONTHS_PER_YEAR,
);
let monthly_volatility = annualized_std(&monthly_portfolio_returns, MONTHS_PER_YEAR);
)?.sharpe } else { None };
let monthly_volatility = risk_periods_valid.then(||annualized_std(&monthly_portfolio_returns, MONTHS_PER_YEAR)).flatten();
let turnover_by_date = fills
let mut turnover_by_date = fills
.iter()
.fold(BTreeMap::<NaiveDate, f64>::new(), |mut acc, fill| {
*acc.entry(fill.date).or_default() += fill.gross_amount.abs();
acc
});
for execution in manual_executions {
use rust_decimal::prelude::ToPrimitive;
let gross = execution.ledger_gross_amount.parse::<rust_decimal::Decimal>()
.ok().and_then(|value| value.to_f64()).filter(|value| value.is_finite() && *value >= 0.)
.ok_or("manual turnover requires its validated ledger gross amount")?;
let date = execution.observed_at.with_timezone(&chrono::FixedOffset::east_opt(8 * 3600).unwrap()).date_naive();
*turnover_by_date.entry(date).or_default() += gross;
}
let equity_by_date = equity_curve
.iter()
.map(|point| (point.date, point.total_equity))
@@ -277,6 +370,7 @@ pub fn compute_backtest_metrics(
let total_trade_days = equity_by_date.len();
Ok(BacktestMetrics {
risk_statistics_version: RISK_STATISTICS_VERSION.into(),
total_return,
annual_return,
sharpe,
@@ -453,80 +547,21 @@ fn effective_annual_risk_free_rate(daily_rates: &[f64], periods_per_year: f64) -
(mean_log * periods_per_year).exp_m1()
}
fn annualized_sharpe(returns: &[f64], daily_risk_free_rates: &[f64], periods_per_year: f64) -> f64 {
if returns.len() < 2 || returns.len() != daily_risk_free_rates.len() {
return 0.0;
}
let adjusted = returns
.iter()
.zip(daily_risk_free_rates)
.map(|(value, risk_free)| value - risk_free)
.collect::<Vec<_>>();
let mean_ret = mean(&adjusted);
let std = std_dev(&adjusted);
if std <= f64::EPSILON {
0.0
} else {
mean_ret / std * periods_per_year.sqrt()
}
}
fn annualized_sortino(
returns: &[f64],
daily_risk_free_rates: &[f64],
periods_per_year: f64,
) -> f64 {
if returns.is_empty() || returns.len() != daily_risk_free_rates.len() {
return 0.0;
}
let adjusted = returns
.iter()
.zip(daily_risk_free_rates)
.map(|(value, risk_free)| value - risk_free)
.collect::<Vec<_>>();
let downside = adjusted
.iter()
.map(|value| value.min(0.0).powi(2))
.sum::<f64>();
let downside_dev = (downside / adjusted.len() as f64).sqrt();
if downside_dev <= f64::EPSILON {
0.0
} else {
mean(&adjusted) / downside_dev * periods_per_year.sqrt()
}
}
fn annualized_downside_risk(
returns: &[f64],
daily_risk_free_rates: &[f64],
periods_per_year: f64,
) -> f64 {
if returns.is_empty() || returns.len() != daily_risk_free_rates.len() {
return 0.0;
}
let downside_mean_square = returns
.iter()
.zip(daily_risk_free_rates)
.map(|(value, risk_free)| (value - risk_free).min(0.0).powi(2))
.sum::<f64>()
/ returns.len() as f64;
downside_mean_square.sqrt() * periods_per_year.sqrt()
}
fn annualized_std(values: &[f64], periods_per_year: f64) -> f64 {
std_dev(values) * periods_per_year.sqrt()
fn annualized_std(values: &[f64], periods_per_year: f64) -> Option<f64> {
(values.len() > 1).then(|| std_dev(values) * periods_per_year.sqrt())
.filter(|value| value.is_finite())
}
fn alpha_beta(
returns: &[f64],
benchmark_returns: &[f64],
daily_risk_free_rates: &[f64],
) -> (f64, f64) {
) -> (Option<f64>, Option<f64>) {
if returns.len() < 2
|| returns.len() != benchmark_returns.len()
|| returns.len() != daily_risk_free_rates.len()
{
return (0.0, 0.0);
return (None, None);
}
let strategy_excess = returns
.iter()
@@ -544,7 +579,7 @@ fn alpha_beta(
let mean_raw_benchmark = mean(benchmark_returns);
let variance_benchmark = variance(benchmark_returns);
if variance_benchmark <= f64::EPSILON {
return (0.0, 0.0);
return (None, None);
}
let covariance = returns
.iter()
@@ -554,7 +589,7 @@ fn alpha_beta(
/ (strategy_excess.len() - 1) as f64;
let beta = covariance / variance_benchmark;
let alpha = (mean_strategy - beta * mean_benchmark) * TRADING_DAYS_PER_YEAR;
(alpha, beta)
(Some(alpha).filter(|value| value.is_finite()), Some(beta).filter(|value| value.is_finite()))
}
fn drawdown_stats(nav: &[f64]) -> (f64, usize) {
@@ -911,12 +946,66 @@ mod tests {
/ adjusted.len() as f64)
.sqrt();
let expected_sortino = mean(&adjusted) / downside * TRADING_DAYS_PER_YEAR.sqrt();
assert!((metrics.sharpe - expected_sharpe).abs() < 1e-12);
assert!((metrics.sortino - expected_sortino).abs() < 1e-12);
assert!((metrics.downside_risk - downside * TRADING_DAYS_PER_YEAR.sqrt()).abs() < 1e-12);
assert!((metrics.sharpe.unwrap() - expected_sharpe).abs() < 1e-12);
assert!((metrics.sortino.unwrap() - expected_sortino).abs() < 1e-12);
assert!((metrics.downside_risk.unwrap() - downside * TRADING_DAYS_PER_YEAR.sqrt()).abs() < 1e-12);
assert_eq!(metrics.risk_free_rate_source, "test");
assert_eq!(metrics.risk_free_rate_tenor, "3M");
assert_eq!(metrics.risk_free_rate_observation_count, 4);
assert_ne!(metrics.risk_free_rate, 0.022);
}
#[test]
fn undefined_ratios_remain_null_in_backtest_metrics() {
for curve in [vec![], vec![equity_point("2026-01-02", 100.0, 100.0, 100.0)],
vec![equity_point("2026-01-02", 100.0, 100.0, 100.0),
equity_point("2026-01-05", 100.0, 100.0, 100.0)]] {
let metrics = compute_backtest_metrics(&curve, &[], &[], &[], 100.0, None).unwrap();
let json = serde_json::to_value(&metrics).unwrap();
assert_eq!(json["risk_statistics_version"], RISK_STATISTICS_VERSION);
for field in ["sharpe", "sortino", "alpha", "beta", "information_ratio", "excess_sharpe", "monthly_sharpe"] {
assert!(json[field].is_null(), "{field}: {json}");
}
if curve.len() < 2 { assert_eq!(metrics.volatility, None); }
else { assert_eq!(metrics.volatility, Some(0.0)); }
assert_eq!(metrics.downside_risk, (!curve.is_empty()).then_some(0.0));
}
}
#[test]
fn a_defined_zero_ratio_is_not_missing() {
let stats = risk_adjusted_statistics(&[0.01,-0.01], &[0.0,0.0], 252.0).unwrap();
assert_eq!(stats.sharpe, Some(0.0));
assert_eq!(stats.sortino, Some(0.0));
let (alpha, beta) = alpha_beta(&[0.0,0.0], &[0.01,-0.01], &[0.0,0.0]);
assert_eq!((alpha,beta), (Some(0.0),Some(0.0)));
}
#[test]
fn gains_without_downside_have_no_sortino_or_monthly_sharpe() {
let curve = vec![equity_point("2026-01-02", 101.0, 101.0, 100.0),
equity_point("2026-01-05", 103.02, 103.02, 101.0)];
let metrics = compute_backtest_metrics(&curve,&[],&[],&[],100.0,None).unwrap();
assert!(metrics.sharpe.unwrap() > 0.0);
assert_eq!(metrics.sortino,None);
assert_eq!(metrics.downside_risk,Some(0.0));
assert_eq!(metrics.information_ratio,None);
assert_eq!(metrics.excess_sharpe,None);
assert_eq!(metrics.tracking_error,Some(0.0));
assert_eq!(metrics.monthly_sharpe,None);
assert_eq!(metrics.monthly_volatility,None);
}
#[test]
fn no_risk_ratio_is_fabricated_after_nav_has_reached_zero() {
let curve = vec![equity_point("2026-01-02", 0.0, 101.0, 100.0),
equity_point("2026-01-05", 0.0, 102.0, 101.0)];
let metrics=compute_backtest_metrics(&curve,&[],&[],&[],100.0,None).unwrap();
assert_eq!(metrics.total_return,-1.0);
for value in [metrics.sharpe,metrics.sortino,metrics.alpha,metrics.beta,metrics.information_ratio,metrics.volatility,metrics.monthly_sharpe] {
assert_eq!(value,None);
}
let loss=compute_backtest_metrics(&curve[..1],&[],&[],&[],100.0,None).unwrap();
assert!(loss.sortino.unwrap()<0.0);
}
}
+415
View File
@@ -0,0 +1,415 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
use std::ops::Index;
use compact_str::CompactString;
use serde::de::{MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Sorted numeric fields stored contiguously, without a tree node per snapshot.
#[derive(Clone, Default, PartialEq)]
pub struct NumericFactorMap {
entries: Vec<(CompactString, f64)>,
}
fn compact_key(key: Cow<'static, str>) -> CompactString {
match key {
Cow::Borrowed(value) => CompactString::const_new(value),
Cow::Owned(value) => CompactString::from(value),
}
}
impl NumericFactorMap {
pub const fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
/// Reserve known new fields without geometric spare capacity per snapshot.
pub fn reserve_exact(&mut self, additional: usize) {
self.entries.reserve_exact(additional);
}
pub fn get(&self, key: &str) -> Option<&f64> {
self.entries
.binary_search_by(|(name, _)| name.as_str().cmp(key))
.ok()
.map(|index| &self.entries[index].1)
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut f64> {
self.entries
.binary_search_by(|(name, _)| name.as_str().cmp(key))
.ok()
.map(|index| &mut self.entries[index].1)
}
pub fn contains_key(&self, key: &str) -> bool {
self.get(key).is_some()
}
pub fn insert(&mut self, key: Cow<'static, str>, value: f64) -> Option<f64> {
self.insert_compact(compact_key(key), value)
}
pub fn insert_compact(&mut self, key: CompactString, value: f64) -> Option<f64> {
if self
.entries
.last()
.is_none_or(|(last, _)| last.as_str() < key.as_str())
{
self.entries.push((key, value));
return None;
}
match self
.entries
.binary_search_by(|(name, _)| name.as_str().cmp(key.as_str()))
{
Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
Err(index) => {
self.entries.insert(index, (key, value));
None
}
}
}
pub fn remove(&mut self, key: &str) -> Option<f64> {
self.entries
.binary_search_by(|(name, _)| name.as_str().cmp(key))
.ok()
.map(|index| self.entries.remove(index).1)
}
pub fn retain(&mut self, mut keep: impl FnMut(&CompactString, &mut f64) -> bool) {
self.entries.retain_mut(|(key, value)| keep(key, value));
}
pub fn iter(&self) -> Iter<'_> {
Iter(self.entries.iter())
}
pub fn keys(&self) -> impl DoubleEndedIterator<Item = &CompactString> + ExactSizeIterator {
self.entries.iter().map(|(key, _)| key)
}
pub fn values(&self) -> impl DoubleEndedIterator<Item = &f64> + ExactSizeIterator {
self.entries.iter().map(|(_, value)| value)
}
}
impl fmt::Debug for NumericFactorMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self).finish()
}
}
impl Index<&str> for NumericFactorMap {
type Output = f64;
fn index(&self, key: &str) -> &Self::Output {
self.get(key).expect("numeric factor key not found")
}
}
pub struct Iter<'a>(std::slice::Iter<'a, (CompactString, f64)>);
impl<'a> Iterator for Iter<'a> {
type Item = (&'a CompactString, &'a f64);
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(|(k, v)| (k, v))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for Iter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().map(|(k, v)| (k, v))
}
}
impl ExactSizeIterator for Iter<'_> {}
impl<'a> IntoIterator for &'a NumericFactorMap {
type Item = (&'a CompactString, &'a f64);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl IntoIterator for NumericFactorMap {
type Item = (CompactString, f64);
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl FromIterator<(Cow<'static, str>, f64)> for NumericFactorMap {
fn from_iter<T: IntoIterator<Item = (Cow<'static, str>, f64)>>(iter: T) -> Self {
iter.into_iter().map(|(key, value)| (compact_key(key), value)).collect()
}
}
impl FromIterator<(CompactString, f64)> for NumericFactorMap {
fn from_iter<T: IntoIterator<Item = (CompactString, f64)>>(iter: T) -> Self {
let mut entries: Vec<_> = iter.into_iter().collect();
// Stable sorting preserves last-value-wins for repeated input keys.
if !entries.windows(2).all(|pair| pair[0].0 <= pair[1].0) {
entries.sort_by(|left, right| left.0.cmp(&right.0));
}
entries.dedup_by(|later, earlier| {
if later.0 == earlier.0 {
earlier.1 = later.1;
true
} else {
false
}
});
Self { entries }
}
}
impl Extend<(Cow<'static, str>, f64)> for NumericFactorMap {
fn extend<T: IntoIterator<Item = (Cow<'static, str>, f64)>>(&mut self, iter: T) {
self.extend(iter.into_iter().map(|(key, value)| (compact_key(key), value)));
}
}
impl Extend<(CompactString, f64)> for NumericFactorMap {
fn extend<T: IntoIterator<Item = (CompactString, f64)>>(&mut self, iter: T) {
let mut incoming: Self = iter.into_iter().collect();
if incoming.is_empty() {
return;
}
if self.is_empty() {
*self = incoming;
return;
}
if self.entries.last().unwrap().0 < incoming.entries[0].0 {
self.entries.append(&mut incoming.entries);
return;
}
// Merge sorted sets in linear time; wide factor batches must not shift
// the existing vector once per field. Existing keys keep their identity.
let mut merged = Vec::with_capacity(self.len() + incoming.len());
let mut old = std::mem::take(&mut self.entries).into_iter().peekable();
let mut new = incoming.entries.into_iter().peekable();
while let (Some(left), Some(right)) = (old.peek(), new.peek()) {
match left.0.cmp(&right.0) {
std::cmp::Ordering::Less => merged.push(old.next().unwrap()),
std::cmp::Ordering::Greater => merged.push(new.next().unwrap()),
std::cmp::Ordering::Equal => {
let (key, _) = old.next().unwrap();
merged.push((key, new.next().unwrap().1));
}
}
}
merged.extend(old);
merged.extend(new);
self.entries = merged;
}
}
impl<const N: usize> From<[(Cow<'static, str>, f64); N]> for NumericFactorMap {
fn from(entries: [(Cow<'static, str>, f64); N]) -> Self {
entries.into_iter().collect()
}
}
impl From<BTreeMap<Cow<'static, str>, f64>> for NumericFactorMap {
fn from(entries: BTreeMap<Cow<'static, str>, f64>) -> Self {
entries.into_iter().collect()
}
}
impl Serialize for NumericFactorMap {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(Some(self.len()))?;
for (key, value) in self {
map.serialize_entry(key, value)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for NumericFactorMap {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Fields;
impl<'de> Visitor<'de> for Fields {
type Value = NumericFactorMap;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a numeric factor map")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut entries = Vec::new();
while let Some((key, value)) = map.next_entry::<CompactString, f64>()? {
entries.push((key, value));
}
Ok(entries.into_iter().collect())
}
}
deserializer.deserialize_map(Fields)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_reservation_preserves_values_and_avoids_growth_during_known_inserts() {
let mut map = NumericFactorMap::from([
(Cow::Borrowed("amount"), 125.25),
(Cow::Borrowed("nullable_value"), f64::from_bits(0x7ff8000000000042)),
(Cow::Borrowed("signal"), -0.0),
]);
let original = map.iter().map(|(key, value)| (key.to_string(), value.to_bits())).collect::<Vec<_>>();
map.reserve_exact(2);
assert_eq!(map.iter().map(|(key, value)| (key.to_string(), value.to_bits())).collect::<Vec<_>>(), original);
let buffer = map.entries.as_ptr();
map.insert(Cow::Borrowed("pre_close"), 12.5);
map.insert(Cow::Borrowed("no_limit"), 0.0);
assert_eq!(map.entries.as_ptr(), buffer);
assert_eq!(map.len(), 5);
assert_eq!(map["signal"].to_bits(), (-0.0_f64).to_bits());
assert_eq!(map["nullable_value"].to_bits(), 0x7ff8000000000042);
let before = map.entries.as_ptr();
map.reserve_exact(0);
assert_eq!(map.entries.as_ptr(), before);
}
#[test]
fn compact_keys_inline_dynamic_names_and_keep_long_static_storage() {
const LONG: &str = "a_long_static_factor_identifier_that_must_remain_borrowed";
let map = NumericFactorMap::from([
(Cow::Owned("dynamic_factor_20".to_owned()), -0.0),
(Cow::Borrowed(LONG), 1.0),
]);
let cloned = map.clone();
let short = cloned.keys().find(|key| key.as_str() == "dynamic_factor_20").unwrap();
assert!(!short.is_heap_allocated());
let long = cloned.keys().find(|key| key.as_str() == LONG).unwrap();
assert_eq!(long.as_static_str(), Some(LONG));
assert_eq!(cloned["dynamic_factor_20"].to_bits(), (-0.0_f64).to_bits());
assert_eq!(std::mem::size_of::<(CompactString, f64)>(), std::mem::size_of::<(Cow<'static, str>, f64)>());
}
#[test]
fn long_dynamic_unicode_and_short_keys_keep_the_same_json_map() {
let entries = ["", "a", "a_field_longer_than_the_inline_string_capacity", "价格因子", "ths_up_days_stock"]
.into_iter().enumerate().map(|(index, key)| (Cow::Owned(key.to_string()), index as f64 + 0.25))
.collect::<BTreeMap<_, _>>();
let map = NumericFactorMap::from(entries.clone());
assert_eq!(serde_json::to_string(&map).unwrap(), serde_json::to_string(&entries).unwrap());
let decoded: NumericFactorMap = serde_json::from_str(&serde_json::to_string(&map).unwrap()).unwrap();
assert_eq!(decoded, map);
assert!(!decoded.keys().find(|key| key.as_str() == "ths_up_days_stock").unwrap().is_heap_allocated());
}
#[test]
fn updates_order_removal_and_values_match_tree_map() {
let mut flat = NumericFactorMap::new();
let mut tree = BTreeMap::new();
let mut seed = 71_u64;
for index in 0..10000 {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
let key: Cow<'static, str> = Cow::Owned(format!("factor_{:04}", (seed >> 32) % 1000));
let value = (index as f64 - 5000.0) / 7.0;
if index % 11 == 0 {
assert_eq!(flat.remove(key.as_ref()), tree.remove(key.as_ref()));
} else {
assert_eq!(flat.insert(key.clone(), value), tree.insert(key, value));
}
}
assert_eq!(
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
);
flat.retain(|_, value| *value > 100.0);
tree.retain(|_, value| *value > 100.0);
assert_eq!(
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
);
assert_eq!(
std::mem::size_of::<NumericFactorMap>(),
std::mem::size_of_val(&tree)
);
}
#[test]
fn bulk_load_is_sorted_and_keeps_last_value_for_each_field() {
let input = vec![
(Cow::Borrowed("z"), 2.0),
(Cow::Borrowed("a"), 1.0),
(Cow::Borrowed("z"), 3.0),
(Cow::Borrowed("z"), 4.0),
];
let flat: NumericFactorMap = input.clone().into_iter().collect();
let tree: BTreeMap<_, _> = input.into_iter().collect();
assert_eq!(
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
);
assert_eq!(flat["z"], 4.0);
}
#[test]
fn serialization_keeps_the_map_contract_and_precise_numbers() {
let input = [
(Cow::Borrowed("zero"), -0.0),
(Cow::Borrowed("tiny"), 1.0000000000000002),
(Cow::Borrowed("large"), 9.123456789123456e20),
];
let flat = NumericFactorMap::from(input.clone());
let tree = BTreeMap::from(input);
let json = serde_json::to_string(&flat).unwrap();
assert_eq!(json, serde_json::to_string(&tree).unwrap());
let decoded: NumericFactorMap = serde_json::from_str(&json).unwrap();
for (key, value) in &flat {
assert_eq!(value.to_bits(), decoded[key.as_ref()].to_bits());
}
let duplicate: NumericFactorMap = serde_json::from_str(r#"{"x":1,"x":2}"#).unwrap();
assert_eq!(duplicate["x"], 2.0);
}
#[test]
fn clone_does_not_share_mutable_values() {
let original = NumericFactorMap::from([(Cow::Borrowed("signal"), 1.0)]);
let mut copy = original.clone();
*copy.get_mut("signal").unwrap() = 0.0;
copy.insert(Cow::Borrowed("other"), 2.0);
assert_eq!(original["signal"], 1.0);
assert!(!original.contains_key("other"));
}
#[test]
fn wide_batch_merge_matches_tree_and_preserves_old_key_ownership() {
let entries = (0..4096)
.map(|index| (Cow::Owned(format!("f_{index:05}")), index as f64))
.collect::<Vec<_>>();
let mut flat: NumericFactorMap = entries.clone().into_iter().collect();
let mut tree = BTreeMap::from_iter(entries);
flat.insert(Cow::Borrowed("shared"), -0.0);
tree.insert(Cow::Borrowed("shared"), -0.0);
let incoming = (2048..8192)
.rev()
.map(|index| (Cow::Owned(format!("f_{index:05}")), -(index as f64)))
.chain([(Cow::Owned("shared".to_owned()), 1.0)])
.collect::<Vec<_>>();
flat.extend(incoming.clone());
tree.extend(incoming);
assert_eq!(
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
);
assert_eq!(flat.keys().last().map(CompactString::as_str), Some("shared"));
assert!(!flat.keys().last().unwrap().is_heap_allocated());
flat.extend([(Cow::Borrowed("zz"), f64::NAN)]);
assert!(flat["zz"].is_nan());
flat.extend(std::iter::empty::<(CompactString, f64)>());
assert_eq!(flat.len(), tree.len() + 1);
}
}
+41 -3
View File
@@ -212,6 +212,11 @@ pub fn build_dataset_context(
}
pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> {
// A runner bundle also contains source/extract copies. Follow the same
// authoritative spec selection as the execution loader, not those copies.
if let Some(spec) = value.get("strategySpec").or_else(|| value.get("strategy_spec")) {
return specs_in_value(spec);
}
let mut specs = Vec::new();
match value {
Value::String(text) => specs.extend(crate::daily_patterns::expression_specs(text)?),
@@ -221,7 +226,15 @@ pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> {
}
}
Value::Object(items) => {
for v in items.values() {
let typed_pool = items.get("stockPool").or_else(|| items.get("stock_pool"))
.is_some_and(Value::is_object);
for (key, v) in items {
// The executable pool already supplies decoded expressions.
// Its display/source serialization escapes those expressions
// one more time and is not another executable program.
if typed_pool && matches!(key.as_str(), "sourceCode" | "source_code") {
continue;
}
specs.extend(specs_in_value(v)?);
}
}
@@ -248,6 +261,31 @@ mod tests {
use super::*;
use crate::{BenchmarkSnapshot, DailyFactorSnapshot, DailyMarketSnapshot, Instrument};
use serde_json::json;
#[test]
fn structured_pool_conditions_are_not_rescanned_inside_serialized_source_code() {
let pattern = json!({"template":"expression","parameters":{"history_window":20},
"expression":{"kind":"operator","name":"GT","args":[{"kind":"field","name":"amount"},{"kind":"number","value":0}]}});
let expr = format!("pattern_signal({})", serde_json::to_string(&pattern.to_string()).unwrap());
let pool = json!({"schema_version":1,"pool_id":"fixture","version_id":"v1","members":[],
"allocation_policy":{},"timing_policy":{},"stop_take_policy":{},"out_of_pool_policy":"hold",
"exit_signals":[{"role":"risk_exit","when_expr":expr,"remaining_position_bps":5000,"reason":"fixture"}]});
let source = format!("stock_pool.config({pool})");
for (pool_key, source_key) in [("stockPool", "sourceCode"), ("stock_pool", "source_code")] {
let value = json!({pool_key:pool,source_key:source,"runtimeExpressions":{"trading":{"buyFilterExpr":expr}}});
assert_eq!(specs_in_value(&value).unwrap().len(), 2);
for wrapper in ["strategySpec", "strategy_spec"] {
let bundle = json!({wrapper:value,"strategy_source":{"source_code":source},
"strategy_extract":{"parameters":{"source_code":source}}});
assert_eq!(specs_in_value(&bundle).unwrap().len(), 2);
}
let mut invalid = value.clone();
invalid[pool_key]["exit_signals"][0]["when_expr"] = json!("pattern_signal(not-json)");
assert!(specs_in_value(&invalid).is_err(), "invalid actual conditions must still fail");
assert!(specs_in_value(&json!({"strategySpec":invalid})).is_err());
}
assert_eq!(specs_in_value(&json!({"sourceCode":format!("risk.stop_loss({expr})")})).unwrap().len(),1);
}
#[test]
fn normalized_rule_does_not_turn_an_omitted_window_into_explicit_null() {
let expression:Expr=serde_json::from_value(json!({"kind":"operator","name":"GT","args":[{"kind":"field","name":"close"},{"kind":"number","value":1}]})).unwrap();
@@ -298,7 +336,7 @@ mod tests {
][n][i];
market.push(DailyMarketSnapshot {
date: *d,
symbol: s.to_string(),
symbol: (*s).into(),
timestamp: None,
day_open: c,
open: c,
@@ -321,7 +359,7 @@ mod tests {
});
factors.push(DailyFactorSnapshot {
date: *d,
symbol: s.to_string(),
symbol: (*s).into(),
market_cap_bn: 1.,
free_float_cap_bn: 1.,
pe_ttm: 10.,
File diff suppressed because it is too large Load Diff
+59 -22
View File
@@ -14,6 +14,14 @@ impl PlatformExprStrategy {
.as_ref()
.ok_or_else(|| BacktestError::Execution("stock_pool_program_missing".into()))?
.clone();
if !self.config.stop_loss_expr.trim().is_empty() || !self.config.take_profit_expr.trim().is_empty()
|| self.config.position_target_rules.len() != program.exit_signals.len()
|| self.config.position_target_rules.iter().zip(&program.exit_signals).any(|(compiled, frozen)|
compiled.when_expr != frozen.when_expr || compiled.remaining_position_bps != frozen.remaining_position_bps
|| compiled.reason != frozen.reason || compiled.stock_pool_role != frozen.role)
{
return Err(BacktestError::Execution("stock_pool_exit_roles_required: exit rules must remain bound to the frozen stock_pool program".into()));
}
let mut constraints = pool::stock_pool_constraints_from_configuration(
&program.allocation_policy,
&program.stop_take_policy,
@@ -78,17 +86,29 @@ impl PlatformExprStrategy {
closes,
});
}
let rule = pool::normalize_stock_pool_execution_rule(
let rule = pool::normalize_stock_pool_execution_rule_with_exit_roles(
Some(&program.timing_policy),
!self.config.buy_filter_expr.trim().is_empty(),
!self.config.stop_loss_expr.trim().is_empty()
|| !self.config.take_profit_expr.trim().is_empty()
|| !self.config.position_target_rules.is_empty(),
self.config.position_target_rules.iter().any(|rule| rule.stock_pool_role == pool::StockPoolExitRole::OrdinarySell),
self.config.position_target_rules.iter().any(|rule| rule.stock_pool_role == pool::StockPoolExitRole::RiskExit),
)
.map_err(BacktestError::Execution)?;
if self.config.in_skip_window(ctx.decision_date) {
return Ok(StrategyDecision::default());
}
let explicit_quote_condition = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly
|| [self.config.buy_filter_expr.as_str(), self.config.stop_loss_expr.as_str(), self.config.take_profit_expr.as_str()]
.into_iter().chain(self.config.position_target_rules.iter().map(|rule|rule.when_expr.as_str()))
.any(|expression|Self::stock_filter_quote_usage_for_expr(expression)!=StockFilterQuoteUsage::DailyOnly);
if explicit_quote_condition && ctx.active_datetime.is_some_and(|at|at.time()<NaiveTime::from_hms_opt(15,0,0).unwrap()) {
for symbol in program.members.iter().map(|member|&member.symbol).chain(ctx.portfolio.positions().keys()) {
if ctx.data.instrument(symbol).is_some_and(|instrument|instrument.is_exchange_traded_fund()&&instrument.dated_market_absence_reason(ctx.execution_date).is_none())
&& self.scheduled_quote_at_time(ctx,ctx.execution_date,symbol,None).is_none()
{
return Err(BacktestError::Execution(format!("etf_intraday_condition_evidence_missing:{symbol}; completed daily references cannot make minute/tick conditions true")));
}
}
}
let day = self.day_state(ctx, ctx.decision_date)?;
let (market_date, universe_date, factor_date) = self.selection_dates(ctx);
let (low, high) = self.market_cap_band(ctx, &day)?;
@@ -106,7 +126,7 @@ impl PlatformExprStrategy {
.portfolio
.positions()
.values()
.filter(|p| p.quantity > 0)
.filter(|p| p.economic_quantity() > 0)
.map(|p| p.symbol.clone())
.collect::<BTreeSet<_>>();
if !self.config.buy_filter_expr.trim().is_empty() {
@@ -120,23 +140,13 @@ impl PlatformExprStrategy {
}
}
}
let native_exits = self.current_stop_take_exit_symbols(ctx, ctx.decision_date, &day)?;
for symbol in native_exits {
constraints.position_target_bps.insert(symbol, 0);
}
for (symbol, (bps, _)) in
self.current_position_target_rules(ctx, ctx.decision_date, factor_date, &day)?
{
constraints
.position_target_bps
.entry(symbol)
.and_modify(|old| *old = (*old).min(bps))
.or_insert(bps);
for (role, targets) in self.current_position_target_rules_by_role(ctx, ctx.decision_date, factor_date, &day)? {
let output = match role { pool::StockPoolExitRole::OrdinarySell => &mut constraints.position_target_bps, pool::StockPoolExitRole::RiskExit => &mut constraints.independent_position_target_bps };
for (symbol, (bps, _)) in targets { output.insert(symbol, bps); }
}
let limit = constraints.target_holding_count.unwrap_or(ranked.len());
let final_symbols = ranked
.iter()
.filter(|symbol| !constraints.position_target_bps.contains_key(*symbol))
.take(limit)
.cloned()
.collect();
@@ -172,6 +182,15 @@ impl PlatformExprStrategy {
scope.push(symbol)
}
}
let allocation_weights = self
.config
.runtime_position_exposure_timeline
.allocation_weights_at(portfolio_loss_decision_at(ctx))
.or_else(|| {
self.config
.position_exposure_timeline
.allocation_weights_at(portfolio_loss_decision_at(ctx))
});
let members = scope
.iter()
.enumerate()
@@ -188,18 +207,36 @@ impl PlatformExprStrategy {
take_profit: constraints.default_take_profit,
});
member.requested_order = index as i32;
if let Some(weights) = allocation_weights {
member.target_weight_bps = Some(*weights.get(symbol).unwrap_or(&0));
}
member
})
.collect();
let (base_ratio, reserve_cash) =
pool::stock_pool_funding_from_configuration(&program.allocation_policy)
.map_err(BacktestError::Execution)?;
let base_exposure = self
.config
.position_exposure_timeline
.exposure_at(
portfolio_loss_decision_at(ctx),
ctx.execution_date,
&self.config.position_exposure_schedule,
f64::from(base_ratio) / 10000.,
)
.unwrap_or(f64::from(base_ratio) / 10000.);
let ratio = self
.config
.position_exposure_schedule
.range(..=ctx.decision_date)
.next_back()
.map(|(_, value)| (*value * 10000.).round() as i64)
.runtime_position_exposure_timeline
.exposure_at(
portfolio_loss_decision_at(ctx),
ctx.execution_date,
&self.config.runtime_position_exposure_schedule,
base_exposure,
)
.or(Some(base_exposure))
.map(|value| (value * 10000.).round() as i64)
.unwrap_or(i64::from(base_ratio));
let invest_ratio_bps = i32::try_from(ratio)
.ok()
+28 -2
View File
@@ -91,6 +91,8 @@ pub struct StrategyRebalanceSpec {
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StrategyExecutionSpec {
#[serde(default, alias = "volume_capacity_mode")]
pub volume_capacity_mode: Option<crate::execution_capacity::VolumeCapacityMode>,
#[serde(default)]
pub frequency: Option<String>,
#[serde(default, alias = "matching_type")]
@@ -164,9 +166,22 @@ pub struct StrategyExecutionSpec {
pub sell_then_buy_delay_slippage_rate: Option<f64>,
}
impl StrategyRuntimeSpec {
pub fn volume_capacity_mode(&self) -> Result<crate::execution_capacity::VolumeCapacityMode, String> {
let engine = self.engine_config.as_ref().and_then(|config| config.volume_capacity_mode);
let execution = self.execution.as_ref().and_then(|config| config.volume_capacity_mode);
if engine.zip(execution).is_some_and(|(a, b)| a != b) {
return Err("conflicting engine/execution volumeCapacityMode".into());
}
Ok(execution.or(engine).unwrap_or_default())
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StrategyEngineConfig {
#[serde(default, alias = "volume_capacity_mode")]
pub volume_capacity_mode: Option<crate::execution_capacity::VolumeCapacityMode>,
#[serde(default)]
pub frequency: Option<String>,
#[serde(default, alias = "template_id")]
@@ -934,6 +949,8 @@ pub struct StrategyExpressionRiskConfig {
pub exposure_expr: Option<String>,
#[serde(default, alias = "position_exposure_schedule")]
pub position_exposure_schedule: Vec<StrategyPositionExposureSchedulePoint>,
#[serde(default, alias = "position_exposure_events")]
pub position_exposure_events: Vec<crate::position_exposure::PositionExposureEvent>,
#[serde(default)]
pub portfolio_drawdown_control: Option<StrategyPortfolioDrawdownControlConfig>,
#[serde(default)]
@@ -1822,6 +1839,7 @@ pub fn platform_expr_config_from_spec(
strategy_spec: Option<&StrategyRuntimeSpec>,
) -> Result<PlatformExprStrategyConfig, String> {
let mut cfg = PlatformExprStrategyConfig::generic();
cfg.volume_capacity_mode = strategy_spec.map(StrategyRuntimeSpec::volume_capacity_mode).transpose()?.unwrap_or_default();
cfg.strategy_name = strategy_id.to_string();
if !signal_symbol.trim().is_empty() {
cfg.signal_symbol = signal_symbol.trim().to_string();
@@ -2212,6 +2230,7 @@ pub fn platform_expr_config_from_spec(
expr.clone()
};
}
cfg.position_exposure_timeline = crate::position_exposure::PositionExposureTimeline::from_events(&risk.position_exposure_events)?;
for point in &risk.position_exposure_schedule {
let effective_date = NaiveDate::parse_from_str(
point.effective_date.trim(),
@@ -2323,6 +2342,7 @@ pub fn platform_expr_config_from_spec(
));
}
cfg.position_target_rules.push(PlatformPositionTargetRule {
stock_pool_role: crate::stock_pool_execution::StockPoolExitRole::OrdinarySell,
when_expr: when_expr.to_string(),
remaining_position_bps: rule.remaining_position_bps,
reason: rule
@@ -2696,9 +2716,14 @@ pub fn platform_expr_config_from_spec(
}
if let Some(pool)=&spec.stock_pool {
if cfg.signal_book.is_some() || spec.signal_book_ref.is_some() || !cfg.explicit_actions.is_empty(){return Err("stock_pool_program_cannot_mix_other_order_programs".into())}
let legacy_exit = !cfg.stop_loss_expr.trim().is_empty() || !cfg.take_profit_expr.trim().is_empty() || !cfg.position_target_rules.is_empty();
if legacy_exit { return Err("stock_pool_exit_roles_required: regenerate this historical stock-pool strategy from its saved configuration; legacy risk expressions do not preserve ordinary/risk exit roles".into()); }
let secondary_buy=!cfg.buy_filter_expr.trim().is_empty();
let secondary_sell=spec.runtime_expressions.as_ref().and_then(|runtime|runtime.risk.as_ref()).is_some_and(|risk|risk.stop_loss_expr.is_some()||risk.take_profit_expr.is_some()) || !cfg.position_target_rules.is_empty();
pool.validate(secondary_buy,secondary_sell)?;
pool.validate(secondary_buy,false)?;
cfg.position_target_rules.extend(pool.exit_signals.iter().map(|signal| PlatformPositionTargetRule {
when_expr: signal.when_expr.clone(), remaining_position_bps: signal.remaining_position_bps,
reason: signal.reason.clone(), stock_pool_role: signal.role,
}));
cfg.stock_pool=Some(pool.clone());
cfg.hold_until_exit_enabled=false;
cfg.daily_top_up_enabled=false;
@@ -3440,6 +3465,7 @@ mod tests {
assert_eq!(
cfg.position_target_rules,
vec![PlatformPositionTargetRule {
stock_pool_role: crate::stock_pool_execution::StockPoolExitRole::OrdinarySell,
when_expr: "factors[\"reduce_signal\"] == 1".to_string(),
remaining_position_bps: 5000,
reason: "factor_reduce_position".to_string(),
File diff suppressed because it is too large Load Diff
+536
View File
@@ -0,0 +1,536 @@
//! Dated manual adjustments are ordered facts; restoring is not a 100% target.
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
pub enum PositionExposureAction {
Scale {
#[serde(rename = "requestedBps", alias = "requested_bps")]
requested_bps: i32,
},
Set {
#[serde(rename = "targetExposureBps", alias = "target_exposure_bps")]
target_exposure_bps: i32,
},
Restore,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionExposureEvent {
#[serde(alias = "event_id")]
pub event_id: String,
pub sequence: u64,
#[serde(alias = "effective_at")]
pub effective_at: DateTime<Utc>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "allocation_weights_bps"
)]
pub allocation_weights_bps: Option<BTreeMap<String, i32>>,
#[serde(flatten)]
pub action: PositionExposureAction,
}
#[derive(Debug, Clone, Default)]
pub struct PositionExposureTimeline {
events: BTreeMap<(DateTime<Utc>, u64), (PositionExposureAction, Option<BTreeMap<String, i32>>)>,
}
impl PositionExposureTimeline {
pub fn from_events(events: &[PositionExposureEvent]) -> Result<Self, String> {
let mut result = Self::default();
let mut ids = BTreeSet::new();
let mut sequences = BTreeSet::new();
for event in events {
if event.event_id.trim().is_empty() || !ids.insert(event.event_id.as_str()) {
return Err("position exposure event id is missing or duplicated".into());
}
if event.sequence == 0 || !sequences.insert(event.sequence) {
return Err("position exposure event sequence must be positive and unique".into());
}
if let PositionExposureAction::Scale { requested_bps } = event.action
&& !(0..=10000).contains(&requested_bps)
{
return Err("position exposure scale must be between 0 and 10000 bps".into());
}
if let PositionExposureAction::Set {
target_exposure_bps,
} = event.action
&& !(0..=10_000).contains(&target_exposure_bps)
{
return Err("position exposure target must be between 0 and 10000 bps".into());
}
if let Some(weights) = &event.allocation_weights_bps {
let target = match event.action {
PositionExposureAction::Set {
target_exposure_bps,
} => target_exposure_bps,
PositionExposureAction::Scale { requested_bps } => requested_bps,
PositionExposureAction::Restore => {
return Err(
"restoring strategy allocation cannot carry manual weights".into()
);
}
};
validate_allocation_weights(weights, target)?;
}
result.events.insert(
(event.effective_at, event.sequence),
(event.action.clone(), event.allocation_weights_bps.clone()),
);
}
Ok(result)
}
/// Legacy day-level contracts remain day-level; never invent intraday times.
pub fn exposure_at(
&self,
at: DateTime<Utc>,
execution_date: NaiveDate,
legacy: &BTreeMap<NaiveDate, f64>,
strategy_exposure: f64,
) -> Option<f64> {
match self
.events
.range(..=(at, u64::MAX))
.next_back()
.map(|(_, (action, _))| action)
{
Some(PositionExposureAction::Scale { requested_bps }) => {
Some(strategy_exposure * f64::from(*requested_bps) / 10000.)
}
Some(PositionExposureAction::Set {
target_exposure_bps,
}) => Some(f64::from(*target_exposure_bps) / 10_000.),
Some(PositionExposureAction::Restore) => None,
None => legacy
.range(..=execution_date)
.next_back()
.map(|(_, value)| *value),
}
}
pub fn scale_at(&self, at: DateTime<Utc>) -> Option<i32> {
match self
.events
.range(..=(at, u64::MAX))
.next_back()
.map(|(_, (action, _))| action)
{
Some(PositionExposureAction::Scale { requested_bps }) => Some(*requested_bps),
_ => None,
}
}
pub fn allocation_weights_at(&self, at: DateTime<Utc>) -> Option<&BTreeMap<String, i32>> {
self.events
.range(..=(at, u64::MAX))
.next_back()
.and_then(|(_, (_, weights))| weights.as_ref())
}
}
pub fn validate_allocation_weights(
weights: &BTreeMap<String, i32>,
exposure_bps: i32,
) -> Result<(), String> {
if !(0..=10000).contains(&exposure_bps) || weights.len() > 10000 {
return Err("invalid allocation exposure or weight count".into());
}
for (symbol, weight) in weights {
if !(0..=10000).contains(weight)
|| !symbol.rsplit_once('.').is_some_and(|(code, exchange)| {
code.len() == 6
&& code.bytes().all(|byte| byte.is_ascii_digit())
&& matches!(exchange, "SH" | "SZ" | "BJ")
})
{
return Err(
"allocation weights require canonical stock/ETF symbols and 0..10000 bps".into(),
);
}
}
if (weights.is_empty() && exposure_bps != 0)
|| (!weights.is_empty() && weights.values().sum::<i32>() != 10000)
{
return Err("manual allocation weights must total 10000 bps; only a zero exposure may have no weights".into());
}
Ok(())
}
/// Scale new buys and desired targets without weakening sell/reduction or
/// cancellation instructions. Prices, subscriptions and cash flows are intact.
pub fn scale_explicit_intent(
mut intent: crate::OrderIntent,
bps: i32,
open_orders: &[crate::OpenOrderView],
) -> Result<Option<crate::OrderIntent>, String> {
use crate::OrderIntent as I;
if !(0..=10000).contains(&bps) {
return Err("position scale out of range".into());
}
if bps == 10000 {
return Ok(Some(intent));
}
if let I::WithTimeInForce {
intent: inner,
time_in_force,
} = intent
{
return Ok(
scale_explicit_intent(*inner, bps, open_orders)?.map(|intent| I::WithTimeInForce {
intent: Box::new(intent),
time_in_force,
}),
);
}
let integer = |value: i32| ((i64::from(value) * i64::from(bps)) / 10000) as i32;
let amount = |value: f64, target: bool| -> Result<f64, String> {
if !value.is_finite() || (target && value < 0.) {
return Err("position override received an invalid original amount".into());
}
Ok(if value > 0. {
value * f64::from(bps) / 10000.
} else {
value
})
};
match &mut intent {
I::Shares { quantity, .. }
| I::LimitShares { quantity, .. }
| I::Lots { lots: quantity, .. }
| I::LimitLots { lots: quantity, .. } => {
if *quantity > 0 {
*quantity = integer(*quantity);
if *quantity == 0 {
return Ok(None);
}
}
}
I::TargetShares {
target_quantity, ..
}
| I::LimitTargetShares {
target_quantity, ..
} => {
if *target_quantity < 0 {
return Err("position override received a negative target quantity".into());
}
*target_quantity = integer(*target_quantity);
}
I::Value { value, .. }
| I::LimitValue { value, .. }
| I::AlgoValue { value, .. }
| I::Percent { percent: value, .. }
| I::LimitPercent { percent: value, .. }
| I::AlgoPercent { percent: value, .. } => {
*value = amount(*value, false)?;
if *value == 0. {
return Ok(None);
}
}
I::TargetValue { target_value, .. }
| I::LimitTargetValue { target_value, .. }
| I::TimedTargetValue { target_value, .. }
| I::TargetPercent {
target_percent: target_value,
..
}
| I::LimitTargetPercent {
target_percent: target_value,
..
} => {
*target_value = amount(*target_value, true)?;
}
I::TargetPortfolioSmart { target_weights, .. } => {
for value in target_weights.values_mut() {
*value = amount(*value, true)?;
}
}
I::ModifyOrder {
order_id,
new_total_quantity: Some(quantity),
..
} => {
let order = open_orders
.iter()
.find(|order| order.order_id == *order_id)
.ok_or("position override cannot resolve the order being modified")?;
if order.side == crate::OrderSide::Buy && *quantity > order.requested_quantity {
let extra = u64::from(*quantity - order.requested_quantity) * bps as u64 / 10000;
*quantity = order.requested_quantity + extra as u32;
}
}
I::Futures { .. } | I::StockPool { .. } => {
return Err("manual equity scaling cannot transform this intent kind".into());
}
I::ModifyOrder { .. }
| I::CancelOrder { .. }
| I::CancelSymbol { .. }
| I::CancelAll { .. }
| I::UpdateUniverse { .. }
| I::Subscribe { .. }
| I::Unsubscribe { .. }
| I::DepositWithdraw { .. }
| I::FinanceRepay { .. }
| I::SetManagementFeeRate { .. } => {}
I::WithTimeInForce { .. } => unreachable!("wrapper handled first"),
}
Ok(Some(intent))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn scalar_preserves_strategy_risk_off_and_restore_keeps_original_exposure() {
let at = DateTime::parse_from_rfc3339("2026-01-05T09:30:00+08:00")
.unwrap()
.with_timezone(&Utc);
let event = PositionExposureEvent {
event_id: "scale".into(),
sequence: 1,
effective_at: at,
allocation_weights_bps: None,
action: PositionExposureAction::Scale {
requested_bps: 5000,
},
};
let timeline = PositionExposureTimeline::from_events(&[event.clone()]).unwrap();
assert_eq!(
timeline.exposure_at(at, at.date_naive(), &BTreeMap::new(), 0.),
Some(0.)
);
assert_eq!(
timeline.exposure_at(at, at.date_naive(), &BTreeMap::new(), 0.2),
Some(0.1)
);
let restored = PositionExposureEvent {
event_id: "restore".into(),
sequence: 2,
effective_at: at,
allocation_weights_bps: None,
action: PositionExposureAction::Restore,
};
let timeline = PositionExposureTimeline::from_events(&[event, restored]).unwrap();
assert_eq!(
timeline
.exposure_at(
at,
at.date_naive(),
&BTreeMap::from([(at.date_naive(), 1.)]),
0.2
)
.unwrap_or(0.2),
0.2
);
}
#[test]
fn allocation_is_dated_and_any_later_scalar_or_restore_clears_it() {
let at = DateTime::parse_from_rfc3339("2026-09-14T10:00:00+08:00")
.unwrap()
.with_timezone(&Utc);
let weights = BTreeMap::from([("000001.SZ".into(), 3000), ("510300.SH".into(), 7000)]);
let event = PositionExposureEvent {
event_id: "allocation".into(),
sequence: 1,
effective_at: at,
action: PositionExposureAction::Set {
target_exposure_bps: 8000,
},
allocation_weights_bps: Some(weights.clone()),
};
let timeline = PositionExposureTimeline::from_events(&[event.clone()]).unwrap();
assert!(
timeline
.allocation_weights_at(at - chrono::Duration::seconds(1))
.is_none()
);
assert_eq!(timeline.allocation_weights_at(at), Some(&weights));
for action in [
PositionExposureAction::Set {
target_exposure_bps: 5000,
},
PositionExposureAction::Restore,
] {
let next = PositionExposureEvent {
event_id: "new".into(),
sequence: 2,
effective_at: at + chrono::Duration::seconds(1),
action,
allocation_weights_bps: None,
};
assert!(
PositionExposureTimeline::from_events(&[event.clone(), next])
.unwrap()
.allocation_weights_at(at + chrono::Duration::seconds(1))
.is_none()
);
}
assert!(
validate_allocation_weights(&BTreeMap::from([("000001.SZ".into(), 9000)]), 5000)
.is_err()
);
assert!(validate_allocation_weights(&BTreeMap::new(), 1).is_err());
assert!(validate_allocation_weights(&BTreeMap::new(), 0).is_ok());
}
#[test]
fn explicit_equity_targets_and_buys_scale_but_sales_cashflows_and_prices_do_not() {
use crate::OrderIntent as I;
let symbol = "000001.SZ".to_string();
let reason = "fixture".to_string();
for bps in [0, 3000, 5000, 10000] {
let ratio = f64::from(bps) / 10000.;
let buy = I::LimitShares {
symbol: symbol.clone(),
quantity: 1000,
limit_price: 12.345,
reason: reason.clone(),
};
let scaled = scale_explicit_intent(buy, bps, &[]).unwrap();
if bps == 0 {
assert!(scaled.is_none())
} else if let Some(I::LimitShares {
quantity,
limit_price,
..
}) = scaled
{
assert_eq!(quantity, (1000. * ratio) as i32);
assert_eq!(limit_price, 12.345);
} else {
panic!("wrong intent")
}
let sell = I::Shares {
symbol: symbol.clone(),
quantity: -1000,
reason: reason.clone(),
};
assert!(matches!(
scale_explicit_intent(sell, bps, &[]).unwrap(),
Some(I::Shares {
quantity: -1000,
..
})
));
let clear = I::TargetShares {
symbol: symbol.clone(),
target_quantity: 0,
reason: reason.clone(),
};
assert!(matches!(
scale_explicit_intent(clear, bps, &[]).unwrap(),
Some(I::TargetShares {
target_quantity: 0,
..
})
));
let target = I::TargetPercent {
symbol: symbol.clone(),
target_percent: 0.2,
reason: reason.clone(),
};
if let Some(I::TargetPercent { target_percent, .. }) =
scale_explicit_intent(target, bps, &[]).unwrap()
{
assert!((target_percent - 0.2 * ratio).abs() < 1e-12)
} else {
panic!("wrong target")
}
let deposit = I::DepositWithdraw {
amount: 123.456,
receiving_days: 2,
reason: reason.clone(),
};
assert!(matches!(
scale_explicit_intent(deposit, bps, &[]).unwrap(),
Some(I::DepositWithdraw {
amount: 123.456,
receiving_days: 2,
..
})
));
}
assert!(
scale_explicit_intent(
I::TargetValue {
symbol,
target_value: f64::NAN,
reason
},
0,
&[]
)
.is_err()
);
}
#[test]
fn same_day_adjustments_restore_and_future_events_keep_their_own_times() {
let events: Vec<PositionExposureEvent> = serde_json::from_value(json!([
{"eventId":"first","sequence":1,"effectiveAt":"2026-09-10T10:00:00+08:00","action":"set","targetExposureBps":0},
{"eventId":"second","sequence":2,"effectiveAt":"2026-09-10T13:00:00+08:00","action":"set","targetExposureBps":5000},
{"eventId":"restore","sequence":3,"effectiveAt":"2026-09-10T14:00:00+08:00","action":"restore"},
{"eventId":"future","sequence":4,"effectiveAt":"2026-09-11T10:00:00+08:00","action":"set","targetExposureBps":1000}
])).unwrap();
let timeline = PositionExposureTimeline::from_events(&events).unwrap();
let date = NaiveDate::from_ymd_opt(2026, 9, 10).unwrap();
let legacy = BTreeMap::from([(date.pred_opt().unwrap(), 0.8)]);
for (time, expected) in [
("09:30:00", Some(0.8)),
("10:00:00", Some(0.)),
("12:59:59", Some(0.)),
("13:00:00", Some(0.5)),
("14:00:00", None),
("15:00:00", None),
] {
let at = DateTime::parse_from_rfc3339(&format!("2026-09-10T{time}+08:00"))
.unwrap()
.with_timezone(&Utc);
assert_eq!(
timeline.exposure_at(at, date, &legacy, 0.2),
expected,
"{time}"
);
}
let next_open = DateTime::parse_from_rfc3339("2026-09-11T09:30:00+08:00")
.unwrap()
.with_timezone(&Utc);
assert_eq!(
timeline.exposure_at(next_open, date.succ_opt().unwrap(), &legacy, 0.2),
None
);
}
#[test]
fn rejects_unknown_actions_duplicate_identity_and_invalid_bps() {
let valid = json!({"eventId":"one","sequence":1,"effectiveAt":"2026-09-10T09:30:00+08:00","action":"set","targetExposureBps":5000});
for (key, value) in [
("action", json!("other")),
("effectiveAt", json!("2026-09-10 09:30:00")),
("targetExposureBps", json!(null)),
] {
let mut invalid = valid.clone();
invalid[key] = value;
assert!(serde_json::from_value::<PositionExposureEvent>(invalid).is_err());
}
let event: PositionExposureEvent = serde_json::from_value(valid).unwrap();
assert!(PositionExposureTimeline::from_events(&[event.clone(), event.clone()]).is_err());
let mut invalid = event.clone();
invalid.action = PositionExposureAction::Set {
target_exposure_bps: 10001,
};
assert!(PositionExposureTimeline::from_events(&[invalid]).is_err());
let mut duplicate = event.clone();
duplicate.event_id = "two".into();
assert!(PositionExposureTimeline::from_events(&[event, duplicate]).is_err());
}
}
+92 -29
View File
@@ -76,6 +76,26 @@ impl Default for StaticRiskRuleConfig {
}
}
impl StaticRiskRuleConfig {
pub(crate) fn selection_checks_enabled(&self) -> bool {
(self.blacklist_enabled && !self.blacklisted_symbols.is_empty())
|| self.selection_state_checks_enabled()
}
fn selection_state_checks_enabled(&self) -> bool {
self.reject_st_selection
|| self.reject_star_st_selection
|| self.reject_paused_selection
|| self.reject_inactive_selection
|| self.reject_new_listing_selection
|| self.reject_kcb_selection
|| self.reject_bjse_selection
|| self.reject_one_yuan_selection
|| self.reject_upper_limit_selection
|| self.reject_lower_limit_selection
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TradingConstraintConfig {
/// Shared execution limits. These fields intentionally use the same
@@ -654,16 +674,7 @@ fn missing_risk_state_fields(code: &str) -> Vec<String> {
fn missing_selection_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) -> bool {
let fields = missing_risk_state_fields(code);
if fields.is_empty() {
return config.static_rules.reject_st_selection
|| config.static_rules.reject_star_st_selection
|| config.static_rules.reject_paused_selection
|| config.static_rules.reject_inactive_selection
|| config.static_rules.reject_new_listing_selection
|| config.static_rules.reject_kcb_selection
|| config.static_rules.reject_bjse_selection
|| config.static_rules.reject_one_yuan_selection
|| config.static_rules.reject_upper_limit_selection
|| config.static_rules.reject_lower_limit_selection;
return config.static_rules.selection_state_checks_enabled();
}
missing_field_rejected(&fields, config, RiskCheckScope::Selection)
}
@@ -778,18 +789,7 @@ fn missing_single_field_rejected(
RiskCheckScope::Sell => config.static_rules.reject_lower_limit_sell,
},
_ => match scope {
RiskCheckScope::Selection => {
config.static_rules.reject_st_selection
|| config.static_rules.reject_star_st_selection
|| config.static_rules.reject_paused_selection
|| config.static_rules.reject_inactive_selection
|| config.static_rules.reject_new_listing_selection
|| config.static_rules.reject_kcb_selection
|| config.static_rules.reject_bjse_selection
|| config.static_rules.reject_one_yuan_selection
|| config.static_rules.reject_upper_limit_selection
|| config.static_rules.reject_lower_limit_selection
}
RiskCheckScope::Selection => config.static_rules.selection_state_checks_enabled(),
RiskCheckScope::Buy => {
config.static_rules.reject_st_buy
|| config.static_rules.reject_star_st_buy
@@ -869,7 +869,7 @@ mod tests {
fn candidate(date: NaiveDate) -> CandidateEligibility {
CandidateEligibility {
date,
symbol: "002633.SZ".to_string(),
symbol: "002633.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -885,7 +885,7 @@ mod tests {
fn market(date: NaiveDate, last_price: f64, lower_limit: f64) -> DailyMarketSnapshot {
DailyMarketSnapshot {
date,
symbol: "002633.SZ".to_string(),
symbol: "002633.SZ".into(),
timestamp: Some(format!("{date} 10:18:00")),
day_open: last_price,
open: last_price,
@@ -914,6 +914,69 @@ mod tests {
position
}
#[test]
fn selection_check_activation_covers_every_configured_flag_and_blacklist_state() {
let fields = [
"reject_st_selection", "reject_star_st_selection", "reject_paused_selection",
"reject_inactive_selection", "reject_new_listing_selection", "reject_kcb_selection",
"reject_bjse_selection", "reject_one_yuan_selection", "reject_upper_limit_selection",
"reject_lower_limit_selection",
];
let base = serde_json::to_value(StaticRiskRuleConfig::default()).unwrap();
let declared = base.as_object().unwrap().keys()
.filter(|key| key.ends_with("_selection"))
.map(String::as_str).collect::<BTreeSet<_>>();
assert_eq!(declared, fields.into_iter().collect());
for mask in 0..(1_u32 << fields.len()) {
for (blacklist_enabled, populated) in [(false, false), (false, true), (true, false), (true, true)] {
let mut value = base.clone();
for (bit, field) in fields.iter().enumerate() {
value[*field] = serde_json::json!(mask & (1 << bit) != 0);
}
value["blacklist_enabled"] = serde_json::json!(blacklist_enabled);
value["blacklisted_symbols"] = if populated {
serde_json::json!(["002633.SZ"])
} else { serde_json::json!([]) };
let config: StaticRiskRuleConfig = serde_json::from_value(value).unwrap();
assert_eq!(config.selection_checks_enabled(), mask != 0 || (blacklist_enabled && populated));
}
}
}
#[test]
fn inactive_selection_checks_preserve_missing_facts_and_execution_rejections() {
let date = d(2025, 2, 6);
let mut candidate = candidate(date);
candidate.is_st = true;
candidate.is_star_st = true;
candidate.is_paused = true;
candidate.is_new_listing = true;
candidate.is_kcb = true;
candidate.is_one_yuan = true;
candidate.allow_buy = false;
let snapshot = market(date, 0.9, 0.9);
let config = FidcRiskControlConfig::default();
assert!(!config.static_rules.selection_checks_enabled());
let instrument = instrument("delisted", Some(date));
for code in [None, Some("not_listed"), Some("inactive_or_delisted"),
Some("missing_risk_state"), Some("missing_risk_state:is_st;is_kcb|allow_buy"),
Some("missing_risk_state:unknown_fact"), Some("missing_risk_state:IS_PAUSED")] {
candidate.risk_level_code = code.map(str::to_owned);
assert_eq!(ChinaAShareRiskControl::selection_rejection_decision_with_config(
date, &candidate, &snapshot, Some(&instrument), &config), None);
}
candidate.risk_level_code = None;
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(
date, &candidate, &snapshot, None, 0.9, &config), Some("paused"));
assert_eq!(ChinaAShareRiskControl::sell_rejection_reason_with_config(
date, &candidate, &snapshot, None, None, 0.9, &config), Some("paused"));
let mut blacklist_only = config;
blacklist_only.static_rules.blacklisted_symbols.insert(candidate.symbol.to_string());
assert!(blacklist_only.static_rules.selection_checks_enabled());
assert_eq!(ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &snapshot, None, &blacklist_only), Some("blacklisted"));
}
#[test]
fn one_yuan_buy_rule_uses_execution_price_not_later_close_or_earlier_open() {
let day = d(2025, 2, 6);
@@ -944,7 +1007,7 @@ mod tests {
let mut candidate=candidate(day);
let mut snapshot=market(day,1.2,0.5);
snapshot.lower_limit=0.01;snapshot.upper_limit=10.;
let instrument=Instrument{symbol:candidate.symbol.clone(),name:"fixture fund".into(),board:"ETF".into(),round_lot:100,listed_at:Some(d(2024,1,2)),delisted_at:None,status:"active".into()};
let instrument=Instrument{symbol:candidate.symbol.to_string(),name:"fixture fund".into(),board:"ETF".into(),round_lot:100,listed_at:Some(d(2024,1,2)),delisted_at:None,status:"active".into()};
let config=FidcRiskControlConfig::default();
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(day,&candidate,&snapshot,Some(&instrument),0.9,&config),None);
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(day,&candidate,&snapshot,Some(&instrument),0.,&config),Some("invalid execution price"));
@@ -1047,7 +1110,7 @@ mod tests {
config
.static_rules
.blacklisted_symbols
.insert(candidate.symbol.clone());
.insert(candidate.symbol.to_string());
let selection_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config,
@@ -1092,7 +1155,7 @@ mod tests {
let mut candidate = candidate(date);
let config = FidcRiskControlConfig::default();
for symbol in ["688001.SH", "689001.SH", "000001.SZ"] {
candidate.symbol = symbol.to_string();
candidate.symbol = symbol.into();
for is_kcb in [false, true] {
candidate.is_kcb = is_kcb;
let reason = ChinaAShareRiskControl::buy_rejection_reason_with_config(
@@ -1200,7 +1263,7 @@ mod tests {
fn configurable_bjse_filter_can_be_disabled() {
let date = d(2025, 1, 2);
let mut candidate = candidate(date);
candidate.symbol = "430047.BJ".to_string();
candidate.symbol = "430047.BJ".into();
candidate.allow_sell = true;
let market = market(date, 6.27, 5.63);
let default_selection =
@@ -1232,7 +1295,7 @@ mod tests {
fn concrete_kcb_reason_wins_over_generic_missing_risk_state() {
let date = d(2025, 1, 2);
let mut candidate = candidate(date);
candidate.symbol = "688506.SH".to_string();
candidate.symbol = "688506.SH".into();
candidate.is_kcb = true;
candidate.risk_level_code = Some("missing_risk_state".to_string());
let market = market(date, 6.27, 5.63);
+1 -1
View File
@@ -233,7 +233,7 @@ impl<'a> Scheduler<'a> {
pub fn default_stage_time(stage: ScheduleStage) -> Option<NaiveTime> {
match stage {
ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 25, 0).expect("valid time")),
ScheduleStage::Bar => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
ScheduleStage::Minute => None,
ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
+191
View File
@@ -0,0 +1,191 @@
use std::ops::Index;
use super::prefix_sums;
#[derive(Debug, Clone)]
pub(super) enum ReferenceMatchedValues {
Identical,
Owned(Vec<f64>),
}
impl ReferenceMatchedValues {
pub(super) fn push(&mut self, value: f64, reference: &[f64], capacity: usize) {
let previous_len = reference.len().checked_sub(1).expect("reference row is missing");
match self {
Self::Identical if value.to_bits() == reference[previous_len].to_bits() => {}
Self::Identical => {
let mut values = Vec::with_capacity(capacity);
values.extend_from_slice(&reference[..previous_len]);
values.push(value);
*self = Self::Owned(values);
}
Self::Owned(values) => {
debug_assert_eq!(values.len(), previous_len);
values.push(value);
}
}
}
pub(super) fn values<'a>(&'a self, reference: &'a [f64]) -> &'a [f64] {
match self {
Self::Identical => reference,
Self::Owned(values) => {
debug_assert_eq!(values.len(), reference.len());
values
}
}
}
pub(super) fn set(&mut self, index: usize, value: f64, reference: &[f64]) {
assert!(index < reference.len(), "series index out of bounds");
match self {
Self::Owned(values) => values[index] = value,
Self::Identical if value.to_bits() == reference[index].to_bits() => {}
Self::Identical => {
let mut values = reference.to_vec();
values[index] = value;
*self = Self::Owned(values);
}
}
}
pub(super) fn prefix(&self) -> Self {
match self {
Self::Identical => Self::Identical,
Self::Owned(values) => Self::Owned(prefix_sums(values)),
}
}
}
#[derive(Debug, Clone)]
pub(super) struct RepeatedValues<T> {
repeated: T,
values: Option<Vec<T>>,
len: usize,
}
impl<T: Default + Clone + Eq> RepeatedValues<T> {
pub(super) fn new() -> Self {
Self { repeated: T::default(), values: None, len: 0 }
}
pub(super) fn push(&mut self, value: &T, capacity: usize) {
if let Some(values) = &mut self.values {
values.push(value.clone());
} else if self.len == 0 {
self.repeated = value.clone();
} else if *value != self.repeated {
let mut values = Vec::with_capacity(capacity);
values.resize(self.len, std::mem::take(&mut self.repeated));
values.push(value.clone());
self.values = Some(values);
}
self.len += 1;
}
pub(super) fn set(&mut self, index: usize, value: T) {
assert!(index < self.len, "series index out of bounds");
if let Some(values) = &mut self.values {
values[index] = value;
} else if value != self.repeated {
let mut values = vec![std::mem::take(&mut self.repeated); self.len];
values[index] = value;
self.values = Some(values);
}
}
}
impl<T> Index<usize> for RepeatedValues<T> {
type Output = T;
fn index(&self, index: usize) -> &T {
assert!(index < self.len, "series index out of bounds");
match &self.values {
Some(values) => &values[index],
None => &self.repeated,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bits(values: &[f64]) -> Vec<u64> {
values.iter().map(|value| value.to_bits()).collect()
}
#[test]
fn identical_prices_share_only_after_exact_bit_comparison() {
let reference = [10., -0., f64::from_bits(0x7ff8_0000_0000_0042), f64::INFINITY];
let mut column = ReferenceMatchedValues::Identical;
for (index, value) in reference.iter().copied().enumerate() {
column.push(value, &reference[..=index], reference.len());
}
assert!(matches!(column, ReferenceMatchedValues::Identical));
assert_eq!(column.values(&reference).as_ptr(), reference.as_ptr());
let prefix = prefix_sums(&reference);
assert_eq!(bits(column.prefix().values(&prefix)), bits(&prefix));
let original = column.clone();
column.set(1, 0., &reference);
assert!(matches!(column, ReferenceMatchedValues::Owned(_)));
assert_eq!(column.values(&reference)[1].to_bits(), 0_f64.to_bits());
assert_eq!(bits(original.values(&reference)), bits(&reference));
assert_eq!(bits(column.prefix().values(&prefix)), bits(&prefix_sums(column.values(&reference))));
}
#[test]
fn differing_prices_preserve_zero_nan_payloads_and_prior_rows() {
let reference = [10., 11., f64::from_bits(0x7ff8_0000_0000_0042), 13.];
for actual in [
[10., 0., reference[2], 13.],
[10., 11., f64::from_bits(0x7ff8_0000_0000_0043), 13.],
] {
let mut column = ReferenceMatchedValues::Identical;
for (index, value) in actual.iter().copied().enumerate() {
column.push(value, &reference[..=index], actual.len());
}
assert!(matches!(column, ReferenceMatchedValues::Owned(_)));
assert_eq!(bits(column.values(&reference)), bits(&actual));
}
}
#[test]
fn repeated_values_preserve_nonzero_values_and_copy_on_change() {
let mut column = RepeatedValues::new();
for _ in 0..128 { column.push(&7_u64, 128); }
assert!(column.values.is_none());
assert_eq!(column[127], 7);
column.set(0, 7);
assert!(column.values.is_none());
let mut changed = column.clone();
changed.set(64, 9);
assert_eq!(changed[64], 9);
assert_eq!(changed[63], 7);
assert_eq!(column[64], 7);
}
#[test]
fn optional_values_keep_none_distinct_from_empty_and_repeated_text() {
for repeated in [None, Some(String::new()), Some("continuous".to_string())] {
let mut column = RepeatedValues::new();
for _ in 0..12 { column.push(&repeated, 16); }
assert!(column.values.is_none());
assert_eq!(column[0], repeated);
column.push(&Some("closing".to_string()), 16);
assert_eq!(column[11], repeated);
assert_eq!(column[12].as_deref(), Some("closing"));
column.set(5, None);
assert_eq!(column[5], None);
assert_eq!(column[4], repeated);
}
}
#[test]
#[should_panic(expected = "series index out of bounds")]
fn repeated_values_reject_out_of_range_access() {
let column = RepeatedValues::<u64>::new();
let _ = column[0];
}
}
+177 -42
View File
@@ -39,6 +39,22 @@ pub enum QuoteConditionScope {
AnyTarget,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StockPoolExitRole {
OrdinarySell,
RiskExit,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StockPoolExitSignal {
pub role: StockPoolExitRole,
pub when_expr: String,
pub remaining_position_bps: u32,
pub reason: String,
}
pub fn stock_pool_target_holding_count(policy: &Value) -> Result<Option<usize>, String> {
let object = policy
.as_object()
@@ -399,6 +415,8 @@ pub struct StockPoolExecutionRule {
pub sell_condition_scope: Option<QuoteConditionScope>,
#[serde(skip)]
pub secondary_sell_condition: bool,
#[serde(skip)]
pub independent_sell_condition: bool,
#[serde(
default,
deserialize_with = "crate::holding_policy::deserialize_optional_policy"
@@ -461,11 +479,14 @@ pub struct StockPoolSelection {
pub generation: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct StockPoolDecisionConstraints {
pub execution_date: Option<NaiveDate>,
pub frozen_positions: BTreeMap<String, FrozenStockPoolPosition>,
pub prior_target_weights: BTreeMap<String, i32>,
/// Sizing ratios from prior plans; integer bps are display/legacy only.
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub prior_target_weight_ratios: BTreeMap<String, Decimal>,
pub pending_entry_symbols: BTreeSet<String>,
pub next_day_outside_exit_symbols: BTreeSet<String>,
pub market_timing_policy: Option<crate::stock_pool_index_policy::MarketTimingPolicy>,
@@ -476,6 +497,10 @@ pub struct StockPoolDecisionConstraints {
pub default_stop_loss: Option<Decimal>,
pub default_take_profit: Option<Decimal>,
pub position_target_bps: BTreeMap<String, u32>,
pub independent_position_target_bps: BTreeMap<String, u32>,
/// First actually planned holding quantity for this generation. Retries
/// apply percentages to this basis, never to the remaining holding.
pub position_action_bases: BTreeMap<String, Decimal>,
pub buy_denials: BTreeMap<String, Vec<String>>,
pub same_day_sold_symbols: BTreeSet<String>,
pub automatic_permissions: BTreeMap<String, crate::holding_policy::AutomaticTradePermission>,
@@ -508,6 +533,10 @@ pub struct StockPoolPlanRow {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StockPoolPlan {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub target_weight_ratios: BTreeMap<String, Decimal>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub position_action_bases: BTreeMap<String, Decimal>,
pub market_timing: Option<crate::stock_pool_index_policy::MarketTimingEvaluation>,
pub rows: Vec<StockPoolPlanRow>,
pub budget: Decimal,
@@ -521,7 +550,7 @@ pub struct StockPoolPlan {
/// A signal-time contract. Only the broker/execution adapter supplies later
/// prices, actual cash and holdings; strategy code never sees those inputs.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct FrozenStockPoolIntent {
pub pool_id: String,
pub signal_date: NaiveDate,
@@ -549,6 +578,8 @@ pub struct StockPoolProgram {
pub timing_policy: Value,
pub stop_take_policy: Value,
pub out_of_pool_policy: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exit_signals: Vec<StockPoolExitSignal>,
}
impl StockPoolProgram {
@@ -562,10 +593,19 @@ impl StockPoolProgram {
normalize_stock_pool_members(&self.members)?;
stock_pool_funding_from_configuration(&self.allocation_policy)?;
stock_pool_constraints_from_configuration(&self.allocation_policy, &self.stop_take_policy)?;
normalize_stock_pool_execution_rule(
let mut identities = BTreeSet::new();
for signal in &self.exit_signals {
if signal.when_expr.trim().is_empty() || signal.reason.trim().is_empty() || signal.remaining_position_bps >= 10000 {
return Err("stock_pool_exit_signal_invalid".into());
}
let identity = serde_json::to_string(signal).map_err(|error| error.to_string())?;
if !identities.insert(identity) { return Err("stock_pool_exit_signal_duplicate".into()); }
}
normalize_stock_pool_execution_rule_with_exit_roles(
Some(&self.timing_policy),
secondary_buy,
secondary_sell,
secondary_sell || self.exit_signals.iter().any(|signal| signal.role == StockPoolExitRole::OrdinarySell),
self.exit_signals.iter().any(|signal| signal.role == StockPoolExitRole::RiskExit),
)?;
if !matches!(
self.out_of_pool_policy.as_str(),
@@ -583,6 +623,7 @@ impl Default for StockPoolExecutionRule {
buy_condition_scope: None,
sell_condition_scope: None,
secondary_sell_condition: false,
independent_sell_condition: false,
automatic_trade_protection: Default::default(),
schema_version: STOCK_POOL_SCHEMA_VERSION,
auto_execute: true,
@@ -669,7 +710,17 @@ pub fn build_stock_pool_target_plan_with_fee_model(
}
}
}
// Validate source targets before a stronger stop/expiry can replace them.
// Otherwise an invalid ratio could be hidden by target consolidation.
for (symbol, target) in constraints.position_target_bps.iter().chain(constraints.independent_position_target_bps.iter()) {
if *target >= 10_000 {
return Err(format!("factor position target for {symbol} must be below 10000 bps"));
}
}
let mut effective_position_targets = constraints.position_target_bps.clone();
for (symbol, target) in &constraints.independent_position_target_bps {
effective_position_targets.entry(symbol.clone()).and_modify(|current| *current = (*current).min(*target)).or_insert(*target);
}
for (symbol, permission) in &constraints.automatic_permissions {
if permission.max_holding_exit {
effective_position_targets.insert(symbol.clone(), 0);
@@ -764,6 +815,19 @@ pub fn build_stock_pool_target_plan_with_fee_model(
if quote_map.len() != quotes.len() {
return Err("duplicate or invalid stock pool execution quotes".into());
}
let declared_symbols = normalized_members.iter().map(|member| member.symbol.as_str()).collect::<BTreeSet<_>>();
for (symbol, quantity) in &constraints.position_action_bases {
if normalize_stock_symbol(symbol).as_ref() != Some(symbol) || *quantity <= Decimal::ZERO {
return Err(format!("invalid stock pool position-action basis:{symbol}"));
}
}
for (symbol, _) in constraints.position_target_bps.iter().chain(constraints.independent_position_target_bps.iter()) {
if normalize_stock_symbol(symbol).as_deref() != Some(symbol.as_str()) || (!declared_symbols.contains(symbol.as_str()) && !current.contains_key(symbol)) {
return Err(format!("position action is outside declared candidates and managed holdings:{symbol}"));
}
}
// Exit rules act on managed holdings, not on an unheld candidate's entry.
effective_position_targets.retain(|symbol, _| current.get(symbol).is_some_and(|position| position.0 > Decimal::ZERO));
frozen::validate(selection.trade_date, constraints, &current)?;
for symbol in constraints.frozen_positions.keys() {
effective_position_targets.remove(symbol);
@@ -863,37 +927,49 @@ pub fn build_stock_pool_target_plan_with_fee_model(
.then(|| symbol.clone())
})
.collect::<BTreeSet<_>>();
// A full stop is stricter than a simultaneous relative reduction. Merge
// the target before selecting its single owner, never emit a second exit.
for symbol in &global_stop_hits {
if let Some(target) = effective_position_targets.get_mut(symbol) {
*target = 0;
}
}
let mut quote_sell_exits = BTreeSet::new();
let mut sell_condition_denials = BTreeSet::new();
if rule.sell_trigger_mode == POOL_SELL_CONDITION {
let ordinary_enabled = !rule.sell_condition.trim().is_empty() || rule.secondary_sell_condition;
// Ordinary sell predicates only depend on positions participating in
// that stage. Independent stops/expiry and protected holdings were
// already decided above; unrelated quote fields must not block them.
let held = current
.iter()
.filter(|(symbol, row)| {
row.0 > Decimal::ZERO && !constraints.frozen_positions.contains_key(*symbol)
row.0 > Decimal::ZERO
&& !protected_positions.contains(*symbol)
&& !global_stop_hits.contains(*symbol)
&& constraints.independent_position_target_bps.get(*symbol) != Some(&0)
&& !constraints.automatic_permissions.get(*symbol)
.is_some_and(|permission| permission.max_holding_exit)
})
.map(|(symbol, _)| symbol.clone())
.collect::<Vec<_>>();
let qualified = quote_condition_results(
let qualified = if ordinary_enabled { quote_condition_results(
&rule.sell_condition,
rule.sell_condition_scope,
&held,
&quote_map,
)?;
)? } else { BTreeMap::new() };
for symbol in held {
if global_stop_hits.contains(&symbol)
|| constraints
.automatic_permissions
.get(&symbol)
.is_some_and(|permission| permission.max_holding_exit)
{
continue;
}
let permitted = qualified.get(&symbol) == Some(&true)
let permitted = ordinary_enabled && qualified.get(&symbol) == Some(&true)
&& (!rule.secondary_sell_condition
|| constraints.position_target_bps.contains_key(&symbol));
if !permitted {
sell_condition_denials.insert(symbol.clone());
effective_position_targets.remove(&symbol);
if let Some(target) = constraints.independent_position_target_bps.get(&symbol) {
effective_position_targets.insert(symbol.clone(), *target);
} else {
sell_condition_denials.insert(symbol.clone());
}
} else if !rule.secondary_sell_condition {
quote_sell_exits.insert(symbol.clone());
effective_position_targets.insert(symbol, 0);
@@ -944,6 +1020,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
let normalized_same_day_sold =
normalize_symbol_set(&same_day_sold_symbols.iter().cloned().collect::<Vec<_>>())?;
let mut rebuy_exclusions = stop_take_exits.clone();
rebuy_exclusions.extend(effective_position_targets.keys().cloned());
rebuy_exclusions.extend(
normalized_same_day_sold
.iter()
@@ -1024,6 +1101,11 @@ pub fn build_stock_pool_target_plan_with_fee_model(
target_count,
)?
};
let target_weight_ratios = frozen::sizing_ratios(
&original_final_symbols, &active_symbols, &explicit_weights,
constraints, reserved_protected_slots, &weights,
)?;
let sizing_ratio = |symbol: &str| target_weight_ratios.get(symbol).copied().unwrap_or(Decimal::ZERO);
for symbol in &rebuy_exclusions {
if original_final_symbols.contains(symbol) || current.contains_key(symbol) {
weights.insert(symbol.clone(), 0);
@@ -1031,7 +1113,12 @@ pub fn build_stock_pool_target_plan_with_fee_model(
}
let mut planning_symbols = active_symbols;
for symbol in &original_final_symbols {
if rebuy_exclusions.contains(symbol) && !planning_symbols.contains(symbol) {
// An explicit quote/expiry position action owns its single target row.
// Keep it excluded from entry sizing without adding a second stop row.
if rebuy_exclusions.contains(symbol)
&& !factor_position_target_bps.contains_key(symbol)
&& !planning_symbols.contains(symbol)
{
planning_symbols.push(symbol.clone());
}
}
@@ -1158,8 +1245,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
.map(|symbol| {
let current_value = current[symbol].0
* frozen::valuation(symbol, &quote_map, &constraints.frozen_positions)?;
let desired =
budget * Decimal::from(*weights.get(symbol).unwrap_or(&0)) / Decimal::from(10_000);
let desired = budget * sizing_ratio(symbol);
if constraints.frozen_positions.contains_key(symbol) {
return Ok((symbol.clone(), desired));
}
@@ -1200,7 +1286,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
let free_desired = weights
.iter()
.filter(|(symbol, _)| !protected_values.contains_key(*symbol))
.map(|(_, weight)| budget * Decimal::from(*weight) / Decimal::from(10_000))
.map(|(symbol, _)| budget * sizing_ratio(symbol))
.sum::<Decimal>();
let free_budget = (budget * Decimal::from(requested_weight_total) / Decimal::from(10_000)
- protected_values.values().copied().sum::<Decimal>())
@@ -1219,7 +1305,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
rows.push(StockPoolPlanRow {
symbol: symbol.clone(),
target_weight_bps: weight,
target_value: budget * Decimal::from(weight) / Decimal::from(10_000),
target_value: budget * sizing_ratio(symbol),
current_quantity: quantity,
target_quantity: quantity,
delta_quantity: Decimal::ZERO,
@@ -1323,24 +1409,11 @@ pub fn build_stock_pool_target_plan_with_fee_model(
}
for (symbol, target_bps) in factor_position_target_bps {
if *target_bps >= 10_000 {
return Err(format!(
"factor position target for {symbol} must be below 10000 bps"
));
}
if !member_map.contains_key(symbol) && !current.contains_key(symbol) {
return Err(format!(
"factor position-action symbol {symbol} is outside candidates and managed holdings"
));
}
if selection.final_symbols.contains(symbol)
&& !maximum_holding_exits.contains(symbol)
&& !quote_sell_exits.contains(symbol)
{
return Err(format!(
"factor position-action symbol {symbol} cannot remain in final selection"
));
}
let current_quantity = current
.get(symbol)
.map(|value| value.0)
@@ -1362,10 +1435,11 @@ pub fn build_stock_pool_target_plan_with_fee_model(
Decimal::ZERO
} else {
floor_step(
current_quantity * Decimal::from(*target_bps) / Decimal::from(10_000),
constraints.position_action_bases.get(symbol).copied().unwrap_or(current_quantity)
* Decimal::from(*target_bps) / Decimal::from(10_000),
step,
)
};
}.min(current_quantity);
let desired_reduction = (current_quantity - requested_target).max(Decimal::ZERO);
let executable = if *target_bps == 0 {
closable_quantity.min(current_quantity).max(Decimal::ZERO)
@@ -1379,13 +1453,33 @@ pub fn build_stock_pool_target_plan_with_fee_model(
if current_quantity == Decimal::ZERO {
(
"FACTOR_EXIT_ALREADY_SATISFIED",
"生产因子持仓动作命中,当前无持仓",
"持仓退出规则命中,当前无持仓",
Decimal::ZERO,
Decimal::ZERO,
None,
None,
None,
)
} else if desired_reduction == Decimal::ZERO {
(
"FACTOR_EXIT_ALREADY_SATISFIED",
"本次信号的持仓退出目标已达到,不重复减仓",
Decimal::ZERO,
current_quantity,
None,
None,
None,
)
} else if executable == Decimal::ZERO && closable_quantity >= desired_reduction {
(
"BELOW_MINIMUM_TRADE_UNIT_ALREADY_SATISFIED",
"目标持仓差额不足最小交易单位,无需重复委托",
Decimal::ZERO,
current_quantity,
None,
None,
None,
)
} else if executable == Decimal::ZERO {
(
"DEFERRED_T_PLUS_ONE",
@@ -1414,6 +1508,10 @@ pub fn build_stock_pool_target_plan_with_fee_model(
"达到最长持有期,按配置退出"
} else if quote_sell_exits.contains(symbol) {
"卖出行情条件命中"
} else if stop_take_exits.contains(symbol) {
"止损/止盈触发,覆盖较弱的减仓目标"
} else if constraints.independent_position_target_bps.get(symbol) == Some(target_bps) {
"独立风险退出条件命中"
} else if *target_bps == 0 {
"生产因子退出条件命中"
} else {
@@ -1481,7 +1579,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
let target_value = protected_values
.get(symbol)
.copied()
.unwrap_or(budget * Decimal::from(weight) / Decimal::from(10_000) * free_scale)
.unwrap_or(if weight == 0 { Decimal::ZERO } else { budget * sizing_ratio(symbol) * free_scale })
.round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven);
let sizing_price = if target_value >= current_quantity * quote.last_price {
quote.buy_sizing_price.unwrap_or(quote.last_price)
@@ -1491,7 +1589,17 @@ pub fn build_stock_pool_target_plan_with_fee_model(
if sizing_price <= Decimal::ZERO {
return Err(format!("{symbol} execution sizing price is invalid"));
}
let raw_target = (target_value / sizing_price).floor();
// Existing shares are marked at the observed market price. Only the
// new buy leg pays its executable/slippage price; repricing the whole
// position would charge fictitious slippage and miss a board lot.
let current_value = current_quantity * quote.last_price;
let raw_target = if target_value >= current_value {
current_quantity + ((target_value - current_value) / sizing_price).floor()
} else {
// Sale slippage changes proceeds, not the marked shares we must
// remove to reach a market-value target.
(target_value / quote.last_price).floor()
};
let (step, minimum_buy) = order_quantity_rules(quote)?;
let mut target_quantity = current_quantity;
let mut delta = Decimal::ZERO;
@@ -1688,6 +1796,14 @@ pub fn build_stock_pool_target_plan_with_fee_model(
}
}
}
// Verify disjoint planning ownership before an index cap can address rows
// by symbol. Never deduplicate emitted intentions or count proceeds twice.
let mut owners = BTreeSet::new();
for row in &rows {
if !owners.insert(row.symbol.as_str()) {
return Err(format!("stock_pool_target_owner_conflict:{}", row.symbol));
}
}
if market_timing.is_some() {
let caps = index_cap::remaining_index_targets(
&current,
@@ -1806,7 +1922,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
let cost = |quantity: Decimal| {
Ok(quantity * price + fee_for(&row.symbol, OrderSide::Buy, quantity * price)?)
};
let own_budget = (row.target_value - row.current_quantity * price).max(Decimal::ZERO);
let own_budget = (row.target_value - row.current_quantity * quote.last_price).max(Decimal::ZERO);
let allocation_quantity = max_affordable_buy_quantity_with_cost(
own_budget,
row.delta_quantity,
@@ -1890,7 +2006,16 @@ pub fn build_stock_pool_target_plan_with_fee_model(
.into_iter()
.sum();
let estimated_cash_after = available_cash - estimated_buy_amount + estimated_sell_amount;
let position_action_bases = rows.iter()
.filter(|row| effective_position_targets.get(&row.symbol).is_some_and(|bps| *bps > 0)
&& row.current_quantity > Decimal::ZERO
&& row.status != "AUTOMATIC_TRADE_PROTECTED"
&& !constraints.frozen_positions.contains_key(&row.symbol))
.map(|row| (row.symbol.clone(), constraints.position_action_bases.get(&row.symbol).copied().unwrap_or(row.current_quantity)))
.collect();
Ok(StockPoolPlan {
target_weight_ratios,
position_action_bases,
market_timing,
rows,
budget,
@@ -2167,6 +2292,15 @@ pub fn normalize_stock_pool_execution_rule(
raw: Option<&Value>,
secondary_buy_condition: bool,
secondary_sell_condition: bool,
) -> Result<StockPoolExecutionRule, String> {
normalize_stock_pool_execution_rule_with_exit_roles(raw, secondary_buy_condition, secondary_sell_condition, false)
}
pub fn normalize_stock_pool_execution_rule_with_exit_roles(
raw: Option<&Value>,
secondary_buy_condition: bool,
secondary_sell_condition: bool,
independent_sell_condition: bool,
) -> Result<StockPoolExecutionRule, String> {
let mut rule = match raw {
None | Some(Value::Null) => StockPoolExecutionRule::default(),
@@ -2174,6 +2308,7 @@ pub fn normalize_stock_pool_execution_rule(
.map_err(|err| format!("stock pool execution_rule is invalid: {err}"))?,
};
rule.secondary_sell_condition = secondary_sell_condition;
rule.independent_sell_condition = independent_sell_condition;
rule.automatic_trade_protection.validate()?;
if rule.schema_version != STOCK_POOL_SCHEMA_VERSION {
return Err(format!(
@@ -2260,7 +2395,7 @@ pub fn normalize_stock_pool_execution_rule(
return Err("stock pool buy_condition is not supported".to_string());
}
if rule.sell_trigger_mode == POOL_SELL_CONDITION {
if (rule.sell_condition.trim().is_empty() && !secondary_sell_condition)
if (rule.sell_condition.trim().is_empty() && !secondary_sell_condition && !independent_sell_condition)
|| (!rule.sell_condition.trim().is_empty()
&& parse_stock_pool_condition(&rule.sell_condition).is_none())
{
@@ -1,4 +1,56 @@
use super::*;
#[test]
fn legacy_constraints_do_not_gain_an_empty_precision_field() {
let value = serde_json::to_value(StockPoolDecisionConstraints::default()).unwrap();
assert!(value.get("prior_target_weight_ratios").is_none());
}
#[test]
fn equal_thirty_seats_use_full_precision_at_a_board_lot_boundary() {
for (equity, price, executable, held) in [("999377.147617", "3.07", "3.070307", 2000), ("995624.8819", "6.42", "6.420642", 900)] {
let pool = members(30);
let mut market = quotes(30);
let last = market.last_mut().unwrap();
last.last_price = price.parse().unwrap();
last.buy_sizing_price = Some(executable.parse().unwrap());
let positions = vec![Position { symbol: last.symbol.clone(), quantity: held.into(), closable_quantity: held.into(), average_cost: last.last_price }];
let mut selection = selection(30, 30);
let constraints = StockPoolDecisionConstraints { target_holding_count: Some(30), reserve_cash_slots: 1, ..Default::default() };
let total: Decimal = equity.parse().unwrap();
let account = AccountSnapshot { total_equity: total, cash: total - positions[0].quantity * last.last_price, frozen_cash: Decimal::ZERO };
let build = |selection: &StockPoolSelection| build_stock_pool_target_plan_with_constraints(selection, &pool, &StockPoolExecutionRule::default(), &account, &positions, &market,
2000, Decimal::ZERO, "hold", "full_rebalance", &constraints, "exact-shares", Decimal::new(2,4), Decimal::ZERO, Decimal::ZERO).unwrap();
let plan = build(&selection);
let target = plan.rows.iter().find(|row| row.symbol == positions[0].symbol).unwrap();
assert_eq!(target.delta_quantity, Decimal::from(100), "{equity}: {target:?}");
let expected = (total * Decimal::new(2,1) / Decimal::from(31)).round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven);
assert!(plan.rows.iter().all(|row| row.target_value == expected));
selection.final_symbols.reverse();
selection.requested_symbols.reverse();
assert_eq!(build(&selection).rows.iter().find(|row| row.symbol == positions[0].symbol).unwrap().delta_quantity, Decimal::from(100));
let date = selection.trade_date;
let state = crate::stock_pool_state::StockPoolExecutionState::default().observe(date,date,&[date],&pool,&positions).unwrap().record_plan(date,"exact-shares",&plan).unwrap();
assert_eq!(state.schema_version, 2);
assert_eq!(state.last_target_weight_ratios[&positions[0].symbol], Decimal::ONE / Decimal::from(30));
let restored: crate::stock_pool_state::StockPoolExecutionState = serde_json::from_value(serde_json::to_value(&state).unwrap()).unwrap();
restored.validate().unwrap();
assert_eq!(state, restored);
}
}
#[test]
fn sale_slippage_does_not_prevent_a_marked_value_board_lot_reduction() {
let pool = members(1);
let mut market = quotes(1);
market[0].sell_sizing_price = Some(Decimal::new(99,1));
let positions = vec![Position { symbol:symbol(1),quantity:200.into(),closable_quantity:200.into(),average_cost:10.into() }];
let plan = build_stock_pool_target_plan_with_constraints(&selection(1,1),&pool,&StockPoolExecutionRule::default(),
&AccountSnapshot {total_equity:2000.into(),cash:Decimal::ZERO,frozen_cash:Decimal::ZERO},&positions,&market,
5000,Decimal::ZERO,"hold","full_rebalance",&StockPoolDecisionConstraints::default(),"reduce",Decimal::ZERO,Decimal::ZERO,Decimal::ZERO).unwrap();
assert_eq!(plan.rows[0].target_value,Decimal::from(1000));
assert_eq!(plan.rows[0].delta_quantity,Decimal::from(-100));
}
use serde_json::json;
fn symbol(index: usize) -> String {
@@ -509,6 +561,16 @@ fn condition_plan(
quotes: &[MarketSnapshot],
constraints: &StockPoolDecisionConstraints,
) -> StockPoolPlan {
condition_plan_result(selection, rule, positions, quotes, constraints).unwrap()
}
fn condition_plan_result(
selection: &StockPoolSelection,
rule: &StockPoolExecutionRule,
positions: &[Position],
quotes: &[MarketSnapshot],
constraints: &StockPoolDecisionConstraints,
) -> Result<StockPoolPlan, String> {
let held_value = positions
.iter()
.map(|position| {
@@ -541,7 +603,6 @@ fn condition_plan(
Decimal::ZERO,
Decimal::ZERO,
)
.unwrap()
}
#[test]
@@ -704,6 +765,221 @@ fn native_sell_and_quote_conditions_are_and_but_stop_and_protection_remain_indep
);
}
#[test]
fn independent_stop_does_not_require_unused_ordinary_sell_quote_facts() {
let mut market = quotes(1);
market[0].last_price = 9.into();
market[0].volume = None;
let rule = normalize_stock_pool_execution_rule(
Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000"})),
false,
false,
).unwrap();
let constraints = StockPoolDecisionConstraints {
default_stop_loss: Some(Decimal::new(5, 2)),
..Default::default()
};
let plan = condition_plan(&selection(1, 1), &rule, &[position(1)], &market, &constraints);
let exit = plan.rows.iter().find(|row| row.symbol == symbol(1)).unwrap();
assert_eq!(exit.side, Some(OrderSide::Sell), "{plan:?}");
assert_eq!(exit.target_quantity, Decimal::ZERO, "{plan:?}");
}
#[test]
fn ordinary_sell_scope_excludes_independent_exits_and_protected_positions() {
for scope in [QuoteConditionScope::PerSymbol, QuoteConditionScope::AllTargets, QuoteConditionScope::AnyTarget] {
for cause in ["stop_loss", "take_profit", "maximum_holding_exit", "automatic_trade_locked", "buy_fill_protection"] {
let mut market = quotes(2);
market[0].volume = None;
let mut constraints = StockPoolDecisionConstraints::default();
match cause {
"stop_loss" => { market[0].last_price = 9.into(); constraints.default_stop_loss = Some(Decimal::new(5, 2)); },
"take_profit" => { market[0].last_price = 12.into(); constraints.default_take_profit = Some(Decimal::new(10, 2)); },
"maximum_holding_exit" => { constraints.automatic_permissions.insert(symbol(1), crate::holding_policy::AutomaticTradePermission { max_holding_exit: true, ..Default::default() }); },
_ => { constraints.automatic_permissions.insert(symbol(1), crate::holding_policy::AutomaticTradePermission { sell_denial: Some(cause), buy_denial: Some(cause), ..Default::default() }); },
}
let rule = normalize_stock_pool_execution_rule(
Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000","sell_condition_scope":scope})),
false, false,
).unwrap();
let plan = condition_plan(&selection(2, 2), &rule, &[position(1), position(2)], &market, &constraints);
let protected = plan.rows.iter().find(|row| row.symbol == symbol(1)).unwrap();
let normal = plan.rows.iter().find(|row| row.symbol == symbol(2)).unwrap();
assert_eq!(normal.side, Some(OrderSide::Sell), "{scope:?}/{cause}: {plan:?}");
if cause == "automatic_trade_locked" || cause == "buy_fill_protection" {
assert_eq!(protected.side, None, "{scope:?}/{cause}: {plan:?}");
assert_eq!(protected.target_quantity, 1000.into(), "{scope:?}/{cause}: {plan:?}");
assert_eq!(protected.status, "AUTOMATIC_TRADE_PROTECTED", "{scope:?}/{cause}: {plan:?}");
} else {
assert_eq!(protected.side, Some(OrderSide::Sell), "{scope:?}/{cause}: {plan:?}");
assert_eq!(protected.target_quantity, Decimal::ZERO, "{scope:?}/{cause}: {plan:?}");
}
}
}
}
#[test]
fn independent_exit_quote_priority_does_not_bypass_t_plus_one_or_price_validation() {
let rule = normalize_stock_pool_execution_rule(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000"})), false, false).unwrap();
let mut market = quotes(1);
market[0].last_price = 9.into(); market[0].volume = None;
let constraints = StockPoolDecisionConstraints { default_stop_loss: Some(Decimal::new(5,2)), ..Default::default() };
for closable in [0, 400, 1000] {
let mut held = position(1); held.closable_quantity = Decimal::from(closable);
let plan = condition_plan(&selection(1,1), &rule, &[held], &market, &constraints);
let row = plan.rows.iter().find(|row|row.symbol==symbol(1)).unwrap();
assert_eq!(row.delta_quantity, -Decimal::from(closable), "{plan:?}");
assert_eq!(row.target_quantity, Decimal::from(1000-closable), "{plan:?}");
}
market[0].last_price = Decimal::ZERO;
assert!(condition_plan_result(&selection(1,1), &rule, &[position(1)], &market, &constraints).unwrap_err().contains("execution quote is invalid"));
}
#[test]
fn full_stop_overrides_a_simultaneous_factor_reduction_without_a_second_target() {
let mut selected = selection(2, 1);
selected.final_symbols = vec![symbol(2)];
let mut market = quotes(2);
market[0].last_price = 9.into();
let constraints = StockPoolDecisionConstraints {
default_stop_loss: Some(Decimal::new(5, 2)),
position_target_bps: BTreeMap::from([(symbol(1), 5000)]),
..Default::default()
};
let plan = condition_plan(&selected, &StockPoolExecutionRule::default(), &[position(1)], &market, &constraints);
let rows = plan.rows.iter().filter(|row|row.symbol==symbol(1)).collect::<Vec<_>>();
assert_eq!(rows.len(),1,"{plan:?}");
assert_eq!(rows[0].target_quantity,Decimal::ZERO,"a full stop must not be weakened by a 50% reduction: {plan:?}");
assert_eq!(rows[0].delta_quantity,Decimal::from(-1000),"{plan:?}");
}
#[test]
fn stop_reduction_merge_matrix_preserves_protection_t_plus_one_and_invalid_config_errors() {
for take_profit in [false,true] {
for reduction in [0,2500,5000,9999] {
for closable in [0,400,1000] {
for locked in [false,true] {
let mut selected=selection(2,1);selected.final_symbols=vec![symbol(2)];
let mut market=quotes(2);market[0].last_price=if take_profit {12.into()} else {9.into()};market[0].volume=None;
let mut held=position(1);held.closable_quantity=Decimal::from(closable);
let mut constraints=StockPoolDecisionConstraints {position_target_bps:BTreeMap::from([(symbol(1),reduction)]),..Default::default()};
if take_profit {constraints.default_take_profit=Some(Decimal::new(10,2))} else {constraints.default_stop_loss=Some(Decimal::new(5,2))}
if locked {constraints.automatic_permissions.insert(symbol(1),crate::holding_policy::AutomaticTradePermission {sell_denial:Some("automatic_trade_locked"),buy_denial:Some("automatic_trade_locked"),..Default::default()});}
let rule=normalize_stock_pool_execution_rule(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000"})),false,true).unwrap();
let plan=condition_plan(&selected,&rule,&[held],&market,&constraints);
let rows=plan.rows.iter().filter(|row|row.symbol==symbol(1)).collect::<Vec<_>>();
assert_eq!(rows.len(),1,"{plan:?}");
let sold=if locked {0} else {closable};
assert_eq!(rows[0].delta_quantity,-Decimal::from(sold),"{plan:?}");
assert_eq!(rows[0].target_quantity,Decimal::from(1000-sold),"{plan:?}");
assert_eq!(plan.estimated_sell_amount,Decimal::from(sold)*market[0].last_price,"{plan:?}");
if locked {assert_eq!(rows[0].status,"AUTOMATIC_TRADE_PROTECTED","{plan:?}")}
}
}
}
}
let mut invalid=StockPoolDecisionConstraints {default_stop_loss:Some(Decimal::new(5,2)),position_target_bps:BTreeMap::from([(symbol(1),10000)]),..Default::default()};
invalid.automatic_permissions.insert(symbol(1),crate::holding_policy::AutomaticTradePermission {max_holding_exit:true,..Default::default()});
assert!(condition_plan_result(&selection(1,1),&StockPoolExecutionRule::default(),&[position(1)],&quotes(1),&invalid).unwrap_err().contains("must be below 10000"));
}
#[test]
fn ordinary_sell_keeps_required_quote_failures_and_zero_stop_is_not_an_exit() {
let rule = normalize_stock_pool_execution_rule(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000"})), false, false).unwrap();
let mut market = quotes(1); market[0].last_price = 9.into(); market[0].volume = None;
for stop in [None, Some(Decimal::ZERO)] {
let constraints = StockPoolDecisionConstraints { default_stop_loss: stop, ..Default::default() };
assert_eq!(condition_plan_result(&selection(1,1), &rule, &[position(1)], &market, &constraints).unwrap_err(), "condition requires volume");
}
}
#[test]
fn quote_field_operator_side_and_scope_matrix_matches_the_configured_predicate() {
let market = quotes(2);
for (field, threshold) in [("price", "10"), ("last", "10"), ("change_pct", "0"), ("volume", "1000000"), ("amount", "10000000"), ("bid1", "10"), ("ask1", "10")] {
for (operator, matched) in [(">",false), (">=",true), ("<",false), ("<=",true), ("==",true), ("!=",false)] {
for scope in [QuoteConditionScope::PerSymbol, QuoteConditionScope::AllTargets, QuoteConditionScope::AnyTarget] {
for side in [OrderSide::Buy, OrderSide::Sell] {
let condition = format!("{field}{operator}{threshold}");
let config = if side == OrderSide::Buy {
json!({"trigger_mode":"condition","buy_condition":condition,"buy_condition_scope":scope})
} else {
json!({"sell_trigger_mode":"condition","sell_condition":condition,"sell_condition_scope":scope})
};
let rule = normalize_stock_pool_execution_rule(Some(&config), false, false).unwrap();
let held = if side == OrderSide::Sell { vec![position(1),position(2)] } else { vec![] };
let plan = condition_plan(&selection(2,2), &rule, &held, &market, &StockPoolDecisionConstraints::default());
assert_eq!(plan.rows.iter().map(|row| &row.symbol).collect::<BTreeSet<_>>().len(), plan.rows.len(), "duplicate target ownership: {plan:?}");
let orders = plan.rows.iter().filter(|row|row.side==Some(side)).count();
assert_eq!(orders, if matched {2} else {0}, "{side:?}/{scope:?}/{condition}: {plan:?}");
if side == OrderSide::Sell && matched {
assert_eq!(plan.estimated_sell_amount, Decimal::from(20000), "{plan:?}");
assert_eq!(plan.estimated_cash_after, Decimal::from(40000), "{plan:?}");
}
}
}
}
}
}
#[test]
fn typed_exit_roles_merge_only_satisfied_ordinary_actions_with_independent_risk() {
for risk in [None,Some(0),Some(5000)] {
for ordinary in [None,Some(0),Some(7500)] {
for quote in ["","price<9","price>9"] {
for locked in [false,true] {
for closable in [0,400,1000] {
let rule=normalize_stock_pool_execution_rule_with_exit_roles(Some(&json!({"sell_trigger_mode":"condition","sell_condition":quote})),false,true,true).unwrap();
let mut constraints=StockPoolDecisionConstraints {portfolio_policy:Some(StockPoolPortfolioPolicy{schema_version:1,membership:MembershipPolicy::RetainHoldings,rebalance_weights:false}),..Default::default()};
if let Some(target)=ordinary {constraints.position_target_bps.insert(symbol(1),target);}
if let Some(target)=risk {constraints.independent_position_target_bps.insert(symbol(1),target);}
if locked {constraints.automatic_permissions.insert(symbol(1),crate::holding_policy::AutomaticTradePermission{sell_denial:Some("automatic_trade_locked"),buy_denial:Some("automatic_trade_locked"),..Default::default()});}
let mut held=position(1);held.closable_quantity=closable.into();
let plan=condition_plan(&selection(1,1),&rule,&[held],&quotes(1),&constraints);
assert_eq!(plan.rows.len(),1,"{risk:?}/{ordinary:?}/{quote}: {plan:?}");
let ordinary=if quote=="price<9" {None} else {ordinary};
let target_bps=risk.into_iter().chain(ordinary).min().unwrap_or(10000);
let desired=if target_bps==0 {0} else {(1000*target_bps/10000)/100*100};
let sold=if locked {0} else {(1000-desired).min(closable)};
assert_eq!(plan.rows[0].delta_quantity,-Decimal::from(sold),"{risk:?}/{ordinary:?}/{quote}: {plan:?}");
}
}
}
}
}
}
#[test]
fn risk_only_configuration_never_turns_into_an_unconditional_ordinary_exit() {
let rule=normalize_stock_pool_execution_rule_with_exit_roles(Some(&json!({"sell_trigger_mode":"condition"})),false,false,true).unwrap();
let mut constraints=StockPoolDecisionConstraints::default();
let hold=condition_plan(&selection(1,1),&rule,&[position(1)],&quotes(1),&constraints);
assert_ne!(hold.rows[0].side,Some(OrderSide::Sell),"a risk-only configuration must not manufacture an exit: {hold:?}");
constraints.independent_position_target_bps.insert(symbol(1),5000);
let exit=condition_plan(&selection(1,1),&rule,&[position(1)],&quotes(1),&constraints);
assert_eq!(exit.rows[0].delta_quantity,Decimal::from(-500),"{exit:?}");
let unheld=condition_plan(&selection(1,1),&rule,&[],&quotes(1),&constraints);
assert_eq!(unheld.rows[0].side,Some(OrderSide::Buy),"an exit-only rule must not secretly become a selection/buy filter: {unheld:?}");
}
#[test]
fn quote_only_exit_still_works_when_independent_risk_rules_are_configured() {
let rule=normalize_stock_pool_execution_rule_with_exit_roles(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"price>9"})),false,false,true).unwrap();
let plan=condition_plan(&selection(1,1),&rule,&[position(1)],&quotes(1),&StockPoolDecisionConstraints::default());
assert_eq!(plan.rows.len(),1);assert_eq!(plan.rows[0].delta_quantity,Decimal::from(-1000),"{plan:?}");
}
#[test]
fn independent_full_exit_has_no_ordinary_quote_dependency_but_partial_risk_does_not_fake_missing_facts() {
let rule=normalize_stock_pool_execution_rule_with_exit_roles(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>100"})),false,true,true).unwrap();
let mut market=quotes(1);market[0].volume=None;
let mut constraints=StockPoolDecisionConstraints {position_target_bps:BTreeMap::from([(symbol(1),0)]),independent_position_target_bps:BTreeMap::from([(symbol(1),0)]),..Default::default()};
let complete=condition_plan(&selection(1,1),&rule,&[position(1)],&market,&constraints);
assert_eq!(complete.rows[0].delta_quantity,Decimal::from(-1000));
constraints.independent_position_target_bps.insert(symbol(1),5000);
assert!(condition_plan_result(&selection(1,1),&rule,&[position(1)],&market,&constraints).unwrap_err().contains("requires volume"));
}
#[test]
fn partial_sell_cooldown_restricts_increases_without_clearing_the_remainder() {
let mut constraints = StockPoolDecisionConstraints::default();
+91
View File
@@ -26,9 +26,58 @@ pub(super) fn validate(
{
return Err("stock_pool_prior_target_weights_invalid".into());
}
if constraints.prior_target_weight_ratios.iter().any(|(symbol, ratio)| {
normalize_stock_symbol(symbol).as_ref() != Some(symbol) || *ratio < Decimal::ZERO || *ratio > Decimal::ONE
}) { return Err("stock_pool_prior_target_weight_ratios_invalid".into()); }
Ok(())
}
/// Never size cash with the rounded display bps. Paused holdings keep the
/// actually recorded prior ratio. Legacy bps are preserved, not guessed as 1/N.
pub(super) fn sizing_ratios(
original: &[String], active: &[String], explicit: &BTreeMap<String, i32>,
constraints: &StockPoolDecisionConstraints, reserved_slots: usize,
display: &BTreeMap<String, i32>,
) -> Result<BTreeMap<String, Decimal>, String> {
if !explicit.is_empty() {
return Ok(display.iter().map(|(symbol, weight)| (symbol.clone(), Decimal::from(*weight) / Decimal::from(10_000))).collect());
}
if constraints.frozen_positions.is_empty() {
let count = active.len() + reserved_slots;
let share = if count == 0 { Decimal::ZERO } else { Decimal::ONE / Decimal::from(count as u64) };
return Ok(active.iter().map(|symbol| (symbol.clone(), share)).collect());
}
let count = original.len() + reserved_slots;
let base = if count == 0 { Decimal::ZERO } else { Decimal::ONE / Decimal::from(count as u64) };
let mut result = BTreeMap::new();
for symbol in constraints.frozen_positions.keys() {
let ratio = constraints.prior_target_weight_ratios.get(symbol).copied()
.or_else(|| constraints.prior_target_weights.get(symbol).map(|bps| Decimal::from(*bps) / Decimal::from(10_000)))
.unwrap_or(base);
result.insert(symbol.clone(), ratio);
}
let frozen_total = result.values().copied().sum::<Decimal>();
// Only allow last-digit residue from Decimal division, never a meaningful
// over-allocation. All actual cash/fee checks remain downstream.
if frozen_total > Decimal::ONE + Decimal::new(1, 24) {
return Err("stock_pool_frozen_position_ratios_exceed_budget".into());
}
let free_original = original.iter().filter(|symbol| !result.contains_key(*symbol)).collect::<BTreeSet<_>>();
let free_total = (base * Decimal::from(free_original.len() as u64)).min((Decimal::ONE - frozen_total).max(Decimal::ZERO));
let share = if free_original.is_empty() { Decimal::ZERO } else { free_total / Decimal::from(free_original.len() as u64) };
let free = active.iter().filter(|symbol| !constraints.frozen_positions.contains_key(*symbol)).collect::<Vec<_>>();
let promoted = free.iter().filter(|symbol| !free_original.contains(**symbol)).copied().collect::<Vec<_>>();
for symbol in &free { result.insert((*symbol).clone(), if free_original.contains(*symbol) { share } else { Decimal::ZERO }); }
let assigned = free.iter().map(|symbol| result[*symbol]).sum::<Decimal>();
let missing = (free_total - assigned).max(Decimal::ZERO);
let recipients = if promoted.is_empty() { &free } else { &promoted };
if !recipients.is_empty() {
let addition = missing / Decimal::from(recipients.len() as u64);
for symbol in recipients { *result.entry((*symbol).clone()).or_default() += addition; }
}
Ok(result)
}
pub(super) fn valuation(
symbol: &str,
quotes: &HashMap<String, &MarketSnapshot>,
@@ -42,6 +91,48 @@ pub(super) fn valuation(
.ok_or_else(|| format!("{symbol} confirmed holding valuation missing"))
}
#[cfg(test)]
mod ratio_tests {
use super::*;
fn configuration() -> (Vec<String>, StockPoolDecisionConstraints, BTreeMap<String,i32>) {
let symbols: Vec<String> = vec!["000001.SZ".into(),"000002.SZ".into(),"000003.SZ".into()];
let paused = FrozenStockPoolPosition { trade_date: NaiveDate::from_ymd_opt(2026,9,3).unwrap(), reason:"paused".into(), valuation_price:Decimal::from(10) };
let constraints = StockPoolDecisionConstraints { frozen_positions:BTreeMap::from([(symbols[0].clone(),paused)]),
prior_target_weights:BTreeMap::from([(symbols[0].clone(),3334)]), ..Default::default() };
let display = BTreeMap::from([(symbols[0].clone(),3334),(symbols[1].clone(),3333),(symbols[2].clone(),3333)]);
(symbols,constraints,display)
}
#[test]
fn precise_paused_budget_survives_replacement_and_zero_targets() {
let (symbols,mut constraints,display)=configuration();
let third=Decimal::ONE/Decimal::from(3);
constraints.prior_target_weight_ratios.insert(symbols[0].clone(),third);
let mut active=symbols.clone();active[2]="000004.SZ".into();
let ratios=sizing_ratios(&symbols,&active,&BTreeMap::new(),&constraints,0,&display).unwrap();
assert_eq!(ratios[&symbols[0]],third);
assert_eq!(ratios[&symbols[1]],third);
assert!((ratios["000004.SZ"]-third).abs()<Decimal::new(1,24));
assert!(!ratios.contains_key(&symbols[2]));
assert!((ratios.values().copied().sum::<Decimal>()-Decimal::ONE).abs()<Decimal::new(1,24));
}
#[test]
fn legacy_paused_and_explicit_partial_budgets_are_not_reinterpreted() {
let (symbols,constraints,display)=configuration();
let ratios=sizing_ratios(&symbols,&symbols,&BTreeMap::new(),&constraints,0,&display).unwrap();
assert_eq!(ratios[&symbols[0]],Decimal::new(3334,4));
assert!((ratios[&symbols[1]]-Decimal::new(3333,4)).abs()<Decimal::new(1,24));
let partial=BTreeMap::from([(symbols[0].clone(),2000),(symbols[1].clone(),0)]);
assert_eq!(sizing_ratios(&symbols,&symbols,&partial,&constraints,0,&partial).unwrap(),
BTreeMap::from([(symbols[0].clone(),Decimal::new(2,1)),(symbols[1].clone(),Decimal::ZERO)]));
}
#[test]
fn precision_is_checked_before_frozen_budget_is_allocated() {
let (symbols,mut constraints,display)=configuration();
constraints.prior_target_weight_ratios.insert(symbols[0].clone(),Decimal::new(1001,3));
assert!(sizing_ratios(&symbols,&symbols,&BTreeMap::new(),&constraints,0,&display).is_err());
}
}
pub(super) fn weights(
original: &[String],
active: &[String],
@@ -0,0 +1,148 @@
//! Condition facts are distinct from the quote's per-observation fill capacity.
//! Only a complete, declared raw-minute prefix can prove a session total.
use std::collections::BTreeMap;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use rust_decimal::Decimal;
use crate::data::IntradayExecutionQuote;
use crate::stock_pool_execution::{parse_stock_pool_condition, StockPoolExecutionRule};
pub fn requires_session_totals(rule: &StockPoolExecutionRule) -> bool {
[rule.buy_condition.as_str(), if rule.sell_trigger_mode == "condition" { rule.sell_condition.as_str() } else { "" }].into_iter().any(|condition| {
parse_stock_pool_condition(condition).is_some_and(|(_, field, _, _)| matches!(field.as_str(), "volume" | "amount"))
})
}
/// The cash-equity minute feed includes the opening observation and a separate
/// post-close segment. Trading eligibility remains owned by the dated rules.
fn next_minute(time: NaiveTime) -> Option<NaiveTime> {
let minute = time.hour() * 60 + time.minute();
let next = match minute {
570..=689 | 781..=899 | 906..=929 => minute + 1,
690 => 781,
900 => 906,
_ => return None,
};
NaiveTime::from_hms_opt(next / 60, next % 60, 0)
}
#[derive(Debug, Default)]
pub(crate) struct SessionTotalsCache {
pub date: Option<NaiveDate>,
pub symbols: BTreeMap<String, MinutePrefix>,
}
#[derive(Debug)]
pub(crate) struct MinutePrefix {
values: BTreeMap<NaiveTime, (Decimal, Decimal)>,
failure: String,
}
impl MinutePrefix {
pub fn build(date: NaiveDate, symbol: &str, quotes: &[IntradayExecutionQuote]) -> Self {
let mut values = BTreeMap::new();
let mut expected = NaiveTime::from_hms_opt(9, 30, 0).unwrap();
let mut volume = 0_u64;
let mut amount = Decimal::ZERO;
let mut failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:{expected}");
for quote in quotes {
let time = quote.timestamp.time();
if quote.date != date || quote.timestamp.date() != date || quote.symbol != symbol {
failure = format!("stock_pool_session_prefix_identity_invalid:{symbol}:{date}");
break;
}
if time != expected {
failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:expected={expected}:observed={time}");
break;
}
if quote.observation_kind != crate::data::QuoteObservationKind::MinuteBar {
failure = format!("stock_pool_session_prefix_basis_unverified:{symbol}:{date}:{time}");
break;
}
let Some(next_volume) = volume.checked_add(quote.volume_delta) else {
failure = format!("stock_pool_session_volume_overflow:{symbol}:{date}:{time}");
break;
};
let delta = if quote.amount_delta.is_finite() && quote.amount_delta >= 0.0 {
quote.amount_delta.to_string().parse::<Decimal>().ok()
} else { None };
let Some(next_amount) = delta.and_then(|delta| amount.checked_add(delta)) else {
failure = format!("stock_pool_session_amount_invalid:{symbol}:{date}:{time}");
break;
};
volume = next_volume;
amount = next_amount;
values.insert(time, (Decimal::from(volume), amount));
let Some(next) = next_minute(time) else { break };
expected = next;
failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:{expected}");
}
Self { values, failure }
}
pub fn at(&self, at: NaiveDateTime) -> Result<(Decimal, Decimal), String> {
let time = at.time().with_second(0).unwrap().with_nanosecond(0).unwrap();
self.values.get(&time).copied().ok_or_else(|| self.failure.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn quote(hour: u32, minute: u32, volume: u64, amount: f64) -> IntradayExecutionQuote {
let date = NaiveDate::from_ymd_opt(2026, 9, 11).unwrap();
IntradayExecutionQuote { observation_kind: crate::data::QuoteObservationKind::MinuteBar, date, symbol: "000001.SZ".into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
last_price: 10., bid1: 0., ask1: 0., bid1_volume: 0, ask1_volume: 0,
volume_delta: volume, amount_delta: amount, trading_phase: Some("minute_execution_prices:raw-minute".into()) }
}
#[test]
fn totals_use_only_the_complete_observed_prefix_and_keep_decimal_amounts() {
let mut rows = vec![quote(9,30,100,10.01), quote(9,31,0,0.), quote(9,32,200,20.02)];
let prefix = MinutePrefix::build(rows[0].date, "000001.SZ", &rows);
assert_eq!(prefix.at(rows[1].timestamp).unwrap(), (100.into(), Decimal::new(1001,2)));
assert_eq!(prefix.at(rows[2].timestamp).unwrap(), (300.into(), Decimal::new(3003,2)));
rows[2].volume_delta = 999999;
rows[2].amount_delta = f64::NAN;
let changed = MinutePrefix::build(rows[0].date, "000001.SZ", &rows);
assert_eq!(changed.at(rows[1].timestamp).unwrap(), prefix.at(rows[1].timestamp).unwrap());
assert!(changed.at(rows[2].timestamp).unwrap_err().contains("amount_invalid"));
}
#[test]
fn sparse_unverified_and_overflowing_quotes_cannot_be_called_session_totals() {
let first = quote(9,30,100,1000.);
for rows in [vec![quote(9,31,100,1000.)], vec![first.clone(), quote(9,32,100,1000.)]] {
let prefix = MinutePrefix::build(first.date, "000001.SZ", &rows);
assert!(prefix.at(rows.last().unwrap().timestamp).unwrap_err().contains("prefix_missing"));
}
let mut unknown = first.clone(); unknown.observation_kind = Default::default();
assert!(MinutePrefix::build(first.date, "000001.SZ", &[unknown]).at(first.timestamp).unwrap_err().contains("basis_unverified"));
let rows = [quote(9,30,u64::MAX,0.), quote(9,31,1,0.)];
assert!(MinutePrefix::build(first.date, "000001.SZ", &rows).at(rows[1].timestamp).unwrap_err().contains("volume_overflow"));
}
#[test]
fn lunch_and_post_close_gaps_follow_the_minute_feed_segments() {
let mut rows = Vec::new(); let mut time = NaiveTime::from_hms_opt(9,30,0).unwrap();
loop {
rows.push(quote(time.hour(), time.minute(), 1, 0.01));
let Some(next) = next_minute(time) else { break }; time=next;
}
let prefix=MinutePrefix::build(rows[0].date,"000001.SZ",&rows);
assert_eq!(prefix.at(rows.last().unwrap().timestamp).unwrap(), (Decimal::from(rows.len()), Decimal::new(rows.len() as i64,2)));
assert!(!rows.iter().any(|row| row.timestamp.time().hour()==12));
assert!(!rows.iter().any(|row| row.timestamp.time()==NaiveTime::from_hms_opt(13,0,0).unwrap()));
assert!(!rows.iter().any(|row| row.timestamp.time().hour()==15 && (1..6).contains(&row.timestamp.time().minute())));
}
#[test]
#[ignore = "requires FIDC_SESSION_PREFIX_SOURCE_JSON from the frozen Source minute response"]
fn real_source_session_prefix_matches_observed_checkpoints() {
let path=std::env::var("FIDC_SESSION_PREFIX_SOURCE_JSON").expect("explicit Source evidence path");
let rows:Vec<IntradayExecutionQuote>=serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
let date=NaiveDate::from_ymd_opt(2026,9,8).unwrap();
assert_eq!(rows.len(),242);
let prefix=MinutePrefix::build(date,"000063.SZ",&rows);
for (hour,minute,volume,amount) in [(9,30,512700,17103672),(9,31,2296631,76576756),(9,32,2983531,99471024),(11,30,27868847,928167630),(13,1,28495518,948994890),(15,0,45625008,1518115100)] {
assert_eq!(prefix.at(date.and_hms_opt(hour,minute,0).unwrap()).unwrap(),(Decimal::from(volume),Decimal::from(amount)));
}
assert!(prefix.at(date.and_hms_opt(15,30,0).unwrap()).unwrap_err().contains("prefix_missing"),"one final aggregate is not a verified intraday prefix");
}
}
+104 -3
View File
@@ -24,6 +24,14 @@ pub struct StockPoolEntryProgress {
pub completion_quantity: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StockPoolPositionActionBasis {
pub generation: String,
pub first_execution_date: NaiveDate,
pub quantity: Decimal,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StockPoolExecutionState {
@@ -32,13 +40,20 @@ pub struct StockPoolExecutionState {
pub entries: BTreeMap<String, StockPoolEntryProgress>,
#[serde(default)]
pub last_target_weights: BTreeMap<String, i32>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub last_target_weight_ratios: BTreeMap<String, Decimal>,
/// First signal excluding an actually held member; not an acquisition date.
pub removed_since: BTreeMap<String, NaiveDate>,
/// Signal progress, not a fill or holding-period fact. Kept across retries
/// and later execution sessions until a new generation supersedes it.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub position_action_bases: BTreeMap<String, StockPoolPositionActionBasis>,
}
pub struct StockPoolGoalObservation<'a> {
pub symbol: &'a str,
pub target_weight_bps: i32,
pub target_weight_ratio: Option<Decimal>,
pub target_value: Decimal,
pub current_quantity: Decimal,
pub target_quantity: Decimal,
@@ -52,16 +67,20 @@ impl Default for StockPoolExecutionState {
last_execution_date: None,
entries: BTreeMap::new(),
last_target_weights: BTreeMap::new(),
last_target_weight_ratios: BTreeMap::new(),
removed_since: BTreeMap::new(),
position_action_bases: BTreeMap::new(),
}
}
}
impl StockPoolExecutionState {
pub fn validate(&self) -> Result<(), String> {
if self.schema_version != 1
if !matches!(self.schema_version, 1 | 2)
|| (self.schema_version == 1 && !self.last_target_weight_ratios.is_empty())
|| self.entries.len() > 10000
|| self.removed_since.len() > 10000
|| self.position_action_bases.len() > 10000
{
return Err("stock_pool_execution_state_invalid_schema_or_size".into());
}
@@ -70,6 +89,8 @@ impl StockPoolExecutionState {
.keys()
.chain(self.removed_since.keys())
.chain(self.last_target_weights.keys())
.chain(self.last_target_weight_ratios.keys())
.chain(self.position_action_bases.keys())
{
if normalize_stock_symbol(symbol).as_ref() != Some(symbol) {
return Err("stock_pool_execution_state_invalid_symbol".into());
@@ -83,6 +104,9 @@ impl StockPoolExecutionState {
{
return Err("stock_pool_execution_state_invalid_weights".into());
}
if self.last_target_weight_ratios.len() > 10000 || self.last_target_weight_ratios.iter().any(|(symbol, ratio)| {
*ratio < Decimal::ZERO || *ratio > Decimal::ONE || !self.last_target_weights.contains_key(symbol)
}) { return Err("stock_pool_execution_state_invalid_weight_ratios".into()); }
if self.entries.values().any(|entry| {
entry.latest_target_value < Decimal::ZERO
|| entry.completion_quantity.is_some_and(|quantity| quantity <= Decimal::ZERO)
@@ -97,6 +121,12 @@ impl StockPoolExecutionState {
{
return Err("stock_pool_execution_state_invalid_goal_or_clock".into());
}
if self.position_action_bases.values().any(|basis| {
basis.generation.trim().is_empty() || basis.quantity <= Decimal::ZERO
|| self.last_execution_date.is_none_or(|date| basis.first_execution_date > date)
}) {
return Err("stock_pool_execution_state_invalid_action_basis".into());
}
Ok(())
}
@@ -138,6 +168,7 @@ impl StockPoolExecutionState {
});
next.last_target_weights
.retain(|symbol, _| members.contains(symbol) || held.contains(symbol));
next.last_target_weight_ratios.retain(|symbol, _| members.contains(symbol) || held.contains(symbol));
for (symbol, entry) in &mut next.entries {
entry.observed_holding |= held.contains(symbol);
if entry.pending
@@ -186,15 +217,76 @@ impl StockPoolExecutionState {
self.record_targets(
decision_date,
generation,
plan.rows.iter().map(|row| StockPoolGoalObservation {
plan.rows.iter().filter(|row| !plan.position_action_bases.contains_key(&row.symbol)).map(|row| StockPoolGoalObservation {
symbol: &row.symbol,
target_weight_bps: row.target_weight_bps,
target_weight_ratio: plan.target_weight_ratios.get(&row.symbol).copied(),
target_value: row.target_value,
current_quantity: row.current_quantity,
target_quantity: row.target_quantity,
status: &row.status,
}),
)
)?.record_position_action_bases(generation, &plan.position_action_bases)
}
pub fn position_action_bases_for(&self, generation: &str) -> BTreeMap<String, Decimal> {
self.position_action_bases.iter()
.filter(|(_, basis)| basis.generation == generation)
.map(|(symbol, basis)| (symbol.clone(), basis.quantity))
.collect()
}
/// A verified split changes the share unit, not the intended reduction or
/// entry completion. Never infer a split from a changed holding quantity.
pub fn adjust_for_split(&self, symbol: &str, ratio: Decimal) -> Result<Self, String> {
self.validate()?;
if ratio <= Decimal::ZERO || normalize_stock_symbol(symbol).as_deref() != Some(symbol) {
return Err("stock_pool_execution_state_split_invalid".into());
}
let scale = |quantity: Decimal| quantity.checked_mul(ratio)
.map(|value| value.round_dp_with_strategy(0, rust_decimal::RoundingStrategy::MidpointAwayFromZero))
.ok_or_else(|| "stock_pool_execution_state_split_overflow".to_string());
let mut next = self.clone();
if let Some(entry) = next.entries.get_mut(symbol) {
if let Some(quantity) = entry.completion_quantity {
let quantity = scale(quantity)?;
entry.completion_quantity = (quantity > Decimal::ZERO).then_some(quantity);
}
}
if let Some(basis) = next.position_action_bases.get_mut(symbol) {
basis.quantity = scale(basis.quantity)?;
if basis.quantity == Decimal::ZERO { next.position_action_bases.remove(symbol); }
}
next.validate()?;
Ok(next)
}
pub fn record_position_action_bases(
&self,
generation: &str,
quantities: &BTreeMap<String, Decimal>,
) -> Result<Self, String> {
self.validate()?;
if generation.trim().is_empty() {
return Err("stock_pool_execution_state_action_generation_missing".into());
}
let first_execution_date = self.last_execution_date
.ok_or("stock_pool_execution_state_action_clock_missing")?;
let mut next = self.clone();
next.position_action_bases.retain(|_, basis| basis.generation == generation);
for (symbol, quantity) in quantities {
if let Some(basis) = next.position_action_bases.get(symbol) {
if basis.quantity != *quantity {
return Err(format!("stock_pool_execution_state_action_basis_changed:{symbol}"));
}
} else {
next.position_action_bases.insert(symbol.clone(), StockPoolPositionActionBasis {
generation: generation.into(), first_execution_date, quantity: *quantity,
});
}
}
next.validate()?;
Ok(next)
}
pub fn record_targets<'a>(
@@ -213,9 +305,18 @@ impl StockPoolExecutionState {
}
let mut next = self.clone();
for row in rows {
if row.status == "AUTOMATIC_TRADE_PROTECTED" {
continue;
}
if row.target_weight_bps > 0 {
next.last_target_weights
.insert(row.symbol.into(), row.target_weight_bps);
if let Some(ratio) = row.target_weight_ratio {
next.schema_version = 2;
next.last_target_weight_ratios.insert(row.symbol.into(), ratio);
} else {
next.last_target_weight_ratios.remove(row.symbol);
}
}
let eligible = row.target_weight_bps > 0 && row.target_value > Decimal::ZERO;
let completion_quantity = (row.status == "READY"
+102 -12
View File
@@ -20,6 +20,18 @@ use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSe
pub trait Strategy {
fn name(&self) -> &str;
fn bind_runtime_position_configuration(
&mut self,
events: &[crate::position_exposure::PositionExposureEvent],
legacy: &BTreeMap<NaiveDate, i32>,
) -> Result<(), BacktestError> {
if !events.is_empty() || !legacy.is_empty() {
return Err(BacktestError::Execution(
"strategy does not implement runtime position configuration".into(),
));
}
Ok(())
}
fn initial_subscriptions(&self) -> BTreeSet<String> {
BTreeSet::new()
}
@@ -40,6 +52,12 @@ pub trait Strategy {
) -> Result<(), BacktestError> {
Ok(())
}
/// External, already executed manual activity. It is not a new strategy
/// order and must not be run through order generation or transaction costs.
fn on_observed_manual_execution(
&mut self,
_execution: &crate::manual_execution::ManualReplayApplication,
) -> Result<(), BacktestError> { Ok(()) }
fn schedule_rules(&self) -> Vec<ScheduleRule> {
Vec::new()
}
@@ -102,6 +120,7 @@ pub struct OpenOrderView {
pub avg_price: f64,
pub transaction_cost: f64,
pub limit_price: f64,
pub reserved_cash: Option<f64>,
pub reason: String,
}
@@ -497,6 +516,7 @@ impl StrategyContext<'_> {
.iter()
.filter(|order| order.side == OrderSide::Buy)
.map(|order| {
if let Some(reserved) = order.reserved_cash { return reserved; }
let price = if order.limit_price.is_finite() {
order.limit_price.max(0.0)
} else {
@@ -685,7 +705,7 @@ impl StrategyContext<'_> {
self.data.get_price(symbol, start, end, frequency)
}
pub fn get_dividend(&self, symbol: &str, start: NaiveDate) -> Vec<DividendRecord> {
pub fn get_dividend(&self, symbol: &str, start: NaiveDate) -> Result<Vec<DividendRecord>, BacktestError> {
let current_data_date = self.current_data_date();
let end = self
.data
@@ -975,7 +995,7 @@ fn safe_ratio(numerator: f64, denominator: f64) -> f64 {
}
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct StrategyDecision {
pub buy_denials: BTreeMap<String, String>,
pub rebalance: bool,
@@ -988,6 +1008,15 @@ pub struct StrategyDecision {
}
impl StrategyDecision {
pub(crate) fn is_portfolio_target_only(&self) -> bool {
(self.rebalance && self.order_intents.is_empty())
|| (self.order_intents.len() == 1
&& matches!(
self.order_intents[0].unwrapped(),
OrderIntent::StockPool { .. } | OrderIntent::TargetPortfolioSmart { .. }
))
}
pub fn potential_buy_symbols(&self, open_orders: &[OpenOrderView]) -> BTreeSet<String> {
let mut symbols = BTreeSet::new();
if self.rebalance {
@@ -1001,9 +1030,24 @@ impl StrategyDecision {
}
pub fn merge_from(&mut self, mut other: StrategyDecision) {
if self.is_portfolio_target_only() && other.is_portfolio_target_only() {
let mut previous = std::mem::replace(self, other);
previous
.diagnostics
.push("unsubmitted_portfolio_target_superseded".into());
self.notes.splice(0..0, previous.notes);
self.diagnostics.splice(0..0, previous.diagnostics);
return;
}
self.buy_denials.append(&mut other.buy_denials);
self.rebalance |= other.rebalance;
self.target_weights.append(&mut other.target_weights);
if other.rebalance {
// Rebalance targets are a complete portfolio, not an additive
// list. A newer unsent target replaces the earlier allocation.
self.rebalance = true;
self.target_weights = std::mem::take(&mut other.target_weights);
} else {
self.target_weights.append(&mut other.target_weights);
}
self.exit_symbols.append(&mut other.exit_symbols);
self.order_intents.append(&mut other.order_intents);
self.notes.append(&mut other.notes);
@@ -1023,13 +1067,59 @@ impl StrategyDecision {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg(test)]
mod decision_merge_tests {
use super::*;
#[test]
fn newer_complete_target_replaces_old_symbols_without_discarding_explicit_actions() {
let mut earlier = StrategyDecision {
rebalance: true,
target_weights: BTreeMap::from([("A".into(), 0.5), ("B".into(), 0.5)]),
exit_symbols: BTreeSet::from(["risk_exit".into()]),
order_intents: vec![OrderIntent::Shares {
symbol: "explicit".into(),
quantity: 100,
reason: "explicit action".into(),
}],
..Default::default()
};
earlier.merge_from(StrategyDecision {
rebalance: true,
target_weights: BTreeMap::from([("C".into(), 1.)]),
..Default::default()
});
assert_eq!(earlier.target_weights, BTreeMap::from([("C".into(), 1.)]));
assert!(earlier.rebalance);
assert!(earlier.exit_symbols.contains("risk_exit"));
assert_eq!(earlier.order_intents.len(), 1);
}
#[test]
fn explicit_empty_complete_target_replaces_old_allocation_but_empty_callback_does_not() {
let mut decision = StrategyDecision {
rebalance: true,
target_weights: BTreeMap::from([("A".into(), 1.)]),
..Default::default()
};
decision.merge_from(StrategyDecision::default());
assert_eq!(decision.target_weights.len(), 1);
decision.merge_from(StrategyDecision {
rebalance: true,
..Default::default()
});
assert!(decision.target_weights.is_empty());
assert!(decision.rebalance);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum AlgoOrderStyle {
Vwap,
Twap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum OrderTimeInForce {
Day,
Ioc,
@@ -1058,7 +1148,7 @@ impl OrderTimeInForce {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, serde::Serialize)]
pub enum TargetPortfolioOrderPricing {
LimitPrices(BTreeMap<String, f64>),
AlgoOrder {
@@ -1068,7 +1158,7 @@ pub enum TargetPortfolioOrderPricing {
},
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, serde::Serialize)]
pub enum OrderIntent {
StockPool {
contract: Box<crate::stock_pool_execution::FrozenStockPoolIntent>,
@@ -3184,7 +3274,7 @@ mod tests {
.enumerate()
.map(|(index, date)| DailyMarketSnapshot {
date: *date,
symbol: symbol.to_string(),
symbol: symbol.into(),
timestamp: Some(format!("{date} 10:18:00")),
day_open: 10.0 + index as f64,
open: 10.0 + index as f64,
@@ -3210,21 +3300,21 @@ mod tests {
.iter()
.map(|date| DailyFactorSnapshot {
date: *date,
symbol: symbol.to_string(),
symbol: symbol.into(),
market_cap_bn: 10.0,
free_float_cap_bn: 9.0,
pe_ttm: 12.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
})
.collect::<Vec<_>>();
let candidate_rows = dates
.iter()
.map(|date| CandidateEligibility {
date: *date,
symbol: symbol.to_string(),
symbol: symbol.into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
+2 -2
View File
@@ -254,7 +254,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
},
ManualSection {
title: "corporate_actions.dividend_reinvestment".to_string(),
detail: "支持 corporate_actions.dividend_reinvestment(true)。开启后,现金分红到账会优先按 round lot 回补成同一只股票,零头保留为现金。".to_string(),
detail: "历史兼容的回测账务再投模型:corporate_actions.dividend_reinvestment(true) 在分红结算时按调整后的参考价分配整手股数,零头留现金,费用为0;来源标记为 dividend_reinvestment,不是交易所委托或真实自动买入。新策略不应手工处理公司行为。".to_string(),
},
ManualSection {
title: "execution.matching_type / execution.slippage".to_string(),
@@ -362,7 +362,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() },
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前信号日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应平台内核的 is_suspended/is_st_stock 数据能力。执行日停牌或 ST 只能由撮合风控判断,不能在 next_bar_open 的 T 日提前固化。".to_string() },
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟线返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date) 读取明细;平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date)? 读取明细;缺少真实付款日或付款日早于除息日时返回错误,不推定到账日期。平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
ManualFunction { name: "get_split / split_ratio / has_split".to_string(), signature: "split_ratio(lookback) / has_split(lookback)".to_string(), detail: "高级数据 风格拆分/送转 API。Rust Context 可用 ctx.get_split(symbol, start_date) 读取明细;平台表达式可用 split_ratio(lookback) 计算当前股票最近 N 个交易日累计拆分比例,has_split(lookback) 判断是否发生送转。".to_string() },
ManualFunction { name: "get_factor / factor_value".to_string(), signature: "factor_value(\"field\", lookback=1)".to_string(), detail: "数值因子 API。factor(\"field\") 读取当前股票当日因子;factor_value(\"field\", lookback) 会在最近 N 个交易日内取该字段最新数值,适合读取任意可用指标或自定义数值因子。Rust Context 可用 ctx.get_factor(symbol, start, end, field) 读取完整数值序列。".to_string() },
ManualFunction { name: "get_factor_text / factor_text".to_string(), signature: "factor_text(\"field\", lookback=1)".to_string(), detail: "字符串因子 API。读取最近 N 个交易日内指定字段的最新字符串值,适合行业名称、概念、标签、风格分类、自定义文本信号等字段。示例:factor_text(\"concept\") == \"ai_chip\" 或 factor(\"industry_name\") == \"电子\"。Rust Context 可用 ctx.get_factor_text(symbol, start, end, field) 读取完整字符串序列。".to_string() },
+4 -4
View File
@@ -83,7 +83,7 @@ impl SelectionContext<'_> {
for factor in self.data.factor_snapshot_rows_on(self.decision_date) {
if self
.dynamic_universe
.is_some_and(|symbols| !symbols.is_empty() && !symbols.contains(&factor.symbol))
.is_some_and(|symbols| !symbols.is_empty() && !symbols.contains(factor.symbol.as_str()))
{
continue;
}
@@ -307,7 +307,7 @@ mod tests {
fn market(symbol: &str, price: f64) -> DailyMarketSnapshot {
DailyMarketSnapshot {
date: d(),
symbol: symbol.to_string(),
symbol: symbol.into(),
timestamp: Some("2025-01-02 10:00:00".to_string()),
day_open: price,
open: price,
@@ -333,7 +333,7 @@ mod tests {
fn factor(symbol: &str, market_cap_bn: f64) -> DailyFactorSnapshot {
DailyFactorSnapshot {
date: d(),
symbol: symbol.to_string(),
symbol: symbol.into(),
market_cap_bn,
free_float_cap_bn: market_cap_bn,
pe_ttm: 10.0,
@@ -347,7 +347,7 @@ mod tests {
fn candidate(symbol: &str, is_st: bool, is_kcb: bool) -> CandidateEligibility {
CandidateEligibility {
date: d(),
symbol: symbol.to_string(),
symbol: symbol.into(),
is_st,
is_star_st: false,
is_new_listing: false,
@@ -105,8 +105,164 @@ fn action(quantity: &str, when: &str) -> PlatformTradeAction {
reason: "configured_strategy_action".into(),
}
}
#[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", 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":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":quantity,"price":price,"totalFee":fee
}]
}]})
}).collect::<Vec<_>>();
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-09-18T08:00:00Z","actions":actions,
})).unwrap();
replay.content_sha256 = replay.content_digest().unwrap();
let mut parts = data().snapshot_components();
for row in &mut parts.market {
if row.date >= d(15) {
row.day_open = 5.;
row.open = 5.;
row.high = 5.;
row.low = 5.;
row.close = 5.;
row.last_price = 5.;
row.bid1 = 5.;
row.ask1 = 5.;
row.prev_close = 5.;
row.upper_limit = 5.5;
row.lower_limit = 4.5;
}
}
parts.corporate_actions.push(fidc_core::CorporateAction {
date: d(15),
symbol: "000001.SZ".into(),
payable_date: None,
record_date: Some(d(14)),
share_effective_date: Some(d(15)),
share_rounding_rule: None,
share_cash: 0.,
share_bonus: 1.,
share_gift: 0.,
issue_quantity: 0.,
issue_price: 0.,
reform: false,
adjust_factor: None,
successor_symbol: None,
successor_ratio: None,
successor_cash: None,
});
let data = DataSet::from_components_with_actions(
parts.instruments,
parts.market,
parts.factors,
parts.candidates,
parts.benchmarks,
parts.corporate_actions,
)
.unwrap();
let mut config = PlatformExprStrategyConfig::generic();
config.signal_symbol = "000001.SZ".into();
config.benchmark_symbol = "000300.SH".into();
config.rotation_enabled = false;
config.matching_type = MatchingType::CurrentBarClose;
config.volume_capacity_mode =
fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
config.automatic_trade_protection = AutomaticTradeProtection {
buy_protection_days: 3,
sell_cooldown_days: 3,
max_holding_days: 1,
locks: vec![AutomaticTradeLock {
symbol: "000001.SZ".into(),
start_date: d(16),
end_date: Some(d(17)),
}],
};
config.explicit_actions = vec![action("-200", "decision_date >= \"2026-09-14\"")];
let result = BacktestEngine::new(
data,
PlatformExprStrategy::new(config),
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_matching_type(MatchingType::CurrentBarClose)
.with_volume_capacity_mode(
fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit,
),
BacktestConfig {
initial_cash: 10000.,
benchmark_code: "000300.SH".into(),
start_date: Some(d(11)),
end_date: Some(d(18)),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Close,
},
)
.with_observed_manual_executions(replay)
.unwrap()
.run()
.unwrap();
assert_eq!(result.manual_executions.len(), 3);
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!(
(
result.fills[0].date,
result.fills[0].side,
result.fills[0].quantity,
result.fills[0].price
),
(d(18), OrderSide::Sell, 200, 5.)
);
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}");
}
}
for day in [16, 17] {
assert!(result.risk_decisions.iter().any(|audit| audit.date == d(day)
&& audit.symbol == "000001.SZ" && audit.rule_code == "automatic_trade_locked" && !audit.accepted));
}
assert!(
result
.daily_holdings
.iter()
.any(|row| row.date == d(15) && row.quantity == if sell_during_lock { 400 } else { 200 })
);
assert!(result.holdings_summary.is_empty());
assert!(
result
.equity_curve
.iter()
.all(|point| point.external_cash_flow == 0.)
);
}
}
fn run(policy: AutomaticTradeProtection) -> fidc_core::BacktestResult {
let mut config = PlatformExprStrategyConfig::generic();
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
config.signal_symbol = "000001.SZ".into();
config.benchmark_symbol = "000300.SH".into();
config.rotation_enabled = false;
@@ -119,7 +275,7 @@ fn run(policy: AutomaticTradeProtection) -> fidc_core::BacktestResult {
action("-100", "decision_date >= \"2026-09-14\""),
];
config.matching_type = MatchingType::CurrentBarClose;
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_matching_type(MatchingType::CurrentBarClose);
BacktestEngine::new(
data(),
@@ -276,6 +432,7 @@ fn locked_holding_keeps_its_slot_even_when_cash_can_buy_the_next_candidate() {
)
.unwrap();
let mut config = PlatformExprStrategyConfig::generic();
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
config.signal_symbol = "000001.SZ".into();
config.benchmark_symbol = "000300.SH".into();
config.strategy_name = "protection_test".into();
@@ -294,7 +451,7 @@ fn locked_holding_keeps_its_slot_even_when_cash_can_buy_the_next_candidate() {
}],
..Default::default()
};
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_matching_type(MatchingType::CurrentBarClose);
let result = BacktestEngine::new(
dataset,
+3 -3
View File
@@ -15,7 +15,7 @@ fn d(year: i32, month: u32, day: u32) -> NaiveDate {
fn candidate() -> CandidateEligibility {
CandidateEligibility {
date: d(2024, 1, 3),
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -31,7 +31,7 @@ fn candidate() -> CandidateEligibility {
fn snapshot(open: f64, upper_limit: f64, lower_limit: f64) -> DailyMarketSnapshot {
DailyMarketSnapshot {
date: d(2024, 1, 3),
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2024-01-03 10:18:00".to_string()),
day_open: open,
open,
@@ -256,7 +256,7 @@ fn china_rule_hooks_allow_sell_when_last_price_is_above_lower_limit() {
let snapshot = DailyMarketSnapshot {
date: d(2024, 4, 7),
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2024-04-07 10:18:00".to_string()),
day_open: 2.53,
open: 2.53,
+179 -19
View File
@@ -113,7 +113,7 @@ impl Strategy for BuyAndHoldStrategy {
fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
DailyMarketSnapshot {
date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some(format!("{date} 10:18:00")),
day_open: 10.0,
open: 10.0,
@@ -139,21 +139,21 @@ fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
fn stock_factor_snapshot(date: NaiveDate) -> DailyFactorSnapshot {
DailyFactorSnapshot {
date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 20.0,
free_float_cap_bn: 18.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
}
}
fn stock_candidate(date: NaiveDate) -> CandidateEligibility {
CandidateEligibility {
date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -177,6 +177,160 @@ fn benchmark_snapshot(date: NaiveDate) -> BenchmarkSnapshot {
}
}
#[test]
fn successor_conversion_depletes_older_source_lots_before_newer_successor_buys() {
struct ConvertedSale {
dates: [NaiveDate; 3],
seen: std::rc::Rc<std::cell::RefCell<Option<(Option<NaiveDate>, Option<NaiveDate>)>>>,
}
impl Strategy for ConvertedSale {
fn name(&self) -> &str {
"successor FIFO"
}
fn on_day(
&mut self,
ctx: &StrategyContext<'_>,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
let (symbol, quantity) = if ctx.execution_date == self.dates[0] {
("000001.SZ", 100)
} else if ctx.execution_date == self.dates[1] {
("000002.SZ", 100)
} else {
let holding = ctx.portfolio.position("000002.SZ").unwrap();
*self.seen.borrow_mut() = Some((holding.opened_date(), holding.last_buy_date()));
("000002.SZ", -200)
};
Ok(StrategyDecision {
order_intents: vec![fidc_core::OrderIntent::Shares {
symbol: symbol.into(),
quantity,
reason: "dated lot test".into(),
}],
..Default::default()
})
}
}
let dates = [d(2026, 9, 11), d(2026, 9, 14), d(2026, 9, 15)];
let symbols = ["000001.SZ", "000002.SZ"];
let mut market = Vec::new();
let mut factors = Vec::new();
let mut candidates = Vec::new();
for date in dates {
for symbol in symbols {
let price = if symbol == symbols[0] {
10.
} else if date == dates[2] {
6.
} else {
20.
};
let mut quote = stock_market_snapshot(date);
quote.symbol = symbol.into();
quote.day_open = price;
quote.open = price;
quote.high = price;
quote.low = price;
quote.close = price;
quote.last_price = price;
quote.bid1 = price;
quote.ask1 = price;
quote.prev_close = price;
quote.upper_limit = price * 1.1;
quote.lower_limit = price * 0.9;
market.push(quote);
let mut factor = stock_factor_snapshot(date);
factor.symbol = symbol.into();
factors.push(factor);
let mut candidate = stock_candidate(date);
candidate.symbol = symbol.into();
candidates.push(candidate);
}
}
let data = DataSet::from_components_with_actions(
symbols
.into_iter()
.map(|symbol| Instrument {
symbol: symbol.into(),
name: symbol.into(),
board: "SZ".into(),
round_lot: 100,
listed_at: Some(d(2020, 1, 1)),
delisted_at: None,
status: "active".into(),
})
.collect(),
market,
factors,
candidates,
dates.map(benchmark_snapshot).into(),
vec![CorporateAction {
date: dates[2],
symbol: symbols[0].into(),
payable_date: None,
record_date: Some(dates[1]),
share_effective_date: None,
share_rounding_rule: None,
share_cash: 0.,
share_bonus: 0.,
share_gift: 0.,
issue_quantity: 0.,
issue_price: 0.,
reform: false,
adjust_factor: None,
successor_symbol: Some(symbols[1].into()),
successor_ratio: Some(2.),
successor_cash: Some(0.),
}],
)
.unwrap();
let seen = std::rc::Rc::new(std::cell::RefCell::new(None));
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_matching_type(fidc_core::MatchingType::NextBarOpen)
.with_volume_limit(false)
.with_liquidity_limit(false);
let result = BacktestEngine::new(
data,
ConvertedSale {
dates,
seen: seen.clone(),
},
broker,
BacktestConfig {
initial_cash: 10000.,
benchmark_code: "000300.SH".into(),
start_date: Some(dates[0]),
end_date: Some(dates[2]),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Open,
},
)
.run()
.unwrap();
assert_eq!(*seen.borrow(), Some((Some(dates[0]), Some(dates[1]))));
assert_eq!(result.fills.len(), 3);
assert_eq!(result.fills[2].quantity, 200);
assert_eq!(result.fills[2].symbol, symbols[1]);
let remaining = result
.holdings_summary
.iter()
.find(|row| row.symbol == symbols[1])
.unwrap();
assert_eq!(remaining.quantity, 100);
assert_eq!(remaining.realized_pnl, 200.);
assert!(
result
.position_events
.iter()
.any(|event| event.symbol == symbols[0]
&& event.quantity_after == 0
&& event.reason.starts_with("successor_conversion"))
);
}
#[test]
fn engine_reinvests_dividend_receivable_in_round_lots() {
let buy_date = d(2025, 1, 1);
@@ -195,7 +349,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
vec![
DailyMarketSnapshot {
date: buy_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2025-01-01 10:18:00".to_string()),
day_open: 10.0,
open: 10.0,
@@ -218,7 +372,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
},
DailyMarketSnapshot {
date: ex_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2025-01-02 10:18:00".to_string()),
day_open: 10.0,
open: 10.0,
@@ -241,7 +395,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
},
DailyMarketSnapshot {
date: payable_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2025-01-03 10:18:00".to_string()),
day_open: 10.0,
open: 10.0,
@@ -266,42 +420,42 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
vec![
DailyFactorSnapshot {
date: buy_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 20.0,
free_float_cap_bn: 18.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
DailyFactorSnapshot {
date: ex_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 20.0,
free_float_cap_bn: 18.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
DailyFactorSnapshot {
date: payable_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 20.0,
free_float_cap_bn: 18.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
],
vec![
CandidateEligibility {
date: buy_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -314,7 +468,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
},
CandidateEligibility {
date: ex_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -327,7 +481,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
},
CandidateEligibility {
date: payable_date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -369,6 +523,9 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
date: ex_date,
symbol: "000001.SZ".to_string(),
payable_date: Some(payable_date),
record_date: Some(buy_date),
share_effective_date: None,
share_rounding_rule: None,
share_cash: 1.05,
share_bonus: 0.0,
share_gift: 0.0,
@@ -394,7 +551,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
.with_minimum_commission(0.0),
ChinaEquityRuleHooks::default(),
PriceField::Open,
),
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit),
BacktestConfig {
initial_cash: 11_008.0,
benchmark_code: "000300.SH".to_string(),
@@ -452,6 +609,9 @@ fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
date: ex_date,
symbol: "000001.SZ".to_string(),
payable_date: Some(ex_date),
record_date: Some(buy_date),
share_effective_date: Some(ex_date),
share_rounding_rule: None,
share_cash: 1.05,
share_bonus: 0.2,
share_gift: 0.0,
@@ -475,7 +635,7 @@ fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
),
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit),
BacktestConfig {
initial_cash: 11_008.0,
benchmark_code: "000300.SH".to_string(),
@@ -507,7 +667,7 @@ fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
result
.position_events
.iter()
.any(|event| event.reason == "stock_split 1.200000" && event.quantity_after == 1_200)
.any(|event| event.reason == "registered_share_distribution_delivered" && event.quantity_after == 1_200)
);
assert!(
result
@@ -5,7 +5,7 @@ use fidc_core::{
Instrument, IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext,
StrategyDecision,
};
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use std::sync::{Arc, Mutex};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -105,7 +105,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
fixture_instruments(),
vec![DailyMarketSnapshot {
date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some(format!("{date} 15:00:00")),
day_open: 10.0,
open: 10.0,
@@ -128,7 +128,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
}],
vec![DailyFactorSnapshot {
date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
@@ -139,7 +139,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
}],
vec![CandidateEligibility {
date,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -163,14 +163,55 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
}
#[test]
fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
fn runtime_account_dependent_quote_scope_uses_the_actual_account() {
struct AccountDependentQuoteReader;
impl Strategy for AccountDependentQuoteReader {
fn name(&self) -> &str { "account_dependent_quote_reader" }
fn decision_quote_times(&self) -> Vec<NaiveTime> { vec![t(10, 18, 0)] }
fn decision_quote_symbols(&mut self, ctx: &StrategyContext<'_>) -> Result<BTreeSet<String>, fidc_core::BacktestError> {
Ok(if ctx.portfolio.cash() < 50_000.0 {
BTreeSet::from(["000001.SZ".into()])
} else { BTreeSet::new() })
}
fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result<StrategyDecision, fidc_core::BacktestError> {
let loaded = ctx.data.execution_quotes_on(ctx.execution_date, "000001.SZ").iter().any(|quote|
quote.timestamp.time()==t(10,17,59) && quote.last_price==10.0);
assert_eq!(loaded, ctx.portfolio.cash() < 50_000.0,
"quote scope must match this account, not a fixed-capital planning account");
Ok(StrategyDecision::default())
}
}
let date = d(2026, 1, 5);
for initial_cash in [10_000.0, 100_000.0] {
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(), ChinaEquityRuleHooks, PriceField::Close,
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_matching_type(MatchingType::CurrentBarClose);
let config = BacktestConfig {
initial_cash, benchmark_code:"000852.SH".into(),
start_date:Some(date), end_date:Some(date), decision_lag_trading_days:0,
execution_price_field:PriceField::Close,
};
let mut engine = BacktestEngine::new(single_day_quote_plan_data(date), AccountDependentQuoteReader, broker, config)
.with_execution_quote_loader(move |request| Ok(request.symbols.into_iter().map(|symbol| IntradayExecutionQuote {
observation_kind:Default::default(), date:request.date, symbol,
timestamp:request.date.and_time(t(10,17,59)), last_price:10.0,bid1:10.0,ask1:10.0,
bid1_volume:10_000,ask1_volume:10_000,volume_delta:10_000,amount_delta:100_000.0,
trading_phase:Some("continuous".into()),
}).collect()));
engine.run().expect("account-dependent quote planning");
}
}
#[test]
fn engine_resolves_the_runtime_strategy_scope_when_a_loader_exists() {
let date = d(2026, 1, 5);
let data = single_day_quote_plan_data(date);
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Close,
)
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_matching_type(MatchingType::CurrentBarClose);
let config = BacktestConfig {
initial_cash: 10_000.0,
@@ -186,17 +227,13 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
symbol_plan_calls: Arc::clone(&symbol_plan_calls),
};
let captured_loader_calls = Arc::clone(&loader_calls);
let preplanned = Arc::new(BTreeMap::from([(
date,
BTreeSet::from(["000001.SZ".to_string()]),
)]));
let mut engine = BacktestEngine::new(data, strategy, broker, config)
.with_execution_quote_loader(move |request| {
*captured_loader_calls.lock().expect("loader counter mutex") += 1;
Ok(request
.symbols
.into_iter()
.map(|symbol| IntradayExecutionQuote {
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date: request.date,
symbol,
timestamp: request.date.and_time(t(10, 17, 59)),
@@ -210,20 +247,19 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
trading_phase: Some("continuous".to_string()),
})
.collect())
})
.with_preplanned_decision_quote_symbols_by_date(preplanned);
});
engine.run().expect("backtest should run");
assert_eq!(
*symbol_plan_calls.lock().expect("symbol plan counter mutex"),
0,
"the strategy plan must not be recomputed after a complete plan is supplied"
1,
"quote planning must use the actual run context"
);
assert_eq!(
*loader_calls.lock().expect("loader counter mutex"),
1,
"the supplied symbols must still pass through the normal quote loader"
0,
"an empty runtime scope must not fetch unrequested symbols"
);
}
@@ -235,7 +271,7 @@ fn engine_skips_decision_quote_symbol_plan_without_loader() {
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Close,
)
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_matching_type(MatchingType::CurrentBarClose);
let config = BacktestConfig {
initial_cash: 10_000.0,
@@ -269,7 +305,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
vec![
DailyMarketSnapshot {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2026-01-05 15:00:00".to_string()),
day_open: 10.0,
open: 10.0,
@@ -292,7 +328,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
},
DailyMarketSnapshot {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2026-01-06 15:00:00".to_string()),
day_open: 10.5,
open: 10.5,
@@ -317,7 +353,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
vec![
DailyFactorSnapshot {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
@@ -328,7 +364,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
},
DailyFactorSnapshot {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
@@ -341,7 +377,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
vec![
CandidateEligibility {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -354,7 +390,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
},
CandidateEligibility {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -391,7 +427,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_matching_type(MatchingType::MinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig {
@@ -411,7 +447,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
Ok(request
.symbols
.into_iter()
.map(|symbol| IntradayExecutionQuote {
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date: request.date,
symbol,
timestamp: request.date.and_time(t(10, 39, 59)),
@@ -439,7 +475,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
vec![
DailyMarketSnapshot {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2026-01-05 15:00:00".to_string()),
day_open: 10.0,
open: 10.0,
@@ -462,7 +498,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
},
DailyMarketSnapshot {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2026-01-06 15:00:00".to_string()),
day_open: 10.5,
open: 10.5,
@@ -487,7 +523,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
vec![
DailyFactorSnapshot {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
@@ -498,7 +534,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
},
DailyFactorSnapshot {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
@@ -511,7 +547,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
vec![
CandidateEligibility {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -524,7 +560,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
},
CandidateEligibility {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -556,7 +592,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
],
Vec::new(),
vec![
IntradayExecutionQuote {
IntradayExecutionQuote { observation_kind: Default::default(),
date: first,
symbol: "000001.SZ".to_string(),
timestamp: first.and_time(t(10, 39, 59)),
@@ -569,7 +605,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
amount_delta: 100_000.0,
trading_phase: Some("continuous".to_string()),
},
IntradayExecutionQuote {
IntradayExecutionQuote { observation_kind: Default::default(),
date: second,
symbol: "000001.SZ".to_string(),
timestamp: second.and_time(t(10, 39, 59)),
@@ -590,7 +626,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_matching_type(MatchingType::MinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig {
@@ -674,7 +710,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
vec![
DailyMarketSnapshot {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2026-01-05 15:00:00".to_string()),
day_open: 10.0,
open: 10.0,
@@ -697,7 +733,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
},
DailyMarketSnapshot {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2026-01-06 15:00:00".to_string()),
day_open: 10.5,
open: 10.5,
@@ -722,7 +758,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
vec![
DailyFactorSnapshot {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
@@ -733,7 +769,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
},
DailyFactorSnapshot {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
@@ -746,7 +782,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
vec![
CandidateEligibility {
date: first,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -759,7 +795,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
},
CandidateEligibility {
date: second,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -796,7 +832,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_matching_type(MatchingType::MinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig {
@@ -826,7 +862,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
Ok(request
.symbols
.into_iter()
.map(|symbol| IntradayExecutionQuote {
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date: request.date,
symbol,
timestamp: request.date.and_time(start_time) - Duration::seconds(1),
+33 -30
View File
@@ -71,7 +71,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
vec![
DailyMarketSnapshot {
date: date1,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2025-01-02 10:18:00".to_string()),
day_open: 10.0,
open: 10.0,
@@ -94,7 +94,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
},
DailyMarketSnapshot {
date: date1,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
timestamp: Some("2025-01-02 10:18:00".to_string()),
day_open: 5.0,
open: 5.0,
@@ -117,7 +117,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
},
DailyMarketSnapshot {
date: delist_date,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
timestamp: Some("2025-01-03 10:18:00".to_string()),
day_open: 5.05,
open: 5.05,
@@ -140,7 +140,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
},
DailyMarketSnapshot {
date: date2,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
timestamp: Some("2025-01-06 10:18:00".to_string()),
day_open: 5.1,
open: 5.1,
@@ -165,53 +165,53 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
vec![
DailyFactorSnapshot {
date: date1,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 20.0,
free_float_cap_bn: 18.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
DailyFactorSnapshot {
date: date1,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
market_cap_bn: 30.0,
free_float_cap_bn: 28.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
DailyFactorSnapshot {
date: delist_date,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
market_cap_bn: 30.5,
free_float_cap_bn: 28.5,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
DailyFactorSnapshot {
date: date2,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
market_cap_bn: 31.0,
free_float_cap_bn: 29.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
],
vec![
CandidateEligibility {
date: date1,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -224,7 +224,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
},
CandidateEligibility {
date: date1,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -237,7 +237,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
},
CandidateEligibility {
date: delist_date,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -250,7 +250,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
},
CandidateEligibility {
date: date2,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -295,7 +295,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
let mut engine = BacktestEngine::new(
data,
BuyThenHoldStrategy,
@@ -364,7 +364,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
vec![
DailyMarketSnapshot {
date: date1,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
timestamp: Some("2025-01-02 10:18:00".to_string()),
day_open: 10.0,
open: 10.0,
@@ -387,7 +387,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
},
DailyMarketSnapshot {
date: date1,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
timestamp: Some("2025-01-02 10:18:00".to_string()),
day_open: 20.0,
open: 20.0,
@@ -410,7 +410,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
},
DailyMarketSnapshot {
date: date2,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
timestamp: Some("2025-01-03 10:18:00".to_string()),
day_open: 21.0,
open: 21.0,
@@ -435,42 +435,42 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
vec![
DailyFactorSnapshot {
date: date1,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
market_cap_bn: 20.0,
free_float_cap_bn: 18.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
DailyFactorSnapshot {
date: date1,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
market_cap_bn: 30.0,
free_float_cap_bn: 28.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
DailyFactorSnapshot {
date: date2,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
market_cap_bn: 31.0,
free_float_cap_bn: 29.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
extra_factors: Default::default(),
},
],
vec![
CandidateEligibility {
date: date1,
symbol: "000001.SZ".to_string(),
symbol: "000001.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -483,7 +483,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
},
CandidateEligibility {
date: date1,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -496,7 +496,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
},
CandidateEligibility {
date: date2,
symbol: "000002.SZ".to_string(),
symbol: "000002.SZ".into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
@@ -530,6 +530,9 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
date: date2,
symbol: "000001.SZ".to_string(),
payable_date: None,
record_date: Some(date1),
share_effective_date: None,
share_rounding_rule: None,
share_cash: 0.0,
share_bonus: 0.0,
share_gift: 0.0,
@@ -548,7 +551,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
let mut engine = BacktestEngine::new(
data,
BuyThenHoldStrategy,
@@ -0,0 +1,478 @@
use chrono::{NaiveDate, NaiveTime};
use fidc_core::{
BacktestConfig, BacktestEngine, BrokerSimulator, ChinaAShareCostModel, ChinaEquityRuleHooks,
DataSet, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext, StrategyDecision,
};
use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
const SYMBOL: &str = "000001.SZ";
fn day(value: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(2026, 9, value).unwrap()
}
fn data() -> DataSet {
let days = [11, 14, 15].map(day);
DataSet::from_components_with_actions_and_quotes(
vec![fidc_core::Instrument {
symbol: SYMBOL.into(),
name: "fixture".into(),
board: "SZ".into(),
round_lot: 100,
listed_at: Some(day(1)),
delisted_at: None,
status: "active".into(),
}],
days.iter()
.map(|&date| {
let price = if date == day(11) { 10. } else { 8.95 };
fidc_core::DailyMarketSnapshot {
date,
symbol: SYMBOL.into(),
timestamp: Some(format!("{date} 15:00:00")),
day_open: price,
open: price,
high: price,
low: price,
close: price,
last_price: price,
bid1: price,
ask1: price,
prev_close: price,
volume: 100000,
minute_volume: 100000,
bid1_volume: 100000,
ask1_volume: 100000,
trading_phase: Some("continuous".into()),
paused: false,
upper_limit: price * 1.1,
lower_limit: price * 0.9,
price_tick: 0.01,
}
})
.collect(),
days.iter()
.map(|&date| fidc_core::DailyFactorSnapshot {
date,
symbol: SYMBOL.into(),
market_cap_bn: 10.,
free_float_cap_bn: 10.,
pe_ttm: 10.,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: Some(1.),
extra_factors: Default::default(),
})
.collect(),
days.iter()
.map(|&date| fidc_core::CandidateEligibility {
date,
symbol: SYMBOL.into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: None,
})
.collect(),
days.iter()
.map(|&date| fidc_core::BenchmarkSnapshot {
date,
benchmark: "000300.SH".into(),
open: 100.,
close: 100.,
prev_close: 100.,
volume: 100000,
})
.collect(),
vec![fidc_core::CorporateAction {
date: day(14),
symbol: SYMBOL.into(),
payable_date: Some(day(14)),
record_date: Some(day(11)),
share_effective_date: None,
share_rounding_rule: None,
share_cash: 1.05,
share_bonus: 0.,
share_gift: 0.,
issue_quantity: 0.,
issue_price: 0.,
reform: false,
adjust_factor: None,
successor_symbol: None,
successor_ratio: None,
successor_cash: None,
}],
[(9, 15), (9, 31)]
.into_iter()
.map(|(hour, minute)| fidc_core::IntradayExecutionQuote {
observation_kind: fidc_core::data::QuoteObservationKind::MinuteBar,
date: day(14),
symbol: SYMBOL.into(),
timestamp: day(14).and_hms_opt(hour, minute, 0).unwrap(),
last_price: 8.95,
bid1: 8.95,
ask1: 8.95,
bid1_volume: 100000,
ask1_volume: 100000,
volume_delta: 10000,
amount_delta: 89500.,
trading_phase: Some("continuous".into()),
})
.collect(),
)
.unwrap()
}
struct Hold {
seen: Rc<RefCell<Vec<(NaiveTime, u32)>>>,
}
impl Strategy for Hold {
fn name(&self) -> &str {
"accounting reinvestment contract"
}
fn initial_subscriptions(&self) -> BTreeSet<String> {
[SYMBOL.into()].into()
}
fn on_day(
&mut self,
ctx: &StrategyContext<'_>,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
Ok(if ctx.execution_date == day(11) {
StrategyDecision {
order_intents: vec![OrderIntent::Shares {
symbol: SYMBOL.into(),
quantity: 1000,
reason: "initial".into(),
}],
..Default::default()
}
} else {
StrategyDecision::default()
})
}
fn on_minute(
&mut self,
ctx: &StrategyContext<'_>,
_: &fidc_core::IntradayExecutionQuote,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
self.seen.borrow_mut().push((
ctx.current_time().unwrap(),
ctx.portfolio
.position(SYMBOL)
.map_or(0, |position| position.quantity),
));
Ok(Default::default())
}
}
fn engine() -> BacktestEngine<Hold, ChinaAShareCostModel, ChinaEquityRuleHooks> {
BacktestEngine::new(
data(),
Hold {
seen: Rc::new(RefCell::new(Vec::new())),
},
BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default()
.with_commission_rate(0.0008)
.with_minimum_commission(0.),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_matching_type(MatchingType::NextBarOpen)
.with_volume_limit(false)
.with_liquidity_limit(false),
BacktestConfig {
initial_cash: 50000.,
benchmark_code: "000300.SH".into(),
start_date: Some(day(11)),
end_date: Some(day(15)),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Open,
},
)
.with_dividend_reinvestment(true)
}
#[test]
fn accounting_reinvestment_has_an_explicit_origin_clock_and_progress_delivery() {
let mut progress = Vec::new();
let result = engine()
.run_with_progress(|event| progress.push(event.clone()))
.unwrap();
let reinvest = result
.fills
.iter()
.find(|fill| fill.reason == "dividend_reinvestment")
.unwrap();
assert_eq!(
(
reinvest.quantity,
reinvest.price,
reinvest.commission,
reinvest.order_id
),
(100, 8.95, 0., None)
);
assert_eq!(
serde_json::to_value(reinvest).unwrap()["origin"],
"dividend_reinvestment"
);
assert_eq!(reinvest.execution_timestamp, day(14).and_hms_opt(0, 0, 0));
let received = progress.iter().find(|event| event.date == day(14)).unwrap();
assert!(
received
.fills
.iter()
.any(|fill| fill.reason == "dividend_reinvestment")
);
assert_eq!(
progress
.iter()
.map(|event| event.daily_fill_count)
.sum::<usize>(),
result.fills.len()
);
}
fn manual_source(delayed: bool) -> fidc_core::manual_execution::ManualExecutionReplay {
let observed = if delayed {
"2026-09-14T01:15:00Z"
} else {
"2026-09-11T06:00:01Z"
};
let mut source: 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-09-15T08:00:00Z","actions":[{"actionId":"manual","source":"manual_security_trade","auditEventIds":["audit"],
"confirmedAt":"2026-09-11T05:59:59Z","confirmationObservedAt":"2026-09-11T05:59:59Z","outcome":"orders_terminal","orders":[{
"orderId":"manual-order","sourceAdapter":"paper","symbol":SYMBOL,"side":"Buy","quantity":1000,
"orderCreatedAt":"2026-09-11T05:59:59Z","terminalObservedAt":observed,"terminalStatus":"filled","fills":[{
"tradeId":"manual-fill","observationEventId":"receipt","observationSequence":1,"feeObservationEventId":"receipt","feeObservationSequence":1,
"feeObservedAt":observed,"tradeDate":"2026-09-11","executedAt":"2026-09-11T06:00:00Z","observedAt":observed,
"timestampPrecision":"second","quantity":1000,"price":"10","totalFee":"1"
}]
}]}]
})).unwrap();
source.content_sha256 = source.content_digest().unwrap();
source
}
#[test]
fn delayed_receipt_before_market_open_reconciles_accounting_not_future_market_fills() {
let timely = engine()
.with_observed_manual_executions(manual_source(false))
.unwrap()
.run()
.unwrap();
let delayed = engine()
.with_observed_manual_executions(manual_source(true))
.unwrap()
.run()
.unwrap();
assert_eq!(delayed.holdings_summary[0].quantity, 2200);
assert_eq!(
delayed.holdings_summary[0].quantity,
timely.holdings_summary[0].quantity
);
assert_eq!(
delayed.equity_curve.last().unwrap().cash,
timely.equity_curve.last().unwrap().cash
);
assert_eq!(
delayed.manual_executions[0]
.corporate_adjustment
.as_ref()
.unwrap()
.corporate_cash_delta,
"155"
);
}
#[test]
fn weekend_receipts_and_morning_allocations_are_in_the_next_progress_batch() {
let mut source = manual_source(true);
let observed = "2026-09-12T02:00:00Z".parse().unwrap();
let order = &mut source.actions[0].orders[0];
order.terminal_observed_at = observed;
order.fills[0].observed_at = observed;
order.fills[0].fee_observed_at = observed;
source.content_sha256 = source.content_digest().unwrap();
let mut progress = Vec::new();
let result = engine()
.with_observed_manual_executions(source)
.unwrap()
.run_with_progress(|event| progress.push(event.clone()))
.unwrap();
let monday = progress.iter().find(|event| event.date == day(14)).unwrap();
assert_eq!(monday.daily_manual_fill_count, 1);
assert_eq!(monday.manual_executions[0].observed_at, observed);
assert!(
monday
.fills
.iter()
.any(|fill| fill.origin == fidc_core::FillOrigin::DividendReinvestment)
);
assert!(
monday
.process_events
.iter()
.any(|event| event.kind == fidc_core::ProcessEventKind::ManualExecutionObserved)
);
assert_eq!(
progress
.iter()
.map(|event| event.daily_fill_count)
.sum::<usize>(),
result.fills.len() + result.manual_executions.len()
);
}
fn exposure_event(id: &str, sequence: u64, at: &str, action: fidc_core::position_exposure::PositionExposureAction)
-> fidc_core::position_exposure::PositionExposureEvent {
fidc_core::position_exposure::PositionExposureEvent {
event_id: id.into(), sequence, effective_at: at.parse().unwrap(), allocation_weights_bps: None, action,
}
}
fn cleared_reinvestment_case(
events: Vec<fidc_core::position_exposure::PositionExposureEvent>,
legacy: std::collections::BTreeMap<NaiveDate, i32>,
extra_buy_delayed: Option<bool>,
) -> fidc_core::BacktestResult {
let mut parts = data().snapshot_components();
parts.corporate_actions[0].payable_date = Some(day(15));
let data = DataSet::from_components_with_actions_and_quotes(parts.instruments, parts.market,
parts.factors, parts.candidates, parts.benchmarks, parts.corporate_actions, parts.execution_quotes).unwrap();
let mut source = manual_source(false);
let mut sale = source.actions[0].clone();
sale.action_id = "clear".into(); sale.audit_event_ids = vec!["clear-audit".into()];
sale.confirmed_at = "2026-09-14T05:59:59Z".parse().unwrap();
sale.confirmation_observed_at = sale.confirmed_at;
let order = &mut sale.orders[0];
order.order_id = "clear-order".into(); order.side = fidc_core::OrderSide::Sell;
order.order_created_at = sale.confirmed_at;
order.terminal_observed_at = "2026-09-14T06:00:01Z".parse().unwrap();
let fill = &mut order.fills[0];
fill.trade_id = "clear-fill".into(); fill.observation_event_id = "clear-receipt".into();
fill.observation_sequence = 2; fill.fee_observation_event_id = "clear-receipt".into();
fill.fee_observation_sequence = 2; fill.trade_date = day(14);
fill.executed_at = "2026-09-14T06:00:00Z".parse().unwrap();
fill.observed_at = order.terminal_observed_at; fill.fee_observed_at = order.terminal_observed_at;
fill.price = "8.95".parse().unwrap();
source.actions.push(sale);
if let Some(delayed) = extra_buy_delayed {
let mut extra = source.actions[0].clone();
extra.action_id = "extra".into(); extra.audit_event_ids = vec!["extra-audit".into()];
extra.confirmed_at = "2026-09-11T06:00:59Z".parse().unwrap();
extra.confirmation_observed_at = extra.confirmed_at;
let order = &mut extra.orders[0];
order.order_id = "extra-order".into(); order.order_created_at = extra.confirmed_at;
order.terminal_observed_at = if delayed { "2026-09-15T01:15:00Z" } else { "2026-09-11T06:01:01Z" }.parse().unwrap();
let fill = &mut order.fills[0];
fill.trade_id = "extra-fill".into(); fill.observation_event_id = "extra-receipt".into();
fill.observation_sequence = if delayed { 3 } else { 2 };
fill.fee_observation_event_id = "extra-receipt".into(); fill.fee_observation_sequence = fill.observation_sequence;
fill.executed_at = "2026-09-11T06:01:00Z".parse().unwrap(); fill.observed_at = order.terminal_observed_at;
fill.fee_observed_at = order.terminal_observed_at;
if !delayed {
source.actions[1].orders[0].fills[0].observation_sequence = 3;
source.actions[1].orders[0].fills[0].fee_observation_sequence = 3;
}
source.actions.push(extra);
}
source.position_exposure_events = events;
source.legacy_position_exposure_bps = legacy;
source.content_sha256 = source.content_digest().unwrap();
let mut config = fidc_core::PlatformExprStrategyConfig::generic();
config.signal_symbol = SYMBOL.into(); config.benchmark_symbol = "000300.SH".into();
config.rotation_enabled = false; config.matching_type = MatchingType::CurrentBarClose;
BacktestEngine::new(data, fidc_core::PlatformExprStrategy::new(config),
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_matching_type(MatchingType::CurrentBarClose).with_volume_limit(false).with_liquidity_limit(false),
BacktestConfig { initial_cash: 50000., benchmark_code: "000300.SH".into(), start_date: Some(day(11)),
end_date: Some(day(15)), decision_lag_trading_days: 0, execution_price_field: PriceField::Close })
.with_dividend_reinvestment(true).with_observed_manual_executions(source).unwrap().run().unwrap()
}
#[test]
fn an_effective_manual_zero_must_not_recreate_a_cleared_position_on_dividend_payment() {
use fidc_core::position_exposure::PositionExposureAction as Action;
let zero = exposure_event("zero", 1, "2026-09-14T07:00:00Z", Action::Set { target_exposure_bps: 0 });
let result = cleared_reinvestment_case(vec![zero], Default::default(), None);
assert!(result.fills.is_empty(), "{:?}", result.fills);
assert!(result.holdings_summary.is_empty());
assert_eq!(result.equity_curve.last().unwrap().cash, 49998.);
assert!(result.equity_curve.last().unwrap().notes.contains("runtime_zero_exposure"));
}
#[test]
fn a_clear_without_a_manual_zero_keeps_the_declared_legacy_reinvestment_model() {
let result = cleared_reinvestment_case(vec![], Default::default(), None);
assert_eq!(result.fills.len(), 1);
assert_eq!((result.fills[0].quantity, result.fills[0].price, result.fills[0].commission), (100, 8.95, 0.));
assert_eq!(result.fills[0].gross_amount, 895.);
assert_eq!(result.fills[0].net_cash_flow, -895.);
assert!(result.order_events.is_empty());
}
#[test]
fn reinvestment_respects_zero_restore_same_instant_sequence_and_legacy_granularity() {
use fidc_core::position_exposure::PositionExposureAction as Action;
let before = "2026-09-14T07:00:00Z";
let settlement = "2026-09-14T16:00:00Z";
let later = "2026-09-15T01:31:00Z";
let zero = || exposure_event("zero", 1, before, Action::Scale { requested_bps: 0 });
let cases = vec![
(vec![zero()], Default::default(), false),
(vec![exposure_event("zero-at-settlement", 1, settlement, Action::Set { target_exposure_bps: 0 })], Default::default(), false),
(vec![exposure_event("later-zero", 1, later, Action::Set { target_exposure_bps: 0 })], Default::default(), true),
(vec![zero(), exposure_event("restore", 2, before, Action::Restore)], Default::default(), true),
(vec![exposure_event("restore", 1, before, Action::Restore), exposure_event("last-zero", 2, before, Action::Set { target_exposure_bps: 0 })], Default::default(), false),
(vec![exposure_event("restore", 2, before, Action::Restore), zero()], Default::default(), true),
(vec![zero(), exposure_event("later-restore", 2, later, Action::Restore)], Default::default(), false),
(vec![], std::collections::BTreeMap::from([(day(14), 0)]), false),
(vec![exposure_event("restore-legacy", 1, before, Action::Restore)], std::collections::BTreeMap::from([(day(14), 0)]), true),
(vec![exposure_event("positive", 1, before, Action::Set { target_exposure_bps: 3000 })], Default::default(), true),
];
for (events, legacy, allowed) in cases {
let result = cleared_reinvestment_case(events, legacy, None);
assert_eq!(result.fills.len(), usize::from(allowed));
assert_eq!(result.equity_curve.last().unwrap().cash, if allowed { 49103. } else { 49998. });
assert_eq!(result.holdings_summary.iter().map(|holding| holding.quantity).sum::<u32>(), if allowed {100} else {0});
assert!(result.order_events.is_empty());
}
}
#[test]
fn an_explicit_zero_member_weight_or_omission_blocks_only_that_reinvestment() {
use fidc_core::position_exposure::PositionExposureAction as Action;
for included in [false, true] {
for weight in [0, 10000] {
let mut event = exposure_event("allocation", 1, "2026-09-14T07:00:00Z", Action::Set { target_exposure_bps: 5000 });
let mut weights = std::collections::BTreeMap::from([("000002.SZ".into(), if included {10000-weight} else {10000})]);
if included { weights.insert(SYMBOL.into(), weight); }
event.allocation_weights_bps = Some(weights);
let allowed = included && weight > 0;
let result = cleared_reinvestment_case(vec![event], Default::default(), None);
assert_eq!(result.fills.len(), usize::from(allowed));
if !allowed { assert!(result.equity_curve.last().unwrap().notes.contains("runtime_zero_allocation")); }
}
}
}
#[test]
fn late_corporate_replay_uses_the_same_zero_policy_and_retains_actual_manual_shares() {
use fidc_core::position_exposure::PositionExposureAction as Action;
let event = exposure_event("zero", 1, "2026-09-14T07:00:00Z", Action::Set { target_exposure_bps: 0 });
let timely = cleared_reinvestment_case(vec![event.clone()], Default::default(), Some(false));
let late = cleared_reinvestment_case(vec![event], Default::default(), Some(true));
assert!(timely.fills.is_empty()); assert!(late.fills.is_empty());
assert_eq!(timely.equity_curve.last().unwrap().cash, 41047.);
assert_eq!(late.equity_curve.last().unwrap().cash, timely.equity_curve.last().unwrap().cash);
assert_eq!(late.holdings_summary[0].quantity, 1000);
assert_eq!(late.holdings_summary[0].quantity, timely.holdings_summary[0].quantity);
assert_eq!(late.manual_executions.last().unwrap().corporate_adjustment.as_ref().unwrap().corporate_cash_delta, "1050");
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -17,7 +17,7 @@ fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
.iter()
.map(|date| DailyMarketSnapshot {
date: *date,
symbol: SYMBOL.to_string(),
symbol: SYMBOL.into(),
timestamp: None,
day_open: 10.0,
open: 10.0,
@@ -55,7 +55,7 @@ fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
let session_start = date.and_hms_opt(9, 30, 0).expect("valid session start");
for offset in 0..bars_per_day {
let timestamp = session_start + Duration::minutes(offset as i64);
quotes.push(IntradayExecutionQuote {
quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
date: *date,
symbol: SYMBOL.to_string(),
timestamp,
@@ -0,0 +1,737 @@
use chrono::{DateTime, NaiveDate, Utc};
use fidc_core::manual_execution::{MANUAL_REPLAY_SCHEMA, ManualExecutionReplay};
use fidc_core::{
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
ChinaAShareCostModel, ChinaEquityRuleHooks, CorporateAction, DailyFactorSnapshot,
DailyMarketSnapshot, DataSet, Instrument, MatchingType, PriceField, Strategy,
};
fn date(day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(2026, 9, day).unwrap()
}
#[derive(Clone, Copy, Debug)]
enum Action {
Split,
Dividend,
Successor,
}
fn data(action: Action) -> DataSet {
data_with_successor_metadata(action, true)
}
fn data_with_successor_metadata(action: Action, include_successor: bool) -> DataSet {
let days = [10, 11, 14, 15].map(date);
let mut market = Vec::new();
let mut factors = Vec::new();
let mut candidates = Vec::new();
for day in days {
for symbol in ["000001.SZ", "000002.SZ"] {
if matches!(action, Action::Successor) && symbol == "000001.SZ" && day >= date(14) {
continue;
}
let price = if day < date(14)
|| (symbol == "000002.SZ" && !matches!(action, Action::Successor))
{
10.
} else if matches!(action, Action::Dividend) {
9.
} else {
5.
};
market.push(DailyMarketSnapshot {
date: day,
symbol: symbol.into(),
timestamp: Some(format!("{day} 15:00:00")),
day_open: price,
open: price,
high: price,
low: price,
close: price,
last_price: price,
bid1: price,
ask1: price,
prev_close: price,
volume: 100000,
minute_volume: 100000,
bid1_volume: 100000,
ask1_volume: 100000,
trading_phase: Some("continuous".into()),
paused: false,
upper_limit: price * 1.1,
lower_limit: price * 0.9,
price_tick: 0.01,
});
factors.push(DailyFactorSnapshot {
date: day,
symbol: symbol.into(),
market_cap_bn: 10.,
free_float_cap_bn: 10.,
pe_ttm: 10.,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: Some(1.),
extra_factors: Default::default(),
});
candidates.push(CandidateEligibility {
date: day,
symbol: symbol.into(),
is_st: false,
is_star_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: None,
});
}
}
DataSet::from_components_with_actions(
["000001.SZ", "000002.SZ"]
.into_iter()
.filter(|symbol| include_successor || *symbol != "000002.SZ")
.map(|symbol| Instrument {
symbol: symbol.into(),
name: symbol.into(),
board: "SZ".into(),
round_lot: 100,
listed_at: Some(date(1)),
delisted_at: (matches!(action, Action::Successor) && symbol == "000001.SZ")
.then_some(date(14)),
status: "active".into(),
})
.collect(),
market,
factors,
candidates,
days.map(|day| BenchmarkSnapshot {
date: day,
benchmark: "000300.SH".into(),
open: 100.,
close: 100.,
prev_close: 100.,
volume: 100000,
})
.into(),
vec![CorporateAction {
date: date(14),
symbol: "000001.SZ".into(),
payable_date: Some(date(14)),
record_date: Some(date(11)),
share_effective_date: Some(date(14)),
share_rounding_rule: Some("aggregate_half_up".into()),
share_cash: if matches!(action, Action::Dividend) {
1.
} else {
0.
},
share_bonus: if matches!(action, Action::Split) {
1.
} else {
0.
},
share_gift: 0.,
issue_quantity: 0.,
issue_price: 0.,
reform: false,
adjust_factor: None,
successor_symbol: matches!(action, Action::Successor).then(|| "000002.SZ".into()),
successor_ratio: matches!(action, Action::Successor).then_some(2.),
successor_cash: matches!(action, Action::Successor).then_some(0.5),
}],
)
.unwrap()
}
fn source(delayed: bool, sell: bool) -> ManualExecutionReplay {
let trades = if sell {
vec![
("initial-buy", "Buy", 10, 200, false),
("sale", "Sell", 11, 100, delayed),
]
} else {
vec![("buy", "Buy", 11, 100, delayed)]
};
let actions = trades.into_iter().enumerate().map(|(index, (id, side, day, quantity, late))| {
let executed = format!("2026-09-{day:02}T06:00:00Z").parse::<DateTime<Utc>>().unwrap();
let observed = if late { "2026-09-15T05:00:00Z".parse().unwrap() } else { executed + chrono::Duration::seconds(1) };
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":quantity,
"orderCreatedAt":created,"terminalObservedAt":observed,"terminalStatus":"filled","fills":[{
"tradeId":id,"observationEventId":id,"observationSequence":index+1,"tradeDate":date(day),
"executedAt":executed,"observedAt":observed,"feeObservationEventId":id,"feeObservationSequence":index+1,
"feeObservedAt":observed,"timestampPrecision":"second","quantity":quantity,"price":"10","totalFee":"1"
}]
}]})
}).collect::<Vec<_>>();
let mut source: ManualExecutionReplay = serde_json::from_value(serde_json::json!({
"schema":MANUAL_REPLAY_SCHEMA,"runtimeId":"r","accountId":"a","sourceContractSha256":"a".repeat(64),
"contentSha256":"","observationCutoff":"2026-09-15T08:00:00Z","actions":actions,
})).unwrap();
source.content_sha256 = source.content_digest().unwrap();
source.validate().unwrap();
source
}
struct Hold;
impl Strategy for Hold {
fn name(&self) -> &str {
"manual corporate observation"
}
fn requires_minute_callbacks(&self) -> bool {
false
}
}
fn run_custom<S: Strategy>(
data: DataSet,
source: ManualExecutionReplay,
strategy: S,
cash_dividends: bool,
adjust_cost: bool,
) -> Result<fidc_core::BacktestResult, fidc_core::BacktestError> {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_matching_type(MatchingType::NextBarOpen)
.with_volume_limit(false)
.with_liquidity_limit(false);
BacktestEngine::new(
data,
strategy,
broker,
BacktestConfig {
initial_cash: 10000.,
benchmark_code: "000300.SH".into(),
start_date: Some(date(10)),
end_date: Some(date(15)),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Open,
},
)
.with_cash_dividends(cash_dividends)
.with_cash_dividend_cost_basis_adjustment(adjust_cost)
.with_observed_manual_executions(source)
.unwrap()
.run()
}
fn run(
action: Action,
delayed: bool,
sell: bool,
) -> Result<fidc_core::BacktestResult, fidc_core::BacktestError> {
run_custom(data(action), source(delayed, sell), Hold, true, true)
}
#[test]
fn delayed_buy_does_not_lose_corporate_entitlements() {
for action in [Action::Split, Action::Dividend, Action::Successor] {
let timely = run(action, false, false).unwrap();
let delayed = run(action, true, false).unwrap();
let project = |result: &fidc_core::BacktestResult| {
(
result.equity_curve.last().unwrap().cash,
result.equity_curve.last().unwrap().total_equity,
result
.holdings_summary
.iter()
.map(|row| (row.symbol.clone(), row.quantity))
.collect::<Vec<_>>(),
)
};
assert_eq!(project(&delayed), project(&timely), "{action:?}");
assert_eq!(delayed.manual_executions.len(), 1);
assert!(delayed.fills.is_empty());
if matches!(action, Action::Successor)
&& let Ok(directory) = std::env::var("FIDC_CORPORATE_QA_OUTPUT")
{
use std::io::Write;
let path = std::path::Path::new(&directory).join("corporate-successor-result.json");
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path).unwrap();
file.write_all(&serde_json::to_vec(&serde_json::json!({
"source":delayed.manual_execution_source.as_deref(), "applications":delayed.manual_executions,
})).unwrap()).unwrap();
}
}
}
#[test]
fn delayed_sale_does_not_keep_unearned_corporate_entitlements() {
for action in [Action::Split, Action::Dividend, Action::Successor] {
let timely = run(action, false, true).unwrap();
let delayed = run(action, true, true).unwrap();
let project = |result: &fidc_core::BacktestResult| {
(
result.equity_curve.last().unwrap().cash,
result.equity_curve.last().unwrap().total_equity,
result
.holdings_summary
.iter()
.map(|row| (row.symbol.clone(), row.quantity))
.collect::<Vec<_>>(),
)
};
assert_eq!(project(&delayed), project(&timely), "{action:?}");
assert_eq!(delayed.manual_executions.len(), 2);
assert!(delayed.fills.is_empty());
}
}
#[test]
fn paper_and_broker_observations_require_the_same_frozen_successor_scope() {
for adapter in ["paper", "gt", "qmt"] {
for delayed in [false, true] {
for sell in [false, true] {
let mut replay = source(delayed, sell);
for action in &mut replay.actions {
for order in &mut action.orders { order.source_adapter = Some(adapter.into()); }
}
replay.content_sha256 = replay.content_digest().unwrap();
replay.validate().unwrap();
let complete = run_custom(data(Action::Successor), replay.clone(), Hold, true, true).unwrap();
assert_eq!(complete.holdings_summary[0].symbol, "000002.SZ");
assert_eq!(complete.holdings_summary[0].quantity, 200);
assert!(complete.fills.is_empty());
assert!(complete.order_events.is_empty());
let error = run_custom(data_with_successor_metadata(Action::Successor, false),
replay, Hold, true, true).unwrap_err();
assert!(error.to_string().contains("successor_instrument_missing"),
"{adapter} delayed={delayed} sell={sell}: {error}");
}
}
}
}
fn protected_successor_run(delayed: bool, locked: bool, amount: i32)
-> fidc_core::BacktestResult {
let policy = fidc_core::holding_policy::AutomaticTradeProtection {
sell_cooldown_days: if locked { 0 } else { 3 },
locks: if locked { vec![fidc_core::holding_policy::AutomaticTradeLock {
symbol: "000001.SZ".into(), start_date: date(11), end_date: Some(date(15)),
}] } else { vec![] }, ..Default::default()
};
protected_successor_case(delayed, policy, amount, "partial")
}
fn protected_successor_case(delayed: bool,
policy: fidc_core::holding_policy::AutomaticTradeProtection, amount: i32, scenario: &str)
-> fidc_core::BacktestResult {
let mut config = fidc_core::PlatformExprStrategyConfig::generic();
config.signal_symbol = "000002.SZ".into();
config.benchmark_symbol = "000300.SH".into();
config.rotation_enabled = false;
config.matching_type = MatchingType::CurrentBarClose;
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
config.explicit_action_schedule = Some(fidc_core::PlatformRebalanceSchedule {
frequency: fidc_core::PlatformScheduleFrequency::Daily,
time_rule: Some(fidc_core::ScheduleTimeRule::physical_time(14, 30)),
});
config.automatic_trade_protection = policy;
config.explicit_actions = vec![fidc_core::PlatformTradeAction::Order {
kind: fidc_core::PlatformExplicitOrderKind::Shares, symbol: "000002.SZ".into(),
amount_expr: amount.to_string(), when_expr: Some("decision_date == \"2026-09-15\"".into()),
limit_price_expr: None, time_in_force: None, start_time_expr: None, end_time_expr: None,
reason: "configured_successor_action".into(),
}];
let data = successor_execution_data();
let mut replay = source(delayed, true);
if scenario == "sold_before" {
let order = &mut replay.actions[1].orders[0];
order.quantity = 200; order.fills[0].quantity = 200;
} else if scenario == "cleared_after" {
let mut row = serde_json::to_value(&replay.actions[1]).unwrap();
let at = "2026-09-15T05:30:00Z";
let receipt = "2026-09-15T05:30:01Z";
row["actionId"] = "clear".into(); row["auditEventIds"] = serde_json::json!(["audit-clear"]);
row["confirmedAt"] = at.into(); row["confirmationObservedAt"] = at.into();
let order = &mut row["orders"][0];
order["orderId"] = "clear-order".into(); order["brokerOrderId"] = "clear-order".into();
order["symbol"] = "000002.SZ".into(); order["quantity"] = 200.into();
order["orderCreatedAt"] = at.into(); order["terminalObservedAt"] = receipt.into();
let fill = &mut order["fills"][0];
fill["tradeId"] = "clear-trade".into(); fill["observationEventId"] = "clear-receipt".into();
fill["observationSequence"] = 3.into(); fill["tradeDate"] = "2026-09-15".into();
fill["executedAt"] = at.into(); fill["observedAt"] = receipt.into();
fill["feeObservationEventId"] = "clear-receipt".into(); fill["feeObservationSequence"] = 3.into();
fill["feeObservedAt"] = receipt.into(); fill["price"] = "5".into(); fill["quantity"] = 200.into();
replay.actions.push(serde_json::from_value(row).unwrap());
}
replay.content_sha256 = replay.content_digest().unwrap();
BacktestEngine::new(data, fidc_core::PlatformExprStrategy::new(config),
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_matching_type(MatchingType::CurrentBarClose)
.with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit),
BacktestConfig { initial_cash: 10000., benchmark_code: "000300.SH".into(),
start_date: Some(date(10)), end_date: Some(date(15)), decision_lag_trading_days: 0,
execution_price_field: PriceField::Close,
}).with_observed_manual_executions(replay).unwrap().run().unwrap()
}
fn successor_execution_data() -> DataSet {
let parts = data(Action::Successor).snapshot_components();
DataSet::from_components_with_actions_and_quotes(parts.instruments, parts.market,
parts.factors, parts.candidates, parts.benchmarks, parts.corporate_actions,
[30, 31].into_iter().map(|minute| fidc_core::IntradayExecutionQuote {
observation_kind: fidc_core::data::QuoteObservationKind::MinuteBar,
date: date(15), symbol: "000002.SZ".into(), timestamp: date(15).and_hms_opt(14,minute,0).unwrap(),
last_price: 5., bid1: 5., ask1: 5., bid1_volume: 100000, ask1_volume: 100000,
volume_delta: 100000, amount_delta: 500000., trading_phase: Some("continuous".into()),
}).collect()).unwrap()
}
#[test]
fn native_pool_rebalance_applies_inherited_protection_without_rewriting_its_target() {
use fidc_core::stock_pool_execution as pool;
struct NativePool { locked: bool, expires: u32, exposure: i32 }
impl Strategy for NativePool {
fn name(&self) -> &str { "native pool successor protection" }
fn requires_minute_callbacks(&self) -> bool { false }
fn schedule_rules(&self) -> Vec<fidc_core::ScheduleRule> {
vec![fidc_core::ScheduleRule::daily("pool", fidc_core::ScheduleStage::OnDay)
.with_time_rule(fidc_core::ScheduleTimeRule::physical_time(14,30))]
}
fn on_scheduled(&mut self, ctx: &fidc_core::StrategyContext<'_>, _: &fidc_core::ScheduleRule)
-> Result<fidc_core::StrategyDecision, fidc_core::BacktestError> {
if ctx.execution_date != date(15) { return Ok(Default::default()); }
let symbols = vec!["000002.SZ".to_owned()];
let policy = fidc_core::holding_policy::AutomaticTradeProtection {
sell_cooldown_days: if self.locked { 0 } else { 3 },
locks: if self.locked { vec![fidc_core::holding_policy::AutomaticTradeLock {
symbol: "000001.SZ".into(), start_date: date(11), end_date: Some(date(self.expires)),
}] } else { vec![] }, ..Default::default()
};
let contract = pool::FrozenStockPoolIntent {
pool_id: "pool".into(), signal_date: date(15), frozen_equity: 10000.into(),
selection: pool::StockPoolSelection { trade_date: date(15), requested_symbols: symbols.clone(),
normal_trading_symbols: symbols.clone(), risk_eligible_symbols: symbols.clone(), final_symbols: symbols,
exclusion_reasons: Default::default(), inherited_from_generation: None, explicit_empty: false,
generation: Some("latest".into()),
}, members: vec![pool::StockPoolMemberSpec { symbol: "000002.SZ".into(), requested_order: 0,
recommendation_reason: String::new(), target_weight_bps: None, stop_loss: None, take_profit: None }],
rule: pool::StockPoolExecutionRule { pricing_mode: pool::POOL_PRICE_FIRST_TICK.into(),
window_start: "14:30".into(), window_end: "15:00".into(), automatic_trade_protection: policy,
..Default::default() }, constraints: pool::StockPoolDecisionConstraints {
target_holding_count: Some(1), ..Default::default() },
invest_ratio_bps: self.exposure, reserve_cash: 0.into(), out_of_pool_policy: "hold".into(), generation: "latest".into(),
};
Ok(fidc_core::StrategyDecision { order_intents: vec![fidc_core::OrderIntent::StockPool { contract: Box::new(contract) }], ..Default::default() })
}
}
for delayed in [false, true] {
for (locked, expires, exposure) in [(false, 15, 10000), (true, 15, 10000), (true, 15, 0), (true, 14, 10000)] {
let result = BacktestEngine::new(successor_execution_data(), NativePool { locked, expires, exposure },
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(chrono::NaiveTime::from_hms_opt(14,30,0).unwrap())
.with_volume_limit(false).with_liquidity_limit(false),
BacktestConfig { initial_cash: 10000., benchmark_code: "000300.SH".into(), start_date: Some(date(10)),
end_date: Some(date(15)), decision_lag_trading_days: 0, execution_price_field: PriceField::Last })
.with_observed_manual_executions(source(delayed, true)).unwrap().run().unwrap();
if expires == 14 {
assert!(!result.fills.is_empty(), "positive control {:?}", result.order_events);
} else {
assert!(result.fills.is_empty(), "delayed={delayed} locked={locked} exposure={exposure}: {:?}", result.fills);
assert!(result.order_events.is_empty());
assert_eq!(result.holdings_summary[0].quantity, 200);
}
}
}
}
#[test]
fn successor_keeps_sell_cooldown_for_timely_and_delayed_receipts() {
for delayed in [false, true] {
let result = protected_successor_run(delayed, false, 100);
assert!(result.fills.is_empty(), "delayed={delayed}: {:?}", result.fills);
assert!(result.order_events.is_empty(), "delayed={delayed}: {:?}", result.order_events);
assert!(result.risk_decisions.iter().any(|row| row.symbol == "000002.SZ"
&& row.date == date(15) && !row.accepted && row.rule_code == "sell_fill_cooldown"),
"orders={:?} risk={:?} notes={:?}", result.order_events, result.risk_decisions,
result.equity_curve.iter().map(|row| (&row.date, &row.notes)).collect::<Vec<_>>());
assert_eq!(result.holdings_summary[0].quantity, 200);
}
}
#[test]
fn converted_holding_does_not_lose_its_configured_date_lock() {
for delayed in [false, true] {
for amount in [-100, 100] {
let result = protected_successor_run(delayed, true, amount);
assert!(result.fills.is_empty(), "delayed={delayed} amount={amount}: {:?}", result.fills);
assert!(result.order_events.is_empty(), "delayed={delayed} amount={amount}: {:?}", result.order_events);
assert!(result.risk_decisions.iter().any(|row| row.symbol == "000002.SZ"
&& row.date == date(15) && !row.accepted && row.rule_code == "automatic_trade_locked"),
"orders={:?} risk={:?} notes={:?}", result.order_events, result.risk_decisions,
result.equity_curve.iter().map(|row| (&row.date, &row.notes)).collect::<Vec<_>>());
assert_eq!(result.holdings_summary[0].quantity, 200);
}
}
}
#[test]
fn successor_lock_expires_on_the_original_configured_date_not_the_conversion_date() {
for delayed in [false, true] {
let policy = fidc_core::holding_policy::AutomaticTradeProtection {
locks: vec![fidc_core::holding_policy::AutomaticTradeLock {
symbol: "000001.SZ".into(), start_date: date(11), end_date: Some(date(14)),
}], ..Default::default()
};
let result = protected_successor_case(delayed, policy, 100, "partial");
assert_eq!(result.fills.len(), 1);
assert_eq!(result.fills[0].quantity, 100);
assert_eq!(result.holdings_summary[0].quantity, 300);
}
}
#[test]
fn lock_survives_a_manual_clear_after_conversion_but_not_a_disproved_conversion() {
for delayed in [false, true] {
let policy = fidc_core::holding_policy::AutomaticTradeProtection {
locks: vec![fidc_core::holding_policy::AutomaticTradeLock {
symbol: "000001.SZ".into(), start_date: date(11), end_date: Some(date(15)),
}], ..Default::default()
};
let cleared = protected_successor_case(delayed, policy.clone(), 100, "cleared_after");
assert!(cleared.fills.is_empty());
assert!(cleared.order_events.is_empty());
assert!(cleared.holdings_summary.is_empty());
assert_eq!(cleared.manual_executions.len(), 3);
let unconverted = protected_successor_case(delayed, policy, 100, "sold_before");
assert_eq!(unconverted.fills.len(), 1);
assert_eq!(unconverted.holdings_summary[0].quantity, 100);
}
}
#[test]
fn corporate_replay_preserves_issued_orders_cash_flows_financing_and_charged_fees() {
struct ExistingActivity {
receiving_days: usize,
}
impl Strategy for ExistingActivity {
fn name(&self) -> &str {
"corporate replay with original activity"
}
fn requires_minute_callbacks(&self) -> bool {
false
}
fn on_day(
&mut self,
ctx: &fidc_core::StrategyContext<'_>,
) -> Result<fidc_core::StrategyDecision, fidc_core::BacktestError> {
use fidc_core::OrderIntent;
let order_intents = if ctx.execution_date == date(10) {
vec![
OrderIntent::DepositWithdraw {
amount: 500.,
receiving_days: self.receiving_days,
reason: "original deposit".into(),
},
OrderIntent::FinanceRepay {
amount: 200.,
reason: "original financing".into(),
},
OrderIntent::SetManagementFeeRate {
rate: 0.001,
reason: "original fee policy".into(),
},
]
} else if ctx.execution_date == date(11) {
vec![OrderIntent::Shares {
symbol: "000002.SZ".into(),
quantity: 100,
reason: "unrelated stock".into(),
}]
} else if ctx.execution_date == date(14) {
vec![OrderIntent::Shares {
symbol: "000001.SZ".into(),
quantity: 100,
reason: "already issued after corporate action".into(),
}]
} else {
vec![]
};
Ok(fidc_core::StrategyDecision {
order_intents,
..Default::default()
})
}
fn management_fee(
&mut self,
_: &fidc_core::StrategyContext<'_>,
_: f64,
) -> Result<Option<f64>, fidc_core::BacktestError> {
Ok(Some(0.25))
}
}
for receiving_days in [0, 1] {
for sell in [false, true] {
let timely = run_custom(
data(Action::Split),
source(false, sell),
ExistingActivity { receiving_days },
true,
true,
)
.unwrap();
let delayed = run_custom(
data(Action::Split),
source(true, sell),
ExistingActivity { receiving_days },
true,
true,
)
.unwrap();
assert_eq!(
serde_json::to_value(&timely.fills).unwrap(),
serde_json::to_value(&delayed.fills).unwrap()
);
assert_eq!(delayed.fills.len(), 2);
assert_eq!(
delayed.equity_curve.last().unwrap().cash,
timely.equity_curve.last().unwrap().cash
);
assert_eq!(
delayed.equity_curve.last().unwrap().total_equity,
timely.equity_curve.last().unwrap().total_equity
);
assert_eq!(
delayed
.equity_curve
.iter()
.map(|row| row.external_cash_flow)
.sum::<f64>(),
500.
);
assert_eq!(delayed.manual_executions.len(), if sell { 2 } else { 1 });
}
}
}
#[test]
fn pending_and_disabled_dividends_keep_the_configured_cash_and_cost_contract() {
for paid in [false, true] {
for enabled in [false, true] {
for adjust_cost in [false, true] {
let fixture = || {
let mut parts = data(Action::Dividend).snapshot_components();
parts.corporate_actions[0].payable_date =
Some(date(if paid { 14 } else { 16 }));
DataSet::from_components_with_actions(
parts.instruments,
parts.market,
parts.factors,
parts.candidates,
parts.benchmarks,
parts.corporate_actions,
)
.unwrap()
};
let timely =
run_custom(fixture(), source(false, false), Hold, enabled, adjust_cost)
.unwrap();
let delayed =
run_custom(fixture(), source(true, false), Hold, enabled, adjust_cost).unwrap();
assert_eq!(
delayed.equity_curve.last().unwrap().cash,
timely.equity_curve.last().unwrap().cash
);
let financial = |result: &fidc_core::BacktestResult| {
result
.holdings_summary
.iter()
.map(|row| {
(
row.symbol.clone(),
row.quantity,
row.average_cost,
row.last_price,
row.market_value,
row.unrealized_pnl,
row.realized_pnl,
row.pnl,
row.dividend_receivable,
)
})
.collect::<Vec<_>>()
};
// Receipt-day turnover is deliberately different when the
// dividend option is disabled and no adjustment is required.
assert_eq!(financial(&delayed), financial(&timely));
assert_eq!(
delayed.manual_executions[0].corporate_adjustment.is_some(),
enabled
);
if enabled && !paid {
assert_eq!(delayed.terminal_audit.cash_receivable_count, 1);
}
}
}
}
}
#[test]
fn late_fill_replays_aggregate_split_rounding_not_an_independent_rounded_fragment() {
let fixture = || {
let mut parts = data(Action::Split).snapshot_components();
parts.corporate_actions[0].share_bonus = 0.125;
for row in &mut parts.market {
if row.symbol == "000001.SZ" && row.date >= date(14) {
row.day_open = 8.89;
row.open = 8.89;
row.close = 8.89;
row.last_price = 8.89;
row.high = 8.89;
row.low = 8.89;
row.prev_close = 8.89;
row.bid1 = 8.89;
row.ask1 = 8.89;
row.upper_limit = 9.78;
row.lower_limit = 8.;
}
}
DataSet::from_components_with_actions(
parts.instruments,
parts.market,
parts.factors,
parts.candidates,
parts.benchmarks,
parts.corporate_actions,
)
.unwrap()
};
let input = |delayed| {
let mut value = source(delayed, true);
value.actions[0].orders[0].quantity = 100;
value.actions[0].orders[0].fills[0].quantity = 100;
value.actions[1].orders[0].side = fidc_core::OrderSide::Buy;
value.content_sha256 = value.content_digest().unwrap();
value
};
let timely = run_custom(fixture(), input(false), Hold, true, true).unwrap();
let delayed = run_custom(fixture(), input(true), Hold, true, true).unwrap();
assert_eq!(timely.holdings_summary[0].quantity, 225);
assert_eq!(delayed.holdings_summary[0].quantity, 225);
assert_eq!(
delayed.equity_curve.last().unwrap().total_equity,
timely.equity_curve.last().unwrap().total_equity
);
assert_eq!(
delayed.manual_executions[1]
.corporate_adjustment
.as_ref()
.unwrap()
.positions["000001.SZ"]
.quantity_before,
113
);
}
File diff suppressed because it is too large Load Diff
@@ -158,6 +158,42 @@ fn legacy_state_without_quantity_keeps_its_serialized_identity() {
assert_eq!(serde_json::to_value(state).unwrap(), original);
}
#[test]
fn partial_exit_basis_is_immutable_restart_safe_and_scoped_to_the_signal() {
let original = StockPoolExecutionState::default()
.observe(day(11), day(11), &[day(11), day(14)], &[member()], &[held(1000, 1000)]).unwrap();
let basis = BTreeMap::from([("000001.SZ".into(), Decimal::from(1000))]);
let saved = original.record_position_action_bases("sell-signal", &basis).unwrap();
assert!(original.position_action_bases.is_empty(), "a preview must not mutate its input");
let restored: StockPoolExecutionState = serde_json::from_slice(&serde_json::to_vec(&saved).unwrap()).unwrap();
let next_day = restored.observe(day(11), day(14), &[day(11), day(14)], &[member()], &[held(500, 500)]).unwrap();
assert_eq!(next_day.position_action_bases_for("sell-signal"), basis);
assert!(next_day.position_action_bases_for("new-signal").is_empty());
assert!(next_day.record_position_action_bases("sell-signal", &BTreeMap::from([("000001.SZ".into(), Decimal::from(500))])).unwrap_err().contains("basis_changed"));
let new_signal = next_day.record_position_action_bases("new-signal", &BTreeMap::from([("000001.SZ".into(), Decimal::from(500))])).unwrap();
assert!(new_signal.position_action_bases_for("sell-signal").is_empty());
assert_eq!(new_signal.position_action_bases_for("new-signal")["000001.SZ"], Decimal::from(500));
for invalid in [Decimal::ZERO, Decimal::NEGATIVE_ONE] {
assert!(original.record_position_action_bases("signal", &BTreeMap::from([("000001.SZ".into(), invalid)])).is_err());
}
assert!(original.record_position_action_bases(" ", &basis).is_err());
}
#[test]
fn verified_split_adjusts_exit_basis_and_entry_completion_not_generation() {
let initial = StockPoolExecutionState::default()
.observe(day(11), day(11), &[day(11)], &[member()], &[]).unwrap();
let entry_plan = plan(&initial, day(11), &[member()], &[], 10000, "hold");
let entered = initial.record_plan(day(11), "entry", &entry_plan).unwrap();
let saved = entered.record_position_action_bases("sell", &BTreeMap::from([("000001.SZ".into(), Decimal::from(1000))])).unwrap();
let adjusted = saved.adjust_for_split("000001.SZ", Decimal::new(15,1)).unwrap();
assert_eq!(adjusted.position_action_bases_for("sell")["000001.SZ"], Decimal::from(1500));
assert_eq!(adjusted.entries["000001.SZ"].completion_quantity, Some(Decimal::from(1500)));
assert_eq!(adjusted.position_action_bases["000001.SZ"].first_execution_date, day(11));
assert_eq!(saved.position_action_bases_for("sell")["000001.SZ"], Decimal::from(1000));
assert!(saved.adjust_for_split("000001.SZ", Decimal::ZERO).is_err());
}
#[test]
fn partial_entry_continues_after_restart_then_completed_holdings_are_preserved() {
let members = vec![member()];
+3 -3
View File
@@ -35,7 +35,7 @@ fn market(
) -> DailyMarketSnapshot {
DailyMarketSnapshot {
date: d(date),
symbol: symbol.to_string(),
symbol: symbol.into(),
timestamp: None,
day_open: open,
open,
@@ -66,7 +66,7 @@ fn factor(
) -> DailyFactorSnapshot {
DailyFactorSnapshot {
date: d(date),
symbol: symbol.to_string(),
symbol: symbol.into(),
market_cap_bn,
free_float_cap_bn,
pe_ttm: 18.0,
@@ -87,7 +87,7 @@ fn candidate(
) -> CandidateEligibility {
CandidateEligibility {
date: d(date),
symbol: symbol.to_string(),
symbol: symbol.into(),
is_st: false,
is_star_st: false,
is_new_listing,
@@ -13,6 +13,7 @@
- 成交日及后续N个完整正式交易日均受保护。例如周五成交、N=3,保护到下周三结束,周四恢复;不按72小时或自然日替代。
- 最长持有从连续持仓第一次实际买入开始,跨正式交易日计数;加仓、部分卖出和有证据的证券转换不重置,完全卖出再买入开启新周期。锁定和买入保护优先于最长持有退出。
- 日期锁定禁止自动买卖,已接受的挂单不自动撤销;手工路径只绕过自动策略保护,不绕过账户授权、T+1、券商和风控。
- 已校验且实际发生的持仓换股继承原保护期限与日期锁,不因改代码解锁或重计时;无实际转换的目录映射不继承。换股后手工清仓仍受原有效日期锁;确认换股前已清仓时不保留推定关系。原生策略、股票池与ETF顺延消费的修复及未发布边界见 `successor-protection-20260914.md`。
- 保留的真实持仓继续占用资金与席位,不把未完成卖出当现金。最长持有退出先形成唯一最终目标,不能叠加一笔策略部分卖出和一笔框架全量卖出。
- 在线上下文重建必须注入已经校验的真实成交/持仓快照,不能把重建日或旧行情日当建仓日。期货或股票期货混合账户未纳入本合同,显式拒绝。
@@ -0,0 +1,55 @@
# 回报上下文、盘前意图与尚未提交的目标
2026-09-14。本轮已配套发布177,annotated tag `v2026.9.14.5`。Engine81acc54 / Service e81bf47 / Trading94f99d2;完整股票池Goal继续,不据本阶段关闭。
## 已复现问题
1. `on_process_event`总是收到`active_datetime=None`及空委托/成交数组。10:00账本已有100股,但Trade/PostMinute回调的成交数量仍为0;不能靠普通`on_minute`已修复就认为通知链也完整。
2. 15:05盘后成交后,PreAfterTrading仍被标为15:00;跨日模式的PostOnDay又使用信号日描述执行日已发生的成交。
3. BeforeTrading调度只处理订阅、账户和期货指令,剩余股票买卖/撤改意图没有后续消费。简单在开盘调用普通broker执行还会让旧挂单先成交再撤单。
4. 合并完整目标时只追加权重会保留旧证券;更重要的是,不能先提交盘前旧组合,之后才计算同一窗口的新目标,否则T+1可能使错误买入无法纠正。
5. 策略计算前的空broker调用也会恢复上一目标的未提交买入腿。反例中原持仓100股,09:30卖25股、13:00卖剩余75股;若此时先恢复旧买入,已经准备将新目标设为0%的策略仍会买入另一股票3000股。
## 本轮处理
- 事件通知显式携带当前可见的委托、成交与回调时钟,移动已完成记录后再通知,不按每个回调复制整段历史。上下文是通知时已完成批次的最新状态,不冒充每一历史通知发生瞬间的账本快照。
- 信号计算回调保留信号日;账户/委托通知使用实际执行日与物理时钟。默认收盘和结算不早于已处理时刻及当前适用的盘后结束点,管理费回调沿用同一完成时钟。
- 盘前撤改走明确的非撮合控制阶段,保持原订单ID和实际已成交量;该入口拒绝买卖目标,不会顺带撮合旧单。普通显式买卖按原配置窗口执行,后续回调读取撤改后的真实活动订单。
- 盘前与集合竞价的显式命令保留各自批次及约束。纯完整组合(完整rebalance或单一StockPool/TargetPortfolioSmart)可以被更新的完整意图替换;空回调不等于清仓,显式空完整目标才清仓。被替换意图的旧买入限制不能污染新完整目标。
- 尚未提交的完整目标保留到当前窗口日度策略算完;新执行意图优先,只有没有新执行意图时才使用前面的目标。已提交挂单可以先更新实际成交,但策略计算前不恢复旧的未提交买入腿,之后再由正常执行路径处理当前意图。
- 订阅/账户/直接期货指令通知同样获得完成后的历史;本轮不改变期货成交、会话或费用规则。
## 回归证据
- 通知链:09:30为空、10:00/10:01均看到100股及1笔实际成交,Trade通知可找到相同订单。
- 盘后:15:05成交后的默认收盘/结算和管理费通知不倒退;next-open保持独立信号日和执行日。
- 盘前:09:00生成100股命令,分别只在09:30/13:00配置窗口成交;保留备注/诊断。跨日撤销原GTC订单后,新订单只成交100股,未让旧单先成交。
- 完整目标:盘前A、集合竞价B、日度A或显式空目标,最终只采用有效最新目标;日度无新信号时保持B。显式逐股命令不会被目标合并丢弃。
- 恢复顺序:开启正常旧恢复的单点负向对照确实多买3000股;恢复BeforeStrategy阶段后,只有原股票同一卖单的25+75股成交,无新增买入,最终持仓为空。
- 本机Core834项通过(9项原有ignore),Trading613、最新main Runner446/API119通过。外部数据库及平台ignore不当作通过。
精确只读快照在Linux通过Core834及Trading613。旧二进制先独立归档,构建保持1GiB磁盘余量;本轮未再次删除缓存或业务文件。
## 发布与真实历史复验
已推送annotated tag `v2026.9.14.5`对应Engine `81acc5422878abc855fca72b35766ffad6159200`、Service `e81bf47806f5ac4ae4798bb5f5955a56638f754c`、Trading `94f99d20f49f6cd1810996706cb94f610c302385`。回测API/Runner于06:15:53 CST切换,五交易单元06:21:09切换,06:22实际运行文件和业务事实复核通过。
三组冻结合同共六次独立原生A/B,完整Canonical及equity/orders/trades/holdings逐行一致;再通过生产HTTP各提交一次,结果分别匹配原生候选,旧记录未改写:
| 案例 | 生产回测ID | 成交 / 持仓 | 期末权益 |
| --- | --- | --- | ---: |
| 手选优先四证券 | btr_req_44f1bb067559946ef22941a0c425ed53e47515e04b399e20 | 10 / 4 | 9706248.648662 |
| 自动优先四证券 | btr_req_078f129ed46b55ba72b47605983a00ae3eef14b1995e1237 | 10 / 4 | 9706248.648662 |
| 许总24只原v3 | btr_req_ef37b8ea403f489e4798d2878e0ef85966ace550f6b4f3c4 | 51 / 21 | 9685563.876924999 |
重复目标委托0。三条新记录各5个交易日事件落库,持久事件27/18/32条,唯一键计数分别相同;旧流式样本仍27条/5日。仍为原合同下的日终容量审计,不外推实时盘口成交能力;首次Source准备和后续快速返回也不作为性能优化证明。
API SHA `dea170902d77734d0a77c4da7dad71a70b33f76467e0608675dfbcc9d35d67fc`,Runner SHA `b1d93215deb275fbec6217c6b9afbf717d5649600716bf4f3a1bf5d1cfa69731`,运行实现身份 `fed10e9fa61836aa271921f5d58490054210d83da935cad5de11cfacab45c13e`。API发布目录`/srv/fidc/canonical/run/backtest-api/releases/callback-81acc54-7w1zx9fb`,回退目录`/srv/fidc/canonical/run/build/callback-rollback-j7oje2tz`;交易回退`holding-protection-rollback-czuric4r`。
六服务实际SHA与manifest吻合,新增ERROR0。3Paper/0Live、配置、旧活动单、3个未确认Paper预览、迁移、影子配置0及disabled未变;发布后Paper/Live新订单0,未发送真实通知、委托或撤单。Source d5/PID1700096与UI6a2/PID3089476未重启,研究/信号暂停不变。177维护中的Engine9a54156完整保留,实际构建使用81acc54/e81bf47及81acc54/94f99d2的只读Git快照。
原始回放/HTTP证据`/srv/fidc/canonical/run/research/stock-pool-callback-20260914/`;发布和最终审计`/tmp/fidc-callback-{candidate,api-release,trading-release,final-audit}-20260914.json`;非敏感汇总在`docs/evidence/callback-target-20260914/acceptance.json`。
## 继续范围
显式逐笔手工影子回放仍未完成,四类手工来源继续拒绝纯比例影子;原始撤单意图时刻不能用网关回报时刻冒充。还需继续检查会话外调度产生的未提交意图、完整阶段日历与其余参数/生命周期/适配器矩阵。Source冻结、研究/信号暂停、现有任务配置和真实路由不改。
+25
View File
@@ -0,0 +1,25 @@
# 应收股息资产与支付日净值守恒修复
2026-10-05。发现总权益只计算现金、持仓市值和负债,遗漏已经确认但尚未支付的应收股息。这会在除息后的持仓计价下降时低估资产,再在真正支付时把资产转移误呈现成收益。本次修复账簿计价;没有部署活动运行版本或改写历史研究结果,生产策略仍为 0/10。
## 行为更正
`PortfolioState::total_equity` 现在加入独立应收股息资产。应收金额逐项转换为固定精度货币并检查非负及溢出,既不变成可用现金,也不计入证券市值。真实支付通过既有结算链路将同一资产从应收转为现金,不凭空新增权益;已卖光或被清理的持仓不能让应收资产消失。
具体回归:账户现金 1,000、登记日持有 100 股、价格 10、税前每股股息 0.5。除息后持仓市值 950、应收 50,账户权益仍为 2,000;支付前现金仍为 1,000,支付后现金 1,050、应收零,总权益保持 2,000。没有实际到账前,提现仍受现金余额约束。独立无持仓案例验证应收 50 不因空持仓清理而丢失。
同时移除对原始股息率的 `f64::EPSILON` 过滤。明确的极小非零值仍出现在股息查询中,也必须具备支付日、登记日和实际登记持仓证据;不能因为最终乘积可能按货币精度舍入为零,就在校验前抹掉原始率或绕过门禁。新增 1e-16 原始率的保留及未知支付日拒绝回归。
## 代码、运行与证据
权益计价提交为 `af655de0c169cd92ea8bf9bbc77c34a5c157bdf3`,极小非零率提交为 `d3f49a17ef284dba587607ad0ac160cc394bf677`,已推送既有工作分支。没有重写正式历史输入、模型、评分、旧日志或旧报告。
重构建及测试在 177 的 Boris UID 1003 私有 checkout 中执行,显式使用批准的 `stable-x86_64-unknown-linux-gnu` / Rust 1.98.0、锁文件和离线依赖。编译输出写入已核验可执行的 `/tmp` tmpfs,单元 CPUQuota=400%、MemoryMax=16GiB、MemorySwapMax=0、4 个构建 worker;不使用低于保留线的 SSD 进行批量构建,不改系统工具链。
首次权益计价版本通过 18 项定向测试及全工作区 963 项测试。最终版本 `cargo test --locked --offline -p fidc-core --all-targets` 通过 **964 项测试、9 项既有忽略项**,日志 SHA256 为 `7e25d69e1adec3183394a90a6c3dfab01c56982f9a4aa64afd261af69be72551`。新改动后复核完整核心回归,未跳过失败样本。
验证回执 SHA256 为 `c92576e6ecb36727444baf4f0eeebd5a1d9f401cc60cc88f541482e7c2645640`,PG18 artifact 为 `strategy_research_fcae25fd6b283a74016bd99e`,完整 JSONB 回读通过。回执、PG 原记录、首次及最终测试日志见 `docs/evidence/cash-receivable-equity-20261005/`。真实冻结研究执行器仍为 PID 1396112、InvocationID `0eb28e3a45694a00951aa3aec0e13465`,原数值研究预算未重开。
## 未完成门禁
此次结果证明应收现金计入权益及支付转移的账簿回归,不等同于完整实盘或策略验收。股份应收、登记日送转权益、真实股份交付与可卖日仍需继续实现;Rust 接收端对股份交付日的承接、AiQuant 同条件运行、完整 Source 事件/无事件与修订证据、样本外、容量、模拟盘和人工审批仍未闭环。`production_runtime_deployed=false`、`production_ready=false`、`trading_allowed=false` 保留。
@@ -0,0 +1,191 @@
{
"verified_at": "2026-09-13T22:22:38.597836+00:00",
"tag": "v2026.9.14.5",
"processes": {
"fidc-backtest-service-highmem177.service": {
"pid": 3692551,
"sha256": "dea170902d77734d0a77c4da7dad71a70b33f76467e0608675dfbcc9d35d67fc",
"journal_since": "2026-09-13T22:15:53.225719+00:00",
"journal_lines": 54,
"error_lines": 0
},
"fidc-trading-control-highmem177.service": {
"pid": 3697497,
"sha256": "a8f62ba74caf7ce2f5ba9cc6f67f41c844dee3747852611051c8dfb7b36295a3",
"journal_since": "2026-09-13T22:21:09.122548+00:00",
"journal_lines": 5,
"error_lines": 0
},
"fidc-market-data-highmem177.service": {
"pid": 3697498,
"sha256": "89395ab4c9e11274f171f4386f88949ce616fe45d15aae9a829216d53ed11db7",
"journal_since": "2026-09-13T22:21:09.122548+00:00",
"journal_lines": 5,
"error_lines": 0
},
"fidc-strategy-runtime-highmem177.service": {
"pid": 3697688,
"sha256": "041a0103d2c46c55221d169965ece9fdacee3905abc0d46edd9c8a2a86f6cd54",
"journal_since": "2026-09-13T22:21:09.122548+00:00",
"journal_lines": 5,
"error_lines": 0
},
"fidc-paper-trading-highmem177.service": {
"pid": 3697792,
"sha256": "8460008f712810f7d3876b9f2274aef88f82d46cd33e1593dbd0361f6d158b75",
"journal_since": "2026-09-13T22:21:09.122548+00:00",
"journal_lines": 6,
"error_lines": 0
},
"fidc-live-trading-highmem177.service": {
"pid": 3697762,
"sha256": "e89d6ba68655a5d4a164793ad67a9003cf2c49733063018497339930feafaac3",
"journal_since": "2026-09-13T22:21:09.122548+00:00",
"journal_lines": 6,
"error_lines": 0
}
},
"source": {
"commit": "d5b682c6d097",
"pid": 1700096,
"loaded_at": "2026-09-12T03:57:06.665536+00:00",
"source_stale": false,
"loaded_server_sha256": "ef827ce6b95e0ea63047a0068af2677633716e0a5d63cf350de6c91a3413e352"
},
"source_checkouts": {
"fidc-backtest-engine": {
"head": "9a54156df94cfbf11a1e6335ec6ef5449bd6ac17",
"runtime_commit": "81acc5422878abc855fca72b35766ffad6159200",
"tracked_dirty": false
},
"fidc-backtest-service": {
"head": "5ec8dc86d99736a0c0140440bd039d11e118c1c6",
"runtime_commit": "e81bf47806f5ac4ae4798bb5f5955a56638f754c",
"tracked_dirty": false
},
"fidc-trading-platform": {
"head": "94f99d20f49f6cd1810996706cb94f610c302385",
"runtime_commit": "94f99d20f49f6cd1810996706cb94f610c302385",
"tracked_dirty": false
},
"omniquant": {
"head": "6a2b2604b40505fa754453307c517fef60743426",
"runtime_commit": "6a2b2604b40505fa754453307c517fef60743426",
"tracked_dirty": false
}
},
"ui_unchanged": {
"commit": "6a2b2604b40505fa754453307c517fef60743426",
"pid": 3089476
},
"http_cases": [
{
"name": "manual_first",
"run_id": "btr_req_44f1bb067559946ef22941a0c425ed53e47515e04b399e20",
"status": "succeeded",
"canonical_sha256": "0830216850b64d6e83291e072b31a9989f179915ee3341a75a77c73d1f9081a3",
"trade_count": 10,
"holding_count": 4,
"final_equity": 9706248.648662,
"old_result_unchanged": true
},
{
"name": "automatic_first",
"run_id": "btr_req_078f129ed46b55ba72b47605983a00ae3eef14b1995e1237",
"status": "succeeded",
"canonical_sha256": "c75cabcc03760f415bb664d20060e81c620d7a0201dd348ea71f75c932571de7",
"trade_count": 10,
"holding_count": 4,
"final_equity": 9706248.648662,
"old_result_unchanged": true
},
{
"name": "stock24",
"run_id": "btr_req_ef37b8ea403f489e4798d2878e0ef85966ace550f6b4f3c4",
"status": "succeeded",
"canonical_sha256": "270b403542ab41290c3d6e027b89cdab24dd41a8e2851d8786b33daa51e0051f",
"trade_count": 51,
"holding_count": 21,
"final_equity": 9685563.876924999,
"old_result_unchanged": true
}
],
"durable_events": [
{
"run_id": "btr_req_44f1bb067559946ef22941a0c425ed53e47515e04b399e20",
"count": 27,
"unique_keys": 27,
"days": 5
},
{
"run_id": "btr_req_078f129ed46b55ba72b47605983a00ae3eef14b1995e1237",
"count": 18,
"unique_keys": 18,
"days": 5
},
{
"run_id": "btr_req_ef37b8ea403f489e4798d2878e0ef85966ace550f6b4f3c4",
"count": 32,
"unique_keys": 32,
"days": 5
},
{
"run_id": "btr_req_a3c3dfe5cd81e27e565064a65665f561c60adebdc6c9c9b4",
"count": 27,
"unique_keys": 27,
"days": 5
}
],
"trading_state": {
"paper": {
"configuration": {
"count": 3,
"hash": "93f3224edef59c381164e0236529dacc"
},
"active": {
"claims": 0,
"orders": 0
}
},
"live": {
"configuration": {
"count": 0,
"hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"active": {
"claims": 0,
"orders": 1,
"today_orders": 0,
"orders_hash": "d4b56fbf3a541a41a383ad4e48891bb8",
"route_mode": "disabled"
}
}
},
"manual_facts_unchanged": {
"paper": {
"shadow_configurations": 0,
"shadow_runs": 0,
"manual_count": 3,
"manual_hash": "82572901ac0b5fdb4d8b984f71e1763d",
"migrations_hash": "21d711b2ee52d2d66a8be4e99b179190",
"new_orders": 0
},
"live": {
"shadow_configurations": 0,
"shadow_runs": 0,
"manual_count": 0,
"manual_hash": "d41d8cd98f00b204e9800998ecf8427e",
"migrations_hash": "610528d4f350309379c9398c4ea43f66",
"new_orders": 0
}
},
"broker_submission": false,
"linux_core_tests": {
"passed": 834,
"failed": 0,
"ignored": 9,
"log": "/srv/fidc/canonical/run/fidc-private/evidence/callback-candidate-6qumwky7/linux-core-tests.log"
},
"scope": "Callback and pending-target release verification; historical simulation only. Full manual shadow replay remains incomplete.",
"native_replays": 6
}
@@ -0,0 +1,27 @@
{
"full-linux-tests.log": {
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/full-linux-tests.log",
"bytes": 98059,
"sha256": "7e25d69e1adec3183394a90a6c3dfab01c56982f9a4aa64afd261af69be72551"
},
"validation-receipt.json": {
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/validation-receipt.json",
"bytes": 1728,
"sha256": "c92576e6ecb36727444baf4f0eeebd5a1d9f401cc60cc88f541482e7c2645640"
},
"validation-pg-receipt.json": {
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/validation-pg-receipt.json",
"bytes": 2771,
"sha256": "eb2e367fc18a43297b70f9bd7967b317463369365291644d1383aaebb012eb7c"
},
"first-targeted-linux-tests.log": {
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-af655de/targeted-linux-tests.log",
"bytes": 8284,
"sha256": "10e4936faf4ac2955d2e3d9038f5d56c43e7eaccb0b62b6fcaa701bb68a4b1fd"
},
"first-full-linux-tests.log": {
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-af655de/full-linux-tests.log",
"bytes": 101035,
"sha256": "6e90211e4df5c91d81e55f3de4cbd6ed9b24b20cdf7c4927d7cdcadcdda41467"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,177 @@
Compiling version_check v0.9.5
Compiling proc-macro2 v1.0.106
Compiling unicode-ident v1.0.24
Compiling quote v1.0.45
Compiling autocfg v1.5.0
Compiling serde_core v1.0.228
Compiling libc v0.2.184
Compiling cfg-if v1.0.4
Compiling crunchy v0.2.4
Compiling generic-array v0.14.7
Compiling tiny-keccak v2.0.2
Compiling crossbeam-utils v0.8.22
Compiling num-traits v0.2.19
Compiling syn v2.0.117
Compiling typenum v1.20.1
Compiling serde v1.0.228
Compiling portable-atomic v1.13.1
Compiling crossbeam-epoch v0.9.20
Compiling getrandom v0.2.17
Compiling getrandom v0.3.4
Compiling once_cell v1.21.4
Compiling rustversion v1.0.22
Compiling crossbeam-deque v0.8.7
Compiling zerocopy v0.8.48
Compiling const-random-macro v0.1.16
Compiling smartstring v1.0.1
Compiling ahash v0.8.12
Compiling static_assertions v1.1.0
Compiling rayon-core v1.13.0
Compiling zmij v1.0.23
Compiling const-random v0.1.18
Compiling crypto-common v0.1.7
Compiling block-buffer v0.10.4
Compiling itoa v1.0.18
Compiling thiserror v2.0.18
Compiling serde_json v1.0.145
Compiling rust_decimal v1.39.0
Compiling castaway v0.2.4
Compiling digest v0.10.7
Compiling iana-time-zone v0.1.65
Compiling ryu v1.0.23
Compiling hashbrown v0.16.1
Compiling equivalent v1.0.2
Compiling bitflags v2.11.1
Compiling smallvec v1.15.1
Compiling either v1.17.0
Compiling thin-vec v0.2.16
Compiling serde_derive v1.0.228
Compiling thiserror-impl v2.0.18
Compiling rhai_codegen v3.1.0
Compiling arrayvec v0.7.8
Compiling memchr v2.8.0
Compiling ta-lib-dispatch v0.1.2 (https://github.com/TA-Lib/ta-lib.git?rev=dd5a90259a3f9e04e2da9f38bf0719a841b40108#dd5a9025)
Compiling cpufeatures v0.2.17
Compiling sha2 v0.10.9
Compiling rhai v1.23.6
Compiling ta-lib v0.8.1 (https://github.com/TA-Lib/ta-lib.git?rev=dd5a90259a3f9e04e2da9f38bf0719a841b40108#dd5a9025)
Compiling indexmap v2.11.4
Compiling chrono v0.4.44
Compiling compact_str v0.10.0
Compiling rayon v1.12.0
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-cash-receivable-validation-20261005-af655de/crates/fidc-core)
Finished `test` profile [unoptimized] target(s) in 56.65s
Running unittests src/lib.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/fidc_core-dbea4d4f6cb5d2d7)
running 18 tests
test corporate_book::tests::closing_register_cannot_be_replaced_by_later_holdings ... ok
test corporate_book::tests::dividend_receivable_survives_a_flat_position_without_becoming_spendable_cash ... ok
test corporate_book::tests::entitlement_survives_complete_sale_and_a_successor_unit_conversion ... ok
test corporate_book::tests::explicitly_delayed_dividend_remains_receivable_until_actual_payment ... ok
test corporate_book::tests::a_late_corporate_batch_failure_keeps_prior_cash_positions_and_notes ... ok
test corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta ... ok
test corporate_book::tests::missing_accounting_reference_is_atomic_not_a_silent_cash_only_success ... ok
test corporate_book::tests::manual_zero_skips_only_unused_allocation_facts_not_invalid_cash_evidence ... ok
test corporate_book::tests::flat_position_retirement_does_not_change_issued_lot_state_or_next_purchase_origin ... ok
test corporate_book::tests::per_share_precision_is_retained_until_the_registered_cash_amount_is_rounded ... ok
test corporate_book::tests::missing_or_backward_cash_payment_date_never_updates_the_account ... ok
test corporate_book::tests::missing_record_date_or_unobserved_closing_book_is_atomic ... ok
test corporate_book::tests::post_record_buy_cannot_receive_the_earlier_dividend ... ok
test corporate_book::tests::terminated_security_keeps_paid_cash_and_is_not_recreated_by_reinvestment ... ok
test corporate_book::tests::sale_after_registration_preserves_cash_and_excludes_new_lots_from_cost_adjustment ... ok
test corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security ... ok
test corporate_book::tests::verified_conversion_preserves_lots_without_creating_orders_or_fills ... ok
test corporate_book::tests::invalid_successor_terms_cannot_be_replaced_with_one_share_or_zero_cash ... ok
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 764 filtered out; finished in 0.01s
Running unittests src/bin/dump_platform_runtime_schema.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/dump_platform_runtime_schema-c4b52627aec309c9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/automatic_trade_protection.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/automatic_trade_protection-e76e4284ad200e69)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s
Running tests/core_rules.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/core_rules-6365eded07d721e8)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s
Running tests/corporate_actions.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/corporate_actions-05d8a8628f15e674)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s
Running tests/decision_quote_preload.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/decision_quote_preload-f63700247a753869)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s
Running tests/delisting.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/delisting-4a43fc52b1f023c5)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/dividend_reinvestment_contract.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/dividend_reinvestment_contract-69149bb471610f44)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s
Running tests/engine_hooks.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/engine_hooks-3b06d8e9ecb68f45)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s
Running tests/explicit_order_flow.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/explicit_order_flow-fe395fa4711b4010)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 58 filtered out; finished in 0.00s
Running tests/futures_account.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/futures_account-47b777d1d163df6f)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
Running tests/intraday_history_performance.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/intraday_history_performance-7ce6b1e7b0291409)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s
Running tests/manual_corporate_observation.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/manual_corporate_observation-8dc2a04a201f24a0)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 11 filtered out; finished in 0.00s
Running tests/stock_pool_execution_contract.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/stock_pool_execution_contract-a11a2ea3e2392446)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s
Running tests/stock_pool_execution_state.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/stock_pool_execution_state-a8b965ddcfd186e1)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s
Running tests/strategy_selection.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/strategy_selection-bf5a72b2003c833f)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
{
"artifact_bytes": 1728,
"artifact_id": "strategy_research_fcae25fd6b283a74016bd99e",
"artifact_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/validation-receipt.json",
"artifact_sha256": "c92576e6ecb36727444baf4f0eeebd5a1d9f401cc60cc88f541482e7c2645640",
"artifact_type": "research_source_scope_probe",
"contract_sha256": "39388b1bb63377e170145fcff03cee9bb3996283e5fa7c2c44453bbc82525e36",
"created_at": "2026-10-04T19:06:15Z",
"created_by": "boris",
"department_id": null,
"organization_id": "local_org",
"persisted_at": "2026-10-04T19:06:15Z",
"production_ready": false,
"record_json": {
"cash_entitlement_asset_included_without_spendable_cash_inference": true,
"component": "fidc-backtest-engine",
"contract_sha256": "31d7028d123a9abd2004d93534f3d8768704b2904529a3666d6146d67de36018",
"engine_commit": "d3f49a17ef284dba587607ad0ac160cc394bf677",
"ignored_benchmarks": 9,
"log_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/full-linux-tests.log",
"sha256": "7e25d69e1adec3183394a90a6c3dfab01c56982f9a4aa64afd261af69be72551"
},
"numeric_research_budget_reopened": false,
"observed_at": "2026-10-04T19:06:11.362993+00:00",
"original_numerical_research_executor": {
"ActiveState": "active",
"InvocationID": "0eb28e3a45694a00951aa3aec0e13465",
"MainPID": "1396112"
},
"owner_uid": 1003,
"payment_is_receivable_to_cash_asset_transfer_not_new_income": true,
"production_ready": false,
"production_runtime_deployed": false,
"production_strategy_count": 0,
"resource_contract": {
"build_jobs": 4,
"cpu_percent": 400,
"memory_max_bytes": 17179869184,
"memory_swap_max_bytes": 0,
"rust_toolchain": "stable-x86_64-unknown-linux-gnu",
"rustc": "1.98.0",
"target_filesystem": "executable_tmpfs",
"target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de"
},
"schema_version": "fidc.linux-cash-receivable-equity-validation/v1",
"sharedelivery_ledger_implemented": false,
"source_path": "/srv/fidc/programs/src/fidc-cash-receivable-validation-20261005-d3f49a1",
"status": "full_engine_regression_passed_not_runtime_or_strategy_acceptance",
"tests_passed": 964,
"tiny_nonzero_original_rate_preserved_and_dates_still_required": true,
"trading_allowed": false
},
"research_key": "fidc-cash-receivable-equity-validation-20261005-d3f49a1",
"schema_table": "strategy_research_artifact",
"schema_version": "fidc.linux-cash-receivable-equity-validation/v1",
"status": "full_engine_regression_passed_not_runtime_or_strategy_acceptance",
"storage": "postgres"
}
@@ -0,0 +1,40 @@
{
"cash_entitlement_asset_included_without_spendable_cash_inference": true,
"component": "fidc-backtest-engine",
"contract_sha256": "31d7028d123a9abd2004d93534f3d8768704b2904529a3666d6146d67de36018",
"engine_commit": "d3f49a17ef284dba587607ad0ac160cc394bf677",
"ignored_benchmarks": 9,
"log_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/full-linux-tests.log",
"sha256": "7e25d69e1adec3183394a90a6c3dfab01c56982f9a4aa64afd261af69be72551"
},
"numeric_research_budget_reopened": false,
"observed_at": "2026-10-04T19:06:11.362993+00:00",
"original_numerical_research_executor": {
"ActiveState": "active",
"InvocationID": "0eb28e3a45694a00951aa3aec0e13465",
"MainPID": "1396112"
},
"owner_uid": 1003,
"payment_is_receivable_to_cash_asset_transfer_not_new_income": true,
"production_ready": false,
"production_runtime_deployed": false,
"production_strategy_count": 0,
"resource_contract": {
"build_jobs": 4,
"cpu_percent": 400,
"memory_max_bytes": 17179869184,
"memory_swap_max_bytes": 0,
"rust_toolchain": "stable-x86_64-unknown-linux-gnu",
"rustc": "1.98.0",
"target_filesystem": "executable_tmpfs",
"target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de"
},
"schema_version": "fidc.linux-cash-receivable-equity-validation/v1",
"sharedelivery_ledger_implemented": false,
"source_path": "/srv/fidc/programs/src/fidc-cash-receivable-validation-20261005-d3f49a1",
"status": "full_engine_regression_passed_not_runtime_or_strategy_acceptance",
"tests_passed": 964,
"tiny_nonzero_original_rate_preserved_and_dates_still_required": true,
"trading_allowed": false
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,258 @@
{
"verified_at": "2026-09-13T20:22:24.750379+00:00",
"tag": "v2026.9.14.4",
"processes": {
"fidc-backtest-service-highmem177.service": {
"pid": 3612875,
"sha256": "4e9f142be0ae3f9ca8e1c126507d4a9905cde4b69859df4544472afd1bda1ff2",
"journal_since": "2026-09-13T20:14:17.444770+00:00",
"journal_lines": 54,
"error_lines": 0
},
"fidc-trading-control-highmem177.service": {
"pid": 3617963,
"sha256": "cd587928591fef952f2e98b47aa338a1702def5edf016a7da9751296667f6674",
"journal_since": "2026-09-13T20:19:57.821843+00:00",
"journal_lines": 5,
"error_lines": 0
},
"fidc-market-data-highmem177.service": {
"pid": 3617964,
"sha256": "2efb1d3ad7d510cf85e6047dd6d1981d0a30d768ff3d33c842adc52211002bdc",
"journal_since": "2026-09-13T20:19:57.821843+00:00",
"journal_lines": 5,
"error_lines": 0
},
"fidc-strategy-runtime-highmem177.service": {
"pid": 3618140,
"sha256": "3d7f3f2e8756e7f3439075344fe9c8bc0b55df33e7e6f251282712274c00339d",
"journal_since": "2026-09-13T20:19:57.821843+00:00",
"journal_lines": 5,
"error_lines": 0
},
"fidc-paper-trading-highmem177.service": {
"pid": 3618260,
"sha256": "0383b1d6cc7b3c48c6902dd7fd4760a38698c1916e0eaff63be26fe3f6b1a2ab",
"journal_since": "2026-09-13T20:19:57.821843+00:00",
"journal_lines": 6,
"error_lines": 0
},
"fidc-live-trading-highmem177.service": {
"pid": 3618246,
"sha256": "583f52e204aeb416574ee17daa20cebed49e0659a194c81e8072a9249824e48e",
"journal_since": "2026-09-13T20:19:57.821843+00:00",
"journal_lines": 6,
"error_lines": 0
}
},
"source": {
"commit": "d5b682c6d097",
"pid": 1700096,
"loaded_at": "2026-09-12T03:57:06.665536+00:00",
"source_stale": false,
"loaded_server_sha256": "ef827ce6b95e0ea63047a0068af2677633716e0a5d63cf350de6c91a3413e352"
},
"source_checkouts": {
"fidc-backtest-engine": {
"head": "9a54156df94cfbf11a1e6335ec6ef5449bd6ac17",
"runtime_commit": "237ee15a518a668297959509daffc4b88995f310",
"tracked_dirty": false
},
"fidc-backtest-service": {
"head": "5ec8dc86d99736a0c0140440bd039d11e118c1c6",
"runtime_commit": "e81bf47806f5ac4ae4798bb5f5955a56638f754c",
"tracked_dirty": false
},
"fidc-trading-platform": {
"head": "dab98e0cc09793df15b8c72841a6dc7e9a58a208",
"runtime_commit": "dab98e0cc09793df15b8c72841a6dc7e9a58a208",
"tracked_dirty": false
},
"omniquant": {
"head": "6a2b2604b40505fa754453307c517fef60743426",
"runtime_commit": "6a2b2604b40505fa754453307c517fef60743426",
"tracked_dirty": false
}
},
"ui_unchanged": {
"commit": "6a2b2604b40505fa754453307c517fef60743426",
"pid": 3089476
},
"http_cases": [
{
"name": "manual_first",
"run_id": "btr_req_60612ec2af9f97df26a81c13448aec7d95b23a93f824c303",
"status": "succeeded",
"canonical_sha256": "0830216850b64d6e83291e072b31a9989f179915ee3341a75a77c73d1f9081a3",
"trade_count": 10,
"holding_count": 4,
"final_equity": 9706248.648662,
"old_result_unchanged": true
},
{
"name": "automatic_first",
"run_id": "btr_req_373da23c5ea5aaf4e59c38fbe37d663ae37731aeed8cc054",
"status": "succeeded",
"canonical_sha256": "c75cabcc03760f415bb664d20060e81c620d7a0201dd348ea71f75c932571de7",
"trade_count": 10,
"holding_count": 4,
"final_equity": 9706248.648662,
"old_result_unchanged": true
},
{
"name": "stock24",
"run_id": "btr_req_5bb965ea83e047c998ec16be656f40ec28a4e5d870aa6d74",
"status": "succeeded",
"canonical_sha256": "270b403542ab41290c3d6e027b89cdab24dd41a8e2851d8786b33daa51e0051f",
"trade_count": 51,
"holding_count": 21,
"final_equity": 9685563.876924999,
"old_result_unchanged": true
}
],
"durable_events": [
{
"run_id": "btr_req_60612ec2af9f97df26a81c13448aec7d95b23a93f824c303",
"count": 27,
"unique_keys": 27,
"days": 5
},
{
"run_id": "btr_req_373da23c5ea5aaf4e59c38fbe37d663ae37731aeed8cc054",
"count": 18,
"unique_keys": 18,
"days": 5
},
{
"run_id": "btr_req_5bb965ea83e047c998ec16be656f40ec28a4e5d870aa6d74",
"count": 32,
"unique_keys": 32,
"days": 5
},
{
"run_id": "btr_req_a3c3dfe5cd81e27e565064a65665f561c60adebdc6c9c9b4",
"count": 27,
"unique_keys": 27,
"days": 5
}
],
"trading_state": {
"paper": {
"configuration": {
"count": 3,
"hash": "93f3224edef59c381164e0236529dacc"
},
"active": {
"claims": 0,
"orders": 0
}
},
"live": {
"configuration": {
"count": 0,
"hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"active": {
"claims": 0,
"orders": 1,
"today_orders": 0,
"orders_hash": "d4b56fbf3a541a41a383ad4e48891bb8",
"route_mode": "disabled"
}
}
},
"manual_facts_unchanged": {
"paper": {
"shadow_configurations": 0,
"shadow_runs": 0,
"manual_count": 3,
"manual_hash": "82572901ac0b5fdb4d8b984f71e1763d",
"migrations_hash": "21d711b2ee52d2d66a8be4e99b179190",
"new_orders": 0
},
"live": {
"shadow_configurations": 0,
"shadow_runs": 0,
"manual_count": 0,
"manual_hash": "d41d8cd98f00b204e9800998ecf8427e",
"migrations_hash": "610528d4f350309379c9398c4ea43f66",
"new_orders": 0
}
},
"broker_submission": false,
"linux_core_tests": {
"passed": 822,
"failed": 0,
"ignored": 9,
"log": "/srv/fidc/canonical/run/fidc-private/evidence/clock-candidate-gqx8g70l/linux-core-tests.log"
},
"cleanup": {
"apply": true,
"deleted": [
{
"path": "/srv/fidc/canonical/build/holding-protection-stage-wywd2682/fidc-trading-platform/debug/incremental",
"kind": "incremental_compiler_state",
"bytes": 9553190912,
"device": 2101,
"inode": 39877787,
"mtime_ns": 1789318996850462500,
"links": 176,
"size_bytes": 12288
},
{
"path": "/srv/fidc/canonical/build/target-backtest/debug/incremental",
"kind": "incremental_compiler_state",
"bytes": 2103459840,
"device": 2101,
"inode": 29904450,
"mtime_ns": 1789318494701447400,
"links": 46,
"size_bytes": 4096
},
{
"path": "/srv/fidc/canonical/build/target-backtest/debug/deps/fidc_backtest_service-46310ff8aeeb4040",
"kind": "superseded_test_binary",
"bytes": 398401536,
"device": 2101,
"inode": 29934330,
"mtime_ns": 1789117975169420000,
"links": 1,
"size_bytes": 399242488
},
{
"path": "/srv/fidc/canonical/build/target-backtest/debug/deps/fidc_core-18c9b2429fdf6026",
"kind": "superseded_test_binary",
"bytes": 207015936,
"device": 2101,
"inode": 29918792,
"mtime_ns": 1789166943885717800,
"links": 1,
"size_bytes": 207144208
},
{
"path": "/srv/fidc/canonical/build/target-backtest/debug/deps/fidc_core-42f704a330411730",
"kind": "superseded_test_binary",
"bytes": 191205376,
"device": 2101,
"inode": 29933759,
"mtime_ns": 1789117542401109800,
"links": 1,
"size_bytes": 191333360
}
],
"reclaimed_allocated_bytes": 12453273600,
"before": {
"total": 1749269057536,
"used": 1659299954688,
"free": 1035599872
},
"after": {
"total": 1749269057536,
"used": 1647166930944,
"free": 13168623616
},
"observed_free_change": 12133023744
},
"scope": "Intraday clock release verification; historical simulation only, not a performance or real broker liquidity acceptance.",
"native_replays": 6
}
@@ -0,0 +1,40 @@
{
"source": "/srv/fidc/programs/runtime-snapshots/lot-lifecycle-20260914-AEGDYL",
"build": "/srv/fidc/canonical/build/lot-lifecycle-20260914-AEGDYL",
"commits": {
"fidc-backtest-engine": "b4c68be29b98dc814fa75641f629dd18e1dd803b",
"fidc-backtest-service": "49f280075e6b9dce2ef149fc190cfe663411b905",
"fidc-trading-platform": "ae83fd30f56a5420a235022b0a169aaf0bae55cf"
},
"source_unchanged": true,
"results": [
{
"repository": "fidc-backtest-engine",
"exit_code": 0,
"elapsed_seconds": 60.008,
"passed": 889,
"failed": 0,
"ignored": 9,
"log_sha256": "7d0760a024a564cd2c18b7bf3b41ab8ba6f95d7e259d49a43191f0ebe666989e"
},
{
"repository": "fidc-trading-platform",
"exit_code": 0,
"elapsed_seconds": 102.011,
"passed": 625,
"failed": 0,
"ignored": 63,
"log_sha256": "ab55da8d47425a0acc6c33dc4bc352f463ad4e1aac887b0169a90f18cb6763cb"
},
{
"repository": "fidc-backtest-service",
"exit_code": 0,
"elapsed_seconds": 118.011,
"passed": 589,
"failed": 0,
"ignored": 16,
"log_sha256": "ff6946b18567dad6dab41b6c36f65ea1181d7ee6b7d53b6ceb568b1dedb7abae"
}
],
"available_bytes": 94029492224
}
@@ -0,0 +1,61 @@
{
"observed_date": "2026-09-14",
"status": "tested_not_deployed",
"source_snapshot_manifest_sha256": "5560614fa2ce2b0c905c44011ff4147df1fe7c04be93cd3c1410cd16fca025de",
"immutable_source_file_count": 962,
"test_scope": "fidc-lot-lifecycle-check-20260914-AEGDYL.scope",
"test_cpu_quota_percent": 400,
"test_memory_max_gib": 12,
"source_runtime_commit": "d5b682c6d09704ff23d725a8dd8b155db3eb6967",
"source_runtime_pid": 1700096,
"source_runtime_changed": false,
"runtime_services": [
{
"service": "omniquant-highmem177.service",
"pid": 987464,
"state": "active",
"version": "v2026.9.14.8"
},
{
"service": "fidc-backtest-service-highmem177.service",
"pid": 4108679,
"state": "active",
"exe_sha256": "5933b7423c6722e5fe644a604c915a27ceb89900f690b092d1689bfe0fe389b7"
},
{
"service": "fidc-paper-trading-highmem177.service",
"pid": 3765829,
"state": "active",
"exe_sha256": "3ebd7ce815e325ff0bb38632ef5b5e5cd5aff7a4e0601ba0229feb3201cdefe3"
},
{
"service": "fidc-live-trading-highmem177.service",
"pid": 3765783,
"state": "active",
"exe_sha256": "1f4b2d697accdbb76d89cd7cb9c3ae4dad942d86e3dde2e7d211eee5879b9144"
},
{
"service": "fidc-strategy-runtime-highmem177.service",
"pid": 3765705,
"state": "active",
"exe_sha256": "5a160ba9588f0ba9275f7d33152667be176cece310fc3026d4c043d897d44b4c"
},
{
"service": "fidc-trading-control-highmem177.service",
"pid": 3765532,
"state": "active",
"exe_sha256": "9feade5eed04007595f6ef88e076a3f501631b06cb6dc9d9b6d7f5be379b0a20"
}
],
"engine_development_checkout": "b4c68be29b98dc814fa75641f629dd18e1dd803b",
"preserved_service_development_checkout": "5991e7733d9e2a770f740e971924b12cc5f7d29d",
"production_order_or_cancel_requests_by_this_verification": 0,
"production_task_configuration_writes_by_this_verification": 0,
"remaining": [
"Source freeze authorization and physical partition validation",
"Actual Source and Runner shadow joint acceptance",
"Unaccepted Arrow performance candidate gate",
"Delayed observations spanning already applied corporate actions and remaining cross-mode matrix"
],
"source_runtime_tracked_clean": true
}
@@ -0,0 +1,40 @@
{
"source": "/srv/fidc/programs/runtime-snapshots/corporate-replay-20260914-ZAnvlO",
"build": "/srv/fidc/canonical/build/corporate-replay-20260914-ZAnvlO",
"commits": {
"fidc-backtest-engine": "05f1cbbe00ffe75b72de8a24cea6a4e40e8142b0",
"fidc-backtest-service": "879a5a7fe07620415b00023f4628cc81e45903fb",
"fidc-trading-platform": "559f5b1d24d98a67606e859a1f2105bee4c0bc01"
},
"source_unchanged": true,
"results": [
{
"repository": "fidc-backtest-engine",
"exit_code": 0,
"elapsed_seconds": 58.007,
"passed": 895,
"failed": 0,
"ignored": 9,
"log_sha256": "d50fc04699eaa39399016e3b9f470e9adf3e8da0d968ada2e212ef606bdfb37a"
},
{
"repository": "fidc-trading-platform",
"exit_code": 0,
"elapsed_seconds": 94.009,
"passed": 625,
"failed": 0,
"ignored": 63,
"log_sha256": "eafd3d792dc54ff90efaae9aa1e9e0c15bb191f8b679585c982b8c1bab3211ad"
},
{
"repository": "fidc-backtest-service",
"exit_code": 0,
"elapsed_seconds": 148.015,
"passed": 591,
"failed": 0,
"ignored": 16,
"log_sha256": "2ae31905c43697177af50825270d4f7830361d6e1d5e3d22a26179dd726abe36"
}
],
"available_bytes": 90635833344
}
@@ -0,0 +1,61 @@
{
"observed_date": "2026-09-14",
"status": "tested_not_deployed",
"source_snapshot_manifest_sha256": "9b61a3586248c80bac30ce2107c9323a4d9a1ac7bdf29a42d3c423bd5c615ec4",
"immutable_source_file_count": 972,
"test_scope": "fidc-corporate-replay-check-20260914-ZAnvlO.scope",
"test_cpu_quota_percent": 400,
"test_memory_max_gib": 12,
"source_runtime_commit": "d5b682c6d09704ff23d725a8dd8b155db3eb6967",
"source_runtime_pid": 1700096,
"source_runtime_changed": false,
"runtime_services": [
{
"service": "omniquant-highmem177.service",
"pid": 987464,
"state": "active",
"version": "v2026.9.14.8"
},
{
"service": "fidc-backtest-service-highmem177.service",
"pid": 4108679,
"state": "active",
"exe_sha256": "5933b7423c6722e5fe644a604c915a27ceb89900f690b092d1689bfe0fe389b7"
},
{
"service": "fidc-paper-trading-highmem177.service",
"pid": 3765829,
"state": "active",
"exe_sha256": "3ebd7ce815e325ff0bb38632ef5b5e5cd5aff7a4e0601ba0229feb3201cdefe3"
},
{
"service": "fidc-live-trading-highmem177.service",
"pid": 3765783,
"state": "active",
"exe_sha256": "1f4b2d697accdbb76d89cd7cb9c3ae4dad942d86e3dde2e7d211eee5879b9144"
},
{
"service": "fidc-strategy-runtime-highmem177.service",
"pid": 3765705,
"state": "active",
"exe_sha256": "5a160ba9588f0ba9275f7d33152667be176cece310fc3026d4c043d897d44b4c"
},
{
"service": "fidc-trading-control-highmem177.service",
"pid": 3765532,
"state": "active",
"exe_sha256": "9feade5eed04007595f6ef88e076a3f501631b06cb6dc9d9b6d7f5be379b0a20"
}
],
"engine_development_checkout": "05f1cbbe00ffe75b72de8a24cea6a4e40e8142b0",
"preserved_service_development_checkout": "5991e7733d9e2a770f740e971924b12cc5f7d29d",
"production_order_or_cancel_requests_by_this_verification": 0,
"production_task_configuration_writes_by_this_verification": 0,
"remaining": [
"Source freeze authorization and physical partition validation",
"Actual Source and Runner shadow joint acceptance",
"Unaccepted Arrow performance candidate gate",
"Dividend reinvestment execution clocks, successor data scope, target state and cross-mode ETF matrix"
],
"source_runtime_tracked_clean": true
}
@@ -0,0 +1,40 @@
{
"source": "/srv/fidc/programs/runtime-snapshots/zero-reinvestment-20260915-Yovt6m",
"build": "/srv/fidc/canonical/build/zero-reinvestment-20260915-Yovt6m",
"commits": {
"fidc-backtest-engine": "818552bc969e0ea40081f0770c1d97db574a1981",
"fidc-backtest-service": "234d85bdb8ed12b552ba8e9eff47fddc5e683acc",
"fidc-trading-platform": "d7d492541e60eb16e58dbff74b664b8ac85f6ccf"
},
"source_unchanged": true,
"results": [
{
"repository": "fidc-backtest-engine",
"exit_code": 0,
"elapsed_seconds": 56.008,
"passed": 919,
"failed": 0,
"ignored": 9,
"log_sha256": "87a4daa4db1c0f735399ce243feff8e514e5f75f72b0dbfdb69da0c21d02f2b4"
},
{
"repository": "fidc-trading-platform",
"exit_code": 0,
"elapsed_seconds": 90.029,
"passed": 625,
"failed": 0,
"ignored": 63,
"log_sha256": "e7f15eb7463f1cc33b7122020549af082d2070ad38392d0918c9c7309a68cc26"
},
{
"repository": "fidc-backtest-service",
"exit_code": 0,
"elapsed_seconds": 112.014,
"passed": 594,
"failed": 0,
"ignored": 16,
"log_sha256": "4faa52e9a87056c192341622d0273942f3daf963cd75a381ad913c76d47aed29"
}
],
"available_bytes": 77131395072
}
@@ -0,0 +1,76 @@
{
"date": "2026-09-15",
"baseline_engine": "534ab42906c7e7a4e253b7791cc143dec894a7f2",
"candidate_engine": "818552bc969e0ea40081f0770c1d97db574a1981",
"reproduced": {
"manual_zero_effective_before_payment": true,
"original_position_cleared": true,
"unexpected_accounting_allocation": {
"symbol": "000001.SZ",
"quantity": 100,
"price": 8.95
},
"original_gross_output": 894.9999999999999,
"actual_posted_amount": 895
},
"verified": {
"global_set_and_scale_zero_leave_cash": true,
"zero_or_excluded_manual_member_leaves_cash": true,
"same_instant_uses_sequence": true,
"input_array_order_does_not_change_result": true,
"future_settings_are_not_backdated": true,
"restore_does_not_mean_forced_full_exposure": true,
"legacy_day_granularity_preserved": true,
"normal_settlement_and_late_replay_share_control": true,
"actual_manual_fills_and_cash_entitlements_not_erased": true,
"invalid_cash_is_rejected_atomically": true,
"unused_reinvestment_reference_not_read_under_zero": true,
"no_control_keeps_legacy_price_quantity_and_fees": true
},
"exact_outcomes": {
"cleared_with_zero": {
"cash": 49998,
"quantity": 0
},
"cleared_without_zero": {
"cash": 49103,
"quantity": 100,
"price": 8.95,
"fee": 0,
"gross": 895
},
"actual_extra_buy_timely_and_late": {
"cash": 41047,
"quantity": 1000,
"corporate_cash_adjustment": 1050,
"optional_allocations": 0
}
},
"local_tests": {
"core": {
"passed": 919,
"ignored": 9
},
"trading": {
"passed": 625,
"ignored": 63
},
"runner": {
"passed": 463,
"ignored": 9
},
"api": {
"passed": 129,
"ignored": 7
}
},
"new_test_functions": 6,
"production_runtime_modified": false,
"production_order_or_cancel_requests": 0,
"limits": [
"isolated replay not actual Source/Runner acceptance",
"does not evaluate arbitrary strategy expressions at settlement",
"Source runtime freeze unchanged",
"no new production UI or broker validation"
]
}
@@ -0,0 +1,53 @@
{
"observed_at": "2026-09-14T16:34:44.320112+00:00",
"status": "tested_not_deployed",
"runtime_services": [
{
"service": "omniquant-highmem177.service",
"pid": 987464,
"state": "active"
},
{
"service": "fidc-backtest-service-highmem177.service",
"pid": 4108679,
"state": "active",
"exe_sha256": "5933b7423c6722e5fe644a604c915a27ceb89900f690b092d1689bfe0fe389b7"
},
{
"service": "fidc-paper-trading-highmem177.service",
"pid": 3765829,
"state": "active",
"exe_sha256": "3ebd7ce815e325ff0bb38632ef5b5e5cd5aff7a4e0601ba0229feb3201cdefe3"
},
{
"service": "fidc-live-trading-highmem177.service",
"pid": 3765783,
"state": "active",
"exe_sha256": "1f4b2d697accdbb76d89cd7cb9c3ae4dad942d86e3dde2e7d211eee5879b9144"
},
{
"service": "fidc-strategy-runtime-highmem177.service",
"pid": 3765705,
"state": "active",
"exe_sha256": "5a160ba9588f0ba9275f7d33152667be176cece310fc3026d4c043d897d44b4c"
},
{
"service": "fidc-trading-control-highmem177.service",
"pid": 3765532,
"state": "active",
"exe_sha256": "9feade5eed04007595f6ef88e076a3f501631b06cb6dc9d9b6d7f5be379b0a20"
}
],
"source_runtime_commit": "d5b682c6d09704ff23d725a8dd8b155db3eb6967",
"source_runtime_tracked_clean": true,
"engine_development_checkout": "818552bc969e0ea40081f0770c1d97db574a1981",
"preserved_service_development_checkout": "5991e7733d9e2a770f740e971924b12cc5f7d29d",
"immutable_source_file_count": 989,
"source_snapshot_manifest_sha256": "9ffdc1bc86e1b4b085caa694e921fc230ce0f09c57d34254058ae28e631d5bb3",
"test_scope": "fidc-zero-reinvestment-check-20260915-Yovt6m.scope",
"test_cpu_quota_percent": 400,
"test_memory_max_gib": 12,
"source_runtime_modified": false,
"production_order_or_cancel_requests_by_this_verification": 0,
"production_task_configuration_writes_by_this_verification": 0
}
@@ -0,0 +1,40 @@
{
"source": "/srv/fidc/programs/runtime-snapshots/opening-clock-20260914-UUx5ru",
"build": "/srv/fidc/canonical/build/opening-clock-20260914-UUx5ru",
"commits": {
"fidc-backtest-engine": "13c89e8d59f21df6280d722f369d0ec8d9a6457e",
"fidc-backtest-service": "49f280075e6b9dce2ef149fc190cfe663411b905",
"fidc-trading-platform": "ae83fd30f56a5420a235022b0a169aaf0bae55cf"
},
"source_unchanged": true,
"results": [
{
"repository": "fidc-backtest-engine",
"exit_code": 0,
"elapsed_seconds": 56.007,
"passed": 885,
"failed": 0,
"ignored": 9,
"log_sha256": "9a887a23fc656d0f448502ecb2fca5fcfd82db15346f567e6a8b0bcaa88180d7"
},
{
"repository": "fidc-trading-platform",
"exit_code": 0,
"elapsed_seconds": 90.054,
"passed": 625,
"failed": 0,
"ignored": 63,
"log_sha256": "048a7690ce874bccc58e3111decd183c3f88f649397c59cdfbaa3e4533662589"
},
{
"repository": "fidc-backtest-service",
"exit_code": 0,
"elapsed_seconds": 118.011,
"passed": 589,
"failed": 0,
"ignored": 16,
"log_sha256": "ec4f51b30dd69d9c86812cbb1b1e310c277fdfcf29465978b8d87eb83f40204c"
}
],
"available_bytes": 97373085696
}
@@ -0,0 +1,60 @@
{
"observed_date": "2026-09-14",
"status": "tested_not_deployed",
"source_snapshot_manifest_sha256": "bf01cd040bdcbeaa0c81a93153eb8e505dd7dcb8c761731f7f3ed81b1b2b6b67",
"immutable_source_file_count": 959,
"test_scope": "fidc-opening-clock-check-20260914-UUx5ru.scope",
"test_cpu_quota_percent": 400,
"test_memory_max_gib": 12,
"source_runtime_commit": "d5b682c6d09704ff23d725a8dd8b155db3eb6967",
"source_runtime_pid": 1700096,
"source_runtime_changed": false,
"runtime_services": [
{
"service": "omniquant-highmem177.service",
"pid": 987464,
"state": "active",
"version": "v2026.9.14.8"
},
{
"service": "fidc-backtest-service-highmem177.service",
"pid": 4108679,
"state": "active",
"exe_sha256": "5933b7423c6722e5fe644a604c915a27ceb89900f690b092d1689bfe0fe389b7"
},
{
"service": "fidc-paper-trading-highmem177.service",
"pid": 3765829,
"state": "active",
"exe_sha256": "3ebd7ce815e325ff0bb38632ef5b5e5cd5aff7a4e0601ba0229feb3201cdefe3"
},
{
"service": "fidc-live-trading-highmem177.service",
"pid": 3765783,
"state": "active",
"exe_sha256": "1f4b2d697accdbb76d89cd7cb9c3ae4dad942d86e3dde2e7d211eee5879b9144"
},
{
"service": "fidc-strategy-runtime-highmem177.service",
"pid": 3765705,
"state": "active",
"exe_sha256": "5a160ba9588f0ba9275f7d33152667be176cece310fc3026d4c043d897d44b4c"
},
{
"service": "fidc-trading-control-highmem177.service",
"pid": 3765532,
"state": "active",
"exe_sha256": "9feade5eed04007595f6ef88e076a3f501631b06cb6dc9d9b6d7f5be379b0a20"
}
],
"engine_development_checkout": "13c89e8d59f21df6280d722f369d0ec8d9a6457e",
"preserved_service_development_checkout": "5991e7733d9e2a770f740e971924b12cc5f7d29d",
"production_order_or_cancel_requests_by_this_verification": 0,
"production_task_configuration_writes_by_this_verification": 0,
"remaining": [
"Source freeze authorization and physical partition validation",
"Actual Source and Runner shadow joint acceptance",
"Unaccepted Arrow performance candidate gate",
"Remaining corporate-action, holding-protection and full parameter matrix"
]
}
@@ -0,0 +1,11 @@
{
"files": [
{
"bytes": 827,
"file": "validation-probe-assembly-failure.json",
"sha256": "7e648d3fc3d3e77b6ce5c30f1c7dc49035d0a0467e433ea2afd0ed2256fe451f"
}
],
"original_manifest_sha256": "e26c6bb619b9d0f0183983da3e8c030dfc61e610cf0b3a4c2311d3525cc85355",
"schema_version": "fidc.additional-share-delivery-evidence/v1"
}
@@ -0,0 +1,853 @@
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-receivable-validation-20261005-0caac44/crates/fidc-core)
Finished `test` profile [unoptimized] target(s) in 41.35s
Running unittests src/lib.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/fidc_core-dbea4d4f6cb5d2d7)
running 786 tests
test broker::tests::aiquant_rules_keep_structured_buy_risk_while_using_aiquant_limit_price ... ok
test broker::tests::aiquant_sell_rule_still_blocks_real_allow_sell_false ... ok
test broker::tests::aiquant_sell_rule_uses_intraday_quote_when_daily_snapshot_is_stale_lower_limit ... ok
test broker::tests::aiquant_target_portfolio_smart_keeps_batch_semantics_after_failed_full_close_sell ... ok
test broker::tests::aiquant_target_portfolio_smart_rejects_deferred_buy_risk_at_order_stage ... ok
test broker::stock_pool::successor_protection_tests::deferred_etf_open_rechecks_inherited_locks_and_cooldown_before_any_order ... ok
test broker::tests::aiquant_target_value_delta_uses_scheduled_mark_price ... ok
test broker::tests::algorithm_clock::algorithm_expiry_without_a_quote_does_not_reuse_old_liquidity ... ok
test broker::tests::algorithm_clock::a_clock_slice_does_not_turn_window_twap_into_an_unlimited_instant_order ... ok
test broker::tests::algorithm_clock::algorithm_sell_honors_t_plus_one_and_keeps_original_quantity_after_partial_fills ... ok
test broker::tests::algorithm_clock::an_explicit_ioc_or_fok_does_not_become_a_persistent_algorithm ... ok
test broker::tests::algorithm_clock::control_only_phase_cannot_be_used_to_submit_an_order_or_leave_matching_disabled ... ok
test broker::tests::algorithm_clock::non_matching_controls_amend_or_cancel_without_filling_a_crossing_quote ... ok
test broker::tests::algorithm_clock::changing_the_later_daily_close_does_not_resize_an_algorithm_submitted_now ... ok
test broker::tests::algorithm_clock::global_vwap_matching_keeps_the_same_working_order_between_clock_ticks ... ok
test broker::tests::algorithm_clock::partial_algorithm_cancel_releases_reservation_and_never_executes_the_remainder ... ok
test broker::tests::algorithm_clock::separate_buy_cannot_spend_the_working_algorithms_cash_budget ... ok
test broker::tests::algorithm_clock::two_working_algorithms_reserve_only_real_cash_without_starving_the_first ... ok
test broker::tests::complete_daily_target_batch_uses_positive_target_count_for_slot_limit ... ok
test broker::tests::daily_capacity_requires_a_timed_observation_instead_of_falling_back_to_total_volume ... ok
test broker::tests::algorithm_clock::twap_clock_preserves_quantity_prices_fees_budget_and_parent_order ... ok
test broker::tests::daily_matching_does_not_require_intraday_quote_quantity ... ok
test broker::tests::custom_valuation_missing_with_current_market_still_fails_closed ... ok
test broker::tests::decision_buy_denial_allows_reducing_an_existing_buy ... ok
test broker::tests::algorithm_clock::vwap_clock_preserves_cash_costs_and_does_not_spend_future_volume ... ok
test broker::tests::decision_buy_denial_does_not_rewrite_existing_pending_order ... ok
test broker::tests::decision_buy_denial_blocks_topup_but_allows_sell_and_does_not_leak ... ok
test broker::tests::daily_session_volume_changes_only_audit_not_opening_fills ... ok
test broker::tests::decision_buy_denial_rejects_increasing_amendments_without_mutation ... ok
test broker::tests::each_execution_leg_rechecks_one_yuan_including_slippage_and_limit_price ... ok
test broker::tests::explicit_quote_price_models_do_not_replace_missing_prices_or_reuse_older_ones ... ok
test broker::tests::decision_buy_denial_uses_actual_next_open_target_delta ... ok
test broker::tests::failed_target_exit_blocks_replacement_entry_when_no_position_slot_is_released ... ok
test broker::tests::gtc_resting_order_keeps_its_session_and_original_dates_across_days ... ok
test broker::tests::deferred_etf_batch_failure_keeps_both_targets_and_prior_generation_progress ... ok
test broker::tests::failed_new_batch_does_not_erase_prior_success_or_double_charge_on_retry ... ok
test broker::tests::historical_slippage_rejects_missing_future_or_invalid_calibration_without_raw_price_fallback ... ok
test broker::tests::instantaneous_twap_without_limits_does_not_cap_quote_quantity ... ok
test broker::tests::intraday_execution_rejects_buy_at_upper_limit_price ... ok
test broker::tests::independent_target_entry_is_not_subject_to_replacement_slot_limit ... ok
test broker::tests::intraday_execution_rejects_sell_at_lower_limit_price ... ok
test broker::tests::later_execution_clocks_do_not_replenish_the_same_observed_volume ... ok
test broker::tests::minute_last_exact_time_does_not_fallback_to_older_liquid_quote ... ok
test broker::tests::minute_last_execution_uses_latest_quote_before_decision_time ... ok
test broker::tests::market_share_buy_skips_quantity_that_rounds_below_one_lot ... ok
test broker::tests::minute_last_prefilter_does_not_use_snapshot_minute_volume ... ok
test broker::tests::minute_last_quote_quantity_cap_depends_on_liquidity_limit ... ok
test broker::tests::minute_last_uses_volume_delta_when_level1_depth_missing ... ok
test broker::tests::minute_last_volume_limit_caps_quote_volume_delta_without_liquidity_limit ... ok
test broker::tests::minute_last_volume_limit_rejects_sub_lot_quote_volume ... ok
test broker::tests::minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity ... ok
test broker::tests::historical_slippage_does_not_read_later_daily_fields_for_open_or_minute_fills ... ok
test broker::tests::next_open_buy_limit_risk_rejects_open_upper_limit_even_when_close_is_clean ... ok
test broker::tests::next_open_buy_limit_risk_uses_open_not_close ... ok
test broker::tests::next_open_buy_risk_rejects_execution_date_delisted_even_without_market_snapshot ... ok
test broker::tests::next_open_buy_risk_uses_execution_date_not_signal_date ... ok
test broker::tests::next_open_day_limit_remainder_matches_intraday_with_execution_day_ttl ... ok
test broker::tests::next_open_sell_limit_risk_rejects_open_lower_limit_even_when_close_is_clean ... ok
test broker::tests::next_open_buy_risk_rejects_execution_date_st_even_if_signal_date_was_clean ... ok
test broker::tests::next_open_sell_limit_risk_uses_open_not_close ... ok
test broker::tests::next_open_sell_risk_rejects_execution_date_lower_limit_even_if_signal_date_was_clean ... ok
test broker::tests::next_open_sell_risk_uses_execution_date_not_signal_date ... ok
test broker::tests::next_open_target_portfolio_unpriced_symbol_does_not_abort_batch ... ok
test broker::tests::next_open_target_value_valuation_uses_execution_open ... ok
test broker::tests::next_open_target_value_recomputes_quantity_from_execution_open ... ok
test broker::tests::next_open_unpriced_active_snapshot_rejects_without_historical_fill ... ok
test broker::tests::open_order_upsert_replaces_in_place_and_preserves_queue_position ... ok
test broker::tests::next_open_with_observations_uses_opening_volume_not_daily_totals_or_future_quotes ... ok
test broker::tests::next_open_unpriced_paused_snapshot_prefers_execution_day_pause_risk ... ok
test broker::tests::post_close_phase_is_derived_from_actual_same_day_submission_time ... ok
test broker::tests::post_close_order_uses_close_without_slippage_and_waits_until_matching_window ... ok
test broker::tests::queued_order_retains_the_real_creation_clock_when_retried ... ok
test broker::tests::session_audit_does_not_infer_an_opening_suspension_from_future_zero_volume ... ok
test broker::tests::scheduled_target_value_valuation_and_sizing_use_same_intraday_price ... ok
test broker::tests::session_capacity_audit_never_caps_an_early_odd_lot_sell ... ok
test broker::tests::public_callback_unwind_does_not_leak_order_context_or_authoritative_prior_state ... ok
test broker::tests::stock_pool_after_sell_rejects_missing_quote_instead_of_reusing_daily_open ... ok
test broker::tests::stock_pool_after_sell_uses_fresh_quotes_and_actual_submission_clock ... ok
test broker::tests::stock_pool_delayed_sell_does_not_start_buys_after_the_configured_window ... ok
test broker::tests::stock_pool_delayed_take_profit_does_not_rebuy_the_same_generation_exit ... ok
test broker::tests::stock_pool_new_signal_supersedes_the_unsubmitted_buy_phase ... ok
test broker::tests::stock_pool_engine_drives_the_pending_buy_without_a_minute_strategy_callback ... ok
test broker::tests::stock_pool_pending_phase_cannot_cross_the_execution_session ... ok
test broker::tests::stock_pool_partial_sell_waits_for_the_whole_batch_and_never_reissues_buys ... ok
test broker::tests::strict_value_buy_budget_includes_commission_at_execution_price ... ok
test broker::tests::successful_target_exit_releases_position_slot_for_replacement_entry ... ok
test broker::tests::stock_pool_pending_sell_continues_buy_after_actual_fill_without_strategy_rerun ... ok
test broker::tests::target_percent_reduction_from_full_to_twenty_percent_sells_eighty_percent ... ok
test broker::tests::target_portfolio_smart_budgets_each_buy_before_cash_optimization ... ok
test broker::tests::target_portfolio_smart_custom_valuation_does_not_create_limit_orders ... ok
test broker::tests::target_portfolio_smart_defers_buy_risk_until_order_validation ... ok
test broker::tests::target_portfolio_smart_ignores_zero_weight_symbols_without_market_snapshot ... ok
test broker::tests::target_portfolio_smart_keeps_position_when_sell_snapshot_missing ... ok
test broker::tests::target_portfolio_smart_open_auction_uses_day_open_for_valuation ... ok
test broker::tests::target_portfolio_smart_records_buy_rejection_when_target_is_blacklisted ... ok
test broker::tests::target_portfolio_smart_pre_open_cash_does_not_spend_same_batch_sell_proceeds ... ok
test broker::tests::target_portfolio_smart_uses_prior_close_only_for_missing_day_valuation ... ok
test broker::tests::target_value_delta_below_order_step_is_audited_without_creating_zero_quantity_order ... ok
test broker::tests::target_value_batch_executes_reductions_before_increases_when_sell_cash_is_reusable ... ok
test broker::tests::target_value_reduction_from_full_to_twenty_percent_sells_eighty_percent ... ok
test broker::tests::target_value_zero_rejects_sell_when_market_snapshot_missing ... ok
test broker::tests::target_weight_buy_quantity_respects_per_symbol_budget_after_slippage_and_fees ... ok
test broker::tests::twap_earlier_fills_do_not_depend_on_later_quote_liquidity_or_count ... ok
test broker::tests::timed_target_value_zero_sells_full_position_at_requested_time_quote ... ok
test broker::tests::unchanged_targets_do_not_create_zero_quantity_orders ... ok
test broker::tests::unwinding_an_unpublished_simulator_transaction_restores_its_state ... ok
test broker::tests::value_buy_rejects_when_slippage_clamps_execution_price_to_upper_limit ... ok
test broker::tests::value_buy_process_uses_latest_quote_before_decision_time ... ok
test corporate_book::tests::closing_register_cannot_be_replaced_by_later_holdings ... ok
test broker::tests::failed_resting_order_batch_keeps_accepted_orders_and_unpublished_financial_state ... ok
test corporate_book::tests::a_late_corporate_batch_failure_keeps_prior_cash_positions_and_notes ... ok
test corporate_book::tests::dividend_receivable_survives_a_flat_position_without_becoming_spendable_cash ... ok
test corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta ... ok
test corporate_book::tests::explicitly_delayed_dividend_remains_receivable_until_actual_payment ... ok
test corporate_book::tests::entitlement_survives_complete_sale_and_a_successor_unit_conversion ... ok
test corporate_book::tests::flat_position_retirement_does_not_change_issued_lot_state_or_next_purchase_origin ... ok
test corporate_book::tests::manual_zero_skips_only_unused_allocation_facts_not_invalid_cash_evidence ... ok
test corporate_book::tests::missing_accounting_reference_is_atomic_not_a_silent_cash_only_success ... ok
test corporate_book::tests::invalid_delivery_negative_rates_and_unresolved_fractional_claims_are_atomic ... ok
test corporate_book::tests::missing_or_backward_cash_payment_date_never_updates_the_account ... ok
test corporate_book::tests::missing_record_date_or_unobserved_closing_book_is_atomic ... ok
test corporate_book::tests::per_share_precision_is_retained_until_the_registered_cash_amount_is_rounded ... ok
test corporate_book::tests::post_record_buy_cannot_receive_the_earlier_dividend ... ok
test corporate_book::tests::post_record_purchase_does_not_inherit_registered_shares ... ok
test corporate_book::tests::registered_shares_are_valued_but_not_delivered_or_sellable_before_the_original_date ... ok
test corporate_book::tests::sale_after_registration_preserves_cash_and_excludes_new_lots_from_cost_adjustment ... ok
test corporate_book::tests::terminated_security_keeps_paid_cash_and_is_not_recreated_by_reinvestment ... ok
test corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security ... ok
test corporate_book::tests::verified_conversion_preserves_lots_without_creating_orders_or_fills ... ok
test corporate_book::tests::tiny_known_cash_rate_is_preserved_and_cannot_waive_payment_or_record_evidence ... ok
test cost::tests::cost_model_can_use_configurable_stamp_tax_change_date ... ok
test cost::tests::default_quantizes_fees_to_micro_yuan ... ok
test cost::tests::fund_fees_use_admitted_instrument_type_and_share_the_order_commission_budget ... ok
test daily_patterns::tests::daily_patterns_adjusts_all_prices_not_volume ... ok
test daily_patterns::tests::breakout_retest_needs_a_later_retest_not_the_breakout_candle_itself ... ok
test corporate_book::tests::invalid_successor_terms_cannot_be_replaced_with_one_share_or_zero_cash ... ok
test daily_patterns::tests::daily_patterns_flat_decimal_prices_do_not_create_a_sell_signal ... ok
test daily_patterns::tests::daily_patterns_helper_literal_preserves_parameters ... ok
test daily_patterns::tests::daily_patterns_rejects_future_and_duplicate_bars ... ok
test daily_patterns::tests::daily_patterns_all_templates_and_score_absence ... ok
test daily_patterns::tests::daily_patterns_no_missing_data_fallback ... ok
test daily_patterns::tests::limit_consolidation_requires_real_limit_and_never_infers_ten_percent ... ok
test daily_patterns::tests::mean_volume_followup_requires_bullish_breakout_and_shrink ... ok
test daily_patterns::tests::mean_volume_is_not_prior_max_and_excludes_current_bar ... ok
test daily_patterns::tests::expression_condition_preserves_native_types_and_rejects_numeric_as_signal ... ok
test daily_patterns::tests::research_index_and_ranking_context_never_unlock_strategy_mapping ... ok
test data::series_columns::tests::differing_prices_preserve_zero_nan_payloads_and_prior_rows ... ok
test data::series_columns::tests::identical_prices_share_only_after_exact_bit_comparison ... ok
test daily_patterns::tests::research_isolates_missing_listing_day_without_weakening_execution ... ok
test data::series_columns::tests::optional_values_keep_none_distinct_from_empty_and_repeated_text ... ok
test data::series_columns::tests::repeated_values_preserve_nonzero_values_and_copy_on_change ... ok
test daily_patterns::tests::research_numeric_output_keeps_warmup_unknown_without_a_false_signal ... ok
test data::series_columns::tests::repeated_values_reject_out_of_range_access - should panic ... ok
test data::tests::adjusted_close_average_normalization_prevents_strict_crossover_drift ... ok
test data::tests::baseline_selection_uses_dated_lifecycle_not_latest_undated_status ... ok
test data::tests::benchmark_current_rolling_reuses_symbol_boundary ... ignored, manual release-mode current rolling boundary benchmark
test data::tests::benchmark_daily_snapshot_view_lookup ... ignored, manual component benchmark
test data::tests::additional_terminal_calendar_dates_are_isolated_from_shared_market_data ... ok
test data::tests::benchmark_decision_close_windows_exclude_current_close ... ok
test data::tests::benchmark_instrument_symbol_id_lookup ... ignored, manual release-mode instrument lookup benchmark
test data::tests::benchmark_series_boundary_reuses_calendar_index ... ignored, manual release-mode calendar index reuse benchmark
test data::tests::batched_standard_rolling_means_match_scalar_lookups ... ok
test data::tests::daily_bundle_constructor_rejects_duplicate_or_mismatched_dates ... ok
test data::tests::current_close_average_uses_backward_adjustment_factor_and_current_base ... ok
test data::tests::day_open_moving_average_uses_its_own_historical_column ... ok
test data::tests::dataset_clone_shares_immutable_base_and_isolates_execution_quotes ... ok
test data::tests::decision_close_average_ignores_current_day_close ... ok
test data::tests::decision_market_cap_uses_factor_date_snapshot_without_price_reconstruction ... ok
test data::tests::decision_volume_average_ignores_paused_zero_volume_days ... ok
test data::tests::decision_volume_average_uses_previous_completed_days_only ... ok
test data::tests::daily_bundle_constructor_matches_flat_component_constructor ... ok
test data::tests::eligible_universe_can_use_configured_risk_policy ... ok
test data::tests::eligible_universe_does_not_require_candidate_risk_state_when_selection_risk_is_disabled ... ok
test data::tests::direct_symbol_id_snapshot_lookups_preserve_alignment_for_sparse_rows ... ok
test data::tests::eligible_universe_uses_decision_market_cap_same_date ... ok
test data::tests::factor_numeric_value_normalizes_fields_without_changing_aliases ... ok
test data::tests::factor_snapshot_normalization_moves_clean_maps_and_repairs_dirty_maps ... ok
test data::tests::factor_snapshot_rejects_legacy_or_invalid_adjustment_storage ... ok
test data::tests::execution_quotes_use_stable_k_way_merge_and_release_by_date ... ok
test data::tests::intraday_overlay_fails_closed_when_daily_panel_is_shared ... ok
test data::tests::future_missing_adjustment_factor_does_not_invalidate_historical_window ... ok
test data::tests::parallel_dense_index_rejects_invalid_days_without_publishing_partial_index ... ok
test data::tests::replacing_execution_quotes_preserves_duplicate_timestamp_rows ... ok
test data::tests::parallel_daily_symbol_indices_match_scalar_for_sparse_and_empty_days ... ok
test data::tests::series_end_position_index_preserves_decision_and_current_boundaries ... ok
test data::tests::series_columns_preserve_full_snapshots_and_distinct_price_bits ... ok
test data::tests::series_overlay_materializes_only_changed_values_and_preserves_history_cutoff ... ok
test data::tests::shared_execution_quote_release_does_not_clone_the_base_map ... ok
test data::tests::symbol_id_union_preserves_lexical_order_and_all_component_sources ... ok
test data::tests::symbol_price_series_test_constructor_sorts_unsorted_rows ... ok
test data::tests::source_volume_contract_rejects_windows_containing_missing_values ... ok
test data::tests::unique_dataset_applies_sparse_intraday_overlay_to_daily_and_symbol_views ... ok
test decision_projection::tests::exact_projection_coverage_and_classified_absence_are_receipted ... ok
test decision_projection::tests::projection_contract_does_not_shadow_core_market_or_typed_fields ... ok
test data::tests::volume_rolling_ignores_zero_volume_rows_for_source_and_legacy_data ... ok
test decision_projection::tests::projection_rejects_future_drift_or_unbound_receipts ... ok
test decision_projection::tests::projection_rejects_incomplete_duplicate_extra_and_overlapping_population ... ok
test decision_projection::tests::projection_rejects_nonfinite_wrong_fields_and_unproven_absence ... ok
test data::tests::parallel_daily_bundle_validation_keeps_earliest_error_and_component_order ... ok
test engine::tests::backtest_execution_dates_match_sparse_lagged_equity_schedule ... ok
test engine::tests::after_trading_cancel_is_executed_as_a_control_not_a_next_session_order ... ok
test engine::tests::compact_progress_keeps_counts_without_event_payload_clones ... ok
test engine::tests::current_bar_close_uses_decision_day_close_for_fill ... ok
test engine::tests::decision_projection_does_not_silently_shift_a_start_without_prior_inputs ... ok
test engine::tests::current_close_order_at_1500_loads_and_uses_post_close_matching_window ... ok
test engine::tests::decision_projection_failure_never_reaches_strategy_and_does_not_blindly_retry ... ok
test engine::tests::compact_manual_progress_keeps_exact_applications_and_fallible_delivery_stops_the_run ... ok
test engine::tests::decision_projection_rejects_future_response_before_any_strategy_callback ... ok
test engine::tests::decision_projection_is_called_before_callbacks_and_is_not_shared_or_carried_forward ... ok
test engine::tests::decision_projection_rejects_missing_calendar_inputs_instead_of_dropping_a_session ... ok
test engine::tests::decision_projection_refuses_static_scores_and_same_session_inputs ... ok
test engine::tests::decision_projection_requested_first_session_uses_visible_preperiod_inputs ... ok
test engine::tests::default_close_phase_waits_for_close_and_callbacks_receive_completed_trade_history ... ok
test engine::tests::early_pre_open_schedules_observe_manual_receipts_in_chronological_order ... ok
test engine::tests::empty_manual_source_does_not_retime_natural_strategy_execution ... ok
test engine::tests::join_text_parts_matches_vec_join_contract ... ok
test engine::tests::final_day_late_intents_are_retained_without_orders_or_fills_past_the_end ... ok
test engine::tests::full_minute_coverage_rejects_missing_active_bars_but_allows_paused_or_zero_volume ... ok
test engine::tests::lagged_signal_callbacks_keep_signal_date_but_process_notifications_use_execution_date ... ok
test engine::tests::late_day_callbacks_read_the_actual_earlier_trade_and_keep_their_current_clock ... ok
test engine::tests::late_intents_do_not_wait_for_another_selection_snapshot ... ok
test engine::tests::late_complete_targets_use_latest_target_and_next_day_zero_supersedes_them ... ok
test engine::tests::late_imperative_orders_execute_once_in_next_session_with_original_dates_and_new_price ... ok
test engine::tests::late_opening_callback_is_interleaved_with_earlier_quotes_and_manual_observations ... ok
test engine::tests::lifecycle_quote_filter_skips_only_dated_legal_absence_before_loading ... ok
test engine::tests::minute_event_clock_uses_timestamp_order_and_coalesces_equal_heads ... ok
test engine::tests::latest_unsent_complete_portfolio_does_not_union_an_older_pre_market_target ... ok
test engine::tests::minute_schedules_fire_at_each_declared_time_without_market_rows_at_those_times ... ok
test engine::tests::next_bar_open_eligible_universe_helper_does_not_block_on_decision_day_risk ... ok
test engine::tests::minute_observer_never_sees_a_later_fill_from_a_coarse_phase ... ok
test engine::tests::next_bar_open_executes_last_decision_without_execution_day_factor_snapshot ... ok
test engine::tests::manual_observations_are_visible_at_their_real_phase_and_each_receipt_sequence ... ok
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_st_state ... ok
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_paused_state ... ok
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_star_st_state ... ok
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_upper_limit_state ... ok
test engine::tests::next_bar_open_execution_risk_ignores_later_one_yuan_close ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_paused_state ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_delisted_state ... ok
test engine::tests::next_bar_open_execution_risk_rejects_blacklisted_buy_on_execution_day ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_st_state ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_star_st_state ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_upper_limit_buy ... ok
test engine::tests::next_bar_open_execution_risk_rejects_one_yuan_open_despite_higher_close ... ok
test engine::tests::next_bar_open_execution_risk_uses_open_not_close_for_upper_limit_buy ... ok
test engine::tests::next_bar_open_same_day_rebuy_uses_actual_execution_date ... ok
test engine::tests::next_bar_open_scheduled_signal_uses_decision_date_not_execution_date ... ok
test engine::tests::next_bar_open_sell_respects_allow_sell_policy_on_execution_day ... ok
test engine::tests::next_bar_open_sell_risk_rejects_execution_day_lower_limit_state ... ok
test engine::tests::next_bar_open_sell_risk_ignores_decision_day_paused_and_lower_limit_state ... ok
test engine::tests::next_bar_open_sell_risk_uses_open_not_close_for_lower_limit_sell ... ok
test engine::tests::next_bar_open_sell_volume_limit_ignores_decision_day_zero_volume ... ok
test engine::tests::next_bar_open_session_audit_flags_zero_volume_without_rewriting_fills ... ok
test engine::tests::next_bar_open_skips_unavailable_lag_day_and_fills_next_open ... ok
test engine::tests::next_bar_open_target_percent_freezes_decision_day_equity ... ok
test engine::tests::next_bar_open_strategy_context_data_helpers_use_decision_date ... ok
test engine::tests::next_bar_open_target_portfolio_smart_sizes_with_execution_day_open ... ok
test engine::tests::next_open_observation_loads_quotes_even_when_execution_price_is_open ... ok
test engine::tests::observed_manual_sale_updates_the_shared_same_day_rebuy_rule ... ok
test engine::tests::opening_schedule_cannot_cross_the_after_trading_phase ... ok
test engine::tests::opening_and_late_lifecycle_schedules_do_not_read_past_or_future_manual_state ... ok
test engine::tests::opening_orders_are_submitted_once_at_their_real_clock_even_with_an_earlier_execution_window ... ok
test engine::tests::physical_on_day_rules_keep_each_actual_submission_time ... ok
test engine::tests::post_close_phase_callbacks_never_rewind_before_completed_fills ... ok
test engine::tests::opening_subscription_is_loaded_without_replaying_quotes_before_its_activation ... ok
test engine::tests::post_close_wait_has_no_position_before_the_first_actual_matching_tick ... ok
test engine::tests::process_callbacks_observe_the_completed_minute_trade_and_its_clock ... ok
test engine::tests::pre_trading_orders_wait_for_the_configured_window_and_keep_their_audit ... ok
test engine::tests::scheduled_event_detail_records_actual_time_only_for_timed_rules ... ok
test engine::tests::pre_market_cancel_is_observed_before_the_next_open_and_new_signal ... ok
test engine::tests::unknown_identity_or_missing_candidate_does_not_waive_quote_coverage ... ok
test engine::tests::wholly_prelisting_universe_retains_cash_days_without_fabricating_prices ... ok
test etf_execution::tests::latest_generation_overwrites_pending_targets_and_preserves_candidate_order ... ok
test engine::tests::window_algorithm_cannot_publish_future_fill_quantity_to_earlier_callbacks ... ok
test etf_execution::tests::no_loaded_next_session_is_not_guessed_from_natural_days ... ok
test events::tests::accounting_origin_cannot_disguise_an_exchange_order_or_fee ... ok
test events::tests::fill_execution_timestamp_range_is_explicit_and_backward_compatible ... ok
test events::tests::order_event_status_quantity_contract_is_explicit ... ok
test events::tests::process_event_business_lifecycle_filter_keeps_audit_events_only ... ok
test execution_capacity::tests::completed_volume_cannot_be_used_for_an_earlier_open ... ok
test execution_capacity::tests::decimal_participation_never_rounds_a_fractional_share_up_or_overflows ... ok
test execution_capacity::tests::delayed_publication_and_invalid_bounds_are_not_treated_as_zero_volume ... ok
test execution_capacity::tests::session_audit_changes_verdict_not_executed_quantity ... ok
test execution_schedule::tests::clock_entitlements_are_exact_and_allow_backlog_without_future_quote_counts ... ok
test factor_cross_section::tests::neutralization_preserves_input_order ... ok
test factor_cross_section::tests::ties_keep_equal_rank_and_missing_universe_rejects ... ok
test factor_cross_section::tests::historical_ranks_keep_ties_and_unknown_full_cross_sections ... ok
test factor_events::tests::cross_is_event_not_state_and_never_uses_future ... ok
test factor_events::tests::every_parameter_domain_is_structured_and_matches_native_defaults ... ok
test factor_events::tests::missing_is_not_zero_and_breakout_excludes_current ... ok
test factor_events::tests::no_event_has_no_bars_since_and_type_errors_reject ... ok
test engine::tests::scheduled_day_limit_order_loads_later_quotes_without_strategy_minute_subscription ... ok
test factor_events::tests::ta_rewarms_after_gap_and_const_zscore_is_unknown ... ok
test factor_events::tests::ta_sma_real_values_and_parameter_validation ... ok
test finite_serialization::tests::pending_numbers_cannot_be_silently_serialized_as_optional_nulls ... ok
test fixed_point::tests::decimal_parser_rounds_only_beyond_money_scale ... ok
test fixed_point::tests::fixed_budget_comparison_rejects_one_micro_yuan_overrun ... ok
test fixed_point::tests::fixed_budget_never_exceeds_cash_after_cost ... ok
test fixed_point::tests::fixed_fifo_pnl_and_external_flow_are_deterministic ... ok
test fixed_point::tests::runtime_cost_model_matches_fixed_execution_primitive ... ok
test fixed_point::tests::runtime_split_commission_matches_fixed_execution_primitive ... ok
test holding_policy::tests::absolute_locks_are_inclusive_and_override_timed_exit_without_changing_other_symbols ... ok
test holding_policy::tests::manual_origin_only_bypasses_automatic_policy_not_an_order_or_broker_permission ... ok
test holding_policy::tests::opening_date_follows_fills_not_partial_sales_or_corporate_conversions ... ok
test holding_policy::tests::three_complete_sessions_protect_through_wednesday_not_72_hours ... ok
test holding_policy::tests::zero_disabled_and_missing_calendar_or_opened_date_are_not_inferred ... ok
test instrument::tests::lifecycle_is_dated_and_latest_undated_terminal_status_is_not_historical_evidence ... ok
test instrument::tests::listing_sector_is_explicit_and_unknown_stays_unknown ... ok
test instrument::tests::order_quantity_rules_are_case_insensitive_without_allocating_normalized_boards ... ok
test manual_execution::tests::a_failed_corporate_projection_does_not_change_the_book_or_receipt_cursor ... ok
test manual_execution::tests::all_required_money_and_binding_fields_reject_missing_or_wrong_values ... ok
test manual_execution::tests::authoritative_total_fee_does_not_require_inventing_unknown_components ... ok
test manual_execution::tests::changing_any_external_price_or_identity_invalidates_the_frozen_trace ... ok
test manual_execution::tests::complete_exact_decimal_evidence_allows_later_observation_and_retains_source_digits ... ok
test manual_execution::tests::confirmed_manual_fill_changes_cash_and_lots_but_not_external_cash_flow_units ... ok
test manual_execution::tests::confirmed_no_order_outcome_is_distinct_from_unconfirmed_or_unknown_work ... ok
test manual_execution::tests::cursor_waits_for_observation_and_never_reapplies_or_rewinds ... ok
test manual_execution::tests::data_scope_only_contains_actual_filled_securities_and_validates_the_source ... ok
test manual_execution::tests::failed_multi_receipt_advance_keeps_both_progress_and_portfolio_unchanged ... ok
test manual_execution::tests::fixed_money_decimal_text_preserves_micro_units_without_float_conversion ... ok
test factor_events::tests::literal_unknown_fields_reject_and_catalog_is_not_trading_permission ... ok
test factor_events::tests::parameter_domains_keep_enumeration_labels_without_debug_string_parsing ... ok
test manual_execution::tests::inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected ... ok
test manual_execution::tests::late_buy_fifo_depletion_preserves_costs_and_cannot_unlock_today_lots ... ok
test manual_execution::tests::late_fee_evidence_keeps_the_original_fill_observation_clock ... ok
test manual_execution::tests::late_buy_retains_the_earliest_opening_and_latest_buy_dates ... ok
test manual_execution::tests::manual_fee_total_includes_extra_charges_and_rejects_inconsistent_components ... ok
test manual_execution::tests::manual_mismatches_are_atomic_and_do_not_borrow_shares_cash_or_override_pending_orders ... ok
test manual_execution::tests::partial_cancel_is_valid_but_full_fill_cannot_be_reported_as_cancelled ... ok
test manual_execution::tests::observations_follow_durable_receipt_order_and_not_input_array_order ... ok
test manual_execution::tests::raw_timezone_and_cutoff_are_required ... ok
test manual_execution::tests::runtime_position_events_cannot_claim_observations_after_the_source_cutoff ... ok
test manual_execution::tests::the_next_day_manual_sale_keeps_the_actual_quantity_and_fee_contract ... ok
test manual_execution::tests::v2_facts_keep_their_encoding_but_cannot_silently_carry_new_runtime_settings ... ok
test market_event_context::tests::formulas_use_real_limits_and_full_denominators ... ok
test market_event_context::tests::chunking_and_future_append_preserve_history ... ok
test manual_execution::tests::source_time_precision_is_not_invented_and_submitted_time_must_fit_the_interval ... ok
test market_event_context::tests::missing_duplicate_and_unproven_limit_states_fail ... ok
test market_event_context::tests::missing_industry_does_not_invent_groups_or_disable_independent_market_facts ... ok
test metrics::risk_adjusted_contract_tests::changing_daily_rates_adjusts_each_return_before_variance_and_downside ... ok
test metrics::risk_adjusted_contract_tests::incomplete_or_invalid_rates_are_not_zero_rate_observations ... ok
test metrics::risk_adjusted_contract_tests::zero_deviation_and_single_sample_ratios_remain_undefined ... ok
test metrics::tests::a_defined_zero_ratio_is_not_missing ... ok
test metrics::tests::drawdown_includes_initial_nav_without_adding_a_trading_day ... ok
test metrics::tests::benchmark_cumulative_return_uses_first_day_previous_close ... ok
test metrics::tests::external_cash_flow_is_excluded_from_return_and_reported_separately ... ok
test metrics::tests::first_day_loss_is_preserved_in_shared_backtest_metrics ... ok
test metrics::tests::gains_without_downside_have_no_sortino_or_monthly_sharpe ... ok
test metrics::tests::no_risk_ratio_is_fabricated_after_nav_has_reached_zero ... ok
test metrics::tests::risk_adjusted_metrics_use_daily_pit_rates_and_all_period_downside ... ok
test metrics::tests::signal_baseline_uses_same_close_for_strategy_and_benchmark ... ok
test numeric_expr_vm::tests::benchmark_numeric_vm_against_rhai ... ignored, manual release-mode performance evidence
test numeric_expr_vm::tests::floating_comparisons_match_rhai_epsilon_semantics ... ok
test numeric_expr_vm::tests::evaluates_numeric_boolean_and_if_expressions ... ok
test metrics::tests::undefined_ratios_remain_null_in_backtest_metrics ... ok
test numeric_expr_vm::tests::invalid_clamp_returns_error_without_panicking ... ok
test numeric_expr_vm::tests::nullable_boolean_truth_table_preserves_unknown_under_negation ... ok
test numeric_expr_vm::tests::nullable_short_circuit_and_explicit_conditional_branches_are_lazy ... ok
test numeric_expr_vm::tests::missing_numeric_operands_do_not_become_boolean_false_or_zero ... ok
test numeric_expr_vm::tests::rejects_dynamic_language_features ... ok
test numeric_expr_vm::tests::scratch_reuses_allocations_across_program_runs ... ok
test numeric_expr_vm::tests::short_circuit_does_not_resolve_unused_variable ... ok
test numeric_expr_vm::tests::supports_numeric_builtins_and_local_reuse ... ok
test numeric_factors::tests::bulk_load_is_sorted_and_keeps_last_value_for_each_field ... ok
test numeric_factors::tests::clone_does_not_share_mutable_values ... ok
test numeric_factors::tests::compact_keys_inline_dynamic_names_and_keep_long_static_storage ... ok
test numeric_factors::tests::exact_reservation_preserves_values_and_avoids_growth_during_known_inserts ... ok
test numeric_factors::tests::long_dynamic_unicode_and_short_keys_keep_the_same_json_map ... ok
test numeric_factors::tests::serialization_keeps_the_map_contract_and_precise_numbers ... ok
test numeric_factors::tests::wide_batch_merge_matches_tree_and_preserves_old_key_ownership ... ok
test numeric_expr_vm::tests::matches_rhai_for_representative_numeric_boolean_corpus ... ok
test pattern_context::tests::normalized_rule_does_not_turn_an_omitted_window_into_explicit_null ... ok
test pattern_context::tests::reference_index_never_defaults_to_performance_benchmark ... ok
test pattern_context::tests::runtime_contract_rejects_missing_range_and_recursive_ranks ... ok
test numeric_factors::tests::updates_order_removal_and_values_match_tree_map ... ok
test pattern_context::tests::dataset_rank_is_full_scope_causal_and_equal_to_pure_cross_section ... ok
test pattern_context::tests::structured_pool_conditions_are_not_rescanned_inside_serialized_source_code ... ok
test platform_expr_strategy::tests::allocation_buy_scale_preserves_explicit_overweight_multiplier ... ok
test platform_expr_strategy::tests::benchmark_compiled_runtime_helper_arguments ... ignored, manual release-mode runtime helper binding benchmark
test platform_expr_strategy::tests::bjse_symbol_detection_is_case_insensitive_without_normalizing_all_symbols ... ok
test platform_expr_strategy::tests::available_factor_name_collection_preserves_sparse_and_repeated_fields ... ok
test platform_expr_strategy::tests::buy_filter_attaches_denials_without_rewriting_selection ... ok
test platform_expr_strategy::tests::available_factor_name_collection_preserves_wide_dynamic_field_identity ... ok
test platform_expr_strategy::tests::buy_filter_uses_active_schedule_time_instead_of_first_configured_time ... ok
test platform_expr_strategy::tests::buy_quote_filter_rejects_missing_intraday_quote_not_daily_close ... ok
test platform_expr_strategy::tests::completed_session_factor_dates_exclude_intraday_and_preserve_next_open ... ok
test platform_expr_strategy::tests::completed_session_bound_value_does_not_read_same_day_future_value ... ok
test platform_expr_strategy::tests::daily_replacement_reference_records_an_explicit_empty_previous_target ... ok
test platform_expr_strategy::tests::daily_pattern_runtime_uses_the_shared_kernel_and_rejects_early_visibility ... ok
test platform_expr_strategy::tests::dated_position_exposure_uses_the_latest_effective_point ... ok
test platform_expr_strategy::tests::current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings ... ok
test platform_expr_strategy::tests::daily_replacement_reference_uses_previous_target_before_lagged_orders_fill ... ok
test platform_expr_strategy::tests::expression_plan_only_executes_prelude_when_expression_depends_on_it ... ok
test platform_expr_strategy::tests::expression_plan_skips_unreferenced_market_scope_maps ... ok
test platform_expr_strategy::tests::framework_rolling_factor_aliases_resolve_to_raw_market_series ... ok
test platform_expr_strategy::tests::lagged_rebalance_emits_target_value_even_when_projection_is_unchanged ... ok
test platform_expr_strategy::tests::factor_position_target_reduces_once_without_daily_rebalance_override ... ok
test platform_expr_strategy::tests::market_cap_ordered_selection_range_is_inclusive_and_field_specific ... ok
test platform_expr_strategy::tests::model_target_lifecycle_removes_exits_and_appends_replacements ... ok
test platform_expr_strategy::tests::numeric_vm_compiles_static_runtime_helper_arguments ... ok
test platform_expr_strategy::tests::numeric_vm_reuses_compiled_program_across_invocations ... ok
test platform_expr_strategy::tests::numeric_vm_rewrites_only_valid_iso_date_literals ... ok
test platform_expr_strategy::tests::numeric_vm_reuses_rolling_helper_program_across_dates ... ok
test platform_expr_strategy::tests::market_cap_ascending_rank_reuses_only_equivalent_universe_order ... ok
test platform_expr_strategy::tests::periodic_selected_bjse_buy_rejection_is_audited_without_creating_an_order ... ok
test platform_expr_strategy::tests::platform_aiquant_blocks_target_increase_when_stop_loss_sell_is_lower_limit_blocked ... ok
test platform_expr_strategy::tests::platform_aiquant_buy_rejection_keeps_candidate_buy_disabled ... ok
test platform_expr_strategy::tests::platform_aiquant_buy_rejection_uses_scheduled_price_not_open_limit ... ok
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_releases_slot_after_take_profit_exit ... ok
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_releases_second_slot_after_prior_top_up ... ok
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_uses_remaining_slot_budget ... ok
test platform_expr_strategy::tests::platform_aiquant_marked_total_uses_intraday_last_not_close ... ok
test platform_expr_strategy::tests::platform_aiquant_delays_upper_limit_take_profit_to_configured_open_time ... ok
test platform_expr_strategy::tests::platform_aiquant_marked_total_value_uses_scheduled_quote ... ok
test platform_expr_strategy::tests::platform_aiquant_minute_last_projection_uses_last_quote_for_buy_budget ... ok
test platform_expr_strategy::tests::platform_aiquant_marks_highlimit_before_cptrade_time ... ok
test platform_expr_strategy::tests::platform_aiquant_minute_stop_loss_requires_intraday_quote ... ok
test platform_expr_strategy::tests::platform_aiquant_next_open_stop_loss_uses_signal_close_not_execution_last_price ... ok
test platform_expr_strategy::tests::platform_aiquant_missing_risk_state_does_not_force_existing_position_exit ... ok
test platform_expr_strategy::tests::platform_aiquant_next_open_target_delta_uses_previous_close_and_sizes_at_open ... ok
test platform_expr_strategy::tests::platform_aiquant_projection_allows_signal_day_market_fallback_for_next_open ... ok
test platform_expr_strategy::tests::platform_aiquant_projection_does_not_release_sell_slot_without_execution_quote ... ok
test platform_expr_strategy::tests::platform_aiquant_projection_rejects_missing_symbol_execution_quote ... ok
test platform_expr_strategy::tests::platform_aiquant_projection_reserves_sell_cost_without_same_callback_proceeds ... ok
test platform_expr_strategy::tests::platform_aiquant_periodic_rebalance_uses_total_equity_equal_weight_buy_budget ... ok
test platform_expr_strategy::tests::platform_aiquant_remaining_buy_cash_uses_strategy_visible_price_and_projected_quantity ... ok
test platform_expr_strategy::tests::platform_aiquant_risk_level_forces_existing_position_exit ... ok
test platform_expr_strategy::tests::platform_aiquant_scheduled_quote_uses_configured_intraday_time ... ok
test platform_expr_strategy::tests::platform_aiquant_selection_uses_decision_market_cap ... ok
test platform_expr_strategy::tests::platform_aiquant_sell_projection_uses_signal_day_market_for_lagged_execution ... ok
test platform_expr_strategy::tests::platform_aiquant_sells_down_before_actionable_take_profit_exit ... ok
test platform_expr_strategy::tests::platform_aiquant_stock_expr_uses_scheduled_minute_price_for_limit_check ... ok
test platform_expr_strategy::tests::platform_aiquant_skips_positive_target_adjust_when_position_will_close ... ok
test platform_expr_strategy::tests::platform_aiquant_target_adjust_uses_scheduled_quote_for_position_value ... ok
test platform_expr_strategy::tests::platform_aiquant_target_value_uses_scheduled_mark_for_delta ... ok
test platform_expr_strategy::tests::platform_aiquant_stop_loss_uses_fee_cost_basis_avg_cost ... ok
test platform_expr_strategy::tests::platform_aiquant_upper_limit_buy_disabled_does_not_force_exit_existing_position ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_allows_pending_highlimit_target_adjust_after_open ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_marks_upper_limit_but_allows_target_adjust ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_emits_daily_position_target_adjustment ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_threshold_top_up_uses_projected_remaining_budget ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_waits_pending_highlimit_target_adjust ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_threshold_only_sells_overweight_positions ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_retargets_same_ratio_every_day ... ok
test platform_expr_strategy::tests::platform_buy_rejection_allows_lower_limit_buy_when_ask_is_available ... ok
test platform_expr_strategy::tests::platform_calendar_day_rebalance_interval_can_be_enabled_explicitly ... ok
test platform_expr_strategy::tests::platform_current_rolling_uses_decision_date_row_not_prior_factor_row ... ok
test platform_expr_strategy::tests::platform_daily_top_up_caps_empty_slot_to_equal_weight_target ... ok
test platform_expr_strategy::tests::platform_can_sell_position_uses_configured_lower_limit_policy ... ok
test platform_expr_strategy::tests::platform_daily_top_up_counts_submitted_zero_fill_candidate_as_strategy_slot ... ok
test platform_expr_strategy::tests::platform_daily_top_up_keeps_selection_limited_to_target_count ... ok
test platform_expr_strategy::tests::platform_daily_top_up_does_not_buy_back_same_day_exit ... ok
test platform_expr_strategy::tests::platform_daily_top_up_release_slot_flag_matches_aiquant_exit_signal_state ... ok
test platform_expr_strategy::tests::platform_daily_top_up_releases_unsellable_stop_loss_slot ... ok
test platform_expr_strategy::tests::platform_daily_top_up_skips_current_stop_loss_symbol_like_aiquant ... ok
test platform_expr_strategy::tests::platform_daily_top_up_uses_same_day_sell_cash ... ok
test platform_expr_strategy::tests::platform_day_state_uses_decision_benchmark_close ... ok
test platform_expr_strategy::tests::platform_delayed_open_day_carried_full_close_releases_second_top_up_slot ... ok
test platform_expr_strategy::tests::platform_delayed_open_exit_uses_delayed_time_for_slot_projection ... ok
test platform_expr_strategy::tests::platform_delayed_open_day_take_profit_releases_second_top_up_slot ... ok
test platform_expr_strategy::tests::platform_expr_code_number_extracts_stock_digits ... ok
test platform_expr_strategy::tests::platform_delayed_open_partial_exit_releases_top_up_slot ... ok
test platform_expr_strategy::tests::platform_explicit_actions_keep_every_declared_trade_time ... ok
test platform_expr_strategy::tests::platform_expr_expands_day_factor_in_prelude ... ok
test platform_expr_strategy::tests::platform_expr_limit_flags_are_directional ... ok
test platform_expr_strategy::tests::platform_expr_cost_model_honors_zero_minimum_commission ... ok
test platform_expr_strategy::tests::platform_expr_normalizes_python_style_integer_division ... ok
test platform_expr_strategy::tests::platform_expr_normalizes_signal_field_alias_in_prelude ... ok
test platform_expr_strategy::tests::platform_expr_cost_model_uses_commission_override ... ok
test platform_expr_strategy::tests::platform_expr_preflight_rejects_malformed_expression_with_field_name ... ok
test platform_expr_strategy::tests::platform_expr_preflight_accepts_all_configured_expression_categories ... ok
test platform_expr_strategy::tests::platform_expr_rewrites_nested_ternary ... ok
test platform_expr_strategy::tests::platform_expr_rewrites_prelude_assignment_ternary ... ok
test platform_expr_strategy::tests::platform_expr_missing_requested_factor_does_not_default_to_zero ... ok
test platform_expr_strategy::tests::platform_expr_rewrites_multiple_nested_ternaries_inside_function_calls ... ok
test platform_expr_strategy::tests::platform_expr_safe_div_supports_two_and_three_arg_forms ... ok
test platform_expr_strategy::tests::platform_expr_supports_python_style_string_aliases ... ok
test platform_expr_strategy::tests::platform_expr_tracks_requested_dynamic_factor_identifiers ... ok
test platform_expr_strategy::tests::platform_full_exposure_carried_full_close_releases_second_top_up_slot ... ok
test platform_expr_strategy::tests::platform_lagged_exit_does_not_reserve_replacement_slot_before_execution ... ok
test platform_expr_strategy::tests::platform_full_exposure_partial_stop_loss_releases_second_top_up_slot ... ok
test platform_expr_strategy::tests::platform_helpers_support_generic_rolling_stats_and_normalized_factors ... ok
test platform_expr_strategy::tests::platform_marked_total_value_uses_current_day_close ... ok
test platform_expr_strategy::tests::platform_max_holding_days_counts_decision_trading_days_not_calendar_days ... ok
test platform_expr_strategy::tests::platform_max_holding_days_exit_preempts_take_profit_exit ... ok
test platform_expr_strategy::tests::platform_market_cap_field_uses_storage_unit_without_extra_scaling ... ok
test platform_expr_strategy::tests::platform_minute_last_volume_limit_does_not_require_l1_quantity ... ok
test platform_expr_strategy::tests::platform_next_open_defers_complete_static_selection_risk_before_universe_output ... ok
test platform_expr_strategy::tests::platform_next_open_select_symbols_defers_decision_day_limit_state ... ok
test platform_expr_strategy::tests::platform_next_open_risk_level_exit_ignores_decision_day_sellability ... ok
test platform_expr_strategy::tests::platform_next_open_projected_execution_price_uses_open_not_close ... ok
test platform_expr_strategy::tests::platform_next_open_selection_applies_explicit_signal_day_risk_filter ... ok
test platform_expr_strategy::tests::platform_next_open_selection_ignores_execution_day_limit_state ... ok
test platform_expr_strategy::tests::platform_next_open_sell_risk_uses_open_not_close_for_lower_limit ... ok
test platform_expr_strategy::tests::platform_numeric_stop_loss_is_strictly_below_multiplier ... ok
test platform_expr_strategy::tests::platform_order_value_quantity_includes_buy_commission_budget ... ok
test platform_expr_strategy::tests::platform_periodic_rebalance_caps_empty_slot_to_equal_weight_target ... ok
test platform_expr_strategy::tests::platform_periodic_rebalance_skips_unsellable_stop_loss_symbol ... ok
test platform_expr_strategy::tests::platform_projected_execution_limit_checks_respect_risk_policy ... ok
test platform_expr_strategy::tests::platform_periodic_rebalance_uses_same_day_sell_cash_for_later_buys ... ok
test platform_expr_strategy::tests::platform_periodic_rebalance_tops_up_underweight_selected_holding ... ok
test platform_expr_strategy::tests::platform_rebalance_date_due_does_not_use_the_on_day_default_clock ... ok
test platform_expr_strategy::tests::platform_projection_does_not_consume_cash_from_unfillable_sell ... ok
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_monthly_tradingday ... ok
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_physical_time ... ok
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_weekly_weekday ... ok
test platform_expr_strategy::tests::platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement ... ok
test platform_expr_strategy::tests::platform_projected_market_fill_caps_volume_limit ... ok
test platform_expr_strategy::tests::platform_selection_and_buy_respect_allow_flags_are_configurable ... ok
test platform_expr_strategy::tests::platform_selection_does_not_fill_candidates_with_buy_risk_rejections ... ok
test platform_expr_strategy::tests::platform_selection_expands_pct_change_in_filter_and_rank_expr ... ok
test platform_expr_strategy::tests::platform_refresh_rate_uses_stateful_trading_day_counter ... ok
test platform_expr_strategy::tests::platform_selection_missing_candidate_risk_state_does_not_change_default_universe ... ok
test platform_expr_strategy::tests::platform_selection_kcb_switch_overrides_legacy_universe_exclude ... ok
test platform_expr_strategy::tests::platform_selection_ranks_with_previous_factor_date ... ok
test platform_expr_strategy::tests::platform_signal_price_model_lifecycle_survives_unfilled_buy_and_exits ... ok
test platform_expr_strategy::tests::platform_selection_uses_complete_static_risk_policy_before_universe_output ... ok
test platform_expr_strategy::tests::platform_skip_window_keeps_pending_highlimit_until_open ... ok
test platform_expr_strategy::tests::platform_skip_window_runs_delayed_open_exit_before_seasonal_liquidation ... ok
test broker::tests::simulator_transaction_profile_preserves_successful_output ... ok
test platform_expr_strategy::tests::platform_stock_expr_exposes_star_st_and_bjse_fields_without_default_selection_risk ... ok
test platform_expr_strategy::tests::platform_stock_expr_handles_positive_volume_guard ... ok
test platform_expr_strategy::tests::platform_stock_state_uses_completed_day_high_and_low_for_limit_touch ... ok
test platform_expr_strategy::tests::platform_stock_expr_rejects_when_volume_ma5_exceeds_market_volume_ma100 ... ok
test platform_expr_strategy::tests::platform_selection_ranks_by_candidate_market_cap_with_current_precomputed_factors ... ok
test platform_expr_strategy::tests::platform_stock_state_uses_factor_date_for_selection_market_fields ... ok
test platform_expr_strategy::tests::platform_stock_state_uses_market_series_when_factor_map_has_no_rolling ... ok
test platform_expr_strategy::tests::platform_stock_state_uses_framework_rolling_series ... ok
test platform_expr_strategy::tests::platform_stop_loss_residual_pending_rechecks_signal_next_day ... ok
test platform_expr_strategy::tests::platform_strategy_baseline_risk_rejects_new_listing_buy_without_selection_replacement ... ok
test platform_expr_strategy::tests::platform_strategy_can_cancel_latest_open_order_by_runtime_id ... ok
test platform_expr_strategy::tests::platform_strategy_can_filter_with_string_factors ... ok
test platform_expr_strategy::tests::platform_strategy_emits_explicit_actions_in_open_auction_stage ... ok
test platform_expr_strategy::tests::platform_strategy_emits_algo_order_actions ... ok
test platform_expr_strategy::tests::platform_strategy_emits_explicit_actions_when_rotation_is_disabled ... ok
test platform_expr_strategy::tests::platform_strategy_emits_target_portfolio_smart_explicit_action ... ok
test platform_expr_strategy::tests::platform_stop_take_fraction_values_are_return_thresholds ... ok
test platform_expr_strategy::tests::platform_strategy_emits_target_portfolio_smart_algo_order_style ... ok
test platform_expr_strategy::tests::platform_strategy_emits_target_shares_explicit_action ... ok
test platform_expr_strategy::tests::platform_strategy_emits_universe_management_actions ... ok
test platform_expr_strategy::tests::platform_strategy_exposes_advanced_data_runtime_helpers ... ok
test platform_expr_strategy::tests::platform_strategy_exposes_open_order_runtime_fields ... ok
test platform_expr_strategy::tests::platform_strategy_exposes_position_lifecycle_runtime_fields ... ok
test platform_expr_strategy::tests::platform_strategy_filters_target_portfolio_smart_by_subscription_guard ... ok
test platform_expr_strategy::tests::platform_strategy_empty_exclude_keeps_buy_risk_without_selection_filtering ... ok
test platform_expr_strategy::tests::platform_strategy_exposes_process_event_runtime_fields ... ok
test platform_expr_strategy::tests::platform_strategy_skips_explicit_actions_when_schedule_does_not_match ... ok
test platform_expr_strategy::tests::platform_strategy_treats_missing_stock_rolling_window_as_filter_reject ... ok
test platform_expr_strategy::tests::platform_strategy_suppresses_explicit_target_for_blocked_stop_loss_exit ... ok
test platform_expr_strategy::tests::platform_strategy_uses_decision_date_for_next_bar_open_signals ... ok
test platform_expr_strategy::tests::platform_take_profit_default_uses_position_cost_basis ... ok
test platform_expr_strategy::tests::platform_take_profit_sells_position_at_upper_limit ... ok
test platform_expr_strategy::tests::platform_take_profit_false_does_not_track_upper_limit_pending_sell ... ok
test platform_expr_strategy::tests::platform_weak_exposure_carried_full_close_keeps_residual_slot ... ok
test platform_expr_strategy::tests::platform_weak_exposure_partial_stop_loss_keeps_residual_slot ... ok
test platform_expr_strategy::tests::portfolio_drawdown_control_is_idempotent_and_rearms_after_cooldown ... ok
test platform_expr_strategy::tests::platform_weak_market_skips_target_adjust_before_actionable_stop_loss ... ok
test platform_expr_strategy::tests::platform_weak_market_skips_target_increase_before_intraday_stop_loss_sell_blocked ... ok
test platform_expr_strategy::tests::platform_weak_market_skips_target_increase_before_lower_limit_blocked_stop_loss ... ok
test platform_expr_strategy::tests::selection_buffer_retains_held_symbols_within_buffer_rank ... ok
test platform_expr_strategy::tests::selection_quote_plan_filters_daily_part_before_minute_candidates ... ok
test platform_expr_strategy::tests::selection_quote_plan_keeps_configured_static_selection_risk_for_intraday_fields ... ok
test platform_expr_strategy::tests::selection_replacement_limit_caps_voluntary_daily_removals ... ok
test platform_expr_strategy::tests::projected_historical_slippage_does_not_swallow_missing_calibration ... ok
test platform_expr_strategy::tests::session_event_uses_previous_bar_and_recomputes_at_each_minute_without_becoming_a_quote ... ok
test platform_expr_strategy::tests::portfolio_loss_observes_finalized_nav_after_fees_and_cash_flows ... ok
test platform_expr_strategy::tests::stock_pool_exit_weight_redistribution_is_explicit_and_uses_integer_bps ... ok
test platform_expr_strategy::tests::stock_pool_suspension_keeps_budget_and_exit_redistributes_only_available_weights ... ok
test platform_expr_strategy::tests::stock_pool_suspension_resolution_never_reads_the_future_execution_day ... ok
test platform_expr_strategy::tests::signal_date_explicit_actions_are_due_only_on_declared_snapshot_dates ... ok
test platform_expr_strategy::tests::selection_limit_state_requires_intraday_selection_quotes ... ok
test platform_expr_strategy::tests::stock_pool_target_lifecycle_keeps_exits_for_next_day_reentry ... ok
test platform_expr_strategy::tests::stock_state_cache_resets_before_reusing_compact_keys_on_another_date ... ok
test platform_expr_strategy::tests::strategy_specific_labels_are_not_framework_rolling_factors ... ok
test platform_expr_strategy::tests::stock_snapshot_fields_are_loaded_only_when_the_strategy_uses_them ... ok
test platform_expr_strategy::tests::strategy105_numeric_expressions_compile_to_vm ... ok
test platform_expr_strategy::tests::target_scale_replenishment_preserves_strategy_cash_allocation ... ok
test platform_expr_strategy::tests::target_portfolio_smart_exposes_decision_and_execution_dates ... ok
test platform_expr_strategy::tests::target_portfolio_smart_respects_current_date_weight_conditions ... ok
test platform_expr_strategy::tests::typed_runtime_numbers_preserve_legacy_rhai_formatting ... ok
test platform_expr_strategy::tests::universe_exclude_matches_explicit_symbols_and_bjse_alias ... ok
test platform_runtime_schema::tests::runtime_schema_includes_known_identifiers ... ok
test platform_runtime_schema::tests::runtime_schema_serializes_to_json_object ... ok
test platform_strategy_spec::tests::accepts_equivalent_risk_policy_alias_values ... ok
test platform_expr_strategy::tests::timed_rotation_runs_on_its_schedule_instead_of_the_untimed_day_hook ... ok
test platform_strategy_spec::tests::accepts_scoped_blacklist_context_in_runtime_risk_policy ... ok
test platform_strategy_spec::tests::canonicalizes_duplicate_execution_aliases_without_changing_strategy_intent ... ok
test platform_strategy_spec::tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
test platform_strategy_spec::tests::engine_config_parses_weak_market_shrink_overweight_threshold ... ok
test platform_strategy_spec::tests::engine_stock_ma_filter_generates_price_and_volume_expr ... ok
test platform_strategy_spec::tests::explicit_actions_inherit_top_level_runtime_schedule ... ok
test platform_strategy_spec::tests::execution_limit_aliases_normalize_to_one_serde_field_without_touching_policy ... ok
test platform_strategy_spec::tests::explicit_delayed_limit_open_exit_false_overrides_trade_times ... ok
test platform_strategy_spec::tests::explicit_trading_schedule_overrides_rebalance_trade_times ... ok
test platform_strategy_spec::tests::frozen_candidate_order_is_explicit_and_preserves_source_positions ... ok
test platform_strategy_spec::tests::index_throttle_uses_signal_ma_not_performance_benchmark_ma ... ok
test platform_strategy_spec::tests::native_factor_bindings_declare_completed_session_fields ... ok
test platform_strategy_spec::tests::multiple_trade_times_do_not_imply_delayed_limit_exit ... ok
test platform_strategy_spec::tests::normalize_board_does_not_infer_kcb_from_security_code ... ok
test platform_strategy_spec::tests::numeric_strategy_aliases_accept_exact_zero_but_never_hide_rounding_or_conflicts ... ok
test platform_strategy_spec::tests::parses_and_rejects_invalid_position_target_rules ... ok
test platform_strategy_spec::tests::parses_buy_filter_as_a_separate_trading_condition ... ok
test platform_strategy_spec::tests::parses_daily_schedule_time_for_execution_quotes ... ok
test platform_strategy_spec::tests::parses_delayed_deposit_receiving_days_expression ... ok
test platform_strategy_spec::tests::parses_dynamic_model_score_dates_into_platform_config ... ok
test platform_strategy_spec::tests::parses_execution_cost_overrides_into_platform_config ... ok
test platform_strategy_spec::tests::parses_execution_slippage_overrides_into_platform_config ... ok
test platform_strategy_spec::tests::parses_explicit_historical_slippage_into_platform_config ... ok
test platform_strategy_spec::tests::parses_explicit_delayed_limit_open_exit ... ok
test platform_strategy_spec::tests::parses_lifecycle_target_portfolio_contract ... ok
test platform_strategy_spec::tests::parses_generic_futures_actions_and_rejects_incomplete_contracts ... ok
test platform_strategy_spec::tests::parses_max_holding_days_from_engine_and_runtime_trading ... ok
test platform_expr_strategy::tests::timed_current_close_stop_loss_reloads_state_after_quote_arrives ... ok
test platform_strategy_spec::tests::parses_percent_slippage_alias_into_platform_config ... ok
test platform_strategy_spec::tests::parses_minute_stage_schedule_and_initial_subscriptions ... ok
test platform_strategy_spec::tests::parses_position_average_entry_stop_take_reference_price_mode ... ok
test platform_strategy_spec::tests::parses_portfolio_drawdown_control_into_platform_config ... ok
test platform_strategy_spec::tests::parses_rebalance_cash_mode_and_forces_minute_to_actual_sequence ... ok
test platform_strategy_spec::tests::parses_risk_policy_into_platform_config ... ok
test platform_strategy_spec::tests::parses_runtime_expression_spec_into_platform_config ... ok
test platform_strategy_spec::tests::parses_signal_dates_rebalance_into_platform_config ... ok
test platform_strategy_spec::tests::parses_signal_day_post_adjusted_stop_take_reference_price_mode ... ok
test platform_strategy_spec::tests::parses_st_and_star_st_risk_policy_switches_into_platform_config ... ok
test platform_strategy_spec::tests::parses_typed_time_in_force_for_explicit_orders ... ok
test platform_strategy_spec::tests::rejects_conflicting_non_policy_strategy_aliases ... ok
test platform_strategy_spec::tests::rejects_conflicting_risk_policy_alias_values ... ok
test platform_strategy_spec::tests::rejects_invalid_explicit_volume_percent_in_platform_config ... ok
test platform_strategy_spec::tests::portfolio_loss_contract_is_explicit_and_validated ... ok
test platform_strategy_spec::tests::rejects_invalid_portfolio_drawdown_control ... ok
test platform_strategy_spec::tests::rejects_invalid_or_duplicate_static_universe_symbols ... ok
test platform_strategy_spec::tests::rejects_removed_compatibility_profile ... ok
test platform_strategy_spec::tests::rejects_removed_engine_profile_name ... ok
test platform_strategy_spec::tests::rejects_unknown_stop_take_reference_price_mode ... ok
test platform_strategy_spec::tests::platform_spec_accepts_only_supported_matching_types ... ok
test platform_strategy_spec::tests::rejects_non_positive_shared_order_limits ... ok
test platform_strategy_spec::tests::rejects_unknown_strategy_risk_policy_fields_before_deserialization ... ok
test platform_strategy_spec::tests::rejects_unknown_time_in_force_in_runtime_contract ... ok
test platform_strategy_spec::tests::runtime_expression_parser_does_not_inherit_microcap_template_defaults ... ok
test platform_strategy_spec::tests::retired_or_unknown_slippage_models_do_not_fall_back_to_fixed_or_none ... ok
test platform_strategy_spec::tests::runtime_expression_parses_dated_position_exposure_schedule ... ok
test platform_strategy_spec::tests::runtime_expressions_infer_ma_windows_from_literal_strategy_logic ... ok
test platform_strategy_spec::tests::volume_limit_does_not_enable_quote_quantity_limit_for_minute_last ... ok
test portfolio::tests::buy_after_partial_sell_continues_moving_average_cost_basis ... ok
test portfolio::tests::cash_dividend_can_preserve_avg_cost_for_aiquant_rules ... ok
test portfolio::tests::delayed_withdrawals_are_reserved_and_settled_atomically ... ok
test portfolio::tests::external_cash_flow_rebases_units_without_changing_nav ... ok
test portfolio::tests::fifo_fixed_pnl_conserves_value_while_alv_average_cost_stays_stable ... ok
test portfolio::tests::fixed_lot_allocation_rounds_nearest_and_conserves_total ... ok
test portfolio::tests::holdings_summary_reports_entry_price_pnl_excluding_buy_commission ... ok
test portfolio::tests::partial_sell_preserves_remaining_average_cost ... ok
test portfolio::tests::portfolio_exposes_engine_native_account_metrics ... ok
test portfolio::tests::portfolio_marks_same_day_buy_at_fill_until_next_trading_day ... ok
test portfolio::tests::portfolio_missing_market_requires_formal_suspension_before_carrying_price ... ok
test portfolio::tests::portfolio_tracks_dividend_receivable_and_day_pnl ... ok
test portfolio::tests::position_tracks_day_lifecycle_fields ... ok
test portfolio::tests::positions_preserve_insertion_order ... ok
test portfolio::tests::scoped_checkpoint_restores_order_flat_rows_and_progress_without_copying_untouched_lots ... ok
test portfolio::tests::strategy_entry_price_excludes_buy_commission_cost_basis ... ok
test portfolio_loss::tests::cash_sessions_preserve_continuity_without_inventing_returns ... ok
test platform_strategy_spec::tests::session_rotation_keeps_every_declared_clock_not_only_the_last_one ... ok
test portfolio_loss::tests::refuses_future_missing_corrected_and_incomplete_accounting ... ok
test platform_expr_strategy::tests::shared_signal_reduction_and_stops_use_each_accounts_cost_quantity_and_sold_state ... ok
test portfolio_loss::tests::restored_state_rejects_changed_policy_and_forged_cooldown ... ok
test position_exposure::tests::allocation_is_dated_and_any_later_scalar_or_restore_clears_it ... ok
test position_exposure::tests::explicit_equity_targets_and_buys_scale_but_sales_cashflows_and_prices_do_not ... ok
test position_exposure::tests::rejects_unknown_actions_duplicate_identity_and_invalid_bps ... ok
test position_exposure::tests::same_day_adjustments_restore_and_future_events_keep_their_own_times ... ok
test position_exposure::tests::scalar_preserves_strategy_risk_off_and_restore_keeps_original_exposure ... ok
test risk_control::tests::concrete_kcb_reason_wins_over_generic_missing_risk_state ... ok
test risk_control::tests::configurable_bjse_filter_can_be_disabled ... ok
test risk_control::tests::configurable_blacklist_rejects_selection_and_buy ... ok
test portfolio_loss::tests::restart_is_exact_and_duplicate_decisions_do_not_consume_cooldown ... ok
test risk_control::tests::configurable_kcb_filter_can_be_disabled ... ok
test risk_control::tests::configurable_lower_limit_selection_filter_can_be_disabled ... ok
test risk_control::tests::configurable_upper_limit_buy_filter_can_be_disabled ... ok
test risk_control::tests::configurable_upper_limit_selection_filter_can_be_disabled ... ok
test risk_control::tests::delisting_period_status_is_not_terminal_without_delisted_date ... ok
test risk_control::tests::execution_quote_covers_missing_one_yuan_flag_but_not_other_risk_facts ... ok
test risk_control::tests::explicit_candidate_inactive_status_is_not_treated_as_missing_risk_state ... ok
test risk_control::tests::explicit_one_yuan_selection_policy_still_uses_selection_facts ... ok
test portfolio_loss::tests::nav_serialization_preserves_float_bits ... ok
test risk_control::tests::fund_identity_excludes_stock_one_yuan_rule_but_not_actual_price_and_pause_checks ... ok
test risk_control::tests::inactive_selection_checks_preserve_missing_facts_and_execution_rejections ... ok
test risk_control::tests::kcb_filter_uses_classification_instead_of_security_code ... ok
test risk_control::tests::missing_risk_state_can_be_relaxed_by_disabling_static_filters ... ok
test risk_control::tests::missing_risk_state_default_selection_ignores_allow_flags_but_buy_rejects ... ok
test risk_control::tests::missing_risk_state_rejects_sell_when_sell_risk_facts_are_missing ... ok
test risk_control::tests::missing_risk_state_respects_field_specific_selection_switches ... ok
test risk_control::tests::missing_risk_state_respects_field_specific_buy_switches ... ok
test risk_control::tests::missing_risk_state_selection_audit_keeps_missing_fields_in_reason ... ok
test risk_control::tests::missing_risk_state_selection_still_respects_configured_static_filters ... ok
test risk_control::tests::missing_risk_state_sell_can_be_relaxed_by_disabling_sell_static_filters ... ok
test risk_control::tests::one_yuan_buy_rule_uses_execution_price_not_later_close_or_earlier_open ... ok
test risk_control::tests::sell_rejection_blocks_execution_price_at_lower_limit ... ok
test risk_control::tests::sell_rejection_respects_allow_sell_policy_on_execution_day ... ok
test risk_control::tests::sell_rejects_explicit_candidate_inactive_status_without_instrument_row ... ok
test risk_control::tests::shared_order_size_limits_apply_to_both_sides_and_buy_position ... ok
test risk_control::tests::st_and_star_st_filters_are_independent ... ok
test scheduler::tests::date_due_is_independent_from_the_order_execution_clock ... ok
test scheduler::tests::scheduler_matches_before_trading_and_minute_level_rules ... ok
test scheduler::tests::scheduler_matches_daily_weekly_and_monthly_rules ... ok
test portfolio::tests::cash_ledger_accumulates_micro_yuan_exactly ... ok
test session_events::tests::gaps_and_stale_last_bars_do_not_become_false_or_repeated_signals ... ok
test session_events::tests::opening_range_is_unavailable_before_the_range_has_completed ... ok
test signal_contract::tests::changed_valid_contents_must_not_reuse_a_version_hash ... ok
test signal_contract::tests::completed_daily_inputs_may_be_published_after_market_close ... ok
test signal_contract::tests::coverage_and_duplicate_actions_fail_closed ... ok
test signal_contract::tests::empty_complete_snapshot_clears_only_that_accounts_holdings ... ok
test signal_contract::tests::historical_reconstruction_is_not_online_publication ... ok
test signal_contract::tests::next_open_uses_decision_session_and_never_nearest_signal ... ok
test signal_contract::tests::observed_next_open_never_backdates_a_morning_publication_into_yesterdays_orders ... ok
test signal_contract::tests::platform_spec_consumes_book_without_running_another_selection ... ok
test signal_contract::tests::reconstruction_ignores_research_wall_clock_but_never_early_input_availability ... ok
test signal_contract::tests::reduction_is_resolved_from_each_accounts_actual_position ... ok
test signal_contract::tests::rejects_future_inputs_and_model_knowledge ... ok
test session_events::tests::decision_uses_the_previous_completed_label_and_future_prices_do_not_rewrite ... ok
test signal_contract::tests::rejects_quantities_prices_and_unknown_signal_fields ... ok
test stock_pool_candidates::tests::list_order_reuses_explicit_prefix_and_appends_new_candidates ... ok
test stock_pool_candidates::tests::missing_snapshot_duplicate_input_and_empty_filtered_scope_fail ... ok
test stock_pool_candidates::tests::mixed_sources_preserve_priority_and_both_provenances ... ok
test stock_pool_candidates::tests::zero_automatic_day_keeps_manual_members_without_inheriting_old_auto_targets ... ok
test signal_contract::tests::rejects_overallocation_nonfinite_and_ambiguous_actions ... ok
test stock_pool_execution::frozen::ratio_tests::legacy_paused_and_explicit_partial_budgets_are_not_reinterpreted ... ok
test stock_pool_execution::frozen::ratio_tests::precise_paused_budget_survives_replacement_and_zero_targets ... ok
test stock_pool_execution::frozen::ratio_tests::precision_is_checked_before_frozen_budget_is_allocated ... ok
test stock_pool_execution::tests::confirmed_holdings_not_unsettled_sells_determine_remaining_position_budget ... ok
test stock_pool_execution::tests::all_membership_and_weight_combinations_use_the_same_target_planner ... ok
test stock_pool_execution::tests::frozen_valuation_is_dated_and_cannot_turn_an_unknown_gap_into_a_pause ... ok
test stock_pool_execution::tests::full_stop_overrides_a_simultaneous_factor_reduction_without_a_second_target ... ok
test stock_pool_execution::tests::independent_exit_quote_priority_does_not_bypass_t_plus_one_or_price_validation ... ok
test stock_pool_execution::tests::independent_full_exit_has_no_ordinary_quote_dependency_but_partial_risk_does_not_fake_missing_facts ... ok
test stock_pool_execution::tests::independent_stop_does_not_require_unused_ordinary_sell_quote_facts ... ok
test stock_pool_execution::tests::index_cap_cannot_sell_a_paused_holding_or_use_its_budget_as_free_cash ... ok
test stock_pool_execution::tests::index_cap_keeps_non_sellable_quantity_and_defers_the_remaining_risk_target ... ok
test stock_pool_execution::tests::index_cap_reduces_existing_proportions_without_enabling_weight_rebalancing ... ok
test stock_pool_execution::tests::index_cap_respects_ordinary_weight_changes_and_stronger_fill_protection ... ok
test stock_pool_execution::tests::equal_thirty_seats_use_full_precision_at_a_board_lot_boundary ... ok
test stock_pool_execution::tests::instrument_price_tick_and_quantity_units_are_not_a_two_decimal_stock_assumption ... ok
test stock_pool_execution::tests::legacy_constraints_do_not_gain_an_empty_precision_field ... ok
test stock_pool_execution::tests::market_quantity_budget_includes_execution_slippage_and_fees ... ok
test stock_pool_execution::tests::index_exposure_and_reserved_seats_multiply_without_rounding_the_budget_early ... ok
test stock_pool_execution::tests::optional_configuration_and_conflicting_aliases_are_explicit ... ok
test stock_pool_execution::tests::native_sell_and_quote_conditions_are_and_but_stop_and_protection_remain_independent ... ok
test stock_pool_execution::tests::ordinary_sell_keeps_required_quote_failures_and_zero_stop_is_not_an_exit ... ok
test stock_pool_execution::tests::partial_sell_cooldown_restricts_increases_without_clearing_the_remainder ... ok
test stock_pool_execution::tests::ordinary_sell_scope_excludes_independent_exits_and_protected_positions ... ok
test stock_pool_execution::tests::paused_outside_holding_retains_prior_budget_without_an_execution_quote ... ok
test stock_pool_execution::tests::quote_condition_number_contract_rejects_invalid_facts_and_fractional_shares ... ok
test session_events::tests::all_thirteen_events_return_native_boolean_series ... ok
test stock_pool_execution::tests::paused_held_weight_is_not_replenished_after_another_stock_exits ... ok
test stock_pool_execution::tests::quote_condition_scopes_are_evaluated_before_target_orders ... ok
test stock_pool_execution::tests::quote_only_exit_still_works_when_independent_risk_rules_are_configured ... ok
test stock_pool_execution::tests::retained_holdings_reserve_actual_value_and_zero_funding_still_clears ... ok
test stock_pool_execution::tests::reserve_slots_keep_requested_and_effective_exposure_distinct ... ok
test stock_pool_execution::tests::sale_slippage_does_not_prevent_a_marked_value_board_lot_reduction ... ok
test stock_pool_execution::tests::risk_only_configuration_never_turns_into_an_unconditional_ordinary_exit ... ok
test stock_pool_execution::tests::sell_cooldown_does_not_turn_disabled_weight_rebalancing_back_on ... ok
test stock_pool_execution::tests::sell_quote_condition_creates_exit_and_unmet_holding_keeps_its_slot ... ok
test stock_pool_execution::tests::selection_excluding_paused_stock_keeps_the_same_24_stock_budget_as_broker_execution ... ok
test stock_pool_execution::tests::stop_reduction_merge_matrix_preserves_protection_t_plus_one_and_invalid_config_errors ... ok
test stock_pool_index_policy::tests::disabled_is_optional_but_enabled_parameters_are_not_invented ... ok
test stock_pool_index_policy::tests::missing_or_duplicate_or_future_rows_never_shorten_the_window ... ok
test stock_pool_index_policy::tests::trend_and_caps_match_the_declared_math_and_keep_zero_meaningful ... ok
test stock_pool_indicators::tests::kdj_uses_the_same_frozen_native_kernel_as_event_conditions ... ok
test stock_pool_indicators::tests::missing_inputs_require_source_admission_and_future_inputs_fail ... ok
test stock_pool_indicators::tests::uses_shared_ma_ema_and_preserves_recursive_prefix_before_output_window ... ok
test stock_pool_market_cap::tests::interpolates_declared_endpoints_and_clamps_without_business_defaults ... ok
test stock_pool_market_cap::tests::missing_duplicate_or_mismatched_index_inputs_do_not_shrink_the_calendar ... ok
test stock_pool_quote_facts::tests::lunch_and_post_close_gaps_follow_the_minute_feed_segments ... ok
test stock_pool_quote_facts::tests::real_source_session_prefix_matches_observed_checkpoints ... ignored, requires FIDC_SESSION_PREFIX_SOURCE_JSON from the frozen Source minute response
test stock_pool_quote_facts::tests::sparse_unverified_and_overflowing_quotes_cannot_be_called_session_totals ... ok
test stock_pool_quote_facts::tests::totals_use_only_the_complete_observed_prefix_and_keep_decimal_amounts ... ok
test strategy::decision_merge_tests::explicit_empty_complete_target_replaces_old_allocation_but_empty_callback_does_not ... ok
test strategy::decision_merge_tests::newer_complete_target_replaces_old_symbols_without_discarding_explicit_actions ... ok
test strategy::tests::omni_microcap_projection_uses_configured_trading_cost ... ok
test strategy::tests::omni_microcap_selection_uses_configured_risk_policy ... ok
test strategy::tests::order_time_in_force_parsing_and_order_type_contract_are_explicit ... ok
test strategy_ai::tests::generation_prompt_uses_explicit_performance_acceptance_contract ... ok
test strategy_ai::tests::manual_separates_explicit_business_selection_from_framework_risk_policy ... ok
test strategy_ai::tests::optimization_prompt_uses_explicit_performance_acceptance_contract ... ok
test universe::tests::selector_applies_configured_selection_risk_on_decision_date ... ok
test universe::tests::selector_can_defer_configured_selection_risk_without_losing_diagnostics ... ok
test universe::tests::selector_records_structured_selection_risk_decisions ... ok
test stock_pool_execution::tests::typed_exit_roles_merge_only_satisfied_ordinary_actions_with_independent_risk ... ok
test risk_control::tests::selection_check_activation_covers_every_configured_flag_and_blacklist_state ... ok
test stock_pool_execution::tests::quote_field_operator_side_and_scope_matrix_matches_the_configured_predicate ... ok
test result: ok. 779 passed; 0 failed; 7 ignored; 0 measured; 0 filtered out; finished in 0.54s
Running unittests src/bin/dump_platform_runtime_schema.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/dump_platform_runtime_schema-c4b52627aec309c9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/automatic_trade_protection.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/automatic_trade_protection-e76e4284ad200e69)
running 6 tests
test absolute_lock_blocks_initial_strategy_buy_without_a_rejected_order ... ok
test framework_protection_uses_fills_and_covers_explicit_strategy_orders ... ok
test maximum_holding_policy_applies_to_discrete_strategies_and_yields_to_buy_protection ... ok
test serialized_framework_policy_survives_shared_alias_normalization_and_rejects_conflicts ... ok
test locked_holding_keeps_its_slot_even_when_cash_can_buy_the_next_candidate ... ok
test observed_manual_trades_then_split_keep_real_fill_protection_and_lock_dates ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
Running tests/core_rules.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/core_rules-6365eded07d721e8)
running 8 tests
test china_cost_model_applies_minimum_commission_and_stamp_tax ... ok
test china_cost_model_tracks_minimum_commission_per_order_id ... ok
test china_cost_model_switches_stamp_tax_rate_after_2023_08_28 ... ok
test china_rule_hooks_allow_sell_when_last_price_is_above_lower_limit ... ok
test china_rule_hooks_block_same_day_sell_under_t_plus_one ... ok
test china_rule_hooks_block_buy_at_limit_up_and_sell_at_limit_down ... ok
test china_rule_hooks_use_strict_price_limits ... ok
test configured_cost_model_matches_declared_run_options ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/corporate_actions.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/corporate_actions-05d8a8628f15e674)
running 6 tests
test cash_dividend_adjusts_cost_basis_and_returns_cash_delta ... ok
test stock_split_scales_lots_quantity_and_average_cost ... ok
test portfolio_settles_cash_receivable_on_payable_date ... ok
test engine_settles_same_day_dividend_after_split_for_aiquant_semantics ... FAILED
test engine_reinvests_dividend_receivable_in_round_lots ... ok
test successor_conversion_depletes_older_source_lots_before_newer_successor_buys ... ok
failures:
---- engine_settles_same_day_dividend_after_split_for_aiquant_semantics stdout ----
thread 'engine_settles_same_day_dividend_after_split_for_aiquant_semantics' (4173472) panicked at crates/fidc-core/tests/corporate_actions.rs:663:5:
assertion failed: result.position_events.iter().any(|event|
event.reason == "stock_split 1.200000" &&
event.quantity_after == 1_200)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
engine_settles_same_day_dividend_after_split_for_aiquant_semantics
test result: FAILED. 5 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `-p fidc-core --test corporate_actions`
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-receivable-validation-20261005-0bbeed4/crates/fidc-core)
Finished `test` profile [unoptimized] target(s) in 45.20s
Running unittests src/lib.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/fidc_core-dbea4d4f6cb5d2d7)
running 22 tests
test corporate_book::tests::closing_register_cannot_be_replaced_by_later_holdings ... ok
test corporate_book::tests::dividend_receivable_survives_a_flat_position_without_becoming_spendable_cash ... ok
test corporate_book::tests::entitlement_survives_complete_sale_and_a_successor_unit_conversion ... ok
test corporate_book::tests::a_late_corporate_batch_failure_keeps_prior_cash_positions_and_notes ... ok
thread 'corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta' (4165594) panicked at crates/fidc-core/src/corporate_book.rs:1098:13:
assertion failed: error.to_string().contains("corporate_action_quantity_overflow")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test corporate_book::tests::explicitly_delayed_dividend_remains_receivable_until_actual_payment ... ok
test corporate_book::tests::flat_position_retirement_does_not_change_issued_lot_state_or_next_purchase_origin ... ok
test corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta ... FAILED
test corporate_book::tests::missing_accounting_reference_is_atomic_not_a_silent_cash_only_success ... ok
test corporate_book::tests::manual_zero_skips_only_unused_allocation_facts_not_invalid_cash_evidence ... ok
test corporate_book::tests::invalid_delivery_negative_rates_and_unresolved_fractional_claims_are_atomic ... ok
test corporate_book::tests::missing_or_backward_cash_payment_date_never_updates_the_account ... ok
test corporate_book::tests::per_share_precision_is_retained_until_the_registered_cash_amount_is_rounded ... ok
test corporate_book::tests::missing_record_date_or_unobserved_closing_book_is_atomic ... ok
test corporate_book::tests::post_record_buy_cannot_receive_the_earlier_dividend ... ok
test corporate_book::tests::post_record_purchase_does_not_inherit_registered_shares ... ok
test corporate_book::tests::registered_shares_are_valued_but_not_delivered_or_sellable_before_the_original_date ... ok
test corporate_book::tests::sale_after_registration_preserves_cash_and_excludes_new_lots_from_cost_adjustment ... ok
thread 'corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security' (4165614) panicked at crates/fidc-core/src/corporate_book.rs:998:9:
assertion failed: error.to_string().contains("successor_instrument_missing")
test corporate_book::tests::terminated_security_keeps_paid_cash_and_is_not_recreated_by_reinvestment ... ok
test corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security ... FAILED
test corporate_book::tests::verified_conversion_preserves_lots_without_creating_orders_or_fills ... ok
test corporate_book::tests::tiny_known_cash_rate_is_preserved_and_cannot_waive_payment_or_record_evidence ... ok
test corporate_book::tests::invalid_successor_terms_cannot_be_replaced_with_one_share_or_zero_cash ... ok
failures:
failures:
corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta
corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security
test result: FAILED. 20 passed; 2 failed; 0 ignored; 0 measured; 764 filtered out; finished in 0.01s
error: test failed, to rerun pass `-p fidc-core --lib`
@@ -0,0 +1,140 @@
{
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
"files": [
{
"bytes": 3625,
"file": "core-initial-targeted-failure.log",
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
"source_uid": 1003
},
{
"bytes": 82467,
"file": "core-attempt1-failure.log",
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
"source_uid": 1003
},
{
"bytes": 95343,
"file": "core-attempt2-failure.log",
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
"source_uid": 1003
},
{
"bytes": 98381,
"file": "core-attempt3-success.log",
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
"source_uid": 1003
},
{
"bytes": 98381,
"file": "core-sell-availability-success.log",
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
"source_uid": 1003
},
{
"bytes": 98381,
"file": "core-successor-guard-success.log",
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
"source_uid": 1003
},
{
"bytes": 98572,
"file": "core-final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"source_uid": 1003
},
{
"bytes": 10543,
"file": "runner-initial-compile-failure.log",
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
"source_uid": 1003
},
{
"bytes": 65133,
"file": "runner-attempt2-success.log",
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
"source_uid": 1003
},
{
"bytes": 65133,
"file": "runner-successor-pair-success.log",
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
"source_uid": 1003
},
{
"bytes": 7207,
"file": "prior-validation-receipt.json",
"sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
"source_uid": 1003
},
{
"bytes": 8550,
"file": "prior-validation-pg-receipt.json",
"sha256": "45b483a748c5354239dd678e66b652f53d9225108f2ab41f152a06250e69bd25",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-pg-receipt.json",
"source_uid": 1003
},
{
"bytes": 2275,
"file": "prior-operational-verification.json",
"sha256": "32e634849aecbd2b634c6dcdbad68013f2c5eb603c8d586a43cc4698d8b18a4a",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/operational-verification.json",
"source_uid": 1003
},
{
"bytes": 762,
"file": "receipt-construction-failure.json",
"sha256": "e62a4819bd08599a3af797588211c79add8867bb8b3dfb67c28497bfe5780445",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/receipt-construction-failure.json",
"source_uid": 1003
},
{
"bytes": 65258,
"file": "runner-final-full-linux-tests.log",
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
"source_uid": 1003
},
{
"bytes": 8192,
"file": "validation-receipt.json",
"sha256": "097101988e5f6875c8933012534cfa1ca72f9e2742c04d002aba7d00a69fc346",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/validation-receipt.json",
"source_uid": 1003
},
{
"bytes": 9573,
"file": "validation-pg-receipt.json",
"sha256": "04207632e68efaea406e1357937a5bb75d23fa0d74e5461ccfa7993b07e302e8",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/validation-pg-receipt.json",
"source_uid": 1003
},
{
"bytes": 2275,
"file": "operational-verification.json",
"sha256": "3ce2ba78ed28f549ddc675429565e578fe61eee1db4374c375e5049761c8fd06",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/operational-verification.json",
"source_uid": 1003
},
{
"bytes": 839,
"file": "live-test-resource-verification.json",
"sha256": "1686248392f29ae93f023ff147fcc0abb06b6b2fb15a9806b5718e320ed75a40",
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/live-test-resource-verification.json",
"source_uid": 1003
}
],
"schema_version": "fidc.local-share-delivery-evidence-manifest/v1",
"service_commit": "1d4b32e18d3836b5844130ae0c95d90247768882"
}
@@ -0,0 +1,31 @@
{
"contract_sha256": "fed292d158989d58f63b478d266675e294b1f156f20796406615f9fc4eb7b398",
"observed_at": "2026-10-04T22:06:48.117070+00:00",
"observed_process_owners": [
{
"pid": 83961,
"uid": 1003
},
{
"pid": 84064,
"uid": 1003
},
{
"pid": 84092,
"uid": 1003
}
],
"owner_uid": 1003,
"production_ready": false,
"properties": {
"ActiveState": "active",
"CPUQuotaPerSecUSec": "4s",
"ControlGroup": "/user.slice/user-1003.slice/user@1003.service/app.slice/fidc-share-delivery-quantity-validation-20261005.scope",
"MemoryMax": "17179869184",
"MemorySwapMax": "0",
"TasksMax": "256"
},
"schema_version": "fidc.live-share-delivery-test-scope/v1",
"trading_allowed": false,
"unit": "fidc-share-delivery-quantity-validation-20261005.scope"
}
@@ -0,0 +1,70 @@
{
"authorized_incremental_cache_root": "/srv/fidc/programs/src/fidc-backtest-service/target/debug/incremental",
"contract_sha256": "4696cb0eb31095275a0440c3c25e605502c2b6b6f07acf46032eb552d3547e7e",
"health": [
{
"body_sha256": "3d4310bd55460dc1d83c95d0420b5e2c979a15b90c6d98f4b45378b645f6cd96",
"http_status": 200,
"port": 18095
},
{
"body_sha256": "897da12d1075bd042696ef9b39e0aec42706985e9880887be0daceeab21afb17",
"http_status": 200,
"port": 18096
}
],
"incremental_files_present": 0,
"numeric_research_budget_reopened": false,
"observed_at": "2026-10-04T22:08:10.206192+00:00",
"original_research_plan_sha256": "6aa6932b8b7d4538543e7baf48b1b0ab305e684b36003faeaa7f0f4db4067c0c",
"original_research_source_commit": "d847b82de3f62a369291c13160b31fcfc0075b8b",
"owner_uid": 1003,
"production_ready": false,
"production_runtime_deployed": false,
"production_strategy_count": 0,
"schema_version": "fidc.share-delivery-operational-verification/v1",
"ssd_free_bytes": 9045954560,
"trading_allowed": false,
"units": [
{
"ActiveState": "active",
"InvocationID": "b6bd3c6c3a6d4f729291b969590d66a0",
"MainPID": "3571059",
"NRestarts": "0",
"process_uid": 1003,
"unit": "fidc-private-agent-runtime.service"
},
{
"ActiveState": "active",
"InvocationID": "851771deedc44b78bea58e18c74f9421",
"MainPID": "1073961",
"NRestarts": "1",
"process_uid": 1003,
"unit": "fidc-private-source-lake-worker.service"
},
{
"ActiveState": "active",
"InvocationID": "ce19e205006945e888c42ff385f96a50",
"MainPID": "2371183",
"NRestarts": "0",
"process_uid": 1003,
"unit": "fidc-private-source-lake.service"
},
{
"ActiveState": "active",
"InvocationID": "386b6478a0384505a8c9704e2d2a1478",
"MainPID": "3571320",
"NRestarts": "0",
"process_uid": 1003,
"unit": "fidc-private-strategy-ai.service"
},
{
"ActiveState": "active",
"InvocationID": "0eb28e3a45694a00951aa3aec0e13465",
"MainPID": "1396112",
"NRestarts": "0",
"process_uid": 1003,
"unit": "fidc-projection-research-autopilot.service"
}
]
}
@@ -0,0 +1,70 @@
{
"authorized_incremental_cache_root": "/srv/fidc/programs/src/fidc-backtest-service/target/debug/incremental",
"contract_sha256": "66d0cf1d1a93b7960777c494da152af837f1c86b49bccf5591ba3f295f7383b6",
"health": [
{
"body_sha256": "0dc4f9d9af47e41507d9267ee3e08d4cce2027b0b757f0f87193bf27c73abd91",
"http_status": 200,
"port": 18095
},
{
"body_sha256": "245cfc3e4dd1b3301167429fa8cdfd53e49f7f6a5d8ad754047a6bc1645f5f93",
"http_status": 200,
"port": 18096
}
],
"incremental_files_present": 0,
"numeric_research_budget_reopened": false,
"observed_at": "2026-10-04T22:02:08.949482+00:00",
"original_research_plan_sha256": "6aa6932b8b7d4538543e7baf48b1b0ab305e684b36003faeaa7f0f4db4067c0c",
"original_research_source_commit": "d847b82de3f62a369291c13160b31fcfc0075b8b",
"owner_uid": 1003,
"production_ready": false,
"production_runtime_deployed": false,
"production_strategy_count": 0,
"schema_version": "fidc.share-delivery-operational-verification/v1",
"ssd_free_bytes": 9070112768,
"trading_allowed": false,
"units": [
{
"ActiveState": "active",
"InvocationID": "b6bd3c6c3a6d4f729291b969590d66a0",
"MainPID": "3571059",
"NRestarts": "0",
"process_uid": 1003,
"unit": "fidc-private-agent-runtime.service"
},
{
"ActiveState": "active",
"InvocationID": "851771deedc44b78bea58e18c74f9421",
"MainPID": "1073961",
"NRestarts": "1",
"process_uid": 1003,
"unit": "fidc-private-source-lake-worker.service"
},
{
"ActiveState": "active",
"InvocationID": "ce19e205006945e888c42ff385f96a50",
"MainPID": "2371183",
"NRestarts": "0",
"process_uid": 1003,
"unit": "fidc-private-source-lake.service"
},
{
"ActiveState": "active",
"InvocationID": "386b6478a0384505a8c9704e2d2a1478",
"MainPID": "3571320",
"NRestarts": "0",
"process_uid": 1003,
"unit": "fidc-private-strategy-ai.service"
},
{
"ActiveState": "active",
"InvocationID": "0eb28e3a45694a00951aa3aec0e13465",
"MainPID": "1396112",
"NRestarts": "0",
"process_uid": 1003,
"unit": "fidc-projection-research-autopilot.service"
}
]
}
@@ -0,0 +1,192 @@
{
"artifact_bytes": 7207,
"artifact_id": "strategy_research_cb32517064d7558f69713001",
"artifact_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
"artifact_sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412",
"artifact_type": "research_source_scope_probe",
"contract_sha256": "35ecf44697bf2e35967221e9f6dd9b330f3445137a3b00dadd3da59a01db2720",
"created_at": "2026-10-04T22:03:46Z",
"created_by": "boris",
"department_id": null,
"organization_id": "local_org",
"persisted_at": "2026-10-04T22:03:46Z",
"production_ready": false,
"record_json": {
"all_attempt_log_references": [
{
"bytes": 3625,
"compiler_failure": false,
"complete_success": false,
"failed": 2,
"ignored": 0,
"passed": 20,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49"
},
{
"bytes": 82467,
"compiler_failure": false,
"complete_success": false,
"failed": 1,
"ignored": 7,
"passed": 798,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c"
},
{
"bytes": 95343,
"compiler_failure": false,
"complete_success": false,
"failed": 4,
"ignored": 9,
"passed": 923,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447"
},
{
"bytes": 98572,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 969,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
},
{
"bytes": 10543,
"compiler_failure": true,
"complete_success": false,
"failed": 0,
"ignored": 0,
"passed": 0,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8"
},
{
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7"
},
{
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
}
],
"contract_sha256": "b4885b28b8076c61977dbe24948d2c0eabc7ac32f6e8ad0591b2011f1b9b2c11",
"core_test_result": {
"bytes": 98572,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 969,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
},
"corporate_action_cache_schema_version": 5,
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
"fractional_quantity_requires_explicit_settlement_rule": true,
"full_source_event_and_revision_scope_verified": false,
"new_runtime_release_performed": false,
"numeric_research_budget_reopened": false,
"observed_at": "2026-10-04T22:02:08.963623+00:00",
"operational_verification_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/operational-verification.json",
"sha256": "32e634849aecbd2b634c6dcdbad68013f2c5eb603c8d586a43cc4698d8b18a4a"
},
"owner_uid": 1003,
"pending_share_successor_conversion_without_carry_terms_rejected": true,
"performance_speedup_claimed": false,
"production_ready": false,
"production_runtime_deployed": false,
"production_strategy_count": 0,
"registered_share_entitlements_separate_from_delivered_and_sellable_quantity": true,
"resource_contract_requested": {
"build_jobs": 4,
"core_target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de",
"cpu_quota_percent": 400,
"incremental": false,
"locked": true,
"memory_max_bytes": 17179869184,
"memory_swap_max_bytes": 0,
"offline": true,
"resource_properties_after_transient_scope_exit_are_not_live_validation": true,
"runner_target_path": "/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb",
"rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
"target_filesystem": "executable_tmpfs",
"tasks_max": 256,
"toolchain": "stable-x86_64-unknown-linux-gnu"
},
"runner_api_test_result": {
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
},
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
"service_commit": "ffc6e1f5b8b1d6c9b342a661c7a25ebfc3d8d4c5",
"share_receivable_assets_included_in_equity_and_target_sizing": true,
"simulation_rounding_fixture_not_official_settlement_evidence": true,
"source_pair_root": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13",
"source_publication_times_admitted_as_PIT": false,
"source_rows_semantic_sha256": "eec65df828f97a3e51827cb6b0e9ec2c86079a333481d55ec9eb6ab2845d73c5",
"source_rows_semantic_version": "v10",
"source_transport_commit": "12eeae5d570004499a30f7faa42c0da93c1578cc",
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
"trading_allowed": false,
"two_framework_financial_reconciliation_completed": false,
"unknown_delivery_date_not_defaulted_to_ex_date": true,
"unknown_distribution_quantities_not_defaulted_to_zero": true
},
"research_key": "fidc-registered-share-delivery-validation-20261005-ce46c13-ffc6e1f",
"schema_table": "strategy_research_artifact",
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
"storage": "postgres"
}
@@ -0,0 +1,173 @@
{
"all_attempt_log_references": [
{
"bytes": 3625,
"compiler_failure": false,
"complete_success": false,
"failed": 2,
"ignored": 0,
"passed": 20,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49"
},
{
"bytes": 82467,
"compiler_failure": false,
"complete_success": false,
"failed": 1,
"ignored": 7,
"passed": 798,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c"
},
{
"bytes": 95343,
"compiler_failure": false,
"complete_success": false,
"failed": 4,
"ignored": 9,
"passed": 923,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447"
},
{
"bytes": 98572,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 969,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
},
{
"bytes": 10543,
"compiler_failure": true,
"complete_success": false,
"failed": 0,
"ignored": 0,
"passed": 0,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8"
},
{
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7"
},
{
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
}
],
"contract_sha256": "b4885b28b8076c61977dbe24948d2c0eabc7ac32f6e8ad0591b2011f1b9b2c11",
"core_test_result": {
"bytes": 98572,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 969,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
},
"corporate_action_cache_schema_version": 5,
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
"fractional_quantity_requires_explicit_settlement_rule": true,
"full_source_event_and_revision_scope_verified": false,
"new_runtime_release_performed": false,
"numeric_research_budget_reopened": false,
"observed_at": "2026-10-04T22:02:08.963623+00:00",
"operational_verification_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/operational-verification.json",
"sha256": "32e634849aecbd2b634c6dcdbad68013f2c5eb603c8d586a43cc4698d8b18a4a"
},
"owner_uid": 1003,
"pending_share_successor_conversion_without_carry_terms_rejected": true,
"performance_speedup_claimed": false,
"production_ready": false,
"production_runtime_deployed": false,
"production_strategy_count": 0,
"registered_share_entitlements_separate_from_delivered_and_sellable_quantity": true,
"resource_contract_requested": {
"build_jobs": 4,
"core_target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de",
"cpu_quota_percent": 400,
"incremental": false,
"locked": true,
"memory_max_bytes": 17179869184,
"memory_swap_max_bytes": 0,
"offline": true,
"resource_properties_after_transient_scope_exit_are_not_live_validation": true,
"runner_target_path": "/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb",
"rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
"target_filesystem": "executable_tmpfs",
"tasks_max": 256,
"toolchain": "stable-x86_64-unknown-linux-gnu"
},
"runner_api_test_result": {
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
},
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
"service_commit": "ffc6e1f5b8b1d6c9b342a661c7a25ebfc3d8d4c5",
"share_receivable_assets_included_in_equity_and_target_sizing": true,
"simulation_rounding_fixture_not_official_settlement_evidence": true,
"source_pair_root": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13",
"source_publication_times_admitted_as_PIT": false,
"source_rows_semantic_sha256": "eec65df828f97a3e51827cb6b0e9ec2c86079a333481d55ec9eb6ab2845d73c5",
"source_rows_semantic_version": "v10",
"source_transport_commit": "12eeae5d570004499a30f7faa42c0da93c1578cc",
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
"trading_allowed": false,
"two_framework_financial_reconciliation_completed": false,
"unknown_delivery_date_not_defaulted_to_ex_date": true,
"unknown_distribution_quantities_not_defaulted_to_zero": true
}
@@ -0,0 +1,17 @@
{
"contract_sha256": "6d6cb3b513abce58634f24ed2cc4d691d3c542d0944b2a3408298772806dca67",
"error_type": "TypeError",
"failed_before_pg_persist_call": true,
"failure_function": "build_record",
"missing_required_argument": "organization_id",
"observed_at": "2026-10-04T22:03:37.554100+00:00",
"owner_uid": 1003,
"production_ready": false,
"schema_version": "fidc.share-delivery-receipt-construction-failure/v1",
"test_results_and_frozen_validation_receipt_preserved": true,
"trading_allowed": false,
"validation_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
"sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412"
}
}
@@ -0,0 +1,678 @@
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-engine/crates/fidc-core)
Compiling fidc-common v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-service/crates/fidc-common)
Compiling fidc-signal-client v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-engine/crates/fidc-signal-client)
Compiling fidc-backtest-runner v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-service/services/backtest_runner)
Compiling fidc-backtest-service v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-service/services/api)
Finished `test` profile [unoptimized] target(s) in 40.73s
Running unittests src/lib.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-f9173de301b7a940)
running 483 tests
test backtest::columnar_factor_overlay_tests::cache_range_rejects_date_disorder_outside_the_binary_search_slice ... ok
test backtest::columnar_factor_overlay_tests::cache_range_rejects_symbol_disorder_outside_the_requested_date ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_real_cache_value_equivalence ... ignored, requires a frozen private Boris real-data cache directory on 177
test backtest::columnar_factor_overlay_tests::columnar_overlay_missing_or_stale_cache_does_not_mutate_base ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_window_boundary ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_order_and_count_mismatches ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_market_projection_and_invalid_cached_column_type ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_late_key_mismatch_before_any_base_mutation ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_unknown_keys_are_not_hidden_as_empty_values ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_replaced_file_invalidates_view_without_mutating_pinned_input ... ok
test backtest::columnar_factor_overlay_tests::row_visitor_preserves_selected_offsets_and_late_validation_errors ... ok
test backtest::compact_storage_tests::batch_bound_factor_columns_preserve_types_nulls_and_nonfinite_values ... ok
test backtest::columnar_factor_overlay_tests::valid_cache_reuses_its_key_index_and_failed_indexes_are_not_published ... ok
test backtest::compact_storage_tests::inline_symbols_and_factor_maps_preserve_binary_and_json_contracts ... ok
test backtest::day_snapshot_builder::tests::a_failed_builder_cannot_publish_earlier_valid_rows ... ok
test backtest::day_snapshot_builder::tests::factor_reservation_keeps_missing_and_overwrite_semantics ... ok
test backtest::day_snapshot_builder::tests::late_source_validation_failure_invalidates_the_streamed_day ... ok
test backtest::equity_lifecycle_metadata_tests::empty_quotes_require_full_interval_lifecycle_proof_not_a_latest_status ... ok
test backtest::equity_lifecycle_metadata_tests::lifecycle_cash_days_keep_real_benchmark_without_fabricated_security_rows ... ok
test backtest::equity_lifecycle_metadata_tests::missing_identity_and_dates_do_not_become_active_securities ... ok
test backtest::execution_config_tests::capacity_preflight_requires_explicit_daily_assumptions_without_changing_risk_switches ... ok
test backtest::execution_config_tests::component_execution_quotes_seed_uses_latest_intraday_quote_before_target_time ... ok
test backtest::execution_config_tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
test backtest::execution_config_tests::daily_current_close_preserves_the_strategy_decision_clock ... ok
test backtest::execution_config_tests::daily_execution_price_request_serializes_date_symbol_filter_scope ... ok
test backtest::execution_config_tests::daily_execution_projection_keeps_benchmark_narrow_and_trade_risk_complete ... ok
test backtest::execution_config_tests::data_intraday_snapshot_time_is_strategy_spec_driven ... ok
test backtest::execution_config_tests::dataset_signature_rejects_source_lake_without_frozen_backtest_bundle ... ok
test backtest::day_snapshot_builder::tests::arrow_rows_build_the_same_final_snapshots_without_a_row_collection ... ok
test backtest::execution_config_tests::dataset_signature_scopes_dynamic_model_score_source_rows ... ok
test backtest::execution_config_tests::default_matching_type_follows_backtest_frequency ... ok
test backtest::execution_config_tests::dataset_signature_scopes_backtest_data_bundle_source_rows ... ok
test backtest::execution_config_tests::dynamic_model_score_execution_quotes_are_lazy_by_default ... ok
test backtest::execution_config_tests::dynamic_model_score_rank_does_not_extract_fixed_signal_symbols ... ok
test backtest::execution_config_tests::execution_quote_prefilter_keeps_previous_factor_rows_in_current_band ... ok
test backtest::execution_config_tests::execution_quote_prefilter_prefers_runtime_market_cap_slope ... ok
test backtest::execution_config_tests::execution_quote_query_times_follow_strategy_trade_times ... ok
test backtest::execution_config_tests::execution_quote_prefilter_keeps_candidates_beyond_legacy_limit ... ok
test backtest::execution_config_tests::execution_top_level_fee_overrides_nested_risk_policy_cost ... ok
test backtest::execution_config_tests::extract_score_context_does_not_extract_fixed_signal_symbols ... ok
test backtest::execution_config_tests::execution_top_level_volume_percent_overrides_nested_risk_policy ... ok
test backtest::execution_config_tests::factor_keys_use_compact_storage_without_changing_names ... ok
test backtest::execution_config_tests::fixed_signal_model_score_scope_does_not_validate_candidate_limit ... ok
test backtest::execution_config_tests::fixed_signal_symbol_scope_preserves_explicit_empty_targets ... ok
test backtest::execution_config_tests::minute_session_bounds_extend_only_after_post_close_rule_effective_date ... ok
test backtest::execution_config_tests::full_minute_event_stream_uses_subscriptions_instead_of_point_snapshot ... ok
test backtest::execution_config_tests::model_score_source_rows_scope_evidence_verifies_candidate_limit ... ok
test backtest::execution_config_tests::model_score_source_rows_scope_rejects_unbounded_rows ... ok
test backtest::execution_config_tests::native_next_open_pool_loads_causal_quotes_without_exposing_them_to_selection ... ok
test backtest::execution_config_tests::narrow_overlay_projection_does_not_retain_a_wide_value_buffer ... ok
test backtest::execution_config_tests::owned_date_group_producers_drop_every_input_exactly_once_on_failure ... ok
test backtest::execution_config_tests::owned_trade_day_preparation_matches_borrowed_rows ... ok
test backtest::execution_config_tests::parse_matching_type_limits_global_backtest_matching_modes ... ok
test backtest::execution_config_tests::parse_slippage_model_supports_explicit_historical_model ... ok
test backtest::execution_config_tests::parse_slippage_model_supports_limit_price ... ok
test backtest::execution_config_tests::platform_daily_factor_strategy_uses_lazy_execution_quotes_by_default ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_can_include_recent_candidates ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_does_not_let_candidates_evict_recent_orders ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_keeps_today_candidates_without_candidate_carry ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_prefers_recent_candidates_before_today_candidates ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_prioritizes_order_symbols ... ok
test backtest::execution_config_tests::platform_execution_quote_default_carry_keeps_large_recent_candidate_set ... ok
test backtest::execution_config_tests::parallel_owned_date_groups_keep_exact_rows_and_owned_factor_allocations ... ok
test backtest::execution_config_tests::platform_execution_route_requires_runtime_contract ... ok
test backtest::execution_config_tests::platform_strategy_config_rejects_loose_value_budget_without_panicking ... ok
test backtest::execution_config_tests::platform_runtime_snapshot_preserves_typed_time_in_force ... ok
test backtest::execution_config_tests::prepare_trade_day_candidate_market_cap_falls_back_to_total_market_cap ... ok
test backtest::execution_config_tests::platform_strategy_config_uses_same_intraday_time_as_broker ... ok
test backtest::execution_config_tests::prepare_trade_day_daily_selection_price_uses_close_without_limit_allow_flags ... ok
test backtest::execution_config_tests::prepare_trade_day_does_not_build_execution_quote_from_daily_fallback ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_allow_buy_when_missing_field_is_not_buy_relevant ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_daily_volume_when_snapshot_has_intraday_volume ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_external_signal_warmup_day ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_missing_risk_facts_when_runtime_filters_are_disabled ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_st_and_star_st_flags_independent ... ok
test backtest::execution_config_tests::prepare_trade_day_preserves_missing_daily_volume_contract ... ok
test backtest::execution_config_tests::prepare_trade_day_preserves_zero_ask_from_intraday_snapshot ... ok
test backtest::execution_config_tests::raw_yuan_market_cap_normalizes_to_100m_units ... ok
test backtest::execution_config_tests::prepare_trade_day_records_all_missing_risk_facts_independent_of_policy ... ok
test backtest::execution_config_tests::resolve_dividend_reinvestment_defaults_false_and_reads_engine_flag ... ok
test backtest::execution_config_tests::resolve_execution_config_does_not_allow_overspending_value_budget ... ok
test backtest::execution_config_tests::resolve_execution_config_enables_strict_value_budget_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_liquidity_quote_limit ... ok
test backtest::execution_config_tests::resolve_execution_config_distinguishes_timed_daily_current_close ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_same_day_buy_mark_override ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_snapshot_volume_limit ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_selection_risk_off_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_volume_limit_for_snapshot_minute_last_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_daily_next_open ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_minute_last_to_minute_price ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_rebalance_cash_mode_and_forces_minute ... ok
test backtest::execution_config_tests::resolve_execution_config_preserves_zero_minimum_commission ... ok
test backtest::execution_config_tests::resolve_execution_config_reads_nested_risk_policy ... ok
test backtest::execution_config_tests::resolve_execution_config_rejects_delay_slippage_outside_sell_then_buy ... ok
test backtest::execution_config_tests::resolve_execution_config_rejects_vwap_as_global_matching_type ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_explicit_market_rules_without_profile ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_latest_trade_time_as_main_schedule ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_runtime_schedule_time_for_minute_execution ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_static_universe_contract ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_weak_market_threshold ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_delayed_limit_open_exit ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_release_slot_on_exit_signal ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_stage ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_schedule ... ok
test backtest::execution_config_tests::retired_slippage_and_invalid_coefficients_never_become_no_slippage ... ok
test backtest::execution_config_tests::risk_policy_alias_normalization_preserves_matching_values ... ok
test backtest::execution_config_tests::risk_policy_alias_normalization_rejects_conflicting_values ... ok
test backtest::execution_config_tests::reused_candidate_plan_panel_matches_full_intraday_market_materialization ... ok
test backtest::execution_config_tests::runtime_selection_intraday_quote_detection_reads_quote_fields_only ... ok
test backtest::execution_config_tests::sorted_overlay_keeps_unchanged_zero_bits_and_counts_only_changed_rows ... ok
test backtest::execution_config_tests::sorted_overlay_transfers_owned_factor_storage_without_changing_bits ... ok
test backtest::execution_config_tests::sorted_source_row_insertion_preserves_existing_duplicate_order ... ok
test backtest::execution_config_tests::sorted_sparse_overlay_merge_falls_back_for_duplicate_keys ... ok
test backtest::execution_config_tests::sorted_sparse_overlay_merge_preserves_order_and_unmatched_rows ... ok
test backtest::execution_config_tests::source_lake_execution_price_requests_keep_compact_schema ... ok
test backtest::execution_config_tests::source_lake_backtest_bundle_context_requires_complete_metadata ... ok
test backtest::execution_config_tests::source_lake_market_cap_keeps_current_decision_date_value ... ok
test backtest::execution_config_tests::source_lake_market_cap_uses_current_decision_date_snapshot ... ok
test backtest::execution_config_tests::source_lake_model_score_context_allows_plain_rotation_without_model_score ... ok
test backtest::execution_config_tests::source_lake_model_score_context_accepts_complete_inferred_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_dynamic_mode_without_source_label_or_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_partial_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_sparse_ref_without_scope ... ok
test backtest::execution_config_tests::source_lake_request_keeps_dynamic_model_score_as_all_market_with_score_context ... ok
test backtest::execution_config_tests::source_lake_request_keeps_explicit_empty_symbols_empty ... ok
test backtest::execution_config_tests::source_lake_request_scopes_fixed_signal_symbols_and_keeps_compact_schema ... ok
test backtest::execution_config_tests::source_lake_source_rows_uses_current_row_decision_safe_prev_factor_mode ... ok
test backtest::execution_config_tests::source_lake_request_serializes_date_symbol_filter_scope ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_accepts_positive_finite_values ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_allows_paused_rows_without_close ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_missing_or_invalid_values ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_paused_rows_with_close ... ok
test backtest::execution_config_tests::source_row_cache_and_filter_distinguish_all_from_explicit_empty_symbols ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_skips_unrequested_projection ... ok
test backtest::execution_config_tests::source_row_daily_capacity_is_scoped_to_actual_insertion_dates ... ok
test backtest::execution_config_tests::source_row_grouping_copies_date_keys_only_at_boundaries ... ok
test backtest::execution_config_tests::source_row_missing_open_does_not_mark_candidate_as_one_yuan ... ok
test backtest::execution_config_tests::source_row_model_score_context_accepts_extract_only_score_metadata ... ok
test backtest::execution_config_tests::source_row_model_score_context_can_disable_candidate_limit_inference ... ok
test backtest::execution_config_tests::source_row_model_score_context_infers_candidate_limit_from_selection_limit ... ok
test backtest::execution_config_tests::source_row_daily_capacity_keeps_supplemental_inserts_in_place ... ok
test backtest::execution_config_tests::source_row_model_score_context_merges_bundle_extract_and_scopes_cache ... ok
test backtest::execution_config_tests::source_row_projection_does_not_require_adjustment_factor_for_volume_only_rolling ... ok
test backtest::execution_config_tests::source_row_projection_includes_amount_for_stock_amount_expressions ... ok
test backtest::execution_config_tests::source_row_projection_keeps_select_all_for_generic_empty_factor_refs ... ok
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_empty_factor_refs_narrow ... ok
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_without_strategy_spec_narrow ... ok
test backtest::execution_config_tests::source_row_projection_uses_framework_rolling_inputs ... ok
test backtest::execution_config_tests::source_row_projection_splits_sparse_model_score_factor_refs ... ok
test backtest::execution_config_tests::source_row_sort_preserves_published_order_and_stably_repairs_merges ... ok
test backtest::execution_config_tests::source_row_projection_uses_raw_market_columns_for_framework_rollings ... ok
test backtest::execution_config_tests::source_rows_query_engine_is_validated_and_scopes_cache_epoch ... ok
test backtest::execution_config_tests::source_rows_scope_evidence_preserves_unordered_date_counts ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_dates_exclude_warmup_rows ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_keeps_named_score_when_context_uses_default_score ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_merges_only_requested_extra_factors ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_query_windows_are_chunked_by_date_span ... ok
test backtest::execution_config_tests::source_warmup_uses_required_trading_windows ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_skips_primary_score_columns ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_reuses_source_row_cache_without_http ... ok
test backtest::execution_config_tests::stock_cost_model_for_execution_uses_explicit_fee_overrides ... ok
test backtest::execution_config_tests::sparse_overlay_skips_source_lake_when_scoped_date_symbol_filter_is_empty ... ok
test backtest::execution_config_tests::strategy_execution_top_level_risk_enabled_aliases_are_applied ... ok
test backtest::execution_config_tests::strategy_risk_policy_accepts_camel_case_fields ... ok
test backtest::execution_config_tests::supplemental_execution_key_index_only_materializes_requested_symbols ... ok
test backtest::execution_config_tests::strategy_factory_model_rank_rotation_keeps_explicit_execution_contract ... ok
test backtest::execution_config_tests::timed_daily_current_close_loads_the_intraday_decision_snapshot ... ok
test backtest::execution_config_tests::top_level_execution_limits_override_nested_risk_policy_defaults ... ok
test backtest::execution_config_tests::unique_source_row_trade_dates_preserves_sorted_and_unordered_inputs ... ok
test backtest::execution_config_tests::unsupported_backtest_frequency_is_rejected ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_explicit_no_limit_stock ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_index_without_limits ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_paused_stock_without_limits ... ok
test backtest::execution_config_tests::validated_source_limit_prices_rejects_missing_stock_limit ... ok
test backtest::execution_config_tests::validated_source_limit_prices_trusts_source_limits ... ok
test backtest::factor_history_calendar_tests::history_uses_actual_sessions_and_rejects_short_or_future_calendars ... ok
test backtest::factor_history_calendar_tests::index_policy_preloads_its_reference_without_widening_the_candidate_universe ... ok
test backtest::factor_history_calendar_tests::contextual_pattern_expands_data_scope_but_not_candidate_members ... ok
test backtest::factor_history_calendar_tests::selection_buy_permission_carries_its_complete_pattern_window ... ok
test backtest::fund_metadata::tests::day_preparation_distinguishes_etf_optional_risk_from_missing_prices ... ok
test backtest::fund_metadata::tests::etf_metadata_binds_identity_dates_and_units ... ok
test backtest::fund_metadata::tests::real_etf_fallback_fills_match_source_opens_dates_and_fee_budgets ... ignored, requires exact frontend-compiled frozen Source Lake ETF replay artifacts
test backtest::fund_metadata::tests::real_mixed_prices_preserve_holdings_until_explicit_zero_allocation ... ignored, requires FIDC_FUND_REPLAY_PREFIX containing read-only real Source Arrow fixtures
test backtest::fund_metadata::tests::real_source_arrow_preserves_etf_identity_and_detects_tampering ... ignored, requires FIDC_FUND_METADATA_TEST_ARROW from the read-only real Source probe
test backtest::fund_metadata::tests::preliminary_candidate_panel_uses_verified_etf_rules_without_inventing_limits ... ok
test backtest::manual_data_scope::tests::explicit_data_scope_can_expand_without_mutating_the_strategy_or_empty_pool ... ok
test backtest::manual_data_scope::tests::dataset_cache_tracks_extra_market_demand_not_manual_price_or_execution_identity ... ok
test backtest::prepared_source_row::tests::all_supported_numeric_types_keep_the_existing_conversion_and_null_policy ... ok
test backtest::prepared_source_row::tests::arbitrary_slices_and_supplemental_rows_keep_symbol_order ... ok
test backtest::prepared_source_row::tests::finite_values_keep_exact_float_bits_and_metadata ... ok
test backtest::prepared_source_row::tests::actual_arrow_day_uses_columnar_rows_and_matches_owned_final_snapshots ... ok
test backtest::prepared_source_row::tests::invalid_json_is_not_hidden_by_a_consumer_that_needs_no_factors ... ok
test backtest::prepared_source_row::tests::nonfinite_price_policy_does_not_mutate_original_arrow_values ... ok
test backtest::prepared_source_row::tests::overlay_cannot_relax_base_adjustment_validation ... ok
test backtest::prepared_source_row::tests::json_typed_duplicates_and_projected_overlay_preserve_overwrite_order ... ok
test backtest::risk_free_rate_cache_identity_includes_bundle_and_full_calendar ... ok
test backtest::prepared_source_row::tests::schema_error_precedence_and_null_keys_are_preserved ... ok
test backtest::risk_free_rate_cache_round_trips_validated_contract ... ok
test backtest::saved_risk_cache::tests::saved_reference_cancelled_reader_does_not_leave_a_claim ... ok
test backtest::saved_risk_cache::tests::saved_reference_identity_includes_endpoint_hash_and_calendar ... ok
test backtest::saved_risk_reference_requires_original_content_not_only_server_claimed_hash ... ok
test backtest::source_query_windows::tests::actual_cached_boundaries_are_kept_and_gaps_are_covered ... ok
test backtest::source_query_windows::tests::all_days_remain_once_and_scope_count_is_bounded ... ok
test backtest::source_query_windows::tests::discovery_never_crosses_source_keys_schemas_or_file_types ... ok
test backtest::source_query_windows::tests::overlap_prefers_more_covered_days_then_fewer_windows ... ok
test backtest::source_query_windows::tests::projection_and_symbol_scope_have_distinct_boundary_keys ... ok
test backtest::source_query_windows::tests::uncached_gaps_keep_the_existing_calendar_anchor ... ok
test backtest::source_row_key_index::tests::indexed_date_ranges_cover_days_split_across_batches ... ok
test backtest::saved_risk_cache::tests::saved_reference_coalesces_only_validated_original_contracts ... ok
test backtest::source_row_key_index::tests::normalized_keys_cannot_duplicate_across_batches ... ok
test backtest::source_row_key_index::tests::null_identity_and_declared_count_mismatch_are_rejected ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_cache_identity_cannot_be_reused_as_market_facts ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_merge_preserves_every_base_market_and_risk_fact ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_requires_an_explicit_non_market_projection ... ok
test backtest::source_row_query_purpose_tests::only_explicit_factor_overlays_omit_redundant_risk_columns ... ok
test backtest::source_rows_columnar::tests::cache_write_failure_does_not_replace_the_current_columnar_input ... ok
test backtest::source_rows_columnar::tests::actual_selection_and_empty_outputs_do_not_keep_unused_factor_columns ... ok
test backtest::source_rows_columnar::tests::columnar_normalization_matches_mixed_values_json_aliases_and_sorting ... ok
test backtest::source_rows_columnar::tests::invalid_keys_and_adjustment_are_rejected_before_selection ... ok
test backtest::source_rows_columnar::tests::malformed_json_and_dependency_identity_are_not_accepted ... ok
test backtest::source_rows_columnar::tests::non_cacheable_json_factor_names_keep_the_real_record_path ... ok
test backtest::source_rows_columnar::tests::normalized_duplicate_symbols_are_not_deduplicated ... ok
test backtest::source_rows_input_tests::cache_cannot_include_rows_outside_its_declared_date_range ... ok
test backtest::source_rows_input_tests::cache_write_failure_retains_the_real_window_and_empty_windows_do_not_read_old_rows ... ok
test backtest::source_rows_input_tests::failed_materialization_does_not_turn_the_source_into_an_empty_success ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_crosses_record_batches_and_preserves_key_normalization ... ok
test backtest::source_rows_input_tests::independent_windows_stay_columnar_until_daily_snapshot_construction ... ok
test backtest::source_rows_input_tests::invalid_cache_keys_and_overlay_coverage_never_publish_source_input ... ok
test backtest::source_rows_input_tests::mapped_input_can_be_materialized_for_mutating_model_and_publication_overlays ... ok
test backtest::source_rows_input_tests::mapped_input_preserves_empty_scopes_and_filters_only_base_rows ... ok
test backtest::source_rows_input_tests::mixed_window_storage_and_duplicate_boundaries_keep_original_semantics ... ok
test backtest::source_rows_input_tests::mapped_inventory_and_final_days_match_owned_rows_with_factor_overlays ... ok
test backtest::source_rows_input_tests::unsuccessful_window_publication_cannot_read_a_previous_cache_file ... ok
test backtest::terminal_audit_summary_tests::deferred_strategy_warning_is_not_presented_as_a_submitted_order ... ok
test backtest::terminal_audit_summary_tests::pending_terminal_state_is_explicit_in_summary_warning ... ok
test backtest::terminal_calendar_tests::delayed_deposit_requires_terminal_calendar_but_zero_day_does_not ... ok
test backtest::terminal_calendar_tests::scheduled_terminal_calendar_requires_strict_order_and_range ... ok
test backtest::tests::accounting_allocation_details_do_not_require_or_fabricate_an_order ... ok
test backtest::tests::all_source_lake_runtime_caches_are_scoped_by_backtest_bundle ... ok
test backtest::tests::arrow_reader_accepts_lz4_compressed_stream ... ok
test backtest::tests::audit_instrument_cache_values_from_request ... ignored, read-only Boris cache comparison with an explicit audit request
test backtest::tests::benchmark_borrowed_numeric_factor_keys_against_owned_strings ... ignored, manual release-mode allocation benchmark
test backtest::source_rows_input_tests::separate_arrow_factor_input_preserves_sparse_overlay_values_and_existing_layers ... ok
test backtest::tests::benchmark_date_symbol_filter_query_range_preserves_index_symbols ... ok
test backtest::tests::benchmark_query_windows_split_long_source_lake_ranges ... ok
test backtest::tests::benchmark_row_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::benchmark_row_binary_cache_roundtrips_rows ... ok
test backtest::tests::benchmark_row_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::benchmark_row_cache_path_uses_binary_cache_file ... ok
test backtest::tests::benchmark_row_cache_path_scopes_date_symbol_filter ... ok
test backtest::tests::benchmark_row_candidate_scan_removes_stale_schema_cache ... ok
test backtest::tests::benchmark_row_covering_cache_reuses_superset_symbols_and_dates ... ok
test backtest::tests::benchmark_row_partial_cache_rejects_missing_symbol_segment ... ok
test backtest::tests::benchmark_row_to_source_row_preserves_false_paused_state ... ok
test backtest::tests::benchmark_row_partial_cache_reports_missing_ranges ... ok
test backtest::tests::benchmark_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
test backtest::tests::benchmark_rows_dedup_after_window_merge ... ok
test backtest::tests::benchmark_source_row_arrow_bounded_materialization ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
test backtest::tests::benchmark_source_row_arrow_view_cache ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_actual_order_gaps_without_fills ... ok
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_after_fills_exist ... ok
test backtest::tests::benchmark_rows_empty_date_symbol_filter_does_not_reuse_existing_cache ... ok
test backtest::tests::build_trade_detail_rows_expands_split_fills_by_order_id ... ok
test backtest::tests::build_trade_detail_rows_omits_canceled_orders_without_fills ... ok
test backtest::tests::build_trade_detail_rows_matches_fill_when_order_reason_changes ... ok
test backtest::tests::build_trade_detail_rows_preserves_decision_and_execution_dates ... ok
test backtest::tests::carry_forward_target_snapshot_keeps_transition_day_then_replaces_positions ... ok
test backtest::tests::cn_index_symbols_are_not_stock_execution_quote_repair_symbols ... ok
test backtest::tests::collect_missing_execution_quote_keys_uses_canceled_stock_orders_only ... ok
test backtest::tests::compact_progress_retains_manual_counts_and_rejects_a_missing_source ... ok
test backtest::tests::compacted_carry_forward_filter_keeps_window_semantics ... ok
test backtest::tests::corporate_action_arrow_preserves_the_independent_delivery_date_and_unknown_quantities ... ok
test backtest::tests::corporate_action_binary_cache_roundtrips_rows ... ok
test backtest::tests::corporate_action_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::corporate_action_cache_path_uses_source_lake_provider_epoch ... ok
test backtest::tests::bundle_validation_observes_dependency_semantic_change_and_revocation_without_ttl ... ok
test backtest::tests::corporate_action_covering_cache_reuses_superset_scope ... ok
test backtest::tests::daily_execution_price_arrow_decode_preserves_unpriced_paused_risk_rows ... ok
test backtest::tests::daily_execution_price_arrow_requires_execution_contract_fields ... ok
test backtest::tests::daily_execution_scope_membership_is_normalized_once_and_isolated_by_date ... ok
test backtest::tests::dataset_signature_scopes_fixed_signal_source_rows ... ok
test backtest::tests::dataset_signature_scopes_fixed_signal_date_symbol_filter_with_model_score ... ok
test backtest::tests::date_symbol_filter_query_range_carry_forward_keeps_history_prefix ... ok
test backtest::tests::date_symbol_filter_query_range_exact_keeps_only_window_dates ... ok
test backtest::tests::date_symbol_filter_query_range_preserves_empty_request ... ok
test backtest::tests::dataset_signature_uses_data_requirements_not_strategy_identity_or_thresholds ... ok
test backtest::tests::default_cache_epochs_ignore_local_manifest_directory ... ok
test backtest::tests::dense_execution_quote_plan_uses_requested_trade_dates_only ... ok
test backtest::tests::dynamic_model_score_execution_only_queries_next_date_gaps ... ok
test backtest::tests::dynamic_model_score_execution_skips_rows_present_on_next_full_calendar_date ... ok
test backtest::tests::dynamic_model_score_execution_symbols_include_candidate_universe_for_lagged_execution ... ok
test backtest::tests::dynamic_symbol_expression_does_not_enable_fixed_source_scope ... ok
test backtest::tests::empty_date_symbol_filter_is_empty_request_not_all_market ... ok
test backtest::tests::equity_artifact_keeps_external_deposit_out_of_return ... ok
test backtest::tests::equity_projection_orders_facts_and_includes_the_initial_nav_peak ... ok
test backtest::tests::execution_arrow_does_not_turn_nonfinite_numbers_into_optional_missing_values ... ok
test backtest::tests::execution_arrow_requires_the_strict_source_contract_even_when_empty ... ok
test backtest::tests::execution_price_date_symbol_filter_keeps_benchmark_and_prunes_other_targets ... ok
test backtest::tests::execution_price_only_source_row_marker_round_trips ... ok
test backtest::tests::execution_quantities_are_exact_integers_not_rounded_floats ... ok
test backtest::tests::exact_source_row_windows_restore_in_parallel_and_keep_order ... ok
test backtest::tests::execution_quote_day_binary_cache_invalidates_duplicate_date_symbol ... ok
test backtest::tests::execution_quote_day_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::execution_quote_day_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::execution_quote_day_cache_does_not_persist_empty_negative_result ... ok
test backtest::tests::execution_quote_day_cache_loads_empty_without_provider_url ... ok
test backtest::tests::execution_quote_day_cache_path_includes_minute_lookback_and_price_rule ... ok
test backtest::tests::execution_quote_day_cache_path_uses_quote_epoch ... ok
test backtest::tests::execution_quote_day_cache_path_uses_source_lake_provider_tag ... ok
test backtest::tests::execution_quote_day_cache_tracks_returned_symbols_only ... ok
test backtest::tests::execution_quote_prefetch_seed_tracks_negative_symbols ... ok
test backtest::tests::execution_quote_query_tasks_keep_target_time_symbol_scopes_narrow ... ok
test backtest::tests::execution_quote_query_tasks_merge_identical_symbol_scopes_for_same_day ... ok
test backtest::tests::execution_quote_rows_duplicate_date_symbol_is_rejected ... ok
test backtest::tests::execution_quote_rows_normalize_base_stock_code_symbols ... ok
test backtest::tests::execution_quote_loader_reads_a_bounded_minute_window_instead_of_a_point_snapshot ... ok
test backtest::tests::explicit_cli_source_cache_epoch_is_stable_and_provider_scoped ... ok
test backtest::tests::external_daily_execution_symbols_without_signals_only_queries_benchmark ... ok
test backtest::tests::external_daily_execution_symbols_skip_signals_already_in_source_rows ... ok
test backtest::tests::factor_values_arrow_decode_validates_metadata_and_merges_aliases ... ok
test backtest::tests::fidc_runner_rejects_ficlaw_data_source_rows_table ... ok
test backtest::tests::fixed_minute_subscriptions_expand_static_daily_data_scope ... ok
test backtest::tests::factor_values_arrow_decode_rejects_future_data_and_semantic_drift ... ok
test backtest::tests::fixed_signal_date_symbol_filter_is_disabled_when_static_symbols_are_mixed_in ... ok
test backtest::tests::fixed_signal_date_symbol_filter_survives_model_score_rank_metadata ... ok
test backtest::tests::fixed_signal_date_symbol_filter_seeds_static_symbols_at_query_start ... ok
test backtest::tests::fixed_signal_next_open_exit_scope_keeps_all_prior_unsold_symbols ... ok
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_dated_weights_only ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_runtime_target_weights_expr ... ok
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_target_holding_rows ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_single_quoted_runtime_target_weights_expr ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_strategy_metadata_targets ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_target_weight_codes ... ok
test backtest::tests::fixed_symbol_order_scope_requires_rotation_disabled ... ok
test backtest::tests::fixed_symbol_orders_seed_exact_intraday_snapshot_keys ... ok
test backtest::tests::frozen_bundle_identity_http_failure_is_not_a_mutable_epoch_fallback ... ok
test backtest::tests::full_minute_prefetch_days_respect_total_bar_budget ... ok
test backtest::tests::futures_source_lake_catalog_requires_all_truth_datasets ... ok
test backtest::tests::futures_only_runtime_contract_keeps_stock_scope_empty_and_allocates_cash ... ok
test backtest::tests::instrument_metadata_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::instrument_metadata_binary_cache_roundtrips_rows ... ok
test backtest::tests::instrument_metadata_cache_merges_missing_symbols_incrementally ... ok
test backtest::tests::instrument_metadata_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::instrument_metadata_cache_path_is_scoped_by_historical_window ... ok
test backtest::tests::instrument_metadata_cache_path_uses_binary_format ... ok
test backtest::tests::instrument_metadata_request_serializes_historical_source_window_when_present ... ok
test backtest::tests::intraday_snapshot_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::intraday_snapshot_binary_cache_roundtrips_rows ... ok
test backtest::tests::intraday_snapshot_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::intraday_snapshot_cache_path_uses_source_lake_provider_tag ... ok
test backtest::tests::intraday_snapshot_seeds_execution_quotes_and_negative_gaps ... ok
test backtest::tests::kcb_board_and_quantity_use_source_classification ... ok
test backtest::tests::layered_execution_quote_loader_cache_preserves_shared_seed_and_isolates_overlay ... ok
test backtest::tests::lazy_execution_quote_window_backfill_keeps_future_missing_symbols ... ok
test backtest::tests::lazy_execution_quote_window_dates_use_trading_calendar_only ... ok
test backtest::tests::load_strategy_spec_rejects_removed_compatibility_profile ... ok
test backtest::tests::load_strategy_spec_rejects_removed_engine_profile_name ... ok
test backtest::tests::malformed_execution_observations_are_errors_not_empty_successes ... ok
test backtest::tests::malformed_versioned_factor_ref_fails_closed ... ok
test backtest::tests::managed_source_rows_route_by_exact_table ... ok
test backtest::tests::minute_bar_arrow_decode_accepts_bounded_multi_date_stream ... ok
test backtest::tests::load_strategy_spec_accepts_snake_case_bundle_field ... ok
test backtest::tests::minute_bar_arrow_decode_preserves_timestamp_symbol_order ... ok
test backtest::tests::minute_bar_arrow_decode_rejects_retired_string_time_schema ... ok
test backtest::tests::minute_bar_arrow_decode_rejects_duplicate_or_unordered_rows ... ok
test backtest::tests::mixed_candidate_book_binds_complete_dates_raw_sources_and_resolved_order ... ok
test backtest::tests::mixed_stock_futures_contract_requires_explicit_futures_cash ... ok
test backtest::tests::normalize_symbol_maps_bse_exchange_to_beijing_board ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_matches_row_decoder_for_wide_sparse_null_and_signed_zero_values ... ok
test backtest::tests::minute_bars_retries_transient_http_failure_and_preserves_caller_header ... ok
test backtest::tests::partial_source_row_window_cache_crops_covering_segments_in_arrow ... ok
test backtest::tests::partial_source_row_window_cache_ignores_all_market_cache_for_symbol_scope ... ok
test backtest::tests::partial_source_row_window_cache_ignores_wide_projection_segment ... ok
test backtest::tests::partial_source_row_window_cache_reports_only_missing_gaps ... ok
test backtest::tests::progress_callback_is_forwarded_to_blocking_engine_thread ... ok
test backtest::tests::published_factor_binding_normalizes_ref_and_builds_arrow_request ... ok
test backtest::tests::query_scope_batch_preserves_authoritative_window_identities ... ok
test backtest::tests::platform_quote_plan_uses_current_decision_date_source_rows ... ok
test backtest::tests::query_scope_identity_reuses_physical_window_across_distinct_bundles ... ok
test backtest::tests::real_source_quote_json_and_arrow_match_strict_reader ... ignored, requires a bounded real Source JSON/Arrow probe directory
test backtest::tests::repeated_query_views_share_only_identical_execution_observations ... ok
test backtest::tests::partial_source_row_window_cache_reuses_exact_projection_with_null_dynamic_columns ... ok
test backtest::tests::query_scope_http_carries_rolling_dependencies_and_rejects_bad_contracts ... ok
test backtest::tests::select_instrument_metadata_rejects_missing_symbols ... ok
test backtest::tests::shared_base_source_projection_keeps_raw_adjustment_contract ... ok
test backtest::tests::shared_base_source_rows_cache_is_enabled_by_default ... ok
test backtest::tests::reusable_source_row_window_cache_does_not_reuse_covering_cache ... ok
test backtest::tests::shared_base_source_rows_cache_is_only_for_unscoped_raw_rows ... ok
test backtest::tests::sorted_source_row_validator_rejects_unordered_or_duplicate_keys ... ok
test backtest::tests::source_cache_epoch_default_is_independent_from_quote_plan_epoch ... ok
test backtest::tests::source_dependency_contract_invalidates_all_authoritative_cache_scopes ... ok
test backtest::tests::segmented_source_row_cache_reads_only_uncovered_overlap_ranges ... ok
test backtest::tests::short_range_source_query_window_is_explicit_and_keeps_long_ranges_bounded ... ok
test backtest::tests::source_lake_contract_epoch_ignores_runtime_build_but_tracks_catalog ... ok
test backtest::tests::source_lake_health_contract_epoch_ignores_runtime_build ... ok
test backtest::tests::source_lake_arrow_request_retries_bad_gateway ... ok
test backtest::tests::source_lake_busy_retry_honors_retryable_capacity_response ... ok
test backtest::tests::source_lake_source_query_retries_are_bounded_and_transient_only ... ok
test backtest::tests::source_lake_source_query_timeout_accepts_highmem_alias ... ok
test backtest::tests::source_query_concurrency_defaults_and_clamps ... ok
test backtest::tests::source_lake_run_waits_for_manifest_publication_and_holds_shared_lease ... ok
test backtest::tests::source_row_arrow_cache_rejects_and_does_not_write_empty_rows ... ok
test backtest::tests::source_row_arrow_cache_rejects_duplicate_date_symbol ... ok
test backtest::tests::source_row_arrow_decode_keeps_compact_base_rows_without_extra_json ... ok
test backtest::tests::source_row_arrow_decode_preserves_dynamic_projection_columns ... ok
test backtest::tests::source_row_arrow_extra_factor_helpers_preserve_projected_columns ... ok
test backtest::tests::source_lake_uses_wider_windows_for_narrow_queries ... ok
test backtest::tests::source_row_arrow_dependency_contract_rejects_invalid_metadata ... ok
test backtest::tests::source_row_cache_candidate_scan_removes_previous_schema_files ... ok
test backtest::tests::source_row_cache_directory_index_tracks_files_published_by_other_workers ... ok
test backtest::tests::source_row_arrow_view_keeps_mmap_alive_until_materialization ... ok
test backtest::tests::source_row_arrow_view_cache_reuses_identity_and_invalidates_replacement ... ok
test backtest::tests::source_row_cache_file_range_parses_source_cache_dates ... ok
test backtest::tests::source_row_cache_path_hashes_long_projection_columns ... ok
test backtest::tests::source_row_cache_path_normalizes_projection_column_order ... ok
test backtest::tests::source_row_cache_path_scopes_explicit_source_symbols ... ok
test backtest::tests::source_row_cache_projection_check_allows_source_lake_fixed_aliases ... ok
test backtest::tests::source_row_cache_projection_check_requires_extra_factor_keys ... ok
test backtest::tests::source_row_cache_rejects_legacy_raw_bincode_without_magic_header ... ok
test backtest::tests::source_row_cache_rejects_metadata_mismatch ... ok
test backtest::tests::source_row_cache_segment_selection_prefers_small_complete_windows ... ok
test backtest::tests::source_row_cache_segment_selection_rejects_incomplete_windows ... ok
test backtest::tests::source_row_cache_writes_arrow_file ... ok
test backtest::tests::source_row_date_windows_keep_interior_buckets_stable_across_ranges ... ok
test backtest::tests::source_row_date_windows_split_long_ranges ... ok
test backtest::tests::source_row_date_windows_stay_within_requested_range ... ok
test backtest::tests::source_row_primary_keys_are_inline_without_growing_the_row_header ... ok
test backtest::tests::source_row_request_cache_epoch_scopes_runtime_query_context ... ok
test backtest::tests::source_row_segment_boundary_validator_preserves_fail_closed_integrity ... ok
test backtest::tests::source_row_symbol_normalization_preserves_canonical_and_maps_board ... ok
test backtest::tests::source_row_symbol_scope_epoch_changes_with_fixed_signal_symbols ... ok
test backtest::tests::source_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
test backtest::tests::source_rows_duplicate_date_symbol_is_rejected ... ok
test backtest::tests::source_rows_request_keeps_rolling_enabled_for_rolling_projection_columns ... ok
test backtest::tests::source_rows_unique_date_symbol_passes_without_reordering ... ok
test backtest::tests::source_semantic_change_invalidates_bundle_and_window_cache ... ok
test backtest::tests::static_universe_include_is_an_explicit_source_row_scope ... ok
test backtest::tests::stock_pool_screen_snapshot_uses_exact_selection_dates_and_full_symbol_history ... ok
test backtest::tests::strategy_bundle_path_loads_nested_spec_and_signal_scope ... ok
test backtest::tests::strategy_factor_overlay_only_queries_keys_and_missing_factor_fields ... ok
test backtest::tests::strategy_factor_overlay_rejects_duplicate_and_unordered_keys ... ok
test backtest::tests::strategy_factor_overlay_requires_exact_base_key_coverage ... ok
test backtest::tests::strategy_factory_source_lake_provider_name_is_exact ... ok
test backtest::tests::source_row_cache_lease_serializes_concurrent_misses ... ok
test backtest::tests::source_row_cache_path_separates_projection_columns ... ok
test backtest::tests::strategy_factory_source_lake_requires_url_and_matching_provider ... ok
test backtest::tests::strategy_factory_source_lake_uses_arrow_sized_query_windows_by_default ... ok
test backtest::tests::trade_display_metadata_cache_rejects_rows_outside_exact_request ... ok
test backtest::tests::trade_display_metadata_cache_is_bundle_scoped_and_covers_missing_rows ... ok
test backtest::tests::typed_equity_result_emits_service_schema ... ok
test backtest::tests::trade_display_metadata_disk_cache_roundtrips_frozen_bundle_rows ... ok
test backtest::tests::typed_order_result_keeps_zero_fill_orders ... ok
test backtest::tests::typed_trade_result_preserves_sub_cent_execution_price ... ok
test backtest::tests::validated_manifest_scope_reuses_cache_across_distinct_bundle_ids ... ok
test backtest::tests::versioned_factor_binding_rejects_research_scope ... ok
test backtest::tests::versioned_factor_binding_requires_declared_semantics ... ok
test backtest::tests::versioned_factor_ref_requires_a_binding ... ok
test backtest::typed_exit_dependency_tests::ordinary_strategy_partial_exit_also_contributes_its_window ... ok
test backtest::typed_exit_dependency_tests::typed_stock_pool_exits_contribute_the_full_rolling_window ... ok
test backtest::tests::timed_quote_dataset_signature_includes_daily_selection_plan ... ok
test compute_pool_tests::concurrent_embedded_runs_do_not_change_each_others_pool_budget ... ok
test compute_pool_tests::current_thread_async_runtime_keeps_the_compute_context ... ok
test backtest::tests::unused_daily_market_cap_prefilter_does_not_split_identical_input_cache ... ok
test cache_directory_watch::tests::notifications_cover_publication_rename_delete_and_ignore_unrelated_files ... ok
test decision_projection::tests::projection_metadata_alias_empty_fields_and_credential_paths_fail_closed ... ok
test decision_projection::tests::projection_native_request_is_complete_and_never_truncated_to_top_n ... ok
test compute_pool_tests::embedded_runner_uses_its_declared_budget_and_preserves_results ... ok
test manual_ledger::corporate_tests::actual_core_corporate_projection_is_preserved_in_the_shared_trade_row ... ok
test result_store::tests::canonical_first_divergence_reports_json_pointer ... ok
test result_store::tests::canonical_manifest_is_stable_and_transport_independent ... ok
test decision_projection::tests::projection_response_is_bound_to_source_model_clock_and_exact_population ... ok
test manual_ledger::corporate_tests::corporate_adjustments_reject_forged_money_dates_and_reference_chains ... ok
test result_store::tests::lazy_registration_defers_payload_hashing_to_section_read ... ok
test result_store::tests::fact_manifest_publish_is_idempotent_under_concurrency ... ok
test result_store::tests::process_event_store_is_content_addressed_and_round_trips ... ok
test result_store::tests::run_store_identity_keeps_different_data_epochs_separate ... ok
test result_store::tests::manual_source_and_applications_are_both_hashed_and_persisted ... ok
test result_store::tests::run_store_is_content_addressed_and_round_trips ... ok
test result_store::tests::run_store_reads_only_date_overlapping_blocks ... ok
test result_store::tests::run_store_rejects_corrupt_manifest ... ok
test thread_cpu::tests::thread_cpu_clock_keeps_unavailable_or_cross_thread_samples_unknown ... ok
test thread_cpu::tests::sleeping_time_is_not_reported_as_cpu_execution ... ok
test backtest::source_rows_columnar::tests::columnar_cache_keeps_original_output_batch_boundaries ... ok
test result: ok. 474 passed; 0 failed; 9 ignored; 0 measured; 0 filtered out; finished in 4.12s
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-95d90db6c05a9877)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/allocation_profile.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/allocation_profile-33a6f9f5570d3138)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests examples/export_manual_corporate_evidence.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/export_manual_corporate_evidence-877623d86b4ef06d)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_service-11e588cf5d2c9d39)
running 142 tests
test daily_patterns::tests::arrow_rejects_truncation_and_forged_lengths_before_decoding ... ok
test daily_patterns::tests::column_transport_is_lossless_and_rejects_ambiguous_rows ... ok
test event_stream::tests::effective_initial_cash_is_projected_without_guessing_or_losing_source_precision ... ok
test daily_patterns::tests::numeric_mode_requires_the_explicit_read_only_context ... ok
test daily_patterns::tests::daily_patterns_read_only_transport_binds_exact_input ... ok
test event_stream::tests::ordered_batch_is_idempotent_and_changed_facts_do_not_replace_history ... ignored, requires local FIDC_EVENT_TEST_POSTGRES_DSN on the private fidc-auth-pg cluster
test event_stream::tests::memory_tail_and_durable_prefix_replay_in_publish_order ... ok
test daily_patterns::tests::arrow_uses_same_kernel_and_exact_byte_proof_with_partial_scope ... ok
test event_stream::tests::live_sequence_recent_replay_and_concurrent_runs_remain_isolated ... ok
test execution_ledger::tests::dated_fact_store_rejects_reverse_order_before_projection ... ok
test execution_ledger::tests::actual_http_routes_return_both_sources_and_precise_manual_values ... ok
test execution_ledger::tests::serve_browser_fixture ... ignored, starts a local GET-only API for browser acceptance; set FIDC_LEDGER_UI_QA_PORT explicitly
test execution_ledger::tests::serve_stream_browser_fixture ... ignored, starts isolated real WebSocket/GET handlers; FIDC_LEDGER_STREAM_QA_PORT required
test execution_ledger::tests::source_precision_ids_and_later_status_are_not_rewritten ... ok
test execution_ledger::tests::streamed_manual_facts_equal_final_ledger_without_publishing_a_future_terminal ... ok
test execution_ledger::tests::typed_accounting_rows_keep_origin_without_order_identity ... ok
test execution_ledger::tests::performance_counts_both_origins_and_sums_authoritative_fees_exactly ... ok
test factor_events::tests::market_history_binds_kernel_version_and_preserves_unknown_streak ... ok
test factor_events::tests::no_account_or_execution_payload_is_accepted ... ok
test factor_events::tests::rank_history_binds_exact_scope_and_input_hash ... ok
test factor_events::tests::transport_preserves_input_identity_and_rejects_future_values ... ok
test idempotency::tests::canonical_json_order_does_not_change_request_identity ... ok
test idempotency::tests::identity_binds_owner_and_rejects_changed_request ... ok
test idempotency::tests::malformed_keys_and_unbound_records_are_rejected ... ok
test performance::tests::accounting_allocations_are_not_unidentified_exchange_fills ... ok
test performance::tests::builds_daily_weight_diagnostics_from_persisted_holdings ... ok
test performance::tests::counts_open_underwater_duration_from_the_latest_peak ... ok
test performance::tests::does_not_fake_risk_ratios_for_one_period ... ok
test performance::tests::emits_true_nav_and_multiple_recovery_episodes ... ok
test performance::tests::explicit_frozen_initial_capital_survives_a_first_day_deposit ... ok
test performance::tests::first_period_loss_has_an_initial_peak_episode_without_a_fake_date ... ok
test performance::tests::missing_benchmark_does_not_compact_dates_or_invent_a_drawdown_path ... ok
test performance::tests::missing_order_ids_and_fees_remain_unknown_not_zero_or_report_counts ... ok
test performance::tests::order_metrics_count_identities_separately_from_reports_and_partial_fills ... ok
test performance::tests::preserves_a_first_day_strategy_return_and_initial_capital ... ok
test performance::tests::preserves_the_canonical_pre_period_benchmark_baseline ... ok
test performance::tests::projects_all_canonical_risk_metrics_and_contract_metadata ... ok
test performance::tests::separates_longest_underwater_episode_from_deepest_drawdown ... ok
test performance::tests::zero_nav_is_a_full_loss_not_missing_data ... ok
test performance::tests::zero_orders_are_known_but_conflicting_order_identity_is_not_a_count ... ok
test result_analysis_tests::batched_history_bindings_preserve_validation_and_survive_cache_eviction ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
test factor_events::tests::catalog_validator_binds_the_complete_body_and_preserves_contract ... ok
test result_analysis_tests::copied_real_result_stores_and_browser_fixture ... ignored, requires FIDC_RESULT_QA_FIXTURES containing copied immutable result stores
test result_analysis_tests::failed_optional_classification_keeps_unmodified_facts ... ok
test result_analysis_tests::historical_text_search_filters_before_count_and_pagination ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
test result_analysis_tests::history_filters_use_corrected_drawdown_and_never_treat_unknown_as_zero ... ok
test result_analysis_tests::owned_analysis_reference_does_not_require_a_cached_run_binding ... ok
test result_analysis_tests::projected_history_filter_sort_and_pagination_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN; creates an isolated test schema
test result_analysis_tests::sector_filter_cannot_disable_its_classification_dependency ... ok
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
test submission_pg_tests::atomic_submission_claim_replay_and_rollback_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN and creates only an isolated schema
test tests::backtest_create_body_limit_accepts_bounded_signal_replays ... ok
test tests::backtest_event_stream_exposes_a_websocket_transport ... ok
test tests::backtest_list_cache_key_separates_summary_only_runtime_shape ... ok
test tests::backtest_list_filters_and_sorting_use_generated_summary ... ok
test tests::backtest_list_reports_cache_hit_and_effective_page_limit ... ok
test tests::backtest_list_summary_only_uses_compact_query ... ok
test tests::backtest_performance_contract_records_total_and_runner_seconds ... ok
test tests::backtest_run_compact_cache_key_separates_strategy_detail_shape ... ok
test tests::backtest_runner_data_env_accepts_strategy_factory_source_lake_provider ... ok
test tests::backtest_runner_data_env_infers_strategy_factory_source_lake_from_runtime_source_table ... ok
test tests::backtest_runner_data_env_rejects_legacy_ficlaw_data_source_table ... ok
test tests::backtest_runner_data_env_rejects_legacy_source_provider ... ok
test tests::backtest_runner_data_env_rejects_non_source_lake_provider ... ok
test tests::backtest_runner_data_env_rejects_source_lake_provider_aliases ... ok
test tests::backtest_runner_data_env_requires_strategy_factory_source_lake_url ... ok
test tests::backtest_runner_data_env_uses_strategy_factory_source_lake_only ... ok
test tests::backtest_schema_initialization_never_deletes_run_data ... ok
test tests::backtest_schema_materializes_implementation_identity_for_compact_reads ... ok
test tests::build_run_payload_does_not_mark_empty_running_summary_as_available ... ok
test tests::build_run_payload_exposes_only_canonical_strategy_detail ... ok
test tests::build_run_payload_marks_failed_error_summary_as_available ... ok
test tests::build_run_payload_marks_terminal_metric_summary_as_available ... ok
test tests::build_run_summary_preview_preserves_strategy_runtime_rebalance_signal ... ok
test tests::build_run_summary_preview_preserves_terminal_audit ... ok
test tests::canonical_result_is_recovered_from_runner_summary ... ok
test tests::capacity_override_is_frozen_once_without_changing_execution_risk ... ok
test tests::clear_backtest_list_cache_clears_compact_run_cache ... ok
test tests::compact_result_handler_uses_single_pg_lookup ... ok
test tests::compact_summary_only_runtime_select_avoids_config_json ... ok
test tests::compact_summary_sql_removes_heavy_result_artifacts ... ok
test tests::daily_execution_matching_type_normalizes_next_open_aliases_and_rejects_other_aliases ... ok
test tests::default_backtest_concurrency_scales_with_available_cpu_threads ... ok
test tests::default_backtest_source_table_uses_strategy_factory_source_lake ... ok
test tests::detail_rows_query_page_size_maps_to_offset_limit ... ok
test tests::equity_rows_hide_diagnostics_unless_explicitly_requested ... ok
test tests::execution_cost_overrides_strategy_spec_for_runner ... ok
test tests::execution_dynamic_slippage_overrides_strategy_spec_for_runner ... ok
test tests::execution_matching_type_overrides_strategy_spec_for_runner ... ok
test tests::execution_percent_slippage_alias_normalizes_to_price_ratio ... ok
test tests::execution_rebalance_cash_mode_overrides_strategy_spec_for_runner ... ok
test tests::execution_risk_overrides_strategy_spec_for_runner ... ok
test tests::execution_risk_policy_rejects_conflicting_alias_values ... ok
test tests::execution_risk_policy_rejects_non_object ... ok
test tests::execution_risk_policy_rejects_unknown_fields_before_queueing ... ok
test tests::execution_sell_then_buy_delay_slippage_requires_sell_then_buy_mode ... ok
test tests::execution_slippage_overrides_strategy_spec_for_runner ... ok
test tests::execution_top_level_risk_enabled_aliases_are_frozen_into_strategy_spec ... ok
test tests::explicit_cost_override_removes_old_aliases_and_preserves_zero_minimum ... ok
test tests::history_query_cache_has_a_bounded_entry_budget ... ok
test tests::history_search_is_bounded_and_retains_literal_characters ... ok
test tests::implementation_identity_freezes_source_and_binary_generation ... ok
test tests::manual_observations_are_run_scoped_and_private_in_the_runner_bundle ... ok
test tests::minute_execution_matching_type_uses_minute_price_and_rejects_daily_modes ... ok
test tests::persisted_runtime_projection_never_guesses_missing_frequency ... ok
test tests::persisted_runtime_projection_retains_minute_mode_and_frozen_bundle ... ok
test tests::process_event_projection_uses_stable_kind_and_filters ... ok
test tests::projected_fact_block_cache_key_is_shared_across_run_manifests ... ok
test tests::public_backtest_projection_removes_internal_paths_recursively ... ok
test tests::public_event_message_removes_internal_paths ... ok
test tests::public_result_store_manifest_hides_internal_fact_blocks ... ok
test tests::public_run_projection_does_not_reintroduce_internal_paths ... ok
test tests::result_artifact_cache_keeps_shared_fact_blocks_after_run_eviction ... ok
test tests::result_artifact_cache_rejects_conflicting_reference_for_same_sha ... ok
test tests::result_block_page_plan_reads_only_overlapping_blocks ... ok
test tests::runner_strategy_spec_path_prefers_bundle ... ok
test tests::runtime_frequency_normalizes_and_overrides_strategy_spec_for_runner ... ok
test tests::runtime_frozen_bundle_is_written_to_strategy_metadata_for_runner ... ok
test tests::runtime_projection_migration_preserves_frozen_config ... ok
test tests::runtime_symbols_override_strategy_spec_for_runner ... ok
test tests::source_lake_preflight_rejects_unsupported_minute_cash_mode ... ok
test tests::source_lake_run_contract_preflight_freezes_runtime_and_execution ... ok
test tests::source_lake_runtime_requires_frozen_backtest_bundle_contract ... ok
test tests::start_backtest_run_rejects_unready_data_source_before_persistence ... ok
test tests::startup_reconciliation_closes_orphaned_runs_and_records_audit_event ... ok
test tests::strategy_detail_keeps_strategy_body_without_full_config ... ok
test tests::strategy_detail_returns_none_without_strategy_body ... ok
test tests::strategy_expression_preflight_rejects_invalid_dsl_before_data_load ... ok
test tests::summary_list_projection_keeps_table_metrics_and_removes_detail_payloads ... ok
test tests::terminal_audit_accepts_deferred_intents_and_checks_the_retained_count ... ok
test tests::terminal_audit_contract_rejects_missing_or_inconsistent_state ... ok
test tests::top_level_limits_replace_all_nested_execution_aliases_before_preflight ... ok
test tests::typed_holding_projection_derives_weight_and_final_snapshot ... ok
test tests::typed_result_rows_filter_and_paginate_without_json_table_roundtrip ... ok
test tests::typed_risk_filter_applies_execution_fields ... ok
test event_stream::tests::replay_buffer_is_bounded_and_completed_streams_are_evicted_without_closing_subscribers ... ok
test result_analysis_tests::complete_performance_and_paged_equity_share_corrected_drawdown ... ok
test execution_ledger::tests::combined_pages_filter_before_count_and_keep_native_pages_bounded ... ok
test result: ok. 134 passed; 0 failed; 8 ignored; 0 measured; 0 filtered out; finished in 9.28s
Running unittests examples/risk_projection_readonly.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/risk_projection_readonly-70003ce8ab818e58)
running 4 tests
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests examples/stock_pool_contract_probe.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/stock_pool_contract_probe-d3a42410cb4f098c)
running 1 test
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
@@ -0,0 +1,679 @@
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-engine/crates/fidc-core)
Compiling fidc-common v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-service/crates/fidc-common)
Compiling fidc-signal-client v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-engine/crates/fidc-signal-client)
Compiling fidc-backtest-runner v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-service/services/backtest_runner)
Compiling fidc-backtest-service v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-service/services/api)
Finished `test` profile [unoptimized] target(s) in 40.48s
Running unittests src/lib.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-e8f8f9d86cad5791)
running 484 tests
test backtest::columnar_factor_overlay_tests::cache_range_rejects_symbol_disorder_outside_the_requested_date ... ok
test backtest::columnar_factor_overlay_tests::cache_range_rejects_date_disorder_outside_the_binary_search_slice ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_real_cache_value_equivalence ... ignored, requires a frozen private Boris real-data cache directory on 177
test backtest::columnar_factor_overlay_tests::columnar_overlay_missing_or_stale_cache_does_not_mutate_base ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_order_and_count_mismatches ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_window_boundary ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_late_key_mismatch_before_any_base_mutation ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_market_projection_and_invalid_cached_column_type ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_unknown_keys_are_not_hidden_as_empty_values ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_replaced_file_invalidates_view_without_mutating_pinned_input ... ok
test backtest::columnar_factor_overlay_tests::row_visitor_preserves_selected_offsets_and_late_validation_errors ... ok
test backtest::compact_storage_tests::batch_bound_factor_columns_preserve_types_nulls_and_nonfinite_values ... ok
test backtest::compact_storage_tests::inline_symbols_and_factor_maps_preserve_binary_and_json_contracts ... ok
test backtest::day_snapshot_builder::tests::a_failed_builder_cannot_publish_earlier_valid_rows ... ok
test backtest::columnar_factor_overlay_tests::valid_cache_reuses_its_key_index_and_failed_indexes_are_not_published ... ok
test backtest::day_snapshot_builder::tests::corporate_distribution_quantities_must_be_known_before_a_day_is_published ... ok
test backtest::day_snapshot_builder::tests::factor_reservation_keeps_missing_and_overwrite_semantics ... ok
test backtest::day_snapshot_builder::tests::late_source_validation_failure_invalidates_the_streamed_day ... ok
test backtest::equity_lifecycle_metadata_tests::empty_quotes_require_full_interval_lifecycle_proof_not_a_latest_status ... ok
test backtest::equity_lifecycle_metadata_tests::lifecycle_cash_days_keep_real_benchmark_without_fabricated_security_rows ... ok
test backtest::equity_lifecycle_metadata_tests::missing_identity_and_dates_do_not_become_active_securities ... ok
test backtest::execution_config_tests::capacity_preflight_requires_explicit_daily_assumptions_without_changing_risk_switches ... ok
test backtest::execution_config_tests::component_execution_quotes_seed_uses_latest_intraday_quote_before_target_time ... ok
test backtest::execution_config_tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
test backtest::execution_config_tests::daily_current_close_preserves_the_strategy_decision_clock ... ok
test backtest::execution_config_tests::daily_execution_price_request_serializes_date_symbol_filter_scope ... ok
test backtest::execution_config_tests::daily_execution_projection_keeps_benchmark_narrow_and_trade_risk_complete ... ok
test backtest::execution_config_tests::data_intraday_snapshot_time_is_strategy_spec_driven ... ok
test backtest::execution_config_tests::dataset_signature_rejects_source_lake_without_frozen_backtest_bundle ... ok
test backtest::day_snapshot_builder::tests::arrow_rows_build_the_same_final_snapshots_without_a_row_collection ... ok
test backtest::execution_config_tests::dataset_signature_scopes_dynamic_model_score_source_rows ... ok
test backtest::execution_config_tests::default_matching_type_follows_backtest_frequency ... ok
test backtest::execution_config_tests::dataset_signature_scopes_backtest_data_bundle_source_rows ... ok
test backtest::execution_config_tests::dynamic_model_score_execution_quotes_are_lazy_by_default ... ok
test backtest::execution_config_tests::dynamic_model_score_rank_does_not_extract_fixed_signal_symbols ... ok
test backtest::execution_config_tests::execution_quote_prefilter_keeps_previous_factor_rows_in_current_band ... ok
test backtest::execution_config_tests::execution_quote_prefilter_prefers_runtime_market_cap_slope ... ok
test backtest::execution_config_tests::execution_quote_query_times_follow_strategy_trade_times ... ok
test backtest::execution_config_tests::execution_top_level_fee_overrides_nested_risk_policy_cost ... ok
test backtest::execution_config_tests::execution_quote_prefilter_keeps_candidates_beyond_legacy_limit ... ok
test backtest::execution_config_tests::execution_top_level_volume_percent_overrides_nested_risk_policy ... ok
test backtest::execution_config_tests::factor_keys_use_compact_storage_without_changing_names ... ok
test backtest::execution_config_tests::extract_score_context_does_not_extract_fixed_signal_symbols ... ok
test backtest::execution_config_tests::fixed_signal_model_score_scope_does_not_validate_candidate_limit ... ok
test backtest::execution_config_tests::fixed_signal_symbol_scope_preserves_explicit_empty_targets ... ok
test backtest::execution_config_tests::full_minute_event_stream_uses_subscriptions_instead_of_point_snapshot ... ok
test backtest::execution_config_tests::minute_session_bounds_extend_only_after_post_close_rule_effective_date ... ok
test backtest::execution_config_tests::model_score_source_rows_scope_evidence_verifies_candidate_limit ... ok
test backtest::execution_config_tests::model_score_source_rows_scope_rejects_unbounded_rows ... ok
test backtest::execution_config_tests::native_next_open_pool_loads_causal_quotes_without_exposing_them_to_selection ... ok
test backtest::execution_config_tests::narrow_overlay_projection_does_not_retain_a_wide_value_buffer ... ok
test backtest::execution_config_tests::owned_date_group_producers_drop_every_input_exactly_once_on_failure ... ok
test backtest::execution_config_tests::owned_trade_day_preparation_matches_borrowed_rows ... ok
test backtest::execution_config_tests::parse_matching_type_limits_global_backtest_matching_modes ... ok
test backtest::execution_config_tests::parse_slippage_model_supports_explicit_historical_model ... ok
test backtest::execution_config_tests::parse_slippage_model_supports_limit_price ... ok
test backtest::execution_config_tests::platform_daily_factor_strategy_uses_lazy_execution_quotes_by_default ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_can_include_recent_candidates ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_does_not_let_candidates_evict_recent_orders ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_keeps_today_candidates_without_candidate_carry ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_prefers_recent_candidates_before_today_candidates ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_prioritizes_order_symbols ... ok
test backtest::execution_config_tests::platform_execution_quote_default_carry_keeps_large_recent_candidate_set ... ok
test backtest::execution_config_tests::platform_execution_route_requires_runtime_contract ... ok
test backtest::execution_config_tests::platform_runtime_snapshot_preserves_typed_time_in_force ... ok
test backtest::execution_config_tests::platform_strategy_config_rejects_loose_value_budget_without_panicking ... ok
test backtest::execution_config_tests::parallel_owned_date_groups_keep_exact_rows_and_owned_factor_allocations ... ok
test backtest::execution_config_tests::prepare_trade_day_candidate_market_cap_falls_back_to_total_market_cap ... ok
test backtest::execution_config_tests::prepare_trade_day_daily_selection_price_uses_close_without_limit_allow_flags ... ok
test backtest::execution_config_tests::prepare_trade_day_does_not_build_execution_quote_from_daily_fallback ... ok
test backtest::execution_config_tests::platform_strategy_config_uses_same_intraday_time_as_broker ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_allow_buy_when_missing_field_is_not_buy_relevant ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_daily_volume_when_snapshot_has_intraday_volume ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_external_signal_warmup_day ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_st_and_star_st_flags_independent ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_missing_risk_facts_when_runtime_filters_are_disabled ... ok
test backtest::execution_config_tests::prepare_trade_day_preserves_missing_daily_volume_contract ... ok
test backtest::execution_config_tests::prepare_trade_day_preserves_zero_ask_from_intraday_snapshot ... ok
test backtest::execution_config_tests::prepare_trade_day_records_all_missing_risk_facts_independent_of_policy ... ok
test backtest::execution_config_tests::raw_yuan_market_cap_normalizes_to_100m_units ... ok
test backtest::execution_config_tests::resolve_dividend_reinvestment_defaults_false_and_reads_engine_flag ... ok
test backtest::execution_config_tests::resolve_execution_config_does_not_allow_overspending_value_budget ... ok
test backtest::execution_config_tests::resolve_execution_config_enables_strict_value_budget_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_liquidity_quote_limit ... ok
test backtest::execution_config_tests::resolve_execution_config_distinguishes_timed_daily_current_close ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_same_day_buy_mark_override ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_snapshot_volume_limit ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_selection_risk_off_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_volume_limit_for_snapshot_minute_last_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_daily_next_open ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_minute_last_to_minute_price ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_rebalance_cash_mode_and_forces_minute ... ok
test backtest::execution_config_tests::resolve_execution_config_preserves_zero_minimum_commission ... ok
test backtest::execution_config_tests::resolve_execution_config_reads_nested_risk_policy ... ok
test backtest::execution_config_tests::resolve_execution_config_rejects_delay_slippage_outside_sell_then_buy ... ok
test backtest::execution_config_tests::resolve_execution_config_rejects_vwap_as_global_matching_type ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_explicit_market_rules_without_profile ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_latest_trade_time_as_main_schedule ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_runtime_schedule_time_for_minute_execution ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_static_universe_contract ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_weak_market_threshold ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_delayed_limit_open_exit ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_release_slot_on_exit_signal ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_schedule ... ok
test backtest::execution_config_tests::retired_slippage_and_invalid_coefficients_never_become_no_slippage ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_stage ... ok
test backtest::execution_config_tests::risk_policy_alias_normalization_preserves_matching_values ... ok
test backtest::execution_config_tests::reused_candidate_plan_panel_matches_full_intraday_market_materialization ... ok
test backtest::execution_config_tests::risk_policy_alias_normalization_rejects_conflicting_values ... ok
test backtest::execution_config_tests::runtime_selection_intraday_quote_detection_reads_quote_fields_only ... ok
test backtest::execution_config_tests::sorted_overlay_keeps_unchanged_zero_bits_and_counts_only_changed_rows ... ok
test backtest::execution_config_tests::sorted_overlay_transfers_owned_factor_storage_without_changing_bits ... ok
test backtest::execution_config_tests::sorted_source_row_insertion_preserves_existing_duplicate_order ... ok
test backtest::execution_config_tests::sorted_sparse_overlay_merge_falls_back_for_duplicate_keys ... ok
test backtest::execution_config_tests::sorted_sparse_overlay_merge_preserves_order_and_unmatched_rows ... ok
test backtest::execution_config_tests::source_lake_backtest_bundle_context_requires_complete_metadata ... ok
test backtest::execution_config_tests::source_lake_execution_price_requests_keep_compact_schema ... ok
test backtest::execution_config_tests::source_lake_market_cap_keeps_current_decision_date_value ... ok
test backtest::execution_config_tests::source_lake_market_cap_uses_current_decision_date_snapshot ... ok
test backtest::execution_config_tests::source_lake_model_score_context_accepts_complete_inferred_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_allows_plain_rotation_without_model_score ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_dynamic_mode_without_source_label_or_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_partial_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_sparse_ref_without_scope ... ok
test backtest::execution_config_tests::source_lake_request_keeps_dynamic_model_score_as_all_market_with_score_context ... ok
test backtest::execution_config_tests::source_lake_request_keeps_explicit_empty_symbols_empty ... ok
test backtest::execution_config_tests::source_lake_request_scopes_fixed_signal_symbols_and_keeps_compact_schema ... ok
test backtest::execution_config_tests::source_lake_request_serializes_date_symbol_filter_scope ... ok
test backtest::execution_config_tests::source_lake_source_rows_uses_current_row_decision_safe_prev_factor_mode ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_accepts_positive_finite_values ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_allows_paused_rows_without_close ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_paused_rows_with_close ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_missing_or_invalid_values ... ok
test backtest::execution_config_tests::source_row_cache_and_filter_distinguish_all_from_explicit_empty_symbols ... ok
test backtest::execution_config_tests::source_row_daily_capacity_is_scoped_to_actual_insertion_dates ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_skips_unrequested_projection ... ok
test backtest::execution_config_tests::source_row_grouping_copies_date_keys_only_at_boundaries ... ok
test backtest::execution_config_tests::source_row_missing_open_does_not_mark_candidate_as_one_yuan ... ok
test backtest::execution_config_tests::source_row_model_score_context_accepts_extract_only_score_metadata ... ok
test backtest::execution_config_tests::source_row_model_score_context_can_disable_candidate_limit_inference ... ok
test backtest::execution_config_tests::source_row_model_score_context_infers_candidate_limit_from_selection_limit ... ok
test backtest::execution_config_tests::source_row_daily_capacity_keeps_supplemental_inserts_in_place ... ok
test backtest::execution_config_tests::source_row_model_score_context_merges_bundle_extract_and_scopes_cache ... ok
test backtest::execution_config_tests::source_row_projection_does_not_require_adjustment_factor_for_volume_only_rolling ... ok
test backtest::execution_config_tests::source_row_projection_keeps_select_all_for_generic_empty_factor_refs ... ok
test backtest::execution_config_tests::source_row_projection_includes_amount_for_stock_amount_expressions ... ok
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_empty_factor_refs_narrow ... ok
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_without_strategy_spec_narrow ... ok
test backtest::execution_config_tests::source_row_projection_splits_sparse_model_score_factor_refs ... ok
test backtest::execution_config_tests::source_row_projection_uses_framework_rolling_inputs ... ok
test backtest::execution_config_tests::source_row_sort_preserves_published_order_and_stably_repairs_merges ... ok
test backtest::execution_config_tests::source_row_projection_uses_raw_market_columns_for_framework_rollings ... ok
test backtest::execution_config_tests::source_rows_query_engine_is_validated_and_scopes_cache_epoch ... ok
test backtest::execution_config_tests::source_rows_scope_evidence_preserves_unordered_date_counts ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_dates_exclude_warmup_rows ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_keeps_named_score_when_context_uses_default_score ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_merges_only_requested_extra_factors ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_query_windows_are_chunked_by_date_span ... ok
test backtest::execution_config_tests::source_warmup_uses_required_trading_windows ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_skips_primary_score_columns ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_reuses_source_row_cache_without_http ... ok
test backtest::execution_config_tests::stock_cost_model_for_execution_uses_explicit_fee_overrides ... ok
test backtest::execution_config_tests::sparse_overlay_skips_source_lake_when_scoped_date_symbol_filter_is_empty ... ok
test backtest::execution_config_tests::strategy_execution_top_level_risk_enabled_aliases_are_applied ... ok
test backtest::execution_config_tests::strategy_risk_policy_accepts_camel_case_fields ... ok
test backtest::execution_config_tests::supplemental_execution_key_index_only_materializes_requested_symbols ... ok
test backtest::execution_config_tests::timed_daily_current_close_loads_the_intraday_decision_snapshot ... ok
test backtest::execution_config_tests::strategy_factory_model_rank_rotation_keeps_explicit_execution_contract ... ok
test backtest::execution_config_tests::top_level_execution_limits_override_nested_risk_policy_defaults ... ok
test backtest::execution_config_tests::unique_source_row_trade_dates_preserves_sorted_and_unordered_inputs ... ok
test backtest::execution_config_tests::unsupported_backtest_frequency_is_rejected ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_explicit_no_limit_stock ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_index_without_limits ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_paused_stock_without_limits ... ok
test backtest::execution_config_tests::validated_source_limit_prices_rejects_missing_stock_limit ... ok
test backtest::execution_config_tests::validated_source_limit_prices_trusts_source_limits ... ok
test backtest::factor_history_calendar_tests::history_uses_actual_sessions_and_rejects_short_or_future_calendars ... ok
test backtest::factor_history_calendar_tests::contextual_pattern_expands_data_scope_but_not_candidate_members ... ok
test backtest::factor_history_calendar_tests::index_policy_preloads_its_reference_without_widening_the_candidate_universe ... ok
test backtest::factor_history_calendar_tests::selection_buy_permission_carries_its_complete_pattern_window ... ok
test backtest::fund_metadata::tests::day_preparation_distinguishes_etf_optional_risk_from_missing_prices ... ok
test backtest::fund_metadata::tests::etf_metadata_binds_identity_dates_and_units ... ok
test backtest::fund_metadata::tests::real_etf_fallback_fills_match_source_opens_dates_and_fee_budgets ... ignored, requires exact frontend-compiled frozen Source Lake ETF replay artifacts
test backtest::fund_metadata::tests::real_mixed_prices_preserve_holdings_until_explicit_zero_allocation ... ignored, requires FIDC_FUND_REPLAY_PREFIX containing read-only real Source Arrow fixtures
test backtest::fund_metadata::tests::real_source_arrow_preserves_etf_identity_and_detects_tampering ... ignored, requires FIDC_FUND_METADATA_TEST_ARROW from the read-only real Source probe
test backtest::fund_metadata::tests::preliminary_candidate_panel_uses_verified_etf_rules_without_inventing_limits ... ok
test backtest::manual_data_scope::tests::explicit_data_scope_can_expand_without_mutating_the_strategy_or_empty_pool ... ok
test backtest::manual_data_scope::tests::dataset_cache_tracks_extra_market_demand_not_manual_price_or_execution_identity ... ok
test backtest::prepared_source_row::tests::all_supported_numeric_types_keep_the_existing_conversion_and_null_policy ... ok
test backtest::prepared_source_row::tests::arbitrary_slices_and_supplemental_rows_keep_symbol_order ... ok
test backtest::prepared_source_row::tests::finite_values_keep_exact_float_bits_and_metadata ... ok
test backtest::prepared_source_row::tests::invalid_json_is_not_hidden_by_a_consumer_that_needs_no_factors ... ok
test backtest::prepared_source_row::tests::actual_arrow_day_uses_columnar_rows_and_matches_owned_final_snapshots ... ok
test backtest::prepared_source_row::tests::json_typed_duplicates_and_projected_overlay_preserve_overwrite_order ... ok
test backtest::prepared_source_row::tests::nonfinite_price_policy_does_not_mutate_original_arrow_values ... ok
test backtest::prepared_source_row::tests::overlay_cannot_relax_base_adjustment_validation ... ok
test backtest::risk_free_rate_cache_identity_includes_bundle_and_full_calendar ... ok
test backtest::prepared_source_row::tests::schema_error_precedence_and_null_keys_are_preserved ... ok
test backtest::risk_free_rate_cache_round_trips_validated_contract ... ok
test backtest::saved_risk_cache::tests::saved_reference_cancelled_reader_does_not_leave_a_claim ... ok
test backtest::saved_risk_cache::tests::saved_reference_identity_includes_endpoint_hash_and_calendar ... ok
test backtest::saved_risk_reference_requires_original_content_not_only_server_claimed_hash ... ok
test backtest::source_query_windows::tests::actual_cached_boundaries_are_kept_and_gaps_are_covered ... ok
test backtest::source_query_windows::tests::all_days_remain_once_and_scope_count_is_bounded ... ok
test backtest::source_query_windows::tests::discovery_never_crosses_source_keys_schemas_or_file_types ... ok
test backtest::source_query_windows::tests::overlap_prefers_more_covered_days_then_fewer_windows ... ok
test backtest::source_query_windows::tests::projection_and_symbol_scope_have_distinct_boundary_keys ... ok
test backtest::source_query_windows::tests::uncached_gaps_keep_the_existing_calendar_anchor ... ok
test backtest::source_row_key_index::tests::indexed_date_ranges_cover_days_split_across_batches ... ok
test backtest::saved_risk_cache::tests::saved_reference_coalesces_only_validated_original_contracts ... ok
test backtest::source_row_key_index::tests::normalized_keys_cannot_duplicate_across_batches ... ok
test backtest::source_row_key_index::tests::null_identity_and_declared_count_mismatch_are_rejected ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_cache_identity_cannot_be_reused_as_market_facts ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_merge_preserves_every_base_market_and_risk_fact ... ok
test backtest::source_row_query_purpose_tests::only_explicit_factor_overlays_omit_redundant_risk_columns ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_requires_an_explicit_non_market_projection ... ok
test backtest::source_rows_columnar::tests::cache_write_failure_does_not_replace_the_current_columnar_input ... ok
test backtest::source_rows_columnar::tests::actual_selection_and_empty_outputs_do_not_keep_unused_factor_columns ... ok
test backtest::source_rows_columnar::tests::columnar_normalization_matches_mixed_values_json_aliases_and_sorting ... ok
test backtest::source_rows_columnar::tests::invalid_keys_and_adjustment_are_rejected_before_selection ... ok
test backtest::source_rows_columnar::tests::malformed_json_and_dependency_identity_are_not_accepted ... ok
test backtest::source_rows_columnar::tests::non_cacheable_json_factor_names_keep_the_real_record_path ... ok
test backtest::source_rows_columnar::tests::normalized_duplicate_symbols_are_not_deduplicated ... ok
test backtest::source_rows_input_tests::cache_cannot_include_rows_outside_its_declared_date_range ... ok
test backtest::source_rows_input_tests::cache_write_failure_retains_the_real_window_and_empty_windows_do_not_read_old_rows ... ok
test backtest::source_rows_input_tests::failed_materialization_does_not_turn_the_source_into_an_empty_success ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_crosses_record_batches_and_preserves_key_normalization ... ok
test backtest::source_rows_input_tests::independent_windows_stay_columnar_until_daily_snapshot_construction ... ok
test backtest::source_rows_input_tests::mapped_input_can_be_materialized_for_mutating_model_and_publication_overlays ... ok
test backtest::source_rows_input_tests::invalid_cache_keys_and_overlay_coverage_never_publish_source_input ... ok
test backtest::source_rows_input_tests::mapped_input_preserves_empty_scopes_and_filters_only_base_rows ... ok
test backtest::source_rows_input_tests::mixed_window_storage_and_duplicate_boundaries_keep_original_semantics ... ok
test backtest::source_rows_input_tests::mapped_inventory_and_final_days_match_owned_rows_with_factor_overlays ... ok
test backtest::source_rows_input_tests::separate_arrow_factor_input_preserves_sparse_overlay_values_and_existing_layers ... ok
test backtest::terminal_audit_summary_tests::deferred_strategy_warning_is_not_presented_as_a_submitted_order ... ok
test backtest::terminal_audit_summary_tests::pending_terminal_state_is_explicit_in_summary_warning ... ok
test backtest::terminal_calendar_tests::delayed_deposit_requires_terminal_calendar_but_zero_day_does_not ... ok
test backtest::source_rows_input_tests::unsuccessful_window_publication_cannot_read_a_previous_cache_file ... ok
test backtest::terminal_calendar_tests::scheduled_terminal_calendar_requires_strict_order_and_range ... ok
test backtest::tests::accounting_allocation_details_do_not_require_or_fabricate_an_order ... ok
test backtest::tests::all_source_lake_runtime_caches_are_scoped_by_backtest_bundle ... ok
test backtest::tests::audit_instrument_cache_values_from_request ... ignored, read-only Boris cache comparison with an explicit audit request
test backtest::tests::benchmark_borrowed_numeric_factor_keys_against_owned_strings ... ignored, manual release-mode allocation benchmark
test backtest::tests::benchmark_date_symbol_filter_query_range_preserves_index_symbols ... ok
test backtest::tests::benchmark_query_windows_split_long_source_lake_ranges ... ok
test backtest::tests::arrow_reader_accepts_lz4_compressed_stream ... ok
test backtest::tests::benchmark_row_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::benchmark_row_binary_cache_roundtrips_rows ... ok
test backtest::tests::benchmark_row_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::benchmark_row_cache_path_scopes_date_symbol_filter ... ok
test backtest::tests::benchmark_row_cache_path_uses_binary_cache_file ... ok
test backtest::tests::benchmark_row_candidate_scan_removes_stale_schema_cache ... ok
test backtest::tests::benchmark_row_covering_cache_reuses_superset_symbols_and_dates ... ok
test backtest::tests::benchmark_row_partial_cache_rejects_missing_symbol_segment ... ok
test backtest::tests::benchmark_row_to_source_row_preserves_false_paused_state ... ok
test backtest::tests::benchmark_row_partial_cache_reports_missing_ranges ... ok
test backtest::tests::benchmark_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
test backtest::tests::benchmark_rows_dedup_after_window_merge ... ok
test backtest::tests::benchmark_source_row_arrow_bounded_materialization ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
test backtest::tests::benchmark_source_row_arrow_view_cache ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_actual_order_gaps_without_fills ... ok
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_after_fills_exist ... ok
test backtest::tests::benchmark_rows_empty_date_symbol_filter_does_not_reuse_existing_cache ... ok
test backtest::tests::build_trade_detail_rows_expands_split_fills_by_order_id ... ok
test backtest::tests::build_trade_detail_rows_matches_fill_when_order_reason_changes ... ok
test backtest::tests::build_trade_detail_rows_omits_canceled_orders_without_fills ... ok
test backtest::tests::build_trade_detail_rows_preserves_decision_and_execution_dates ... ok
test backtest::tests::carry_forward_target_snapshot_keeps_transition_day_then_replaces_positions ... ok
test backtest::tests::cn_index_symbols_are_not_stock_execution_quote_repair_symbols ... ok
test backtest::tests::collect_missing_execution_quote_keys_uses_canceled_stock_orders_only ... ok
test backtest::tests::compact_progress_retains_manual_counts_and_rejects_a_missing_source ... ok
test backtest::tests::compacted_carry_forward_filter_keeps_window_semantics ... ok
test backtest::tests::corporate_action_arrow_preserves_the_independent_delivery_date_and_unknown_quantities ... ok
test backtest::tests::corporate_action_binary_cache_roundtrips_rows ... ok
test backtest::tests::corporate_action_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::corporate_action_cache_path_uses_source_lake_provider_epoch ... ok
test backtest::tests::bundle_validation_observes_dependency_semantic_change_and_revocation_without_ttl ... ok
test backtest::tests::corporate_action_covering_cache_reuses_superset_scope ... ok
test backtest::tests::daily_execution_price_arrow_decode_preserves_unpriced_paused_risk_rows ... ok
test backtest::tests::daily_execution_scope_membership_is_normalized_once_and_isolated_by_date ... ok
test backtest::tests::daily_execution_price_arrow_requires_execution_contract_fields ... ok
test backtest::tests::dataset_signature_scopes_fixed_signal_source_rows ... ok
test backtest::tests::dataset_signature_scopes_fixed_signal_date_symbol_filter_with_model_score ... ok
test backtest::tests::date_symbol_filter_query_range_carry_forward_keeps_history_prefix ... ok
test backtest::tests::date_symbol_filter_query_range_exact_keeps_only_window_dates ... ok
test backtest::tests::date_symbol_filter_query_range_preserves_empty_request ... ok
test backtest::tests::dataset_signature_uses_data_requirements_not_strategy_identity_or_thresholds ... ok
test backtest::tests::default_cache_epochs_ignore_local_manifest_directory ... ok
test backtest::tests::dense_execution_quote_plan_uses_requested_trade_dates_only ... ok
test backtest::tests::dynamic_model_score_execution_only_queries_next_date_gaps ... ok
test backtest::tests::dynamic_model_score_execution_skips_rows_present_on_next_full_calendar_date ... ok
test backtest::tests::dynamic_model_score_execution_symbols_include_candidate_universe_for_lagged_execution ... ok
test backtest::tests::dynamic_symbol_expression_does_not_enable_fixed_source_scope ... ok
test backtest::tests::empty_date_symbol_filter_is_empty_request_not_all_market ... ok
test backtest::tests::equity_artifact_keeps_external_deposit_out_of_return ... ok
test backtest::tests::equity_projection_orders_facts_and_includes_the_initial_nav_peak ... ok
test backtest::tests::execution_arrow_does_not_turn_nonfinite_numbers_into_optional_missing_values ... ok
test backtest::tests::execution_arrow_requires_the_strict_source_contract_even_when_empty ... ok
test backtest::tests::execution_price_date_symbol_filter_keeps_benchmark_and_prunes_other_targets ... ok
test backtest::tests::execution_price_only_source_row_marker_round_trips ... ok
test backtest::tests::execution_quantities_are_exact_integers_not_rounded_floats ... ok
test backtest::tests::exact_source_row_windows_restore_in_parallel_and_keep_order ... ok
test backtest::tests::execution_quote_day_binary_cache_invalidates_duplicate_date_symbol ... ok
test backtest::tests::execution_quote_day_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::execution_quote_day_cache_does_not_persist_empty_negative_result ... ok
test backtest::tests::execution_quote_day_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::execution_quote_day_cache_loads_empty_without_provider_url ... ok
test backtest::tests::execution_quote_day_cache_path_includes_minute_lookback_and_price_rule ... ok
test backtest::tests::execution_quote_day_cache_path_uses_quote_epoch ... ok
test backtest::tests::execution_quote_day_cache_path_uses_source_lake_provider_tag ... ok
test backtest::tests::execution_quote_day_cache_tracks_returned_symbols_only ... ok
test backtest::tests::execution_quote_prefetch_seed_tracks_negative_symbols ... ok
test backtest::tests::execution_quote_query_tasks_keep_target_time_symbol_scopes_narrow ... ok
test backtest::tests::execution_quote_query_tasks_merge_identical_symbol_scopes_for_same_day ... ok
test backtest::tests::execution_quote_rows_duplicate_date_symbol_is_rejected ... ok
test backtest::tests::execution_quote_rows_normalize_base_stock_code_symbols ... ok
test backtest::tests::execution_quote_loader_reads_a_bounded_minute_window_instead_of_a_point_snapshot ... ok
test backtest::tests::explicit_cli_source_cache_epoch_is_stable_and_provider_scoped ... ok
test backtest::tests::external_daily_execution_symbols_without_signals_only_queries_benchmark ... ok
test backtest::tests::external_daily_execution_symbols_skip_signals_already_in_source_rows ... ok
test backtest::tests::factor_values_arrow_decode_validates_metadata_and_merges_aliases ... ok
test backtest::tests::fidc_runner_rejects_ficlaw_data_source_rows_table ... ok
test backtest::tests::factor_values_arrow_decode_rejects_future_data_and_semantic_drift ... ok
test backtest::tests::fixed_minute_subscriptions_expand_static_daily_data_scope ... ok
test backtest::tests::fixed_signal_date_symbol_filter_is_disabled_when_static_symbols_are_mixed_in ... ok
test backtest::tests::fixed_signal_date_symbol_filter_seeds_static_symbols_at_query_start ... ok
test backtest::tests::fixed_signal_date_symbol_filter_survives_model_score_rank_metadata ... ok
test backtest::tests::fixed_signal_next_open_exit_scope_keeps_all_prior_unsold_symbols ... ok
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_dated_weights_only ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_runtime_target_weights_expr ... ok
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_target_holding_rows ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_single_quoted_runtime_target_weights_expr ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_strategy_metadata_targets ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_target_weight_codes ... ok
test backtest::tests::fixed_symbol_order_scope_requires_rotation_disabled ... ok
test backtest::tests::fixed_symbol_orders_seed_exact_intraday_snapshot_keys ... ok
test backtest::tests::frozen_bundle_identity_http_failure_is_not_a_mutable_epoch_fallback ... ok
test backtest::tests::full_minute_prefetch_days_respect_total_bar_budget ... ok
test backtest::tests::futures_source_lake_catalog_requires_all_truth_datasets ... ok
test backtest::tests::futures_only_runtime_contract_keeps_stock_scope_empty_and_allocates_cash ... ok
test backtest::tests::instrument_metadata_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::instrument_metadata_binary_cache_roundtrips_rows ... ok
test backtest::tests::instrument_metadata_cache_merges_missing_symbols_incrementally ... ok
test backtest::tests::instrument_metadata_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::instrument_metadata_cache_path_is_scoped_by_historical_window ... ok
test backtest::tests::instrument_metadata_cache_path_uses_binary_format ... ok
test backtest::tests::instrument_metadata_request_serializes_historical_source_window_when_present ... ok
test backtest::tests::intraday_snapshot_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::intraday_snapshot_binary_cache_roundtrips_rows ... ok
test backtest::tests::intraday_snapshot_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::intraday_snapshot_cache_path_uses_source_lake_provider_tag ... ok
test backtest::tests::kcb_board_and_quantity_use_source_classification ... ok
test backtest::tests::intraday_snapshot_seeds_execution_quotes_and_negative_gaps ... ok
test backtest::tests::layered_execution_quote_loader_cache_preserves_shared_seed_and_isolates_overlay ... ok
test backtest::tests::lazy_execution_quote_window_dates_use_trading_calendar_only ... ok
test backtest::tests::lazy_execution_quote_window_backfill_keeps_future_missing_symbols ... ok
test backtest::tests::load_strategy_spec_rejects_removed_compatibility_profile ... ok
test backtest::tests::load_strategy_spec_rejects_removed_engine_profile_name ... ok
test backtest::tests::malformed_execution_observations_are_errors_not_empty_successes ... ok
test backtest::tests::malformed_versioned_factor_ref_fails_closed ... ok
test backtest::tests::managed_source_rows_route_by_exact_table ... ok
test backtest::tests::minute_bar_arrow_decode_accepts_bounded_multi_date_stream ... ok
test backtest::tests::load_strategy_spec_accepts_snake_case_bundle_field ... ok
test backtest::tests::minute_bar_arrow_decode_preserves_timestamp_symbol_order ... ok
test backtest::tests::minute_bar_arrow_decode_rejects_retired_string_time_schema ... ok
test backtest::tests::minute_bar_arrow_decode_rejects_duplicate_or_unordered_rows ... ok
test backtest::tests::mixed_candidate_book_binds_complete_dates_raw_sources_and_resolved_order ... ok
test backtest::tests::mixed_stock_futures_contract_requires_explicit_futures_cash ... ok
test backtest::tests::normalize_symbol_maps_bse_exchange_to_beijing_board ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_matches_row_decoder_for_wide_sparse_null_and_signed_zero_values ... ok
test backtest::tests::minute_bars_retries_transient_http_failure_and_preserves_caller_header ... ok
test backtest::tests::partial_source_row_window_cache_crops_covering_segments_in_arrow ... ok
test backtest::tests::partial_source_row_window_cache_ignores_all_market_cache_for_symbol_scope ... ok
test backtest::tests::partial_source_row_window_cache_ignores_wide_projection_segment ... ok
test backtest::tests::partial_source_row_window_cache_reports_only_missing_gaps ... ok
test backtest::tests::progress_callback_is_forwarded_to_blocking_engine_thread ... ok
test backtest::tests::published_factor_binding_normalizes_ref_and_builds_arrow_request ... ok
test backtest::tests::query_scope_batch_preserves_authoritative_window_identities ... ok
test backtest::tests::partial_source_row_window_cache_reuses_exact_projection_with_null_dynamic_columns ... ok
test backtest::tests::query_scope_identity_reuses_physical_window_across_distinct_bundles ... ok
test backtest::tests::real_source_quote_json_and_arrow_match_strict_reader ... ignored, requires a bounded real Source JSON/Arrow probe directory
test backtest::tests::repeated_query_views_share_only_identical_execution_observations ... ok
test backtest::tests::platform_quote_plan_uses_current_decision_date_source_rows ... ok
test backtest::tests::query_scope_http_carries_rolling_dependencies_and_rejects_bad_contracts ... ok
test backtest::tests::select_instrument_metadata_rejects_missing_symbols ... ok
test backtest::tests::shared_base_source_projection_keeps_raw_adjustment_contract ... ok
test backtest::tests::shared_base_source_rows_cache_is_enabled_by_default ... ok
test backtest::tests::shared_base_source_rows_cache_is_only_for_unscoped_raw_rows ... ok
test backtest::tests::reusable_source_row_window_cache_does_not_reuse_covering_cache ... ok
test backtest::tests::sorted_source_row_validator_rejects_unordered_or_duplicate_keys ... ok
test backtest::tests::source_cache_epoch_default_is_independent_from_quote_plan_epoch ... ok
test backtest::tests::source_dependency_contract_invalidates_all_authoritative_cache_scopes ... ok
test backtest::tests::segmented_source_row_cache_reads_only_uncovered_overlap_ranges ... ok
test backtest::tests::short_range_source_query_window_is_explicit_and_keeps_long_ranges_bounded ... ok
test backtest::tests::source_lake_contract_epoch_ignores_runtime_build_but_tracks_catalog ... ok
test backtest::tests::source_lake_health_contract_epoch_ignores_runtime_build ... ok
test backtest::tests::source_lake_arrow_request_retries_bad_gateway ... ok
test backtest::tests::source_lake_busy_retry_honors_retryable_capacity_response ... ok
test backtest::tests::source_lake_source_query_retries_are_bounded_and_transient_only ... ok
test backtest::tests::source_lake_source_query_timeout_accepts_highmem_alias ... ok
test backtest::tests::source_lake_run_waits_for_manifest_publication_and_holds_shared_lease ... ok
test backtest::tests::source_row_arrow_cache_rejects_and_does_not_write_empty_rows ... ok
test backtest::tests::source_lake_uses_wider_windows_for_narrow_queries ... ok
test backtest::tests::source_query_concurrency_defaults_and_clamps ... ok
test backtest::tests::source_row_arrow_decode_preserves_dynamic_projection_columns ... ok
test backtest::tests::source_row_arrow_cache_rejects_duplicate_date_symbol ... ok
test backtest::tests::source_row_arrow_dependency_contract_rejects_invalid_metadata ... ok
test backtest::tests::source_row_arrow_extra_factor_helpers_preserve_projected_columns ... ok
test backtest::tests::source_row_arrow_decode_keeps_compact_base_rows_without_extra_json ... ok
test backtest::tests::source_row_arrow_view_keeps_mmap_alive_until_materialization ... ok
test backtest::tests::source_row_arrow_view_cache_reuses_identity_and_invalidates_replacement ... ok
test backtest::tests::source_row_cache_file_range_parses_source_cache_dates ... ok
test backtest::tests::source_row_cache_candidate_scan_removes_previous_schema_files ... ok
test backtest::tests::source_row_cache_directory_index_tracks_files_published_by_other_workers ... ok
test backtest::tests::source_row_cache_path_hashes_long_projection_columns ... ok
test backtest::tests::source_row_cache_path_scopes_explicit_source_symbols ... ok
test backtest::tests::source_row_cache_lease_serializes_concurrent_misses ... ok
test backtest::tests::source_row_cache_path_normalizes_projection_column_order ... ok
test backtest::tests::source_row_cache_projection_check_requires_extra_factor_keys ... ok
test backtest::tests::source_row_cache_projection_check_allows_source_lake_fixed_aliases ... ok
test backtest::tests::source_row_cache_path_separates_projection_columns ... ok
test backtest::tests::source_row_cache_rejects_legacy_raw_bincode_without_magic_header ... ok
test backtest::tests::source_row_cache_segment_selection_prefers_small_complete_windows ... ok
test backtest::tests::source_row_cache_segment_selection_rejects_incomplete_windows ... ok
test backtest::tests::source_row_date_windows_keep_interior_buckets_stable_across_ranges ... ok
test backtest::tests::source_row_date_windows_split_long_ranges ... ok
test backtest::tests::source_row_cache_rejects_metadata_mismatch ... ok
test backtest::tests::source_row_date_windows_stay_within_requested_range ... ok
test backtest::tests::source_row_primary_keys_are_inline_without_growing_the_row_header ... ok
test backtest::tests::source_row_cache_writes_arrow_file ... ok
test backtest::tests::source_row_segment_boundary_validator_preserves_fail_closed_integrity ... ok
test backtest::tests::source_row_symbol_normalization_preserves_canonical_and_maps_board ... ok
test backtest::tests::source_row_request_cache_epoch_scopes_runtime_query_context ... ok
test backtest::tests::source_row_symbol_scope_epoch_changes_with_fixed_signal_symbols ... ok
test backtest::tests::source_rows_duplicate_date_symbol_is_rejected ... ok
test backtest::tests::source_rows_request_keeps_rolling_enabled_for_rolling_projection_columns ... ok
test backtest::tests::source_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
test backtest::tests::source_rows_unique_date_symbol_passes_without_reordering ... ok
test backtest::tests::source_semantic_change_invalidates_bundle_and_window_cache ... ok
test backtest::tests::static_universe_include_is_an_explicit_source_row_scope ... ok
test backtest::tests::stock_pool_screen_snapshot_uses_exact_selection_dates_and_full_symbol_history ... ok
test backtest::tests::strategy_bundle_path_loads_nested_spec_and_signal_scope ... ok
test backtest::tests::strategy_factor_overlay_only_queries_keys_and_missing_factor_fields ... ok
test backtest::tests::strategy_factor_overlay_requires_exact_base_key_coverage ... ok
test backtest::tests::strategy_factor_overlay_rejects_duplicate_and_unordered_keys ... ok
test backtest::tests::strategy_factory_source_lake_provider_name_is_exact ... ok
test backtest::tests::strategy_factory_source_lake_requires_url_and_matching_provider ... ok
test backtest::tests::strategy_factory_source_lake_uses_arrow_sized_query_windows_by_default ... ok
test backtest::tests::trade_display_metadata_cache_is_bundle_scoped_and_covers_missing_rows ... ok
test backtest::tests::trade_display_metadata_cache_rejects_rows_outside_exact_request ... ok
test backtest::tests::typed_equity_result_emits_service_schema ... ok
test backtest::tests::trade_display_metadata_disk_cache_roundtrips_frozen_bundle_rows ... ok
test backtest::tests::typed_order_result_keeps_zero_fill_orders ... ok
test backtest::tests::typed_trade_result_preserves_sub_cent_execution_price ... ok
test backtest::tests::validated_manifest_scope_reuses_cache_across_distinct_bundle_ids ... ok
test backtest::tests::versioned_factor_binding_rejects_research_scope ... ok
test backtest::tests::versioned_factor_binding_requires_declared_semantics ... ok
test backtest::tests::versioned_factor_ref_requires_a_binding ... ok
test backtest::typed_exit_dependency_tests::ordinary_strategy_partial_exit_also_contributes_its_window ... ok
test backtest::typed_exit_dependency_tests::typed_stock_pool_exits_contribute_the_full_rolling_window ... ok
test cache_directory_watch::tests::notifications_cover_publication_rename_delete_and_ignore_unrelated_files ... ok
test compute_pool_tests::concurrent_embedded_runs_do_not_change_each_others_pool_budget ... ok
test compute_pool_tests::current_thread_async_runtime_keeps_the_compute_context ... ok
test backtest::tests::timed_quote_dataset_signature_includes_daily_selection_plan ... ok
test decision_projection::tests::projection_metadata_alias_empty_fields_and_credential_paths_fail_closed ... ok
test decision_projection::tests::projection_native_request_is_complete_and_never_truncated_to_top_n ... ok
test decision_projection::tests::projection_response_is_bound_to_source_model_clock_and_exact_population ... ok
test compute_pool_tests::embedded_runner_uses_its_declared_budget_and_preserves_results ... ok
test manual_ledger::corporate_tests::actual_core_corporate_projection_is_preserved_in_the_shared_trade_row ... ok
test result_store::tests::canonical_first_divergence_reports_json_pointer ... ok
test result_store::tests::canonical_manifest_is_stable_and_transport_independent ... ok
test manual_ledger::corporate_tests::corporate_adjustments_reject_forged_money_dates_and_reference_chains ... ok
test backtest::tests::unused_daily_market_cap_prefilter_does_not_split_identical_input_cache ... ok
test result_store::tests::lazy_registration_defers_payload_hashing_to_section_read ... ok
test result_store::tests::fact_manifest_publish_is_idempotent_under_concurrency ... ok
test result_store::tests::process_event_store_is_content_addressed_and_round_trips ... ok
test result_store::tests::run_store_identity_keeps_different_data_epochs_separate ... ok
test result_store::tests::run_store_is_content_addressed_and_round_trips ... ok
test result_store::tests::manual_source_and_applications_are_both_hashed_and_persisted ... ok
test result_store::tests::run_store_rejects_corrupt_manifest ... ok
test result_store::tests::run_store_reads_only_date_overlapping_blocks ... ok
test thread_cpu::tests::thread_cpu_clock_keeps_unavailable_or_cross_thread_samples_unknown ... ok
test thread_cpu::tests::sleeping_time_is_not_reported_as_cpu_execution ... ok
test backtest::source_rows_columnar::tests::columnar_cache_keeps_original_output_batch_boundaries ... ok
test result: ok. 475 passed; 0 failed; 9 ignored; 0 measured; 0 filtered out; finished in 4.31s
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-b8208f20c65c40bc)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/allocation_profile.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/allocation_profile-1ab90294b6f34b33)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests examples/export_manual_corporate_evidence.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/export_manual_corporate_evidence-400d24419df6725a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_service-e322f82562f9dee9)
running 142 tests
test daily_patterns::tests::column_transport_is_lossless_and_rejects_ambiguous_rows ... ok
test daily_patterns::tests::arrow_rejects_truncation_and_forged_lengths_before_decoding ... ok
test event_stream::tests::effective_initial_cash_is_projected_without_guessing_or_losing_source_precision ... ok
test daily_patterns::tests::daily_patterns_read_only_transport_binds_exact_input ... ok
test daily_patterns::tests::numeric_mode_requires_the_explicit_read_only_context ... ok
test event_stream::tests::ordered_batch_is_idempotent_and_changed_facts_do_not_replace_history ... ignored, requires local FIDC_EVENT_TEST_POSTGRES_DSN on the private fidc-auth-pg cluster
test event_stream::tests::memory_tail_and_durable_prefix_replay_in_publish_order ... ok
test daily_patterns::tests::arrow_uses_same_kernel_and_exact_byte_proof_with_partial_scope ... ok
test event_stream::tests::live_sequence_recent_replay_and_concurrent_runs_remain_isolated ... ok
test execution_ledger::tests::actual_http_routes_return_both_sources_and_precise_manual_values ... ok
test execution_ledger::tests::dated_fact_store_rejects_reverse_order_before_projection ... ok
test execution_ledger::tests::serve_browser_fixture ... ignored, starts a local GET-only API for browser acceptance; set FIDC_LEDGER_UI_QA_PORT explicitly
test execution_ledger::tests::serve_stream_browser_fixture ... ignored, starts isolated real WebSocket/GET handlers; FIDC_LEDGER_STREAM_QA_PORT required
test execution_ledger::tests::source_precision_ids_and_later_status_are_not_rewritten ... ok
test execution_ledger::tests::streamed_manual_facts_equal_final_ledger_without_publishing_a_future_terminal ... ok
test execution_ledger::tests::typed_accounting_rows_keep_origin_without_order_identity ... ok
test execution_ledger::tests::performance_counts_both_origins_and_sums_authoritative_fees_exactly ... ok
test factor_events::tests::market_history_binds_kernel_version_and_preserves_unknown_streak ... ok
test factor_events::tests::no_account_or_execution_payload_is_accepted ... ok
test factor_events::tests::rank_history_binds_exact_scope_and_input_hash ... ok
test factor_events::tests::transport_preserves_input_identity_and_rejects_future_values ... ok
test idempotency::tests::canonical_json_order_does_not_change_request_identity ... ok
test idempotency::tests::identity_binds_owner_and_rejects_changed_request ... ok
test idempotency::tests::malformed_keys_and_unbound_records_are_rejected ... ok
test performance::tests::accounting_allocations_are_not_unidentified_exchange_fills ... ok
test performance::tests::builds_daily_weight_diagnostics_from_persisted_holdings ... ok
test performance::tests::counts_open_underwater_duration_from_the_latest_peak ... ok
test performance::tests::does_not_fake_risk_ratios_for_one_period ... ok
test performance::tests::emits_true_nav_and_multiple_recovery_episodes ... ok
test performance::tests::explicit_frozen_initial_capital_survives_a_first_day_deposit ... ok
test performance::tests::first_period_loss_has_an_initial_peak_episode_without_a_fake_date ... ok
test performance::tests::missing_benchmark_does_not_compact_dates_or_invent_a_drawdown_path ... ok
test performance::tests::missing_order_ids_and_fees_remain_unknown_not_zero_or_report_counts ... ok
test performance::tests::order_metrics_count_identities_separately_from_reports_and_partial_fills ... ok
test performance::tests::preserves_a_first_day_strategy_return_and_initial_capital ... ok
test performance::tests::preserves_the_canonical_pre_period_benchmark_baseline ... ok
test performance::tests::projects_all_canonical_risk_metrics_and_contract_metadata ... ok
test performance::tests::separates_longest_underwater_episode_from_deepest_drawdown ... ok
test performance::tests::zero_nav_is_a_full_loss_not_missing_data ... ok
test performance::tests::zero_orders_are_known_but_conflicting_order_identity_is_not_a_count ... ok
test result_analysis_tests::batched_history_bindings_preserve_validation_and_survive_cache_eviction ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
test factor_events::tests::catalog_validator_binds_the_complete_body_and_preserves_contract ... ok
test result_analysis_tests::copied_real_result_stores_and_browser_fixture ... ignored, requires FIDC_RESULT_QA_FIXTURES containing copied immutable result stores
test result_analysis_tests::failed_optional_classification_keeps_unmodified_facts ... ok
test result_analysis_tests::historical_text_search_filters_before_count_and_pagination ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
test result_analysis_tests::history_filters_use_corrected_drawdown_and_never_treat_unknown_as_zero ... ok
test result_analysis_tests::owned_analysis_reference_does_not_require_a_cached_run_binding ... ok
test result_analysis_tests::projected_history_filter_sort_and_pagination_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN; creates an isolated test schema
test result_analysis_tests::sector_filter_cannot_disable_its_classification_dependency ... ok
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
test submission_pg_tests::atomic_submission_claim_replay_and_rollback_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN and creates only an isolated schema
test tests::backtest_create_body_limit_accepts_bounded_signal_replays ... ok
test tests::backtest_event_stream_exposes_a_websocket_transport ... ok
test tests::backtest_list_cache_key_separates_summary_only_runtime_shape ... ok
test tests::backtest_list_filters_and_sorting_use_generated_summary ... ok
test tests::backtest_list_reports_cache_hit_and_effective_page_limit ... ok
test tests::backtest_list_summary_only_uses_compact_query ... ok
test tests::backtest_performance_contract_records_total_and_runner_seconds ... ok
test tests::backtest_run_compact_cache_key_separates_strategy_detail_shape ... ok
test tests::backtest_runner_data_env_accepts_strategy_factory_source_lake_provider ... ok
test tests::backtest_runner_data_env_infers_strategy_factory_source_lake_from_runtime_source_table ... ok
test tests::backtest_runner_data_env_rejects_legacy_ficlaw_data_source_table ... ok
test tests::backtest_runner_data_env_rejects_legacy_source_provider ... ok
test tests::backtest_runner_data_env_rejects_non_source_lake_provider ... ok
test tests::backtest_runner_data_env_rejects_source_lake_provider_aliases ... ok
test tests::backtest_runner_data_env_requires_strategy_factory_source_lake_url ... ok
test tests::backtest_runner_data_env_uses_strategy_factory_source_lake_only ... ok
test tests::backtest_schema_initialization_never_deletes_run_data ... ok
test tests::backtest_schema_materializes_implementation_identity_for_compact_reads ... ok
test tests::build_run_payload_does_not_mark_empty_running_summary_as_available ... ok
test tests::build_run_payload_exposes_only_canonical_strategy_detail ... ok
test tests::build_run_payload_marks_failed_error_summary_as_available ... ok
test tests::build_run_payload_marks_terminal_metric_summary_as_available ... ok
test tests::build_run_summary_preview_preserves_strategy_runtime_rebalance_signal ... ok
test tests::build_run_summary_preview_preserves_terminal_audit ... ok
test tests::canonical_result_is_recovered_from_runner_summary ... ok
test tests::capacity_override_is_frozen_once_without_changing_execution_risk ... ok
test tests::clear_backtest_list_cache_clears_compact_run_cache ... ok
test tests::compact_result_handler_uses_single_pg_lookup ... ok
test tests::compact_summary_only_runtime_select_avoids_config_json ... ok
test tests::compact_summary_sql_removes_heavy_result_artifacts ... ok
test tests::daily_execution_matching_type_normalizes_next_open_aliases_and_rejects_other_aliases ... ok
test tests::default_backtest_concurrency_scales_with_available_cpu_threads ... ok
test tests::default_backtest_source_table_uses_strategy_factory_source_lake ... ok
test tests::detail_rows_query_page_size_maps_to_offset_limit ... ok
test tests::equity_rows_hide_diagnostics_unless_explicitly_requested ... ok
test tests::execution_cost_overrides_strategy_spec_for_runner ... ok
test tests::execution_dynamic_slippage_overrides_strategy_spec_for_runner ... ok
test tests::execution_matching_type_overrides_strategy_spec_for_runner ... ok
test tests::execution_percent_slippage_alias_normalizes_to_price_ratio ... ok
test tests::execution_rebalance_cash_mode_overrides_strategy_spec_for_runner ... ok
test tests::execution_risk_overrides_strategy_spec_for_runner ... ok
test tests::execution_risk_policy_rejects_conflicting_alias_values ... ok
test tests::execution_risk_policy_rejects_non_object ... ok
test tests::execution_risk_policy_rejects_unknown_fields_before_queueing ... ok
test tests::execution_sell_then_buy_delay_slippage_requires_sell_then_buy_mode ... ok
test tests::execution_slippage_overrides_strategy_spec_for_runner ... ok
test tests::execution_top_level_risk_enabled_aliases_are_frozen_into_strategy_spec ... ok
test tests::explicit_cost_override_removes_old_aliases_and_preserves_zero_minimum ... ok
test tests::history_query_cache_has_a_bounded_entry_budget ... ok
test tests::history_search_is_bounded_and_retains_literal_characters ... ok
test tests::implementation_identity_freezes_source_and_binary_generation ... ok
test tests::manual_observations_are_run_scoped_and_private_in_the_runner_bundle ... ok
test tests::minute_execution_matching_type_uses_minute_price_and_rejects_daily_modes ... ok
test tests::persisted_runtime_projection_never_guesses_missing_frequency ... ok
test tests::persisted_runtime_projection_retains_minute_mode_and_frozen_bundle ... ok
test tests::process_event_projection_uses_stable_kind_and_filters ... ok
test tests::projected_fact_block_cache_key_is_shared_across_run_manifests ... ok
test tests::public_backtest_projection_removes_internal_paths_recursively ... ok
test tests::public_event_message_removes_internal_paths ... ok
test tests::public_result_store_manifest_hides_internal_fact_blocks ... ok
test tests::public_run_projection_does_not_reintroduce_internal_paths ... ok
test tests::result_artifact_cache_keeps_shared_fact_blocks_after_run_eviction ... ok
test tests::result_artifact_cache_rejects_conflicting_reference_for_same_sha ... ok
test tests::result_block_page_plan_reads_only_overlapping_blocks ... ok
test tests::runner_strategy_spec_path_prefers_bundle ... ok
test tests::runtime_frequency_normalizes_and_overrides_strategy_spec_for_runner ... ok
test tests::runtime_frozen_bundle_is_written_to_strategy_metadata_for_runner ... ok
test tests::runtime_projection_migration_preserves_frozen_config ... ok
test tests::runtime_symbols_override_strategy_spec_for_runner ... ok
test tests::source_lake_preflight_rejects_unsupported_minute_cash_mode ... ok
test tests::source_lake_run_contract_preflight_freezes_runtime_and_execution ... ok
test tests::source_lake_runtime_requires_frozen_backtest_bundle_contract ... ok
test tests::start_backtest_run_rejects_unready_data_source_before_persistence ... ok
test tests::startup_reconciliation_closes_orphaned_runs_and_records_audit_event ... ok
test tests::strategy_detail_keeps_strategy_body_without_full_config ... ok
test tests::strategy_detail_returns_none_without_strategy_body ... ok
test tests::strategy_expression_preflight_rejects_invalid_dsl_before_data_load ... ok
test tests::summary_list_projection_keeps_table_metrics_and_removes_detail_payloads ... ok
test tests::terminal_audit_accepts_deferred_intents_and_checks_the_retained_count ... ok
test tests::terminal_audit_contract_rejects_missing_or_inconsistent_state ... ok
test tests::top_level_limits_replace_all_nested_execution_aliases_before_preflight ... ok
test tests::typed_holding_projection_derives_weight_and_final_snapshot ... ok
test tests::typed_result_rows_filter_and_paginate_without_json_table_roundtrip ... ok
test tests::typed_risk_filter_applies_execution_fields ... ok
test event_stream::tests::replay_buffer_is_bounded_and_completed_streams_are_evicted_without_closing_subscribers ... ok
test result_analysis_tests::complete_performance_and_paged_equity_share_corrected_drawdown ... ok
test execution_ledger::tests::combined_pages_filter_before_count_and_keep_native_pages_bounded ... ok
test result: ok. 134 passed; 0 failed; 8 ignored; 0 measured; 0 filtered out; finished in 9.37s
Running unittests examples/risk_projection_readonly.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/risk_projection_readonly-5f8d5aa4ff515b03)
running 4 tests
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests examples/stock_pool_contract_probe.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/stock_pool_contract_probe-e8a8af6fd36ebc70)
running 1 test
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
@@ -0,0 +1,302 @@
Compiling proc-macro2 v1.0.106
Compiling unicode-ident v1.0.24
Compiling quote v1.0.45
Compiling libc v0.2.185
Compiling cfg-if v1.0.4
Compiling bytes v1.11.1
Compiling pin-project-lite v0.2.17
Compiling portable-atomic v1.13.1
Compiling version_check v0.9.5
Compiling autocfg v1.5.0
Compiling syn v2.0.117
Compiling typenum v1.20.0
Compiling libm v0.2.16
Compiling num-traits v0.2.19
Compiling once_cell v1.21.4
Compiling serde_core v1.0.228
Compiling zerocopy v0.8.48
Compiling errno v0.3.14
Compiling memchr v2.8.0
Compiling futures-core v0.3.32
Compiling signal-hook-registry v1.4.8
Compiling socket2 v0.6.3
Compiling mio v1.2.0
Compiling itoa v1.0.18
Compiling smallvec v1.15.1
Compiling serde v1.0.228
Compiling log v0.4.29
Compiling shlex v1.3.0
Compiling find-msvc-tools v0.1.9
Compiling futures-sink v0.3.32
Compiling stable_deref_trait v1.2.1
Compiling generic-array v0.14.7
Compiling subtle v2.6.1
Compiling cc v1.2.60
Compiling futures-channel v0.3.32
Compiling tracing-core v0.1.36
Compiling getrandom v0.3.4
Compiling synstructure v0.13.2
Compiling http v1.4.0
Compiling slab v0.4.12
Compiling futures-io v0.3.32
Compiling futures-task v0.3.32
Compiling crypto-common v0.1.7
Compiling block-buffer v0.10.4
Compiling getrandom v0.2.17
Compiling httparse v1.10.1
Compiling const-oid v0.9.6
Compiling percent-encoding v2.3.2
Compiling crunchy v0.2.4
Compiling digest v0.10.7
Compiling http-body v1.0.1
Compiling ring v0.17.14
Compiling zerocopy-derive v0.8.48
Compiling tokio-macros v2.7.0
Compiling zerofrom-derive v0.1.7
Compiling yoke-derive v0.8.2
Compiling tokio v1.52.1
Compiling zerofrom v0.1.7
Compiling serde_derive v1.0.228
Compiling yoke v0.8.2
Compiling zerovec-derive v0.11.3
Compiling zerovec v0.11.6
Compiling tracing-attributes v0.1.31
Compiling futures-macro v0.3.32
Compiling displaydoc v0.2.5
Compiling futures-util v0.3.32
Compiling tracing v0.1.44
Compiling litemap v0.8.2
Compiling tinystr v0.8.3
Compiling writeable v0.6.3
Compiling tiny-keccak v2.0.2
Compiling base64 v0.22.1
Compiling icu_locale_core v2.2.0
Compiling zerotrie v0.2.4
Compiling potential_utf v0.1.5
Compiling icu_properties_data v2.2.0
Compiling icu_normalizer_data v2.2.0
Compiling utf8_iter v1.0.4
Compiling zeroize v1.8.2
Compiling tower-service v0.3.3
Compiling rustls-pki-types v1.14.0
Compiling icu_collections v2.2.0
Compiling icu_provider v2.2.0
Compiling ppv-lite86 v0.2.21
Compiling num-integer v0.1.46
Compiling rand_core v0.10.1
Compiling bitflags v2.11.1
Compiling getrandom v0.4.2
Compiling try-lock v0.2.5
Compiling crc32fast v1.5.0
Compiling cpufeatures v0.2.17
Compiling untrusted v0.9.0
Compiling const-random-macro v0.1.16
Compiling want v0.3.1
Compiling sync_wrapper v1.0.2
Compiling ahash v0.8.12
Compiling crossbeam-utils v0.8.21
Compiling simd-adler32 v0.3.9
Compiling rustls v0.23.38
Compiling tower-layer v0.3.3
Compiling adler2 v2.0.1
Compiling ryu v1.0.23
Compiling httpdate v1.0.3
Compiling serde_json v1.0.145
Compiling atomic-waker v1.1.2
Compiling iana-time-zone v0.1.65
Compiling hyper v1.9.0
Compiling miniz_oxide v0.8.9
Compiling chrono v0.4.44
Compiling icu_properties v2.2.0
Compiling rustls-webpki v0.103.13
Compiling const-random v0.1.18
Compiling icu_normalizer v2.2.0
Compiling num-bigint v0.4.6
Compiling form_urlencoded v1.2.2
Compiling thiserror v2.0.18
Compiling ipnet v2.12.0
Compiling idna_adapter v1.2.1
Compiling flate2 v1.1.9
Compiling hyper-util v0.1.20
Compiling num-rational v0.4.2
Compiling tower v0.5.3
Compiling num-iter v0.1.45
Compiling tokio-util v0.7.18
Compiling http-body-util v0.1.3
Compiling thiserror-impl v2.0.18
Compiling num-complex v0.4.6
Compiling compression-core v0.4.32
Compiling rustversion v1.0.22
Compiling hashbrown v0.16.1
Compiling num v0.4.3
Compiling compression-codecs v0.4.38
Compiling crossbeam-epoch v0.9.18
Compiling idna v1.1.0
Compiling serde_urlencoded v0.7.1
Compiling sha1 v0.10.6
Compiling tokio-rustls v0.26.4
Compiling webpki-roots v1.0.7
Compiling half v2.7.1
Compiling smartstring v1.0.1
Compiling rayon-core v1.13.0
Compiling zmij v1.0.23
Compiling static_assertions v1.1.0
Compiling iri-string v0.7.12
Compiling semver v1.0.28
Compiling lazy_static v1.5.0
Compiling arrow-buffer v56.2.1
Compiling rustc_version v0.4.1
Compiling hyper-rustls v0.27.9
Compiling url v2.5.8
Compiling crossbeam-deque v0.8.6
Compiling async-compression v0.4.42
Compiling tower-http v0.6.8
Compiling sha2 v0.10.9
Compiling async-trait v0.1.89
Compiling rand_core v0.6.4
Compiling hybrid-array v0.4.10
Compiling rust_decimal v1.39.0
Compiling anyhow v1.0.102
Compiling utf8parse v0.2.2
Compiling arrow-schema v56.2.1
Compiling regex-syntax v0.8.10
Compiling anstyle-parse v1.0.0
Compiling reqwest v0.12.24
Compiling rand_chacha v0.3.1
Compiling arrow-data v56.2.1
Compiling castaway v0.2.4
Compiling regex-automata v0.4.14
Compiling flatbuffers v25.12.19
Compiling rhai_codegen v3.1.0
Compiling tikv-jemalloc-sys v0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8
Compiling is_terminal_polyfill v1.70.2
Compiling arrayvec v0.7.8
Compiling anstyle v1.0.14
Compiling colorchoice v1.0.5
Compiling cmov v0.5.3
Compiling either v1.15.0
Compiling equivalent v1.0.2
Compiling anstyle-query v1.1.5
Compiling ta-lib-dispatch v0.1.2 (https://github.com/TA-Lib/ta-lib.git?rev=dd5a90259a3f9e04e2da9f38bf0719a841b40108#dd5a9025)
Compiling thin-vec v0.2.16
Compiling anstream v1.0.0
Compiling ta-lib v0.8.1 (https://github.com/TA-Lib/ta-lib.git?rev=dd5a90259a3f9e04e2da9f38bf0719a841b40108#dd5a9025)
Compiling rhai v1.23.6
Compiling matchers v0.2.0
Compiling indexmap v2.11.4
Compiling rayon v1.12.0
Compiling ctutils v0.4.2
Compiling compact_str v0.10.0
Compiling arrow-array v56.2.1
Compiling rand v0.8.6
Compiling crypto-common v0.2.1
Compiling block-buffer v0.12.0
Compiling sharded-slab v0.1.7
Compiling hmac v0.12.1
Compiling tracing-log v0.2.0
Compiling thread_local v1.1.9
Compiling nu-ansi-term v0.50.3
Compiling const-oid v0.10.2
Compiling fastrand v2.4.1
Compiling clap_lex v1.1.0
Compiling twox-hash v2.1.3
Compiling hex v0.4.3
Compiling strsim v0.11.1
Compiling home v0.5.12
Compiling heck v0.5.0
Compiling clap_derive v4.6.1
Compiling reqsign v0.16.5
Compiling clap_builder v4.6.0
Compiling lz4_flex v0.11.6
Compiling backon v1.6.0
Compiling digest v0.11.2
Compiling tracing-subscriber v0.3.23
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1fa73bb/fidc-backtest-engine/crates/fidc-core)
Compiling arrow-select v56.2.1
Compiling uuid v1.23.1
Compiling futures v0.3.32
Compiling quick-xml v0.36.2
Compiling md-5 v0.10.6
Compiling rmp v0.8.15
Compiling flagset v0.4.7
Compiling dotenvy v0.15.7
Compiling cpufeatures v0.3.0
Compiling tinyvec_macros v0.1.1
Compiling tinyvec v1.11.0
Compiling fidc-common v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1fa73bb/fidc-backtest-service/crates/fidc-common)
Compiling opendal v0.50.2
Compiling rmp-serde v1.3.1
Compiling arrow-ipc v56.2.1
Compiling clap v4.6.1
Compiling bincode v1.3.3
Compiling memmap2 v0.9.11
Compiling unicode-normalization v0.1.25
Compiling chacha20 v0.10.0
Compiling rand_core v0.9.5
Compiling unicode-properties v0.1.4
Compiling parking_lot_core v0.9.12
Compiling unicode-bidi v0.3.18
Compiling stringprep v0.1.5
Compiling rand_chacha v0.9.0
Compiling rand v0.10.1
Compiling sha2 v0.11.0
Compiling hmac v0.13.0
Compiling md-5 v0.11.0
Compiling fallible-iterator v0.2.0
Compiling scopeguard v1.2.0
Compiling byteorder v1.5.0
Compiling siphasher v1.0.2
Compiling phf_shared v0.13.1
Compiling postgres-protocol v0.6.11
Compiling lock_api v0.4.14
Compiling rand v0.9.4
Compiling data-encoding v2.11.1
Compiling parking_lot v0.12.5
Compiling tungstenite v0.29.0
Compiling postgres-types v0.2.13
Compiling phf v0.13.1
Compiling deadpool-runtime v0.1.4
Compiling num_cpus v1.17.0
Compiling whoami v2.1.1
Compiling mime v0.3.17
Compiling axum-core v0.5.6
Compiling tokio-postgres v0.7.17
Compiling fidc-signal-client v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1fa73bb/fidc-backtest-engine/crates/fidc-signal-client)
Compiling deadpool v0.12.3
Compiling tokio-tungstenite v0.29.0
Compiling async-stream-impl v0.3.6
Compiling serde_path_to_error v0.1.20
Compiling matchit v0.8.4
Compiling async-stream v0.3.6
Compiling axum v0.8.9
Compiling deadpool-postgres v0.14.1
Compiling tikv-jemallocator v0.7.0
Compiling fidc-backtest-runner v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1fa73bb/fidc-backtest-service/services/backtest_runner)
error[E0425]: cannot find function `decode_source_lake_corporate_actions_arrow` in this scope
--> services/backtest_runner/src/backtest.rs:25124:20
|
25124 | let rows = decode_source_lake_corporate_actions_arrow(&body).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
39780 | / fn decode_source_lake_source_rows_arrow(
39781 | | body: &[u8],
39782 | | validate_adjustment_factor: bool,
39783 | | ) -> Result<Vec<SourceRowRecord>> {
... |
39801 | | Ok(rows)
39802 | | }
| |_- similarly named function `decode_source_lake_source_rows_arrow` defined here
|
help: a function with a similar name exists
|
25124 - let rows = decode_source_lake_corporate_actions_arrow(&body).unwrap();
25124 + let rows = decode_source_lake_source_rows_arrow(&body).unwrap();
|
help: consider importing this function
|
20351 + use crate::backtest::decode_source_lake_corporate_actions_arrow;
|
For more information about this error, try `rustc --explain E0425`.
error: could not compile `fidc-backtest-runner` (lib test) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
@@ -0,0 +1,678 @@
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-engine/crates/fidc-core)
Compiling fidc-common v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-service/crates/fidc-common)
Compiling fidc-signal-client v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-engine/crates/fidc-signal-client)
Compiling fidc-backtest-runner v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-service/services/backtest_runner)
Compiling fidc-backtest-service v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-service/services/api)
Finished `test` profile [unoptimized] target(s) in 41.16s
Running unittests src/lib.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-01cc3db11a686fbb)
running 483 tests
test backtest::columnar_factor_overlay_tests::cache_range_rejects_date_disorder_outside_the_binary_search_slice ... ok
test backtest::columnar_factor_overlay_tests::cache_range_rejects_symbol_disorder_outside_the_requested_date ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_real_cache_value_equivalence ... ignored, requires a frozen private Boris real-data cache directory on 177
test backtest::columnar_factor_overlay_tests::columnar_overlay_missing_or_stale_cache_does_not_mutate_base ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_window_boundary ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_order_and_count_mismatches ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_market_projection_and_invalid_cached_column_type ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_late_key_mismatch_before_any_base_mutation ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_replaced_file_invalidates_view_without_mutating_pinned_input ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_unknown_keys_are_not_hidden_as_empty_values ... ok
test backtest::columnar_factor_overlay_tests::row_visitor_preserves_selected_offsets_and_late_validation_errors ... ok
test backtest::compact_storage_tests::batch_bound_factor_columns_preserve_types_nulls_and_nonfinite_values ... ok
test backtest::columnar_factor_overlay_tests::valid_cache_reuses_its_key_index_and_failed_indexes_are_not_published ... ok
test backtest::compact_storage_tests::inline_symbols_and_factor_maps_preserve_binary_and_json_contracts ... ok
test backtest::day_snapshot_builder::tests::a_failed_builder_cannot_publish_earlier_valid_rows ... ok
test backtest::day_snapshot_builder::tests::factor_reservation_keeps_missing_and_overwrite_semantics ... ok
test backtest::day_snapshot_builder::tests::late_source_validation_failure_invalidates_the_streamed_day ... ok
test backtest::equity_lifecycle_metadata_tests::empty_quotes_require_full_interval_lifecycle_proof_not_a_latest_status ... ok
test backtest::equity_lifecycle_metadata_tests::lifecycle_cash_days_keep_real_benchmark_without_fabricated_security_rows ... ok
test backtest::equity_lifecycle_metadata_tests::missing_identity_and_dates_do_not_become_active_securities ... ok
test backtest::execution_config_tests::capacity_preflight_requires_explicit_daily_assumptions_without_changing_risk_switches ... ok
test backtest::execution_config_tests::component_execution_quotes_seed_uses_latest_intraday_quote_before_target_time ... ok
test backtest::execution_config_tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
test backtest::execution_config_tests::daily_current_close_preserves_the_strategy_decision_clock ... ok
test backtest::execution_config_tests::daily_execution_price_request_serializes_date_symbol_filter_scope ... ok
test backtest::execution_config_tests::daily_execution_projection_keeps_benchmark_narrow_and_trade_risk_complete ... ok
test backtest::execution_config_tests::data_intraday_snapshot_time_is_strategy_spec_driven ... ok
test backtest::execution_config_tests::dataset_signature_rejects_source_lake_without_frozen_backtest_bundle ... ok
test backtest::day_snapshot_builder::tests::arrow_rows_build_the_same_final_snapshots_without_a_row_collection ... ok
test backtest::execution_config_tests::dataset_signature_scopes_dynamic_model_score_source_rows ... ok
test backtest::execution_config_tests::default_matching_type_follows_backtest_frequency ... ok
test backtest::execution_config_tests::dataset_signature_scopes_backtest_data_bundle_source_rows ... ok
test backtest::execution_config_tests::dynamic_model_score_execution_quotes_are_lazy_by_default ... ok
test backtest::execution_config_tests::dynamic_model_score_rank_does_not_extract_fixed_signal_symbols ... ok
test backtest::execution_config_tests::execution_quote_prefilter_keeps_previous_factor_rows_in_current_band ... ok
test backtest::execution_config_tests::execution_quote_prefilter_prefers_runtime_market_cap_slope ... ok
test backtest::execution_config_tests::execution_quote_query_times_follow_strategy_trade_times ... ok
test backtest::execution_config_tests::execution_quote_prefilter_keeps_candidates_beyond_legacy_limit ... ok
test backtest::execution_config_tests::execution_top_level_fee_overrides_nested_risk_policy_cost ... ok
test backtest::execution_config_tests::execution_top_level_volume_percent_overrides_nested_risk_policy ... ok
test backtest::execution_config_tests::extract_score_context_does_not_extract_fixed_signal_symbols ... ok
test backtest::execution_config_tests::factor_keys_use_compact_storage_without_changing_names ... ok
test backtest::execution_config_tests::fixed_signal_model_score_scope_does_not_validate_candidate_limit ... ok
test backtest::execution_config_tests::fixed_signal_symbol_scope_preserves_explicit_empty_targets ... ok
test backtest::execution_config_tests::full_minute_event_stream_uses_subscriptions_instead_of_point_snapshot ... ok
test backtest::execution_config_tests::minute_session_bounds_extend_only_after_post_close_rule_effective_date ... ok
test backtest::execution_config_tests::model_score_source_rows_scope_evidence_verifies_candidate_limit ... ok
test backtest::execution_config_tests::model_score_source_rows_scope_rejects_unbounded_rows ... ok
test backtest::execution_config_tests::native_next_open_pool_loads_causal_quotes_without_exposing_them_to_selection ... ok
test backtest::execution_config_tests::narrow_overlay_projection_does_not_retain_a_wide_value_buffer ... ok
test backtest::execution_config_tests::owned_date_group_producers_drop_every_input_exactly_once_on_failure ... ok
test backtest::execution_config_tests::owned_trade_day_preparation_matches_borrowed_rows ... ok
test backtest::execution_config_tests::parse_matching_type_limits_global_backtest_matching_modes ... ok
test backtest::execution_config_tests::parse_slippage_model_supports_explicit_historical_model ... ok
test backtest::execution_config_tests::parse_slippage_model_supports_limit_price ... ok
test backtest::execution_config_tests::platform_daily_factor_strategy_uses_lazy_execution_quotes_by_default ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_can_include_recent_candidates ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_does_not_let_candidates_evict_recent_orders ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_keeps_today_candidates_without_candidate_carry ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_prefers_recent_candidates_before_today_candidates ... ok
test backtest::execution_config_tests::platform_execution_quote_carry_prioritizes_order_symbols ... ok
test backtest::execution_config_tests::platform_execution_quote_default_carry_keeps_large_recent_candidate_set ... ok
test backtest::execution_config_tests::platform_execution_route_requires_runtime_contract ... ok
test backtest::execution_config_tests::platform_runtime_snapshot_preserves_typed_time_in_force ... ok
test backtest::execution_config_tests::parallel_owned_date_groups_keep_exact_rows_and_owned_factor_allocations ... ok
test backtest::execution_config_tests::platform_strategy_config_rejects_loose_value_budget_without_panicking ... ok
test backtest::execution_config_tests::platform_strategy_config_uses_same_intraday_time_as_broker ... ok
test backtest::execution_config_tests::prepare_trade_day_candidate_market_cap_falls_back_to_total_market_cap ... ok
test backtest::execution_config_tests::prepare_trade_day_daily_selection_price_uses_close_without_limit_allow_flags ... ok
test backtest::execution_config_tests::prepare_trade_day_does_not_build_execution_quote_from_daily_fallback ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_allow_buy_when_missing_field_is_not_buy_relevant ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_daily_volume_when_snapshot_has_intraday_volume ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_external_signal_warmup_day ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_missing_risk_facts_when_runtime_filters_are_disabled ... ok
test backtest::execution_config_tests::prepare_trade_day_keeps_st_and_star_st_flags_independent ... ok
test backtest::execution_config_tests::prepare_trade_day_preserves_missing_daily_volume_contract ... ok
test backtest::execution_config_tests::prepare_trade_day_preserves_zero_ask_from_intraday_snapshot ... ok
test backtest::execution_config_tests::prepare_trade_day_records_all_missing_risk_facts_independent_of_policy ... ok
test backtest::execution_config_tests::raw_yuan_market_cap_normalizes_to_100m_units ... ok
test backtest::execution_config_tests::resolve_dividend_reinvestment_defaults_false_and_reads_engine_flag ... ok
test backtest::execution_config_tests::resolve_execution_config_does_not_allow_overspending_value_budget ... ok
test backtest::execution_config_tests::resolve_execution_config_enables_strict_value_budget_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_liquidity_quote_limit ... ok
test backtest::execution_config_tests::resolve_execution_config_distinguishes_timed_daily_current_close ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_same_day_buy_mark_override ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_snapshot_volume_limit ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_selection_risk_off_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_keeps_volume_limit_for_snapshot_minute_last_by_default ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_daily_next_open ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_minute_last_to_minute_price ... ok
test backtest::execution_config_tests::resolve_execution_config_maps_rebalance_cash_mode_and_forces_minute ... ok
test backtest::execution_config_tests::resolve_execution_config_preserves_zero_minimum_commission ... ok
test backtest::execution_config_tests::resolve_execution_config_reads_nested_risk_policy ... ok
test backtest::execution_config_tests::resolve_execution_config_rejects_delay_slippage_outside_sell_then_buy ... ok
test backtest::execution_config_tests::resolve_execution_config_rejects_vwap_as_global_matching_type ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_explicit_market_rules_without_profile ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_latest_trade_time_as_main_schedule ... ok
test backtest::execution_config_tests::resolve_execution_config_uses_runtime_schedule_time_for_minute_execution ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_static_universe_contract ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_weak_market_threshold ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_delayed_limit_open_exit ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_release_slot_on_exit_signal ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_schedule ... ok
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_stage ... ok
test backtest::execution_config_tests::retired_slippage_and_invalid_coefficients_never_become_no_slippage ... ok
test backtest::execution_config_tests::risk_policy_alias_normalization_preserves_matching_values ... ok
test backtest::execution_config_tests::risk_policy_alias_normalization_rejects_conflicting_values ... ok
test backtest::execution_config_tests::runtime_selection_intraday_quote_detection_reads_quote_fields_only ... ok
test backtest::execution_config_tests::reused_candidate_plan_panel_matches_full_intraday_market_materialization ... ok
test backtest::execution_config_tests::sorted_overlay_keeps_unchanged_zero_bits_and_counts_only_changed_rows ... ok
test backtest::execution_config_tests::sorted_overlay_transfers_owned_factor_storage_without_changing_bits ... ok
test backtest::execution_config_tests::sorted_source_row_insertion_preserves_existing_duplicate_order ... ok
test backtest::execution_config_tests::sorted_sparse_overlay_merge_falls_back_for_duplicate_keys ... ok
test backtest::execution_config_tests::sorted_sparse_overlay_merge_preserves_order_and_unmatched_rows ... ok
test backtest::execution_config_tests::source_lake_backtest_bundle_context_requires_complete_metadata ... ok
test backtest::execution_config_tests::source_lake_execution_price_requests_keep_compact_schema ... ok
test backtest::execution_config_tests::source_lake_market_cap_keeps_current_decision_date_value ... ok
test backtest::execution_config_tests::source_lake_market_cap_uses_current_decision_date_snapshot ... ok
test backtest::execution_config_tests::source_lake_model_score_context_allows_plain_rotation_without_model_score ... ok
test backtest::execution_config_tests::source_lake_model_score_context_accepts_complete_inferred_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_dynamic_mode_without_source_label_or_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_partial_scope ... ok
test backtest::execution_config_tests::source_lake_model_score_context_rejects_sparse_ref_without_scope ... ok
test backtest::execution_config_tests::source_lake_request_keeps_dynamic_model_score_as_all_market_with_score_context ... ok
test backtest::execution_config_tests::source_lake_request_keeps_explicit_empty_symbols_empty ... ok
test backtest::execution_config_tests::source_lake_request_scopes_fixed_signal_symbols_and_keeps_compact_schema ... ok
test backtest::execution_config_tests::source_lake_request_serializes_date_symbol_filter_scope ... ok
test backtest::execution_config_tests::source_lake_source_rows_uses_current_row_decision_safe_prev_factor_mode ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_accepts_positive_finite_values ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_allows_paused_rows_without_close ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_missing_or_invalid_values ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_paused_rows_with_close ... ok
test backtest::execution_config_tests::source_row_cache_and_filter_distinguish_all_from_explicit_empty_symbols ... ok
test backtest::execution_config_tests::source_row_adjustment_factor_decode_skips_unrequested_projection ... ok
test backtest::execution_config_tests::source_row_daily_capacity_is_scoped_to_actual_insertion_dates ... ok
test backtest::execution_config_tests::source_row_grouping_copies_date_keys_only_at_boundaries ... ok
test backtest::execution_config_tests::source_row_missing_open_does_not_mark_candidate_as_one_yuan ... ok
test backtest::execution_config_tests::source_row_model_score_context_accepts_extract_only_score_metadata ... ok
test backtest::execution_config_tests::source_row_model_score_context_can_disable_candidate_limit_inference ... ok
test backtest::execution_config_tests::source_row_model_score_context_infers_candidate_limit_from_selection_limit ... ok
test backtest::execution_config_tests::source_row_model_score_context_merges_bundle_extract_and_scopes_cache ... ok
test backtest::execution_config_tests::source_row_daily_capacity_keeps_supplemental_inserts_in_place ... ok
test backtest::execution_config_tests::source_row_projection_does_not_require_adjustment_factor_for_volume_only_rolling ... ok
test backtest::execution_config_tests::source_row_projection_includes_amount_for_stock_amount_expressions ... ok
test backtest::execution_config_tests::source_row_projection_keeps_select_all_for_generic_empty_factor_refs ... ok
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_empty_factor_refs_narrow ... ok
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_without_strategy_spec_narrow ... ok
test backtest::execution_config_tests::source_row_projection_splits_sparse_model_score_factor_refs ... ok
test backtest::execution_config_tests::source_row_projection_uses_framework_rolling_inputs ... ok
test backtest::execution_config_tests::source_row_sort_preserves_published_order_and_stably_repairs_merges ... ok
test backtest::execution_config_tests::source_row_projection_uses_raw_market_columns_for_framework_rollings ... ok
test backtest::execution_config_tests::source_rows_query_engine_is_validated_and_scopes_cache_epoch ... ok
test backtest::execution_config_tests::source_rows_scope_evidence_preserves_unordered_date_counts ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_dates_exclude_warmup_rows ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_keeps_named_score_when_context_uses_default_score ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_merges_only_requested_extra_factors ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_query_windows_are_chunked_by_date_span ... ok
test backtest::execution_config_tests::source_warmup_uses_required_trading_windows ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_skips_primary_score_columns ... ok
test backtest::execution_config_tests::sparse_model_score_overlay_reuses_source_row_cache_without_http ... ok
test backtest::execution_config_tests::stock_cost_model_for_execution_uses_explicit_fee_overrides ... ok
test backtest::execution_config_tests::sparse_overlay_skips_source_lake_when_scoped_date_symbol_filter_is_empty ... ok
test backtest::execution_config_tests::strategy_execution_top_level_risk_enabled_aliases_are_applied ... ok
test backtest::execution_config_tests::strategy_risk_policy_accepts_camel_case_fields ... ok
test backtest::execution_config_tests::supplemental_execution_key_index_only_materializes_requested_symbols ... ok
test backtest::execution_config_tests::strategy_factory_model_rank_rotation_keeps_explicit_execution_contract ... ok
test backtest::execution_config_tests::timed_daily_current_close_loads_the_intraday_decision_snapshot ... ok
test backtest::execution_config_tests::top_level_execution_limits_override_nested_risk_policy_defaults ... ok
test backtest::execution_config_tests::unique_source_row_trade_dates_preserves_sorted_and_unordered_inputs ... ok
test backtest::execution_config_tests::unsupported_backtest_frequency_is_rejected ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_explicit_no_limit_stock ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_index_without_limits ... ok
test backtest::execution_config_tests::validated_source_limit_prices_allows_paused_stock_without_limits ... ok
test backtest::execution_config_tests::validated_source_limit_prices_rejects_missing_stock_limit ... ok
test backtest::execution_config_tests::validated_source_limit_prices_trusts_source_limits ... ok
test backtest::factor_history_calendar_tests::history_uses_actual_sessions_and_rejects_short_or_future_calendars ... ok
test backtest::factor_history_calendar_tests::contextual_pattern_expands_data_scope_but_not_candidate_members ... ok
test backtest::factor_history_calendar_tests::index_policy_preloads_its_reference_without_widening_the_candidate_universe ... ok
test backtest::factor_history_calendar_tests::selection_buy_permission_carries_its_complete_pattern_window ... ok
test backtest::fund_metadata::tests::day_preparation_distinguishes_etf_optional_risk_from_missing_prices ... ok
test backtest::fund_metadata::tests::etf_metadata_binds_identity_dates_and_units ... ok
test backtest::fund_metadata::tests::real_etf_fallback_fills_match_source_opens_dates_and_fee_budgets ... ignored, requires exact frontend-compiled frozen Source Lake ETF replay artifacts
test backtest::fund_metadata::tests::real_mixed_prices_preserve_holdings_until_explicit_zero_allocation ... ignored, requires FIDC_FUND_REPLAY_PREFIX containing read-only real Source Arrow fixtures
test backtest::fund_metadata::tests::real_source_arrow_preserves_etf_identity_and_detects_tampering ... ignored, requires FIDC_FUND_METADATA_TEST_ARROW from the read-only real Source probe
test backtest::fund_metadata::tests::preliminary_candidate_panel_uses_verified_etf_rules_without_inventing_limits ... ok
test backtest::manual_data_scope::tests::explicit_data_scope_can_expand_without_mutating_the_strategy_or_empty_pool ... ok
test backtest::manual_data_scope::tests::dataset_cache_tracks_extra_market_demand_not_manual_price_or_execution_identity ... ok
test backtest::prepared_source_row::tests::all_supported_numeric_types_keep_the_existing_conversion_and_null_policy ... ok
test backtest::prepared_source_row::tests::arbitrary_slices_and_supplemental_rows_keep_symbol_order ... ok
test backtest::prepared_source_row::tests::finite_values_keep_exact_float_bits_and_metadata ... ok
test backtest::prepared_source_row::tests::invalid_json_is_not_hidden_by_a_consumer_that_needs_no_factors ... ok
test backtest::prepared_source_row::tests::json_typed_duplicates_and_projected_overlay_preserve_overwrite_order ... ok
test backtest::prepared_source_row::tests::actual_arrow_day_uses_columnar_rows_and_matches_owned_final_snapshots ... ok
test backtest::prepared_source_row::tests::nonfinite_price_policy_does_not_mutate_original_arrow_values ... ok
test backtest::prepared_source_row::tests::schema_error_precedence_and_null_keys_are_preserved ... ok
test backtest::prepared_source_row::tests::overlay_cannot_relax_base_adjustment_validation ... ok
test backtest::risk_free_rate_cache_identity_includes_bundle_and_full_calendar ... ok
test backtest::risk_free_rate_cache_round_trips_validated_contract ... ok
test backtest::saved_risk_cache::tests::saved_reference_cancelled_reader_does_not_leave_a_claim ... ok
test backtest::saved_risk_cache::tests::saved_reference_identity_includes_endpoint_hash_and_calendar ... ok
test backtest::saved_risk_reference_requires_original_content_not_only_server_claimed_hash ... ok
test backtest::source_query_windows::tests::actual_cached_boundaries_are_kept_and_gaps_are_covered ... ok
test backtest::source_query_windows::tests::all_days_remain_once_and_scope_count_is_bounded ... ok
test backtest::source_query_windows::tests::discovery_never_crosses_source_keys_schemas_or_file_types ... ok
test backtest::source_query_windows::tests::overlap_prefers_more_covered_days_then_fewer_windows ... ok
test backtest::source_query_windows::tests::projection_and_symbol_scope_have_distinct_boundary_keys ... ok
test backtest::saved_risk_cache::tests::saved_reference_coalesces_only_validated_original_contracts ... ok
test backtest::source_query_windows::tests::uncached_gaps_keep_the_existing_calendar_anchor ... ok
test backtest::source_row_key_index::tests::indexed_date_ranges_cover_days_split_across_batches ... ok
test backtest::source_row_key_index::tests::normalized_keys_cannot_duplicate_across_batches ... ok
test backtest::source_row_key_index::tests::null_identity_and_declared_count_mismatch_are_rejected ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_cache_identity_cannot_be_reused_as_market_facts ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_merge_preserves_every_base_market_and_risk_fact ... ok
test backtest::source_row_query_purpose_tests::factor_overlay_requires_an_explicit_non_market_projection ... ok
test backtest::source_row_query_purpose_tests::only_explicit_factor_overlays_omit_redundant_risk_columns ... ok
test backtest::source_rows_columnar::tests::cache_write_failure_does_not_replace_the_current_columnar_input ... ok
test backtest::source_rows_columnar::tests::actual_selection_and_empty_outputs_do_not_keep_unused_factor_columns ... ok
test backtest::source_rows_columnar::tests::columnar_normalization_matches_mixed_values_json_aliases_and_sorting ... ok
test backtest::source_rows_columnar::tests::invalid_keys_and_adjustment_are_rejected_before_selection ... ok
test backtest::source_rows_columnar::tests::malformed_json_and_dependency_identity_are_not_accepted ... ok
test backtest::source_rows_columnar::tests::non_cacheable_json_factor_names_keep_the_real_record_path ... ok
test backtest::source_rows_columnar::tests::normalized_duplicate_symbols_are_not_deduplicated ... ok
test backtest::source_rows_input_tests::cache_cannot_include_rows_outside_its_declared_date_range ... ok
test backtest::source_rows_input_tests::cache_write_failure_retains_the_real_window_and_empty_windows_do_not_read_old_rows ... ok
test backtest::source_rows_input_tests::failed_materialization_does_not_turn_the_source_into_an_empty_success ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_crosses_record_batches_and_preserves_key_normalization ... ok
test backtest::source_rows_input_tests::independent_windows_stay_columnar_until_daily_snapshot_construction ... ok
test backtest::source_rows_input_tests::mapped_input_can_be_materialized_for_mutating_model_and_publication_overlays ... ok
test backtest::source_rows_input_tests::invalid_cache_keys_and_overlay_coverage_never_publish_source_input ... ok
test backtest::source_rows_input_tests::mapped_input_preserves_empty_scopes_and_filters_only_base_rows ... ok
test backtest::source_rows_input_tests::mixed_window_storage_and_duplicate_boundaries_keep_original_semantics ... ok
test backtest::source_rows_input_tests::mapped_inventory_and_final_days_match_owned_rows_with_factor_overlays ... ok
test backtest::source_rows_input_tests::unsuccessful_window_publication_cannot_read_a_previous_cache_file ... ok
test backtest::terminal_audit_summary_tests::deferred_strategy_warning_is_not_presented_as_a_submitted_order ... ok
test backtest::terminal_audit_summary_tests::pending_terminal_state_is_explicit_in_summary_warning ... ok
test backtest::terminal_calendar_tests::delayed_deposit_requires_terminal_calendar_but_zero_day_does_not ... ok
test backtest::terminal_calendar_tests::scheduled_terminal_calendar_requires_strict_order_and_range ... ok
test backtest::source_rows_input_tests::separate_arrow_factor_input_preserves_sparse_overlay_values_and_existing_layers ... ok
test backtest::tests::accounting_allocation_details_do_not_require_or_fabricate_an_order ... ok
test backtest::tests::all_source_lake_runtime_caches_are_scoped_by_backtest_bundle ... ok
test backtest::tests::audit_instrument_cache_values_from_request ... ignored, read-only Boris cache comparison with an explicit audit request
test backtest::tests::benchmark_borrowed_numeric_factor_keys_against_owned_strings ... ignored, manual release-mode allocation benchmark
test backtest::tests::benchmark_date_symbol_filter_query_range_preserves_index_symbols ... ok
test backtest::tests::benchmark_query_windows_split_long_source_lake_ranges ... ok
test backtest::tests::arrow_reader_accepts_lz4_compressed_stream ... ok
test backtest::tests::benchmark_row_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::benchmark_row_binary_cache_roundtrips_rows ... ok
test backtest::tests::benchmark_row_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::benchmark_row_cache_path_scopes_date_symbol_filter ... ok
test backtest::tests::benchmark_row_cache_path_uses_binary_cache_file ... ok
test backtest::tests::benchmark_row_candidate_scan_removes_stale_schema_cache ... ok
test backtest::tests::benchmark_row_covering_cache_reuses_superset_symbols_and_dates ... ok
test backtest::tests::benchmark_row_partial_cache_rejects_missing_symbol_segment ... ok
test backtest::tests::benchmark_row_to_source_row_preserves_false_paused_state ... ok
test backtest::tests::benchmark_row_partial_cache_reports_missing_ranges ... ok
test backtest::tests::benchmark_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
test backtest::tests::benchmark_rows_dedup_after_window_merge ... ok
test backtest::tests::benchmark_source_row_arrow_bounded_materialization ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
test backtest::tests::benchmark_source_row_arrow_view_cache ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_actual_order_gaps_without_fills ... ok
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_after_fills_exist ... ok
test backtest::tests::benchmark_rows_empty_date_symbol_filter_does_not_reuse_existing_cache ... ok
test backtest::tests::build_trade_detail_rows_expands_split_fills_by_order_id ... ok
test backtest::tests::build_trade_detail_rows_omits_canceled_orders_without_fills ... ok
test backtest::tests::build_trade_detail_rows_matches_fill_when_order_reason_changes ... ok
test backtest::tests::build_trade_detail_rows_preserves_decision_and_execution_dates ... ok
test backtest::tests::carry_forward_target_snapshot_keeps_transition_day_then_replaces_positions ... ok
test backtest::tests::cn_index_symbols_are_not_stock_execution_quote_repair_symbols ... ok
test backtest::tests::collect_missing_execution_quote_keys_uses_canceled_stock_orders_only ... ok
test backtest::tests::compact_progress_retains_manual_counts_and_rejects_a_missing_source ... ok
test backtest::tests::compacted_carry_forward_filter_keeps_window_semantics ... ok
test backtest::tests::corporate_action_arrow_preserves_the_independent_delivery_date_and_unknown_quantities ... ok
test backtest::tests::corporate_action_binary_cache_roundtrips_rows ... ok
test backtest::tests::corporate_action_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::corporate_action_cache_path_uses_source_lake_provider_epoch ... ok
test backtest::tests::bundle_validation_observes_dependency_semantic_change_and_revocation_without_ttl ... ok
test backtest::tests::corporate_action_covering_cache_reuses_superset_scope ... ok
test backtest::tests::daily_execution_price_arrow_decode_preserves_unpriced_paused_risk_rows ... ok
test backtest::tests::daily_execution_price_arrow_requires_execution_contract_fields ... ok
test backtest::tests::daily_execution_scope_membership_is_normalized_once_and_isolated_by_date ... ok
test backtest::tests::dataset_signature_scopes_fixed_signal_source_rows ... ok
test backtest::tests::dataset_signature_scopes_fixed_signal_date_symbol_filter_with_model_score ... ok
test backtest::tests::date_symbol_filter_query_range_carry_forward_keeps_history_prefix ... ok
test backtest::tests::date_symbol_filter_query_range_exact_keeps_only_window_dates ... ok
test backtest::tests::date_symbol_filter_query_range_preserves_empty_request ... ok
test backtest::tests::dataset_signature_uses_data_requirements_not_strategy_identity_or_thresholds ... ok
test backtest::tests::default_cache_epochs_ignore_local_manifest_directory ... ok
test backtest::tests::dense_execution_quote_plan_uses_requested_trade_dates_only ... ok
test backtest::tests::dynamic_model_score_execution_only_queries_next_date_gaps ... ok
test backtest::tests::dynamic_model_score_execution_skips_rows_present_on_next_full_calendar_date ... ok
test backtest::tests::dynamic_model_score_execution_symbols_include_candidate_universe_for_lagged_execution ... ok
test backtest::tests::dynamic_symbol_expression_does_not_enable_fixed_source_scope ... ok
test backtest::tests::empty_date_symbol_filter_is_empty_request_not_all_market ... ok
test backtest::tests::equity_artifact_keeps_external_deposit_out_of_return ... ok
test backtest::tests::equity_projection_orders_facts_and_includes_the_initial_nav_peak ... ok
test backtest::tests::execution_arrow_does_not_turn_nonfinite_numbers_into_optional_missing_values ... ok
test backtest::tests::execution_arrow_requires_the_strict_source_contract_even_when_empty ... ok
test backtest::tests::execution_price_date_symbol_filter_keeps_benchmark_and_prunes_other_targets ... ok
test backtest::tests::execution_price_only_source_row_marker_round_trips ... ok
test backtest::tests::execution_quantities_are_exact_integers_not_rounded_floats ... ok
test backtest::tests::exact_source_row_windows_restore_in_parallel_and_keep_order ... ok
test backtest::tests::execution_quote_day_binary_cache_invalidates_duplicate_date_symbol ... ok
test backtest::tests::execution_quote_day_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::execution_quote_day_cache_does_not_persist_empty_negative_result ... ok
test backtest::tests::execution_quote_day_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::execution_quote_day_cache_loads_empty_without_provider_url ... ok
test backtest::tests::execution_quote_day_cache_path_includes_minute_lookback_and_price_rule ... ok
test backtest::tests::execution_quote_day_cache_path_uses_quote_epoch ... ok
test backtest::tests::execution_quote_day_cache_path_uses_source_lake_provider_tag ... ok
test backtest::tests::execution_quote_day_cache_tracks_returned_symbols_only ... ok
test backtest::tests::execution_quote_prefetch_seed_tracks_negative_symbols ... ok
test backtest::tests::execution_quote_query_tasks_keep_target_time_symbol_scopes_narrow ... ok
test backtest::tests::execution_quote_query_tasks_merge_identical_symbol_scopes_for_same_day ... ok
test backtest::tests::execution_quote_rows_duplicate_date_symbol_is_rejected ... ok
test backtest::tests::execution_quote_rows_normalize_base_stock_code_symbols ... ok
test backtest::tests::execution_quote_loader_reads_a_bounded_minute_window_instead_of_a_point_snapshot ... ok
test backtest::tests::explicit_cli_source_cache_epoch_is_stable_and_provider_scoped ... ok
test backtest::tests::external_daily_execution_symbols_without_signals_only_queries_benchmark ... ok
test backtest::tests::external_daily_execution_symbols_skip_signals_already_in_source_rows ... ok
test backtest::tests::factor_values_arrow_decode_validates_metadata_and_merges_aliases ... ok
test backtest::tests::fidc_runner_rejects_ficlaw_data_source_rows_table ... ok
test backtest::tests::factor_values_arrow_decode_rejects_future_data_and_semantic_drift ... ok
test backtest::tests::fixed_minute_subscriptions_expand_static_daily_data_scope ... ok
test backtest::tests::fixed_signal_date_symbol_filter_is_disabled_when_static_symbols_are_mixed_in ... ok
test backtest::tests::fixed_signal_date_symbol_filter_seeds_static_symbols_at_query_start ... ok
test backtest::tests::fixed_signal_date_symbol_filter_survives_model_score_rank_metadata ... ok
test backtest::tests::fixed_signal_next_open_exit_scope_keeps_all_prior_unsold_symbols ... ok
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_dated_weights_only ... ok
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_target_holding_rows ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_runtime_target_weights_expr ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_single_quoted_runtime_target_weights_expr ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_strategy_metadata_targets ... ok
test backtest::tests::fixed_signal_source_row_symbols_extracts_target_weight_codes ... ok
test backtest::tests::fixed_symbol_order_scope_requires_rotation_disabled ... ok
test backtest::tests::fixed_symbol_orders_seed_exact_intraday_snapshot_keys ... ok
test backtest::tests::frozen_bundle_identity_http_failure_is_not_a_mutable_epoch_fallback ... ok
test backtest::tests::full_minute_prefetch_days_respect_total_bar_budget ... ok
test backtest::tests::futures_source_lake_catalog_requires_all_truth_datasets ... ok
test backtest::tests::futures_only_runtime_contract_keeps_stock_scope_empty_and_allocates_cash ... ok
test backtest::tests::instrument_metadata_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::instrument_metadata_binary_cache_roundtrips_rows ... ok
test backtest::tests::instrument_metadata_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::instrument_metadata_cache_merges_missing_symbols_incrementally ... ok
test backtest::tests::instrument_metadata_cache_path_is_scoped_by_historical_window ... ok
test backtest::tests::instrument_metadata_cache_path_uses_binary_format ... ok
test backtest::tests::instrument_metadata_request_serializes_historical_source_window_when_present ... ok
test backtest::tests::intraday_snapshot_binary_cache_rejects_metadata_mismatch ... ok
test backtest::tests::intraday_snapshot_binary_cache_roundtrips_rows ... ok
test backtest::tests::intraday_snapshot_cache_ignores_legacy_csv_cache ... ok
test backtest::tests::intraday_snapshot_cache_path_uses_source_lake_provider_tag ... ok
test backtest::tests::kcb_board_and_quantity_use_source_classification ... ok
test backtest::tests::intraday_snapshot_seeds_execution_quotes_and_negative_gaps ... ok
test backtest::tests::layered_execution_quote_loader_cache_preserves_shared_seed_and_isolates_overlay ... ok
test backtest::tests::lazy_execution_quote_window_dates_use_trading_calendar_only ... ok
test backtest::tests::lazy_execution_quote_window_backfill_keeps_future_missing_symbols ... ok
test backtest::tests::load_strategy_spec_rejects_removed_compatibility_profile ... ok
test backtest::tests::load_strategy_spec_rejects_removed_engine_profile_name ... ok
test backtest::tests::malformed_execution_observations_are_errors_not_empty_successes ... ok
test backtest::tests::malformed_versioned_factor_ref_fails_closed ... ok
test backtest::tests::managed_source_rows_route_by_exact_table ... ok
test backtest::tests::minute_bar_arrow_decode_accepts_bounded_multi_date_stream ... ok
test backtest::tests::load_strategy_spec_accepts_snake_case_bundle_field ... ok
test backtest::tests::minute_bar_arrow_decode_preserves_timestamp_symbol_order ... ok
test backtest::tests::minute_bar_arrow_decode_rejects_retired_string_time_schema ... ok
test backtest::tests::minute_bar_arrow_decode_rejects_duplicate_or_unordered_rows ... ok
test backtest::tests::mixed_candidate_book_binds_complete_dates_raw_sources_and_resolved_order ... ok
test backtest::tests::mixed_stock_futures_contract_requires_explicit_futures_cash ... ok
test backtest::tests::normalize_symbol_maps_bse_exchange_to_beijing_board ... ok
test backtest::columnar_factor_overlay_tests::columnar_overlay_matches_row_decoder_for_wide_sparse_null_and_signed_zero_values ... ok
test backtest::tests::minute_bars_retries_transient_http_failure_and_preserves_caller_header ... ok
test backtest::tests::partial_source_row_window_cache_crops_covering_segments_in_arrow ... ok
test backtest::tests::partial_source_row_window_cache_ignores_all_market_cache_for_symbol_scope ... ok
test backtest::tests::partial_source_row_window_cache_ignores_wide_projection_segment ... ok
test backtest::tests::partial_source_row_window_cache_reports_only_missing_gaps ... ok
test backtest::tests::progress_callback_is_forwarded_to_blocking_engine_thread ... ok
test backtest::tests::published_factor_binding_normalizes_ref_and_builds_arrow_request ... ok
test backtest::tests::query_scope_batch_preserves_authoritative_window_identities ... ok
test backtest::tests::partial_source_row_window_cache_reuses_exact_projection_with_null_dynamic_columns ... ok
test backtest::tests::query_scope_identity_reuses_physical_window_across_distinct_bundles ... ok
test backtest::tests::real_source_quote_json_and_arrow_match_strict_reader ... ignored, requires a bounded real Source JSON/Arrow probe directory
test backtest::tests::repeated_query_views_share_only_identical_execution_observations ... ok
test backtest::tests::platform_quote_plan_uses_current_decision_date_source_rows ... ok
test backtest::tests::query_scope_http_carries_rolling_dependencies_and_rejects_bad_contracts ... ok
test backtest::tests::select_instrument_metadata_rejects_missing_symbols ... ok
test backtest::tests::shared_base_source_projection_keeps_raw_adjustment_contract ... ok
test backtest::tests::shared_base_source_rows_cache_is_enabled_by_default ... ok
test backtest::tests::shared_base_source_rows_cache_is_only_for_unscoped_raw_rows ... ok
test backtest::tests::reusable_source_row_window_cache_does_not_reuse_covering_cache ... ok
test backtest::tests::sorted_source_row_validator_rejects_unordered_or_duplicate_keys ... ok
test backtest::tests::source_cache_epoch_default_is_independent_from_quote_plan_epoch ... ok
test backtest::tests::source_dependency_contract_invalidates_all_authoritative_cache_scopes ... ok
test backtest::tests::segmented_source_row_cache_reads_only_uncovered_overlap_ranges ... ok
test backtest::tests::short_range_source_query_window_is_explicit_and_keeps_long_ranges_bounded ... ok
test backtest::tests::source_lake_contract_epoch_ignores_runtime_build_but_tracks_catalog ... ok
test backtest::tests::source_lake_health_contract_epoch_ignores_runtime_build ... ok
test backtest::tests::source_lake_arrow_request_retries_bad_gateway ... ok
test backtest::tests::source_lake_busy_retry_honors_retryable_capacity_response ... ok
test backtest::tests::source_lake_source_query_retries_are_bounded_and_transient_only ... ok
test backtest::tests::source_lake_source_query_timeout_accepts_highmem_alias ... ok
test backtest::tests::source_query_concurrency_defaults_and_clamps ... ok
test backtest::tests::source_lake_run_waits_for_manifest_publication_and_holds_shared_lease ... ok
test backtest::tests::source_row_arrow_cache_rejects_and_does_not_write_empty_rows ... ok
test backtest::tests::source_row_arrow_cache_rejects_duplicate_date_symbol ... ok
test backtest::tests::source_lake_uses_wider_windows_for_narrow_queries ... ok
test backtest::tests::source_row_arrow_decode_preserves_dynamic_projection_columns ... ok
test backtest::tests::source_row_arrow_decode_keeps_compact_base_rows_without_extra_json ... ok
test backtest::tests::source_row_arrow_dependency_contract_rejects_invalid_metadata ... ok
test backtest::tests::source_row_arrow_extra_factor_helpers_preserve_projected_columns ... ok
test backtest::tests::source_row_arrow_view_keeps_mmap_alive_until_materialization ... ok
test backtest::tests::source_row_arrow_view_cache_reuses_identity_and_invalidates_replacement ... ok
test backtest::tests::source_row_cache_file_range_parses_source_cache_dates ... ok
test backtest::tests::source_row_cache_candidate_scan_removes_previous_schema_files ... ok
test backtest::tests::source_row_cache_directory_index_tracks_files_published_by_other_workers ... ok
test backtest::tests::source_row_cache_path_hashes_long_projection_columns ... ok
test backtest::tests::source_row_cache_path_scopes_explicit_source_symbols ... ok
test backtest::tests::source_row_cache_lease_serializes_concurrent_misses ... ok
test backtest::tests::source_row_cache_path_normalizes_projection_column_order ... ok
test backtest::tests::source_row_cache_path_separates_projection_columns ... ok
test backtest::tests::source_row_cache_projection_check_allows_source_lake_fixed_aliases ... ok
test backtest::tests::source_row_cache_projection_check_requires_extra_factor_keys ... ok
test backtest::tests::source_row_cache_rejects_legacy_raw_bincode_without_magic_header ... ok
test backtest::tests::source_row_cache_segment_selection_prefers_small_complete_windows ... ok
test backtest::tests::source_row_cache_segment_selection_rejects_incomplete_windows ... ok
test backtest::tests::source_row_date_windows_keep_interior_buckets_stable_across_ranges ... ok
test backtest::tests::source_row_date_windows_split_long_ranges ... ok
test backtest::tests::source_row_date_windows_stay_within_requested_range ... ok
test backtest::tests::source_row_cache_rejects_metadata_mismatch ... ok
test backtest::tests::source_row_primary_keys_are_inline_without_growing_the_row_header ... ok
test backtest::tests::source_row_segment_boundary_validator_preserves_fail_closed_integrity ... ok
test backtest::tests::source_row_symbol_normalization_preserves_canonical_and_maps_board ... ok
test backtest::tests::source_row_symbol_scope_epoch_changes_with_fixed_signal_symbols ... ok
test backtest::tests::source_row_request_cache_epoch_scopes_runtime_query_context ... ok
test backtest::tests::source_row_cache_writes_arrow_file ... ok
test backtest::tests::source_rows_duplicate_date_symbol_is_rejected ... ok
test backtest::tests::source_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
test backtest::tests::source_rows_request_keeps_rolling_enabled_for_rolling_projection_columns ... ok
test backtest::tests::source_rows_unique_date_symbol_passes_without_reordering ... ok
test backtest::tests::static_universe_include_is_an_explicit_source_row_scope ... ok
test backtest::tests::source_semantic_change_invalidates_bundle_and_window_cache ... ok
test backtest::tests::strategy_factor_overlay_only_queries_keys_and_missing_factor_fields ... ok
test backtest::tests::stock_pool_screen_snapshot_uses_exact_selection_dates_and_full_symbol_history ... ok
test backtest::tests::strategy_bundle_path_loads_nested_spec_and_signal_scope ... ok
test backtest::tests::strategy_factor_overlay_rejects_duplicate_and_unordered_keys ... ok
test backtest::tests::strategy_factor_overlay_requires_exact_base_key_coverage ... ok
test backtest::tests::strategy_factory_source_lake_provider_name_is_exact ... ok
test backtest::tests::strategy_factory_source_lake_requires_url_and_matching_provider ... ok
test backtest::tests::strategy_factory_source_lake_uses_arrow_sized_query_windows_by_default ... ok
test backtest::tests::trade_display_metadata_cache_rejects_rows_outside_exact_request ... ok
test backtest::tests::trade_display_metadata_cache_is_bundle_scoped_and_covers_missing_rows ... ok
test backtest::tests::typed_equity_result_emits_service_schema ... ok
test backtest::tests::trade_display_metadata_disk_cache_roundtrips_frozen_bundle_rows ... ok
test backtest::tests::typed_order_result_keeps_zero_fill_orders ... ok
test backtest::tests::typed_trade_result_preserves_sub_cent_execution_price ... ok
test backtest::tests::validated_manifest_scope_reuses_cache_across_distinct_bundle_ids ... ok
test backtest::tests::versioned_factor_binding_rejects_research_scope ... ok
test backtest::tests::versioned_factor_binding_requires_declared_semantics ... ok
test backtest::tests::versioned_factor_ref_requires_a_binding ... ok
test backtest::typed_exit_dependency_tests::ordinary_strategy_partial_exit_also_contributes_its_window ... ok
test backtest::typed_exit_dependency_tests::typed_stock_pool_exits_contribute_the_full_rolling_window ... ok
test cache_directory_watch::tests::notifications_cover_publication_rename_delete_and_ignore_unrelated_files ... ok
test compute_pool_tests::concurrent_embedded_runs_do_not_change_each_others_pool_budget ... ok
test compute_pool_tests::current_thread_async_runtime_keeps_the_compute_context ... ok
test backtest::tests::timed_quote_dataset_signature_includes_daily_selection_plan ... ok
test decision_projection::tests::projection_metadata_alias_empty_fields_and_credential_paths_fail_closed ... ok
test decision_projection::tests::projection_native_request_is_complete_and_never_truncated_to_top_n ... ok
test compute_pool_tests::embedded_runner_uses_its_declared_budget_and_preserves_results ... ok
test decision_projection::tests::projection_response_is_bound_to_source_model_clock_and_exact_population ... ok
test manual_ledger::corporate_tests::actual_core_corporate_projection_is_preserved_in_the_shared_trade_row ... ok
test result_store::tests::canonical_first_divergence_reports_json_pointer ... ok
test result_store::tests::canonical_manifest_is_stable_and_transport_independent ... ok
test manual_ledger::corporate_tests::corporate_adjustments_reject_forged_money_dates_and_reference_chains ... ok
test backtest::tests::unused_daily_market_cap_prefilter_does_not_split_identical_input_cache ... ok
test result_store::tests::fact_manifest_publish_is_idempotent_under_concurrency ... ok
test result_store::tests::lazy_registration_defers_payload_hashing_to_section_read ... ok
test result_store::tests::process_event_store_is_content_addressed_and_round_trips ... ok
test result_store::tests::run_store_identity_keeps_different_data_epochs_separate ... ok
test result_store::tests::manual_source_and_applications_are_both_hashed_and_persisted ... ok
test result_store::tests::run_store_is_content_addressed_and_round_trips ... ok
test result_store::tests::run_store_reads_only_date_overlapping_blocks ... ok
test thread_cpu::tests::thread_cpu_clock_keeps_unavailable_or_cross_thread_samples_unknown ... ok
test result_store::tests::run_store_rejects_corrupt_manifest ... ok
test thread_cpu::tests::sleeping_time_is_not_reported_as_cpu_execution ... ok
test backtest::source_rows_columnar::tests::columnar_cache_keeps_original_output_batch_boundaries ... ok
test result: ok. 474 passed; 0 failed; 9 ignored; 0 measured; 0 filtered out; finished in 4.24s
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-86f3f46fa8dc66e0)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/allocation_profile.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/allocation_profile-a941e54820726a92)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests examples/export_manual_corporate_evidence.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/export_manual_corporate_evidence-0a5023c51d949afa)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_service-4c9243df6eb45c3d)
running 142 tests
test daily_patterns::tests::column_transport_is_lossless_and_rejects_ambiguous_rows ... ok
test daily_patterns::tests::arrow_rejects_truncation_and_forged_lengths_before_decoding ... ok
test event_stream::tests::effective_initial_cash_is_projected_without_guessing_or_losing_source_precision ... ok
test daily_patterns::tests::daily_patterns_read_only_transport_binds_exact_input ... ok
test daily_patterns::tests::numeric_mode_requires_the_explicit_read_only_context ... ok
test event_stream::tests::ordered_batch_is_idempotent_and_changed_facts_do_not_replace_history ... ignored, requires local FIDC_EVENT_TEST_POSTGRES_DSN on the private fidc-auth-pg cluster
test event_stream::tests::memory_tail_and_durable_prefix_replay_in_publish_order ... ok
test daily_patterns::tests::arrow_uses_same_kernel_and_exact_byte_proof_with_partial_scope ... ok
test event_stream::tests::live_sequence_recent_replay_and_concurrent_runs_remain_isolated ... ok
test execution_ledger::tests::dated_fact_store_rejects_reverse_order_before_projection ... ok
test execution_ledger::tests::actual_http_routes_return_both_sources_and_precise_manual_values ... ok
test execution_ledger::tests::serve_browser_fixture ... ignored, starts a local GET-only API for browser acceptance; set FIDC_LEDGER_UI_QA_PORT explicitly
test execution_ledger::tests::serve_stream_browser_fixture ... ignored, starts isolated real WebSocket/GET handlers; FIDC_LEDGER_STREAM_QA_PORT required
test execution_ledger::tests::source_precision_ids_and_later_status_are_not_rewritten ... ok
test execution_ledger::tests::streamed_manual_facts_equal_final_ledger_without_publishing_a_future_terminal ... ok
test execution_ledger::tests::typed_accounting_rows_keep_origin_without_order_identity ... ok
test execution_ledger::tests::performance_counts_both_origins_and_sums_authoritative_fees_exactly ... ok
test factor_events::tests::market_history_binds_kernel_version_and_preserves_unknown_streak ... ok
test factor_events::tests::no_account_or_execution_payload_is_accepted ... ok
test factor_events::tests::rank_history_binds_exact_scope_and_input_hash ... ok
test factor_events::tests::transport_preserves_input_identity_and_rejects_future_values ... ok
test idempotency::tests::canonical_json_order_does_not_change_request_identity ... ok
test idempotency::tests::identity_binds_owner_and_rejects_changed_request ... ok
test idempotency::tests::malformed_keys_and_unbound_records_are_rejected ... ok
test performance::tests::accounting_allocations_are_not_unidentified_exchange_fills ... ok
test performance::tests::builds_daily_weight_diagnostics_from_persisted_holdings ... ok
test performance::tests::counts_open_underwater_duration_from_the_latest_peak ... ok
test performance::tests::does_not_fake_risk_ratios_for_one_period ... ok
test performance::tests::emits_true_nav_and_multiple_recovery_episodes ... ok
test performance::tests::explicit_frozen_initial_capital_survives_a_first_day_deposit ... ok
test performance::tests::first_period_loss_has_an_initial_peak_episode_without_a_fake_date ... ok
test performance::tests::missing_benchmark_does_not_compact_dates_or_invent_a_drawdown_path ... ok
test performance::tests::missing_order_ids_and_fees_remain_unknown_not_zero_or_report_counts ... ok
test performance::tests::order_metrics_count_identities_separately_from_reports_and_partial_fills ... ok
test performance::tests::preserves_a_first_day_strategy_return_and_initial_capital ... ok
test performance::tests::preserves_the_canonical_pre_period_benchmark_baseline ... ok
test performance::tests::projects_all_canonical_risk_metrics_and_contract_metadata ... ok
test performance::tests::separates_longest_underwater_episode_from_deepest_drawdown ... ok
test performance::tests::zero_nav_is_a_full_loss_not_missing_data ... ok
test performance::tests::zero_orders_are_known_but_conflicting_order_identity_is_not_a_count ... ok
test result_analysis_tests::batched_history_bindings_preserve_validation_and_survive_cache_eviction ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
test factor_events::tests::catalog_validator_binds_the_complete_body_and_preserves_contract ... ok
test result_analysis_tests::copied_real_result_stores_and_browser_fixture ... ignored, requires FIDC_RESULT_QA_FIXTURES containing copied immutable result stores
test result_analysis_tests::failed_optional_classification_keeps_unmodified_facts ... ok
test result_analysis_tests::historical_text_search_filters_before_count_and_pagination ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
test result_analysis_tests::history_filters_use_corrected_drawdown_and_never_treat_unknown_as_zero ... ok
test result_analysis_tests::owned_analysis_reference_does_not_require_a_cached_run_binding ... ok
test result_analysis_tests::projected_history_filter_sort_and_pagination_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN; creates an isolated test schema
test result_analysis_tests::sector_filter_cannot_disable_its_classification_dependency ... ok
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
test submission_pg_tests::atomic_submission_claim_replay_and_rollback_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN and creates only an isolated schema
test tests::backtest_create_body_limit_accepts_bounded_signal_replays ... ok
test tests::backtest_event_stream_exposes_a_websocket_transport ... ok
test tests::backtest_list_cache_key_separates_summary_only_runtime_shape ... ok
test tests::backtest_list_filters_and_sorting_use_generated_summary ... ok
test tests::backtest_list_reports_cache_hit_and_effective_page_limit ... ok
test tests::backtest_list_summary_only_uses_compact_query ... ok
test tests::backtest_performance_contract_records_total_and_runner_seconds ... ok
test tests::backtest_run_compact_cache_key_separates_strategy_detail_shape ... ok
test tests::backtest_runner_data_env_accepts_strategy_factory_source_lake_provider ... ok
test tests::backtest_runner_data_env_infers_strategy_factory_source_lake_from_runtime_source_table ... ok
test tests::backtest_runner_data_env_rejects_legacy_ficlaw_data_source_table ... ok
test tests::backtest_runner_data_env_rejects_legacy_source_provider ... ok
test tests::backtest_runner_data_env_rejects_non_source_lake_provider ... ok
test tests::backtest_runner_data_env_rejects_source_lake_provider_aliases ... ok
test tests::backtest_runner_data_env_requires_strategy_factory_source_lake_url ... ok
test tests::backtest_runner_data_env_uses_strategy_factory_source_lake_only ... ok
test tests::backtest_schema_initialization_never_deletes_run_data ... ok
test tests::backtest_schema_materializes_implementation_identity_for_compact_reads ... ok
test tests::build_run_payload_does_not_mark_empty_running_summary_as_available ... ok
test tests::build_run_payload_exposes_only_canonical_strategy_detail ... ok
test tests::build_run_payload_marks_failed_error_summary_as_available ... ok
test tests::build_run_payload_marks_terminal_metric_summary_as_available ... ok
test tests::build_run_summary_preview_preserves_strategy_runtime_rebalance_signal ... ok
test tests::build_run_summary_preview_preserves_terminal_audit ... ok
test tests::canonical_result_is_recovered_from_runner_summary ... ok
test tests::capacity_override_is_frozen_once_without_changing_execution_risk ... ok
test tests::clear_backtest_list_cache_clears_compact_run_cache ... ok
test tests::compact_result_handler_uses_single_pg_lookup ... ok
test tests::compact_summary_only_runtime_select_avoids_config_json ... ok
test tests::compact_summary_sql_removes_heavy_result_artifacts ... ok
test tests::daily_execution_matching_type_normalizes_next_open_aliases_and_rejects_other_aliases ... ok
test tests::default_backtest_concurrency_scales_with_available_cpu_threads ... ok
test tests::default_backtest_source_table_uses_strategy_factory_source_lake ... ok
test tests::detail_rows_query_page_size_maps_to_offset_limit ... ok
test tests::equity_rows_hide_diagnostics_unless_explicitly_requested ... ok
test tests::execution_cost_overrides_strategy_spec_for_runner ... ok
test tests::execution_dynamic_slippage_overrides_strategy_spec_for_runner ... ok
test tests::execution_matching_type_overrides_strategy_spec_for_runner ... ok
test tests::execution_percent_slippage_alias_normalizes_to_price_ratio ... ok
test tests::execution_rebalance_cash_mode_overrides_strategy_spec_for_runner ... ok
test tests::execution_risk_overrides_strategy_spec_for_runner ... ok
test tests::execution_risk_policy_rejects_conflicting_alias_values ... ok
test tests::execution_risk_policy_rejects_non_object ... ok
test tests::execution_risk_policy_rejects_unknown_fields_before_queueing ... ok
test tests::execution_sell_then_buy_delay_slippage_requires_sell_then_buy_mode ... ok
test tests::execution_slippage_overrides_strategy_spec_for_runner ... ok
test tests::execution_top_level_risk_enabled_aliases_are_frozen_into_strategy_spec ... ok
test tests::explicit_cost_override_removes_old_aliases_and_preserves_zero_minimum ... ok
test tests::history_query_cache_has_a_bounded_entry_budget ... ok
test tests::history_search_is_bounded_and_retains_literal_characters ... ok
test tests::implementation_identity_freezes_source_and_binary_generation ... ok
test tests::manual_observations_are_run_scoped_and_private_in_the_runner_bundle ... ok
test tests::minute_execution_matching_type_uses_minute_price_and_rejects_daily_modes ... ok
test tests::persisted_runtime_projection_never_guesses_missing_frequency ... ok
test tests::persisted_runtime_projection_retains_minute_mode_and_frozen_bundle ... ok
test tests::process_event_projection_uses_stable_kind_and_filters ... ok
test tests::projected_fact_block_cache_key_is_shared_across_run_manifests ... ok
test tests::public_backtest_projection_removes_internal_paths_recursively ... ok
test tests::public_event_message_removes_internal_paths ... ok
test tests::public_result_store_manifest_hides_internal_fact_blocks ... ok
test tests::public_run_projection_does_not_reintroduce_internal_paths ... ok
test tests::result_artifact_cache_keeps_shared_fact_blocks_after_run_eviction ... ok
test tests::result_artifact_cache_rejects_conflicting_reference_for_same_sha ... ok
test tests::result_block_page_plan_reads_only_overlapping_blocks ... ok
test tests::runner_strategy_spec_path_prefers_bundle ... ok
test tests::runtime_frequency_normalizes_and_overrides_strategy_spec_for_runner ... ok
test tests::runtime_frozen_bundle_is_written_to_strategy_metadata_for_runner ... ok
test tests::runtime_projection_migration_preserves_frozen_config ... ok
test tests::runtime_symbols_override_strategy_spec_for_runner ... ok
test tests::source_lake_preflight_rejects_unsupported_minute_cash_mode ... ok
test tests::source_lake_run_contract_preflight_freezes_runtime_and_execution ... ok
test tests::source_lake_runtime_requires_frozen_backtest_bundle_contract ... ok
test tests::start_backtest_run_rejects_unready_data_source_before_persistence ... ok
test tests::startup_reconciliation_closes_orphaned_runs_and_records_audit_event ... ok
test tests::strategy_detail_keeps_strategy_body_without_full_config ... ok
test tests::strategy_detail_returns_none_without_strategy_body ... ok
test tests::strategy_expression_preflight_rejects_invalid_dsl_before_data_load ... ok
test tests::summary_list_projection_keeps_table_metrics_and_removes_detail_payloads ... ok
test tests::terminal_audit_accepts_deferred_intents_and_checks_the_retained_count ... ok
test tests::terminal_audit_contract_rejects_missing_or_inconsistent_state ... ok
test tests::top_level_limits_replace_all_nested_execution_aliases_before_preflight ... ok
test tests::typed_holding_projection_derives_weight_and_final_snapshot ... ok
test tests::typed_result_rows_filter_and_paginate_without_json_table_roundtrip ... ok
test tests::typed_risk_filter_applies_execution_fields ... ok
test event_stream::tests::replay_buffer_is_bounded_and_completed_streams_are_evicted_without_closing_subscribers ... ok
test result_analysis_tests::complete_performance_and_paged_equity_share_corrected_drawdown ... ok
test execution_ledger::tests::combined_pages_filter_before_count_and_keep_native_pages_bounded ... ok
test result: ok. 134 passed; 0 failed; 8 ignored; 0 measured; 0 filtered out; finished in 9.15s
Running unittests examples/risk_projection_readonly.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/risk_projection_readonly-b7328e36ec4d5bbd)
running 4 tests
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests examples/stock_pool_contract_probe.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/stock_pool_contract_probe-58c8f70095cabf04)
running 1 test
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
@@ -0,0 +1,211 @@
{
"artifact_bytes": 8192,
"artifact_id": "strategy_research_59f5a031d3cc7e0e5080c91e",
"artifact_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/validation-receipt.json",
"artifact_sha256": "097101988e5f6875c8933012534cfa1ca72f9e2742c04d002aba7d00a69fc346",
"artifact_type": "research_source_scope_probe",
"contract_sha256": "054241b3df4e522159691c6ee27506667790f73a04c42989b87b5504cf587ee4",
"created_at": "2026-10-04T22:08:19Z",
"created_by": "boris",
"department_id": null,
"organization_id": "local_org",
"persisted_at": "2026-10-04T22:08:19Z",
"production_ready": false,
"record_json": {
"all_attempt_log_references": [
{
"bytes": 3625,
"compiler_failure": false,
"complete_success": false,
"failed": 2,
"ignored": 0,
"passed": 20,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49"
},
{
"bytes": 82467,
"compiler_failure": false,
"complete_success": false,
"failed": 1,
"ignored": 7,
"passed": 798,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c"
},
{
"bytes": 95343,
"compiler_failure": false,
"complete_success": false,
"failed": 4,
"ignored": 9,
"passed": 923,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447"
},
{
"bytes": 98572,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 969,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
},
{
"bytes": 10543,
"compiler_failure": true,
"complete_success": false,
"failed": 0,
"ignored": 0,
"passed": 0,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8"
},
{
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7"
},
{
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
},
{
"bytes": 65258,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 614,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434"
}
],
"contract_sha256": "5b4389afc3bbda8f30cbe141cc2a05af75bc0e69724aa04c09b258b98326105f",
"core_test_result": {
"bytes": 98572,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 969,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
},
"corporate_action_cache_schema_version": 5,
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
"fractional_quantity_requires_explicit_settlement_rule": true,
"full_source_event_and_revision_scope_verified": false,
"new_runtime_release_performed": false,
"numeric_research_budget_reopened": false,
"observed_at": "2026-10-04T22:08:10.219296+00:00",
"operational_verification_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/operational-verification.json",
"sha256": "3ce2ba78ed28f549ddc675429565e578fe61eee1db4374c375e5049761c8fd06"
},
"owner_uid": 1003,
"pending_share_successor_conversion_without_carry_terms_rejected": true,
"performance_speedup_claimed": false,
"prior_validation_pg_artifact": "strategy_research_cb32517064d7558f69713001",
"prior_validation_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
"sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412"
},
"production_ready": false,
"production_runtime_deployed": false,
"production_strategy_count": 0,
"registered_share_entitlements_separate_from_delivered_and_sellable_quantity": true,
"resource_contract_live_verification_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/live-test-resource-verification.json",
"sha256": "1686248392f29ae93f023ff147fcc0abb06b6b2fb15a9806b5718e320ed75a40"
},
"resource_contract_requested": {
"build_jobs": 4,
"core_target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de",
"cpu_quota_percent": 400,
"incremental": false,
"locked": true,
"memory_max_bytes": 17179869184,
"memory_swap_max_bytes": 0,
"offline": true,
"resource_properties_after_transient_scope_exit_are_not_live_validation": true,
"runner_target_path": "/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb",
"rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
"target_filesystem": "executable_tmpfs",
"tasks_max": 256,
"toolchain": "stable-x86_64-unknown-linux-gnu"
},
"runner_api_test_result": {
"bytes": 65258,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 614,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434"
},
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
"service_commit": "1d4b32e18d3836b5844130ae0c95d90247768882",
"share_receivable_assets_included_in_equity_and_target_sizing": true,
"simulation_rounding_fixture_not_official_settlement_evidence": true,
"source_pair_root": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e",
"source_publication_times_admitted_as_PIT": false,
"source_rows_semantic_sha256": "eec65df828f97a3e51827cb6b0e9ec2c86079a333481d55ec9eb6ab2845d73c5",
"source_rows_semantic_version": "v10",
"source_transport_commit": "12eeae5d570004499a30f7faa42c0da93c1578cc",
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
"trading_allowed": false,
"two_framework_financial_reconciliation_completed": false,
"unknown_delivery_date_not_defaulted_to_ex_date": true,
"unknown_distribution_quantities_not_defaulted_to_zero": true
},
"research_key": "fidc-registered-share-delivery-validation-20261005-ce46c13-1d4b32e",
"schema_table": "strategy_research_artifact",
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
"storage": "postgres"
}
@@ -0,0 +1,20 @@
{
"schema_version": "fidc.validation-probe-assembly-failure/v1",
"recorded_on": "2026-10-05",
"error_type": "SyntaxError",
"reported_frames": [
{
"function": "<module>",
"line": 6
}
],
"request_path": "/tmp/fidc-share-delivery-quantity-request-20261005.json",
"request_sha256": "2a91810a4d30ae811fa8dfb010ac7ce761d9bdfd144af608318886617292fed7",
"cause": "probe string replacement left a dot immediately before an equality operator",
"remote_probe_code_execution_started": false,
"remote_source_or_test_outcome_unknown": false,
"correction": "assembled probe compiled locally before executing corrected validation",
"corrected_test_source_pair": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e",
"production_ready": false,
"trading_allowed": false
}
@@ -0,0 +1,192 @@
{
"all_attempt_log_references": [
{
"bytes": 3625,
"compiler_failure": false,
"complete_success": false,
"failed": 2,
"ignored": 0,
"passed": 20,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49"
},
{
"bytes": 82467,
"compiler_failure": false,
"complete_success": false,
"failed": 1,
"ignored": 7,
"passed": 798,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c"
},
{
"bytes": 95343,
"compiler_failure": false,
"complete_success": false,
"failed": 4,
"ignored": 9,
"passed": 923,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c"
},
{
"bytes": 98381,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 967,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447"
},
{
"bytes": 98572,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 969,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
},
{
"bytes": 10543,
"compiler_failure": true,
"complete_success": false,
"failed": 0,
"ignored": 0,
"passed": 0,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8"
},
{
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7"
},
{
"bytes": 65133,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 613,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
},
{
"bytes": 65258,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 614,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434"
}
],
"contract_sha256": "5b4389afc3bbda8f30cbe141cc2a05af75bc0e69724aa04c09b258b98326105f",
"core_test_result": {
"bytes": 98572,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 9,
"passed": 969,
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
},
"corporate_action_cache_schema_version": 5,
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
"fractional_quantity_requires_explicit_settlement_rule": true,
"full_source_event_and_revision_scope_verified": false,
"new_runtime_release_performed": false,
"numeric_research_budget_reopened": false,
"observed_at": "2026-10-04T22:08:10.219296+00:00",
"operational_verification_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/operational-verification.json",
"sha256": "3ce2ba78ed28f549ddc675429565e578fe61eee1db4374c375e5049761c8fd06"
},
"owner_uid": 1003,
"pending_share_successor_conversion_without_carry_terms_rejected": true,
"performance_speedup_claimed": false,
"prior_validation_pg_artifact": "strategy_research_cb32517064d7558f69713001",
"prior_validation_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
"sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412"
},
"production_ready": false,
"production_runtime_deployed": false,
"production_strategy_count": 0,
"registered_share_entitlements_separate_from_delivered_and_sellable_quantity": true,
"resource_contract_live_verification_reference": {
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/live-test-resource-verification.json",
"sha256": "1686248392f29ae93f023ff147fcc0abb06b6b2fb15a9806b5718e320ed75a40"
},
"resource_contract_requested": {
"build_jobs": 4,
"core_target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de",
"cpu_quota_percent": 400,
"incremental": false,
"locked": true,
"memory_max_bytes": 17179869184,
"memory_swap_max_bytes": 0,
"offline": true,
"resource_properties_after_transient_scope_exit_are_not_live_validation": true,
"runner_target_path": "/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb",
"rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
"target_filesystem": "executable_tmpfs",
"tasks_max": 256,
"toolchain": "stable-x86_64-unknown-linux-gnu"
},
"runner_api_test_result": {
"bytes": 65258,
"compiler_failure": false,
"complete_success": true,
"failed": 0,
"ignored": 17,
"passed": 614,
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434"
},
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
"service_commit": "1d4b32e18d3836b5844130ae0c95d90247768882",
"share_receivable_assets_included_in_equity_and_target_sizing": true,
"simulation_rounding_fixture_not_official_settlement_evidence": true,
"source_pair_root": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e",
"source_publication_times_admitted_as_PIT": false,
"source_rows_semantic_sha256": "eec65df828f97a3e51827cb6b0e9ec2c86079a333481d55ec9eb6ab2845d73c5",
"source_rows_semantic_version": "v10",
"source_transport_commit": "12eeae5d570004499a30f7faa42c0da93c1578cc",
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
"trading_allowed": false,
"two_framework_financial_reconciliation_completed": false,
"unknown_delivery_date_not_defaulted_to_ex_date": true,
"unknown_distribution_quantities_not_defaulted_to_zero": true
}
@@ -0,0 +1,809 @@
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-receivable-validation-20261005-4a41057/crates/fidc-core)
Finished `test` profile [unoptimized] target(s) in 42.79s
Running unittests src/lib.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/fidc_core-dbea4d4f6cb5d2d7)
running 789 tests
test broker::tests::aiquant_rules_keep_structured_buy_risk_while_using_aiquant_limit_price ... ok
test broker::tests::aiquant_sell_rule_still_blocks_real_allow_sell_false ... ok
test broker::tests::aiquant_sell_rule_uses_intraday_quote_when_daily_snapshot_is_stale_lower_limit ... ok
test broker::tests::aiquant_target_portfolio_smart_keeps_batch_semantics_after_failed_full_close_sell ... ok
test broker::tests::aiquant_target_portfolio_smart_rejects_deferred_buy_risk_at_order_stage ... ok
test broker::stock_pool::successor_protection_tests::deferred_etf_open_rechecks_inherited_locks_and_cooldown_before_any_order ... ok
test broker::tests::aiquant_target_value_delta_uses_scheduled_mark_price ... ok
test broker::tests::algorithm_clock::algorithm_expiry_without_a_quote_does_not_reuse_old_liquidity ... ok
test broker::tests::algorithm_clock::an_explicit_ioc_or_fok_does_not_become_a_persistent_algorithm ... ok
test broker::tests::algorithm_clock::algorithm_sell_honors_t_plus_one_and_keeps_original_quantity_after_partial_fills ... ok
test broker::tests::algorithm_clock::a_clock_slice_does_not_turn_window_twap_into_an_unlimited_instant_order ... ok
test broker::tests::algorithm_clock::control_only_phase_cannot_be_used_to_submit_an_order_or_leave_matching_disabled ... ok
test broker::tests::algorithm_clock::changing_the_later_daily_close_does_not_resize_an_algorithm_submitted_now ... ok
test broker::tests::algorithm_clock::global_vwap_matching_keeps_the_same_working_order_between_clock_ticks ... ok
test broker::tests::algorithm_clock::non_matching_controls_amend_or_cancel_without_filling_a_crossing_quote ... ok
test broker::tests::algorithm_clock::partial_algorithm_cancel_releases_reservation_and_never_executes_the_remainder ... ok
test broker::tests::algorithm_clock::two_working_algorithms_reserve_only_real_cash_without_starving_the_first ... ok
test broker::tests::algorithm_clock::twap_clock_preserves_quantity_prices_fees_budget_and_parent_order ... ok
test broker::tests::complete_daily_target_batch_uses_positive_target_count_for_slot_limit ... ok
test broker::tests::algorithm_clock::separate_buy_cannot_spend_the_working_algorithms_cash_budget ... ok
test broker::tests::daily_capacity_requires_a_timed_observation_instead_of_falling_back_to_total_volume ... ok
test broker::tests::daily_matching_does_not_require_intraday_quote_quantity ... ok
test broker::tests::custom_valuation_missing_with_current_market_still_fails_closed ... ok
test broker::tests::algorithm_clock::vwap_clock_preserves_cash_costs_and_does_not_spend_future_volume ... ok
test broker::tests::decision_buy_denial_allows_reducing_an_existing_buy ... ok
test broker::tests::decision_buy_denial_does_not_rewrite_existing_pending_order ... ok
test broker::tests::daily_session_volume_changes_only_audit_not_opening_fills ... ok
test broker::tests::decision_buy_denial_blocks_topup_but_allows_sell_and_does_not_leak ... ok
test broker::tests::decision_buy_denial_rejects_increasing_amendments_without_mutation ... ok
test broker::tests::each_execution_leg_rechecks_one_yuan_including_slippage_and_limit_price ... ok
test broker::tests::explicit_quote_price_models_do_not_replace_missing_prices_or_reuse_older_ones ... ok
test broker::tests::decision_buy_denial_uses_actual_next_open_target_delta ... ok
test broker::tests::failed_target_exit_blocks_replacement_entry_when_no_position_slot_is_released ... ok
test broker::tests::gtc_resting_order_keeps_its_session_and_original_dates_across_days ... ok
test broker::tests::deferred_etf_batch_failure_keeps_both_targets_and_prior_generation_progress ... ok
test broker::tests::failed_new_batch_does_not_erase_prior_success_or_double_charge_on_retry ... ok
test broker::tests::historical_slippage_rejects_missing_future_or_invalid_calibration_without_raw_price_fallback ... ok
test broker::tests::instantaneous_twap_without_limits_does_not_cap_quote_quantity ... ok
test broker::tests::intraday_execution_rejects_buy_at_upper_limit_price ... ok
test broker::tests::intraday_execution_rejects_sell_at_lower_limit_price ... ok
test broker::tests::later_execution_clocks_do_not_replenish_the_same_observed_volume ... ok
test broker::tests::independent_target_entry_is_not_subject_to_replacement_slot_limit ... ok
test broker::tests::minute_last_exact_time_does_not_fallback_to_older_liquid_quote ... ok
test broker::tests::minute_last_execution_uses_latest_quote_before_decision_time ... ok
test broker::tests::market_share_buy_skips_quantity_that_rounds_below_one_lot ... ok
test broker::tests::minute_last_prefilter_does_not_use_snapshot_minute_volume ... ok
test broker::tests::minute_last_quote_quantity_cap_depends_on_liquidity_limit ... ok
test broker::tests::minute_last_uses_volume_delta_when_level1_depth_missing ... ok
test broker::tests::minute_last_volume_limit_caps_quote_volume_delta_without_liquidity_limit ... ok
test broker::tests::minute_last_volume_limit_rejects_sub_lot_quote_volume ... ok
test broker::tests::minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity ... ok
test broker::tests::historical_slippage_does_not_read_later_daily_fields_for_open_or_minute_fills ... ok
test broker::tests::next_open_buy_limit_risk_rejects_open_upper_limit_even_when_close_is_clean ... ok
test broker::tests::next_open_buy_limit_risk_uses_open_not_close ... ok
test broker::tests::next_open_buy_risk_rejects_execution_date_delisted_even_without_market_snapshot ... ok
test broker::tests::next_open_buy_risk_rejects_execution_date_st_even_if_signal_date_was_clean ... ok
test broker::tests::next_open_sell_limit_risk_rejects_open_lower_limit_even_when_close_is_clean ... ok
test broker::tests::next_open_buy_risk_uses_execution_date_not_signal_date ... ok
test broker::tests::next_open_day_limit_remainder_matches_intraday_with_execution_day_ttl ... ok
test broker::tests::next_open_sell_limit_risk_uses_open_not_close ... ok
test broker::tests::next_open_sell_risk_rejects_execution_date_lower_limit_even_if_signal_date_was_clean ... ok
test broker::tests::next_open_sell_risk_uses_execution_date_not_signal_date ... ok
test broker::tests::next_open_target_portfolio_unpriced_symbol_does_not_abort_batch ... ok
test broker::tests::next_open_target_value_valuation_uses_execution_open ... ok
test broker::tests::next_open_target_value_recomputes_quantity_from_execution_open ... ok
test broker::tests::next_open_unpriced_active_snapshot_rejects_without_historical_fill ... ok
test broker::tests::next_open_unpriced_paused_snapshot_prefers_execution_day_pause_risk ... ok
test broker::tests::next_open_with_observations_uses_opening_volume_not_daily_totals_or_future_quotes ... ok
test broker::tests::open_order_upsert_replaces_in_place_and_preserves_queue_position ... ok
test broker::tests::pending_registered_shares_do_not_trigger_duplicate_target_buys ... ok
test broker::tests::post_close_phase_is_derived_from_actual_same_day_submission_time ... ok
test broker::tests::queued_order_retains_the_real_creation_clock_when_retried ... ok
test broker::tests::post_close_order_uses_close_without_slippage_and_waits_until_matching_window ... ok
test broker::tests::session_audit_does_not_infer_an_opening_suspension_from_future_zero_volume ... ok
test broker::tests::scheduled_target_value_valuation_and_sizing_use_same_intraday_price ... ok
test broker::tests::session_capacity_audit_never_caps_an_early_odd_lot_sell ... ok
test broker::tests::public_callback_unwind_does_not_leak_order_context_or_authoritative_prior_state ... ok
test broker::tests::stock_pool_after_sell_rejects_missing_quote_instead_of_reusing_daily_open ... ok
test broker::tests::stock_pool_after_sell_uses_fresh_quotes_and_actual_submission_clock ... ok
test broker::tests::stock_pool_delayed_sell_does_not_start_buys_after_the_configured_window ... ok
test broker::tests::stock_pool_delayed_take_profit_does_not_rebuy_the_same_generation_exit ... ok
test broker::tests::stock_pool_engine_drives_the_pending_buy_without_a_minute_strategy_callback ... ok
test broker::tests::stock_pool_new_signal_supersedes_the_unsubmitted_buy_phase ... ok
test broker::tests::stock_pool_partial_sell_waits_for_the_whole_batch_and_never_reissues_buys ... ok
test broker::tests::stock_pool_pending_phase_cannot_cross_the_execution_session ... ok
test broker::tests::strict_value_buy_budget_includes_commission_at_execution_price ... ok
test broker::tests::successful_target_exit_releases_position_slot_for_replacement_entry ... ok
test broker::tests::stock_pool_pending_sell_continues_buy_after_actual_fill_without_strategy_rerun ... ok
test broker::tests::target_percent_reduction_from_full_to_twenty_percent_sells_eighty_percent ... ok
test broker::tests::target_portfolio_smart_custom_valuation_does_not_create_limit_orders ... ok
test broker::tests::target_portfolio_smart_budgets_each_buy_before_cash_optimization ... ok
test broker::tests::target_portfolio_smart_defers_buy_risk_until_order_validation ... ok
test broker::tests::target_portfolio_smart_ignores_zero_weight_symbols_without_market_snapshot ... ok
test broker::tests::target_portfolio_smart_keeps_position_when_sell_snapshot_missing ... ok
test broker::tests::target_portfolio_smart_open_auction_uses_day_open_for_valuation ... ok
test broker::tests::target_portfolio_smart_records_buy_rejection_when_target_is_blacklisted ... ok
test broker::tests::target_portfolio_smart_pre_open_cash_does_not_spend_same_batch_sell_proceeds ... ok
test broker::tests::target_portfolio_smart_uses_prior_close_only_for_missing_day_valuation ... ok
test broker::tests::target_value_delta_below_order_step_is_audited_without_creating_zero_quantity_order ... ok
test broker::tests::target_value_batch_executes_reductions_before_increases_when_sell_cash_is_reusable ... ok
test broker::tests::target_value_reduction_from_full_to_twenty_percent_sells_eighty_percent ... ok
test broker::tests::target_value_zero_rejects_sell_when_market_snapshot_missing ... ok
test broker::tests::target_weight_buy_quantity_respects_per_symbol_budget_after_slippage_and_fees ... ok
test broker::tests::timed_target_value_zero_sells_full_position_at_requested_time_quote ... ok
test broker::tests::failed_resting_order_batch_keeps_accepted_orders_and_unpublished_financial_state ... ok
test broker::tests::twap_earlier_fills_do_not_depend_on_later_quote_liquidity_or_count ... ok
test broker::tests::unchanged_targets_do_not_create_zero_quantity_orders ... ok
test broker::tests::unwinding_an_unpublished_simulator_transaction_restores_its_state ... ok
test broker::tests::value_buy_process_uses_latest_quote_before_decision_time ... ok
test broker::tests::value_buy_rejects_when_slippage_clamps_execution_price_to_upper_limit ... ok
test corporate_book::tests::closing_register_cannot_be_replaced_by_later_holdings ... ok
test corporate_book::tests::dividend_receivable_survives_a_flat_position_without_becoming_spendable_cash ... ok
test corporate_book::tests::a_late_corporate_batch_failure_keeps_prior_cash_positions_and_notes ... ok
test corporate_book::tests::entitlement_survives_complete_sale_and_a_successor_unit_conversion ... ok
test corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta ... ok
test corporate_book::tests::flat_position_retirement_does_not_change_issued_lot_state_or_next_purchase_origin ... ok
test corporate_book::tests::explicitly_delayed_dividend_remains_receivable_until_actual_payment ... ok
test corporate_book::tests::manual_zero_skips_only_unused_allocation_facts_not_invalid_cash_evidence ... ok
test corporate_book::tests::missing_accounting_reference_is_atomic_not_a_silent_cash_only_success ... ok
test corporate_book::tests::invalid_delivery_negative_rates_and_unresolved_fractional_claims_are_atomic ... ok
test corporate_book::tests::missing_or_backward_cash_payment_date_never_updates_the_account ... ok
test corporate_book::tests::per_share_precision_is_retained_until_the_registered_cash_amount_is_rounded ... ok
test corporate_book::tests::missing_record_date_or_unobserved_closing_book_is_atomic ... ok
test corporate_book::tests::post_record_buy_cannot_receive_the_earlier_dividend ... ok
test corporate_book::tests::post_record_purchase_does_not_inherit_registered_shares ... ok
test corporate_book::tests::registered_shares_are_valued_but_not_delivered_or_sellable_before_the_original_date ... ok
test corporate_book::tests::sale_after_registration_preserves_cash_and_excludes_new_lots_from_cost_adjustment ... ok
test corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security ... ok
test corporate_book::tests::terminated_security_keeps_paid_cash_and_is_not_recreated_by_reinvestment ... ok
test corporate_book::tests::successor_conversion_cannot_discard_unresolved_share_entitlements ... ok
test corporate_book::tests::tiny_known_cash_rate_is_preserved_and_cannot_waive_payment_or_record_evidence ... ok
test corporate_book::tests::unresolved_delisting_cannot_zero_a_pending_share_asset ... FAILED
test cost::tests::cost_model_can_use_configurable_stamp_tax_change_date ... ok
test cost::tests::default_quantizes_fees_to_micro_yuan ... ok
test cost::tests::fund_fees_use_admitted_instrument_type_and_share_the_order_commission_budget ... ok
test corporate_book::tests::invalid_successor_terms_cannot_be_replaced_with_one_share_or_zero_cash ... ok
test corporate_book::tests::verified_conversion_preserves_lots_without_creating_orders_or_fills ... ok
test daily_patterns::tests::breakout_retest_needs_a_later_retest_not_the_breakout_candle_itself ... ok
test daily_patterns::tests::daily_patterns_adjusts_all_prices_not_volume ... ok
test daily_patterns::tests::daily_patterns_flat_decimal_prices_do_not_create_a_sell_signal ... ok
test daily_patterns::tests::daily_patterns_helper_literal_preserves_parameters ... ok
test daily_patterns::tests::daily_patterns_rejects_future_and_duplicate_bars ... ok
test daily_patterns::tests::daily_patterns_no_missing_data_fallback ... ok
test daily_patterns::tests::daily_patterns_all_templates_and_score_absence ... ok
test daily_patterns::tests::limit_consolidation_requires_real_limit_and_never_infers_ten_percent ... ok
test daily_patterns::tests::mean_volume_followup_requires_bullish_breakout_and_shrink ... ok
test daily_patterns::tests::mean_volume_is_not_prior_max_and_excludes_current_bar ... ok
test daily_patterns::tests::expression_condition_preserves_native_types_and_rejects_numeric_as_signal ... ok
test daily_patterns::tests::research_index_and_ranking_context_never_unlock_strategy_mapping ... ok
test data::series_columns::tests::differing_prices_preserve_zero_nan_payloads_and_prior_rows ... ok
test data::series_columns::tests::identical_prices_share_only_after_exact_bit_comparison ... ok
test daily_patterns::tests::research_isolates_missing_listing_day_without_weakening_execution ... ok
test data::series_columns::tests::optional_values_keep_none_distinct_from_empty_and_repeated_text ... ok
test data::series_columns::tests::repeated_values_preserve_nonzero_values_and_copy_on_change ... ok
test daily_patterns::tests::research_numeric_output_keeps_warmup_unknown_without_a_false_signal ... ok
test data::series_columns::tests::repeated_values_reject_out_of_range_access - should panic ... ok
test data::tests::adjusted_close_average_normalization_prevents_strict_crossover_drift ... ok
test data::tests::baseline_selection_uses_dated_lifecycle_not_latest_undated_status ... ok
test data::tests::benchmark_current_rolling_reuses_symbol_boundary ... ignored, manual release-mode current rolling boundary benchmark
test data::tests::benchmark_daily_snapshot_view_lookup ... ignored, manual component benchmark
test data::tests::benchmark_decision_close_windows_exclude_current_close ... ok
test data::tests::benchmark_instrument_symbol_id_lookup ... ignored, manual release-mode instrument lookup benchmark
test data::tests::benchmark_series_boundary_reuses_calendar_index ... ignored, manual release-mode calendar index reuse benchmark
test data::tests::additional_terminal_calendar_dates_are_isolated_from_shared_market_data ... ok
test data::tests::current_close_average_uses_backward_adjustment_factor_and_current_base ... ok
test data::tests::batched_standard_rolling_means_match_scalar_lookups ... ok
test data::tests::daily_bundle_constructor_rejects_duplicate_or_mismatched_dates ... ok
test data::tests::day_open_moving_average_uses_its_own_historical_column ... ok
test data::tests::dataset_clone_shares_immutable_base_and_isolates_execution_quotes ... ok
test data::tests::decision_close_average_ignores_current_day_close ... ok
test data::tests::decision_market_cap_uses_factor_date_snapshot_without_price_reconstruction ... ok
test data::tests::decision_volume_average_ignores_paused_zero_volume_days ... ok
test data::tests::decision_volume_average_uses_previous_completed_days_only ... ok
test data::tests::daily_bundle_constructor_matches_flat_component_constructor ... ok
test data::tests::eligible_universe_can_use_configured_risk_policy ... ok
test data::tests::direct_symbol_id_snapshot_lookups_preserve_alignment_for_sparse_rows ... ok
test data::tests::eligible_universe_does_not_require_candidate_risk_state_when_selection_risk_is_disabled ... ok
test data::tests::factor_numeric_value_normalizes_fields_without_changing_aliases ... ok
test data::tests::factor_snapshot_normalization_moves_clean_maps_and_repairs_dirty_maps ... ok
test data::tests::eligible_universe_uses_decision_market_cap_same_date ... ok
test data::tests::execution_quotes_use_stable_k_way_merge_and_release_by_date ... ok
test data::tests::factor_snapshot_rejects_legacy_or_invalid_adjustment_storage ... ok
test data::tests::intraday_overlay_fails_closed_when_daily_panel_is_shared ... ok
test data::tests::future_missing_adjustment_factor_does_not_invalidate_historical_window ... ok
test data::tests::parallel_dense_index_rejects_invalid_days_without_publishing_partial_index ... ok
test data::tests::parallel_daily_symbol_indices_match_scalar_for_sparse_and_empty_days ... ok
test data::tests::replacing_execution_quotes_preserves_duplicate_timestamp_rows ... ok
test data::tests::series_columns_preserve_full_snapshots_and_distinct_price_bits ... ok
test data::tests::series_end_position_index_preserves_decision_and_current_boundaries ... ok
test data::tests::series_overlay_materializes_only_changed_values_and_preserves_history_cutoff ... ok
test data::tests::shared_execution_quote_release_does_not_clone_the_base_map ... ok
test data::tests::symbol_id_union_preserves_lexical_order_and_all_component_sources ... ok
test data::tests::source_volume_contract_rejects_windows_containing_missing_values ... ok
test data::tests::symbol_price_series_test_constructor_sorts_unsorted_rows ... ok
test data::tests::unique_dataset_applies_sparse_intraday_overlay_to_daily_and_symbol_views ... ok
test data::tests::parallel_daily_bundle_validation_keeps_earliest_error_and_component_order ... ok
test decision_projection::tests::projection_contract_does_not_shadow_core_market_or_typed_fields ... ok
test decision_projection::tests::exact_projection_coverage_and_classified_absence_are_receipted ... ok
test decision_projection::tests::projection_rejects_future_drift_or_unbound_receipts ... ok
test data::tests::volume_rolling_ignores_zero_volume_rows_for_source_and_legacy_data ... ok
test decision_projection::tests::projection_rejects_incomplete_duplicate_extra_and_overlapping_population ... ok
test decision_projection::tests::projection_rejects_nonfinite_wrong_fields_and_unproven_absence ... ok
test engine::tests::backtest_execution_dates_match_sparse_lagged_equity_schedule ... ok
test engine::tests::after_trading_cancel_is_executed_as_a_control_not_a_next_session_order ... ok
test engine::tests::compact_progress_keeps_counts_without_event_payload_clones ... ok
test engine::tests::current_bar_close_uses_decision_day_close_for_fill ... ok
test engine::tests::decision_projection_does_not_silently_shift_a_start_without_prior_inputs ... ok
test engine::tests::current_close_order_at_1500_loads_and_uses_post_close_matching_window ... ok
test engine::tests::compact_manual_progress_keeps_exact_applications_and_fallible_delivery_stops_the_run ... ok
test engine::tests::decision_projection_failure_never_reaches_strategy_and_does_not_blindly_retry ... ok
test engine::tests::decision_projection_rejects_future_response_before_any_strategy_callback ... ok
test engine::tests::decision_projection_refuses_static_scores_and_same_session_inputs ... ok
test engine::tests::decision_projection_rejects_missing_calendar_inputs_instead_of_dropping_a_session ... ok
test engine::tests::decision_projection_is_called_before_callbacks_and_is_not_shared_or_carried_forward ... ok
test engine::tests::decision_projection_requested_first_session_uses_visible_preperiod_inputs ... ok
test engine::tests::default_close_phase_waits_for_close_and_callbacks_receive_completed_trade_history ... ok
test engine::tests::early_pre_open_schedules_observe_manual_receipts_in_chronological_order ... ok
test engine::tests::empty_manual_source_does_not_retime_natural_strategy_execution ... ok
test engine::tests::join_text_parts_matches_vec_join_contract ... ok
test engine::tests::final_day_late_intents_are_retained_without_orders_or_fills_past_the_end ... ok
test engine::tests::full_minute_coverage_rejects_missing_active_bars_but_allows_paused_or_zero_volume ... ok
test engine::tests::lagged_signal_callbacks_keep_signal_date_but_process_notifications_use_execution_date ... ok
test engine::tests::late_day_callbacks_read_the_actual_earlier_trade_and_keep_their_current_clock ... ok
test engine::tests::late_complete_targets_use_latest_target_and_next_day_zero_supersedes_them ... ok
test engine::tests::late_intents_do_not_wait_for_another_selection_snapshot ... ok
test engine::tests::late_imperative_orders_execute_once_in_next_session_with_original_dates_and_new_price ... ok
test engine::tests::lifecycle_quote_filter_skips_only_dated_legal_absence_before_loading ... ok
test engine::tests::late_opening_callback_is_interleaved_with_earlier_quotes_and_manual_observations ... ok
test engine::tests::minute_event_clock_uses_timestamp_order_and_coalesces_equal_heads ... ok
test engine::tests::latest_unsent_complete_portfolio_does_not_union_an_older_pre_market_target ... ok
test engine::tests::minute_schedules_fire_at_each_declared_time_without_market_rows_at_those_times ... ok
test engine::tests::next_bar_open_eligible_universe_helper_does_not_block_on_decision_day_risk ... ok
test engine::tests::next_bar_open_executes_last_decision_without_execution_day_factor_snapshot ... ok
test engine::tests::minute_observer_never_sees_a_later_fill_from_a_coarse_phase ... ok
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_paused_state ... ok
test engine::tests::manual_observations_are_visible_at_their_real_phase_and_each_receipt_sequence ... ok
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_st_state ... ok
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_star_st_state ... ok
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_upper_limit_state ... ok
test engine::tests::next_bar_open_execution_risk_ignores_later_one_yuan_close ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_delisted_state ... ok
test engine::tests::next_bar_open_execution_risk_rejects_blacklisted_buy_on_execution_day ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_paused_state ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_star_st_state ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_st_state ... ok
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_upper_limit_buy ... ok
test engine::tests::next_bar_open_execution_risk_rejects_one_yuan_open_despite_higher_close ... ok
test engine::tests::next_bar_open_execution_risk_uses_open_not_close_for_upper_limit_buy ... ok
test engine::tests::next_bar_open_same_day_rebuy_uses_actual_execution_date ... ok
test engine::tests::next_bar_open_scheduled_signal_uses_decision_date_not_execution_date ... ok
test engine::tests::next_bar_open_sell_respects_allow_sell_policy_on_execution_day ... ok
test engine::tests::next_bar_open_sell_risk_ignores_decision_day_paused_and_lower_limit_state ... ok
test engine::tests::next_bar_open_sell_risk_rejects_execution_day_lower_limit_state ... ok
test engine::tests::next_bar_open_sell_risk_uses_open_not_close_for_lower_limit_sell ... ok
test engine::tests::next_bar_open_sell_volume_limit_ignores_decision_day_zero_volume ... ok
test engine::tests::next_bar_open_session_audit_flags_zero_volume_without_rewriting_fills ... ok
test engine::tests::next_bar_open_skips_unavailable_lag_day_and_fills_next_open ... ok
test engine::tests::next_bar_open_strategy_context_data_helpers_use_decision_date ... ok
test engine::tests::next_bar_open_target_percent_freezes_decision_day_equity ... ok
test engine::tests::next_bar_open_target_portfolio_smart_sizes_with_execution_day_open ... ok
test engine::tests::observed_manual_sale_updates_the_shared_same_day_rebuy_rule ... ok
test engine::tests::next_open_observation_loads_quotes_even_when_execution_price_is_open ... ok
test engine::tests::opening_and_late_lifecycle_schedules_do_not_read_past_or_future_manual_state ... ok
test engine::tests::opening_schedule_cannot_cross_the_after_trading_phase ... ok
test engine::tests::physical_on_day_rules_keep_each_actual_submission_time ... ok
test engine::tests::opening_subscription_is_loaded_without_replaying_quotes_before_its_activation ... ok
test engine::tests::opening_orders_are_submitted_once_at_their_real_clock_even_with_an_earlier_execution_window ... ok
test engine::tests::post_close_phase_callbacks_never_rewind_before_completed_fills ... ok
test engine::tests::post_close_wait_has_no_position_before_the_first_actual_matching_tick ... ok
test engine::tests::pre_trading_orders_wait_for_the_configured_window_and_keep_their_audit ... ok
test engine::tests::process_callbacks_observe_the_completed_minute_trade_and_its_clock ... ok
test engine::tests::scheduled_event_detail_records_actual_time_only_for_timed_rules ... ok
test engine::tests::pre_market_cancel_is_observed_before_the_next_open_and_new_signal ... ok
test engine::tests::unknown_identity_or_missing_candidate_does_not_waive_quote_coverage ... ok
test engine::tests::window_algorithm_cannot_publish_future_fill_quantity_to_earlier_callbacks ... ok
test etf_execution::tests::latest_generation_overwrites_pending_targets_and_preserves_candidate_order ... ok
test engine::tests::wholly_prelisting_universe_retains_cash_days_without_fabricating_prices ... ok
test etf_execution::tests::no_loaded_next_session_is_not_guessed_from_natural_days ... ok
test events::tests::accounting_origin_cannot_disguise_an_exchange_order_or_fee ... ok
test events::tests::order_event_status_quantity_contract_is_explicit ... ok
test events::tests::fill_execution_timestamp_range_is_explicit_and_backward_compatible ... ok
test events::tests::process_event_business_lifecycle_filter_keeps_audit_events_only ... ok
test execution_capacity::tests::completed_volume_cannot_be_used_for_an_earlier_open ... ok
test execution_capacity::tests::decimal_participation_never_rounds_a_fractional_share_up_or_overflows ... ok
test execution_capacity::tests::delayed_publication_and_invalid_bounds_are_not_treated_as_zero_volume ... ok
test execution_capacity::tests::session_audit_changes_verdict_not_executed_quantity ... ok
test execution_schedule::tests::clock_entitlements_are_exact_and_allow_backlog_without_future_quote_counts ... ok
test factor_cross_section::tests::neutralization_preserves_input_order ... ok
test factor_cross_section::tests::ties_keep_equal_rank_and_missing_universe_rejects ... ok
test factor_cross_section::tests::historical_ranks_keep_ties_and_unknown_full_cross_sections ... ok
test factor_events::tests::cross_is_event_not_state_and_never_uses_future ... ok
test factor_events::tests::every_parameter_domain_is_structured_and_matches_native_defaults ... ok
test factor_events::tests::missing_is_not_zero_and_breakout_excludes_current ... ok
test factor_events::tests::no_event_has_no_bars_since_and_type_errors_reject ... ok
test engine::tests::scheduled_day_limit_order_loads_later_quotes_without_strategy_minute_subscription ... ok
test factor_events::tests::ta_rewarms_after_gap_and_const_zscore_is_unknown ... ok
test factor_events::tests::ta_sma_real_values_and_parameter_validation ... ok
test finite_serialization::tests::pending_numbers_cannot_be_silently_serialized_as_optional_nulls ... ok
test fixed_point::tests::decimal_parser_rounds_only_beyond_money_scale ... ok
test fixed_point::tests::fixed_budget_comparison_rejects_one_micro_yuan_overrun ... ok
test fixed_point::tests::fixed_budget_never_exceeds_cash_after_cost ... ok
test fixed_point::tests::fixed_fifo_pnl_and_external_flow_are_deterministic ... ok
test fixed_point::tests::runtime_cost_model_matches_fixed_execution_primitive ... ok
test fixed_point::tests::runtime_split_commission_matches_fixed_execution_primitive ... ok
test holding_policy::tests::absolute_locks_are_inclusive_and_override_timed_exit_without_changing_other_symbols ... ok
test holding_policy::tests::manual_origin_only_bypasses_automatic_policy_not_an_order_or_broker_permission ... ok
test holding_policy::tests::opening_date_follows_fills_not_partial_sales_or_corporate_conversions ... ok
test holding_policy::tests::three_complete_sessions_protect_through_wednesday_not_72_hours ... ok
test holding_policy::tests::zero_disabled_and_missing_calendar_or_opened_date_are_not_inferred ... ok
test instrument::tests::lifecycle_is_dated_and_latest_undated_terminal_status_is_not_historical_evidence ... ok
test instrument::tests::listing_sector_is_explicit_and_unknown_stays_unknown ... ok
test instrument::tests::order_quantity_rules_are_case_insensitive_without_allocating_normalized_boards ... ok
test manual_execution::tests::a_failed_corporate_projection_does_not_change_the_book_or_receipt_cursor ... ok
test manual_execution::tests::all_required_money_and_binding_fields_reject_missing_or_wrong_values ... ok
test manual_execution::tests::authoritative_total_fee_does_not_require_inventing_unknown_components ... ok
test manual_execution::tests::changing_any_external_price_or_identity_invalidates_the_frozen_trace ... ok
test manual_execution::tests::complete_exact_decimal_evidence_allows_later_observation_and_retains_source_digits ... ok
test manual_execution::tests::confirmed_manual_fill_changes_cash_and_lots_but_not_external_cash_flow_units ... ok
test manual_execution::tests::confirmed_no_order_outcome_is_distinct_from_unconfirmed_or_unknown_work ... ok
test manual_execution::tests::cursor_waits_for_observation_and_never_reapplies_or_rewinds ... ok
test manual_execution::tests::data_scope_only_contains_actual_filled_securities_and_validates_the_source ... ok
test manual_execution::tests::failed_multi_receipt_advance_keeps_both_progress_and_portfolio_unchanged ... ok
test manual_execution::tests::fixed_money_decimal_text_preserves_micro_units_without_float_conversion ... ok
test factor_events::tests::literal_unknown_fields_reject_and_catalog_is_not_trading_permission ... ok
test manual_execution::tests::inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected ... ok
test factor_events::tests::parameter_domains_keep_enumeration_labels_without_debug_string_parsing ... ok
test manual_execution::tests::late_buy_fifo_depletion_preserves_costs_and_cannot_unlock_today_lots ... ok
test manual_execution::tests::late_fee_evidence_keeps_the_original_fill_observation_clock ... ok
test manual_execution::tests::late_buy_retains_the_earliest_opening_and_latest_buy_dates ... ok
test manual_execution::tests::manual_fee_total_includes_extra_charges_and_rejects_inconsistent_components ... ok
test manual_execution::tests::manual_mismatches_are_atomic_and_do_not_borrow_shares_cash_or_override_pending_orders ... ok
test manual_execution::tests::observations_follow_durable_receipt_order_and_not_input_array_order ... ok
test manual_execution::tests::partial_cancel_is_valid_but_full_fill_cannot_be_reported_as_cancelled ... ok
test manual_execution::tests::runtime_position_events_cannot_claim_observations_after_the_source_cutoff ... ok
test manual_execution::tests::raw_timezone_and_cutoff_are_required ... ok
test manual_execution::tests::the_next_day_manual_sale_keeps_the_actual_quantity_and_fee_contract ... ok
test manual_execution::tests::v2_facts_keep_their_encoding_but_cannot_silently_carry_new_runtime_settings ... ok
test market_event_context::tests::formulas_use_real_limits_and_full_denominators ... ok
test market_event_context::tests::missing_duplicate_and_unproven_limit_states_fail ... ok
test market_event_context::tests::chunking_and_future_append_preserve_history ... ok
test manual_execution::tests::source_time_precision_is_not_invented_and_submitted_time_must_fit_the_interval ... ok
test metrics::risk_adjusted_contract_tests::changing_daily_rates_adjusts_each_return_before_variance_and_downside ... ok
test metrics::risk_adjusted_contract_tests::incomplete_or_invalid_rates_are_not_zero_rate_observations ... ok
test market_event_context::tests::missing_industry_does_not_invent_groups_or_disable_independent_market_facts ... ok
test metrics::risk_adjusted_contract_tests::zero_deviation_and_single_sample_ratios_remain_undefined ... ok
test metrics::tests::a_defined_zero_ratio_is_not_missing ... ok
test metrics::tests::drawdown_includes_initial_nav_without_adding_a_trading_day ... ok
test metrics::tests::benchmark_cumulative_return_uses_first_day_previous_close ... ok
test metrics::tests::external_cash_flow_is_excluded_from_return_and_reported_separately ... ok
test metrics::tests::first_day_loss_is_preserved_in_shared_backtest_metrics ... ok
test metrics::tests::gains_without_downside_have_no_sortino_or_monthly_sharpe ... ok
test metrics::tests::no_risk_ratio_is_fabricated_after_nav_has_reached_zero ... ok
test metrics::tests::signal_baseline_uses_same_close_for_strategy_and_benchmark ... ok
test metrics::tests::risk_adjusted_metrics_use_daily_pit_rates_and_all_period_downside ... ok
test numeric_expr_vm::tests::benchmark_numeric_vm_against_rhai ... ignored, manual release-mode performance evidence
test metrics::tests::undefined_ratios_remain_null_in_backtest_metrics ... ok
test numeric_expr_vm::tests::evaluates_numeric_boolean_and_if_expressions ... ok
test numeric_expr_vm::tests::floating_comparisons_match_rhai_epsilon_semantics ... ok
test numeric_expr_vm::tests::invalid_clamp_returns_error_without_panicking ... ok
test numeric_expr_vm::tests::nullable_boolean_truth_table_preserves_unknown_under_negation ... ok
test numeric_expr_vm::tests::nullable_short_circuit_and_explicit_conditional_branches_are_lazy ... ok
test numeric_expr_vm::tests::missing_numeric_operands_do_not_become_boolean_false_or_zero ... ok
test numeric_expr_vm::tests::rejects_dynamic_language_features ... ok
test numeric_expr_vm::tests::scratch_reuses_allocations_across_program_runs ... ok
test numeric_expr_vm::tests::short_circuit_does_not_resolve_unused_variable ... ok
test numeric_expr_vm::tests::supports_numeric_builtins_and_local_reuse ... ok
test numeric_factors::tests::bulk_load_is_sorted_and_keeps_last_value_for_each_field ... ok
test numeric_factors::tests::clone_does_not_share_mutable_values ... ok
test numeric_factors::tests::compact_keys_inline_dynamic_names_and_keep_long_static_storage ... ok
test numeric_factors::tests::exact_reservation_preserves_values_and_avoids_growth_during_known_inserts ... ok
test numeric_factors::tests::long_dynamic_unicode_and_short_keys_keep_the_same_json_map ... ok
test numeric_factors::tests::serialization_keeps_the_map_contract_and_precise_numbers ... ok
test numeric_expr_vm::tests::matches_rhai_for_representative_numeric_boolean_corpus ... ok
test numeric_factors::tests::wide_batch_merge_matches_tree_and_preserves_old_key_ownership ... ok
test pattern_context::tests::normalized_rule_does_not_turn_an_omitted_window_into_explicit_null ... ok
test pattern_context::tests::reference_index_never_defaults_to_performance_benchmark ... ok
test pattern_context::tests::runtime_contract_rejects_missing_range_and_recursive_ranks ... ok
test numeric_factors::tests::updates_order_removal_and_values_match_tree_map ... ok
test pattern_context::tests::dataset_rank_is_full_scope_causal_and_equal_to_pure_cross_section ... ok
test pattern_context::tests::structured_pool_conditions_are_not_rescanned_inside_serialized_source_code ... ok
test platform_expr_strategy::tests::allocation_buy_scale_preserves_explicit_overweight_multiplier ... ok
test platform_expr_strategy::tests::benchmark_compiled_runtime_helper_arguments ... ignored, manual release-mode runtime helper binding benchmark
test platform_expr_strategy::tests::bjse_symbol_detection_is_case_insensitive_without_normalizing_all_symbols ... ok
test platform_expr_strategy::tests::available_factor_name_collection_preserves_sparse_and_repeated_fields ... ok
test platform_expr_strategy::tests::buy_filter_attaches_denials_without_rewriting_selection ... ok
test platform_expr_strategy::tests::available_factor_name_collection_preserves_wide_dynamic_field_identity ... ok
test platform_expr_strategy::tests::buy_filter_uses_active_schedule_time_instead_of_first_configured_time ... ok
test platform_expr_strategy::tests::completed_session_factor_dates_exclude_intraday_and_preserve_next_open ... ok
test platform_expr_strategy::tests::buy_quote_filter_rejects_missing_intraday_quote_not_daily_close ... ok
test platform_expr_strategy::tests::completed_session_bound_value_does_not_read_same_day_future_value ... ok
test platform_expr_strategy::tests::current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings ... ok
test platform_expr_strategy::tests::daily_pattern_runtime_uses_the_shared_kernel_and_rejects_early_visibility ... ok
test platform_expr_strategy::tests::dated_position_exposure_uses_the_latest_effective_point ... ok
test platform_expr_strategy::tests::daily_replacement_reference_records_an_explicit_empty_previous_target ... ok
test platform_expr_strategy::tests::daily_replacement_reference_uses_previous_target_before_lagged_orders_fill ... ok
test platform_expr_strategy::tests::expression_plan_only_executes_prelude_when_expression_depends_on_it ... ok
test platform_expr_strategy::tests::expression_plan_skips_unreferenced_market_scope_maps ... ok
test platform_expr_strategy::tests::framework_rolling_factor_aliases_resolve_to_raw_market_series ... ok
test platform_expr_strategy::tests::lagged_rebalance_emits_target_value_even_when_projection_is_unchanged ... ok
test platform_expr_strategy::tests::factor_position_target_reduces_once_without_daily_rebalance_override ... ok
test platform_expr_strategy::tests::market_cap_ordered_selection_range_is_inclusive_and_field_specific ... ok
test platform_expr_strategy::tests::model_target_lifecycle_removes_exits_and_appends_replacements ... ok
test platform_expr_strategy::tests::numeric_vm_compiles_static_runtime_helper_arguments ... ok
test platform_expr_strategy::tests::numeric_vm_reuses_compiled_program_across_invocations ... ok
test platform_expr_strategy::tests::numeric_vm_rewrites_only_valid_iso_date_literals ... ok
test platform_expr_strategy::tests::numeric_vm_reuses_rolling_helper_program_across_dates ... ok
test platform_expr_strategy::tests::market_cap_ascending_rank_reuses_only_equivalent_universe_order ... ok
test platform_expr_strategy::tests::periodic_selected_bjse_buy_rejection_is_audited_without_creating_an_order ... ok
test platform_expr_strategy::tests::platform_aiquant_buy_rejection_keeps_candidate_buy_disabled ... ok
test platform_expr_strategy::tests::platform_aiquant_blocks_target_increase_when_stop_loss_sell_is_lower_limit_blocked ... ok
test platform_expr_strategy::tests::platform_aiquant_buy_rejection_uses_scheduled_price_not_open_limit ... ok
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_releases_slot_after_take_profit_exit ... ok
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_uses_remaining_slot_budget ... ok
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_releases_second_slot_after_prior_top_up ... ok
test platform_expr_strategy::tests::platform_aiquant_delays_upper_limit_take_profit_to_configured_open_time ... ok
test platform_expr_strategy::tests::platform_aiquant_marked_total_uses_intraday_last_not_close ... ok
test platform_expr_strategy::tests::platform_aiquant_marked_total_value_uses_scheduled_quote ... ok
test platform_expr_strategy::tests::platform_aiquant_minute_last_projection_uses_last_quote_for_buy_budget ... ok
test platform_expr_strategy::tests::platform_aiquant_marks_highlimit_before_cptrade_time ... ok
test platform_expr_strategy::tests::platform_aiquant_minute_stop_loss_requires_intraday_quote ... ok
test platform_expr_strategy::tests::platform_aiquant_next_open_stop_loss_uses_signal_close_not_execution_last_price ... ok
test platform_expr_strategy::tests::platform_aiquant_missing_risk_state_does_not_force_existing_position_exit ... ok
test platform_expr_strategy::tests::platform_aiquant_next_open_target_delta_uses_previous_close_and_sizes_at_open ... ok
test platform_expr_strategy::tests::platform_aiquant_projection_allows_signal_day_market_fallback_for_next_open ... ok
test platform_expr_strategy::tests::platform_aiquant_projection_does_not_release_sell_slot_without_execution_quote ... ok
test platform_expr_strategy::tests::platform_aiquant_projection_rejects_missing_symbol_execution_quote ... ok
test platform_expr_strategy::tests::platform_aiquant_projection_reserves_sell_cost_without_same_callback_proceeds ... ok
test platform_expr_strategy::tests::platform_aiquant_periodic_rebalance_uses_total_equity_equal_weight_buy_budget ... ok
test platform_expr_strategy::tests::platform_aiquant_remaining_buy_cash_uses_strategy_visible_price_and_projected_quantity ... ok
test platform_expr_strategy::tests::platform_aiquant_risk_level_forces_existing_position_exit ... ok
test platform_expr_strategy::tests::platform_aiquant_selection_uses_decision_market_cap ... ok
test platform_expr_strategy::tests::platform_aiquant_sell_projection_uses_signal_day_market_for_lagged_execution ... ok
test platform_expr_strategy::tests::platform_aiquant_scheduled_quote_uses_configured_intraday_time ... ok
test platform_expr_strategy::tests::platform_aiquant_sells_down_before_actionable_take_profit_exit ... ok
test platform_expr_strategy::tests::platform_aiquant_skips_positive_target_adjust_when_position_will_close ... ok
test platform_expr_strategy::tests::platform_aiquant_stock_expr_uses_scheduled_minute_price_for_limit_check ... ok
test platform_expr_strategy::tests::platform_aiquant_target_adjust_uses_scheduled_quote_for_position_value ... ok
test platform_expr_strategy::tests::platform_aiquant_target_value_uses_scheduled_mark_for_delta ... ok
test platform_expr_strategy::tests::platform_aiquant_stop_loss_uses_fee_cost_basis_avg_cost ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_allows_pending_highlimit_target_adjust_after_open ... ok
test platform_expr_strategy::tests::platform_aiquant_upper_limit_buy_disabled_does_not_force_exit_existing_position ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_emits_daily_position_target_adjustment ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_marks_upper_limit_but_allows_target_adjust ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_threshold_only_sells_overweight_positions ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_threshold_top_up_uses_projected_remaining_budget ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_retargets_same_ratio_every_day ... ok
test platform_expr_strategy::tests::platform_calendar_day_rebalance_interval_can_be_enabled_explicitly ... ok
test platform_expr_strategy::tests::platform_aiquant_weak_market_waits_pending_highlimit_target_adjust ... ok
test platform_expr_strategy::tests::platform_buy_rejection_allows_lower_limit_buy_when_ask_is_available ... ok
test platform_expr_strategy::tests::platform_current_rolling_uses_decision_date_row_not_prior_factor_row ... ok
test platform_expr_strategy::tests::platform_can_sell_position_uses_configured_lower_limit_policy ... ok
test platform_expr_strategy::tests::platform_daily_top_up_caps_empty_slot_to_equal_weight_target ... ok
test platform_expr_strategy::tests::platform_daily_top_up_counts_submitted_zero_fill_candidate_as_strategy_slot ... ok
test platform_expr_strategy::tests::platform_daily_top_up_does_not_buy_back_same_day_exit ... ok
test platform_expr_strategy::tests::platform_daily_top_up_keeps_selection_limited_to_target_count ... ok
test platform_expr_strategy::tests::platform_daily_top_up_releases_unsellable_stop_loss_slot ... ok
test platform_expr_strategy::tests::platform_daily_top_up_release_slot_flag_matches_aiquant_exit_signal_state ... ok
test platform_expr_strategy::tests::platform_daily_top_up_skips_current_stop_loss_symbol_like_aiquant ... ok
test platform_expr_strategy::tests::platform_day_state_uses_decision_benchmark_close ... ok
test platform_expr_strategy::tests::platform_daily_top_up_uses_same_day_sell_cash ... ok
test platform_expr_strategy::tests::platform_delayed_open_day_carried_full_close_releases_second_top_up_slot ... ok
test platform_expr_strategy::tests::platform_delayed_open_exit_uses_delayed_time_for_slot_projection ... ok
test platform_expr_strategy::tests::platform_delayed_open_day_take_profit_releases_second_top_up_slot ... ok
test platform_expr_strategy::tests::platform_expr_code_number_extracts_stock_digits ... ok
test platform_expr_strategy::tests::platform_explicit_actions_keep_every_declared_trade_time ... ok
test platform_expr_strategy::tests::platform_expr_cost_model_honors_zero_minimum_commission ... ok
test platform_expr_strategy::tests::platform_expr_expands_day_factor_in_prelude ... ok
test platform_expr_strategy::tests::platform_expr_limit_flags_are_directional ... ok
test platform_expr_strategy::tests::platform_delayed_open_partial_exit_releases_top_up_slot ... ok
test platform_expr_strategy::tests::platform_expr_normalizes_python_style_integer_division ... ok
test platform_expr_strategy::tests::platform_expr_normalizes_signal_field_alias_in_prelude ... ok
test platform_expr_strategy::tests::platform_expr_cost_model_uses_commission_override ... ok
test platform_expr_strategy::tests::platform_expr_preflight_rejects_malformed_expression_with_field_name ... ok
test platform_expr_strategy::tests::platform_expr_missing_requested_factor_does_not_default_to_zero ... ok
test platform_expr_strategy::tests::platform_expr_preflight_accepts_all_configured_expression_categories ... ok
test platform_expr_strategy::tests::platform_expr_rewrites_nested_ternary ... ok
test platform_expr_strategy::tests::platform_expr_rewrites_prelude_assignment_ternary ... ok
test platform_expr_strategy::tests::platform_expr_rewrites_multiple_nested_ternaries_inside_function_calls ... ok
test platform_expr_strategy::tests::platform_expr_safe_div_supports_two_and_three_arg_forms ... ok
test platform_expr_strategy::tests::platform_expr_supports_python_style_string_aliases ... ok
test platform_expr_strategy::tests::platform_expr_tracks_requested_dynamic_factor_identifiers ... ok
test platform_expr_strategy::tests::platform_full_exposure_carried_full_close_releases_second_top_up_slot ... ok
test platform_expr_strategy::tests::platform_lagged_exit_does_not_reserve_replacement_slot_before_execution ... ok
test platform_expr_strategy::tests::platform_full_exposure_partial_stop_loss_releases_second_top_up_slot ... ok
test platform_expr_strategy::tests::platform_helpers_support_generic_rolling_stats_and_normalized_factors ... ok
test platform_expr_strategy::tests::platform_max_holding_days_counts_decision_trading_days_not_calendar_days ... ok
test platform_expr_strategy::tests::platform_marked_total_value_uses_current_day_close ... ok
test platform_expr_strategy::tests::platform_market_cap_field_uses_storage_unit_without_extra_scaling ... ok
test platform_expr_strategy::tests::platform_minute_last_volume_limit_does_not_require_l1_quantity ... ok
test platform_expr_strategy::tests::platform_max_holding_days_exit_preempts_take_profit_exit ... ok
test platform_expr_strategy::tests::platform_next_open_defers_complete_static_selection_risk_before_universe_output ... ok
test platform_expr_strategy::tests::platform_next_open_risk_level_exit_ignores_decision_day_sellability ... ok
test platform_expr_strategy::tests::platform_next_open_projected_execution_price_uses_open_not_close ... ok
test platform_expr_strategy::tests::platform_next_open_selection_applies_explicit_signal_day_risk_filter ... ok
test platform_expr_strategy::tests::platform_next_open_select_symbols_defers_decision_day_limit_state ... ok
test platform_expr_strategy::tests::platform_next_open_selection_ignores_execution_day_limit_state ... ok
test platform_expr_strategy::tests::platform_next_open_sell_risk_uses_open_not_close_for_lower_limit ... ok
test platform_expr_strategy::tests::platform_order_value_quantity_includes_buy_commission_budget ... ok
test platform_expr_strategy::tests::platform_numeric_stop_loss_is_strictly_below_multiplier ... ok
test platform_expr_strategy::tests::platform_periodic_rebalance_caps_empty_slot_to_equal_weight_target ... ok
test platform_expr_strategy::tests::platform_periodic_rebalance_skips_unsellable_stop_loss_symbol ... ok
test platform_expr_strategy::tests::platform_projected_execution_limit_checks_respect_risk_policy ... ok
test platform_expr_strategy::tests::platform_periodic_rebalance_uses_same_day_sell_cash_for_later_buys ... ok
test platform_expr_strategy::tests::platform_projected_market_fill_caps_volume_limit ... ok
test platform_expr_strategy::tests::platform_rebalance_date_due_does_not_use_the_on_day_default_clock ... ok
test platform_expr_strategy::tests::platform_periodic_rebalance_tops_up_underweight_selected_holding ... ok
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_monthly_tradingday ... ok
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_physical_time ... ok
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_weekly_weekday ... ok
test platform_expr_strategy::tests::platform_projection_does_not_consume_cash_from_unfillable_sell ... ok
test platform_expr_strategy::tests::platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement ... ok
test platform_expr_strategy::tests::platform_selection_does_not_fill_candidates_with_buy_risk_rejections ... ok
test platform_expr_strategy::tests::platform_selection_and_buy_respect_allow_flags_are_configurable ... ok
test platform_expr_strategy::tests::platform_selection_expands_pct_change_in_filter_and_rank_expr ... ok
test platform_expr_strategy::tests::platform_selection_kcb_switch_overrides_legacy_universe_exclude ... ok
test platform_expr_strategy::tests::platform_refresh_rate_uses_stateful_trading_day_counter ... ok
test platform_expr_strategy::tests::platform_selection_missing_candidate_risk_state_does_not_change_default_universe ... ok
test platform_expr_strategy::tests::platform_selection_ranks_with_previous_factor_date ... ok
test broker::tests::simulator_transaction_profile_preserves_successful_output ... ok
test platform_expr_strategy::tests::platform_signal_price_model_lifecycle_survives_unfilled_buy_and_exits ... ok
test platform_expr_strategy::tests::platform_selection_uses_complete_static_risk_policy_before_universe_output ... ok
test platform_expr_strategy::tests::platform_skip_window_keeps_pending_highlimit_until_open ... ok
test platform_expr_strategy::tests::platform_skip_window_runs_delayed_open_exit_before_seasonal_liquidation ... ok
test platform_expr_strategy::tests::platform_stock_expr_exposes_star_st_and_bjse_fields_without_default_selection_risk ... ok
test platform_expr_strategy::tests::platform_selection_ranks_by_candidate_market_cap_with_current_precomputed_factors ... ok
test platform_expr_strategy::tests::platform_stock_expr_rejects_when_volume_ma5_exceeds_market_volume_ma100 ... ok
test platform_expr_strategy::tests::platform_stock_expr_handles_positive_volume_guard ... ok
test platform_expr_strategy::tests::platform_stock_state_uses_completed_day_high_and_low_for_limit_touch ... ok
test platform_expr_strategy::tests::platform_stock_state_uses_factor_date_for_selection_market_fields ... ok
test platform_expr_strategy::tests::platform_stop_loss_residual_pending_rechecks_signal_next_day ... ok
test platform_expr_strategy::tests::platform_stock_state_uses_market_series_when_factor_map_has_no_rolling ... ok
test platform_expr_strategy::tests::platform_stock_state_uses_framework_rolling_series ... ok
test platform_expr_strategy::tests::platform_strategy_baseline_risk_rejects_new_listing_buy_without_selection_replacement ... ok
test platform_expr_strategy::tests::platform_strategy_can_cancel_latest_open_order_by_runtime_id ... ok
test platform_expr_strategy::tests::platform_strategy_emits_algo_order_actions ... ok
test platform_expr_strategy::tests::platform_strategy_can_filter_with_string_factors ... ok
test platform_expr_strategy::tests::platform_strategy_emits_explicit_actions_in_open_auction_stage ... ok
test platform_expr_strategy::tests::platform_strategy_emits_explicit_actions_when_rotation_is_disabled ... ok
test platform_expr_strategy::tests::platform_strategy_emits_target_portfolio_smart_algo_order_style ... ok
test platform_expr_strategy::tests::platform_stop_take_fraction_values_are_return_thresholds ... ok
test platform_expr_strategy::tests::platform_strategy_emits_target_shares_explicit_action ... ok
test platform_expr_strategy::tests::platform_strategy_emits_target_portfolio_smart_explicit_action ... ok
test platform_expr_strategy::tests::platform_strategy_emits_universe_management_actions ... ok
test platform_expr_strategy::tests::platform_strategy_exposes_open_order_runtime_fields ... ok
test platform_expr_strategy::tests::platform_strategy_exposes_advanced_data_runtime_helpers ... ok
test platform_expr_strategy::tests::platform_strategy_exposes_position_lifecycle_runtime_fields ... ok
test platform_expr_strategy::tests::platform_strategy_empty_exclude_keeps_buy_risk_without_selection_filtering ... ok
test platform_expr_strategy::tests::platform_strategy_exposes_process_event_runtime_fields ... ok
test platform_expr_strategy::tests::platform_strategy_filters_target_portfolio_smart_by_subscription_guard ... ok
test platform_expr_strategy::tests::platform_strategy_skips_explicit_actions_when_schedule_does_not_match ... ok
test platform_expr_strategy::tests::platform_strategy_suppresses_explicit_target_for_blocked_stop_loss_exit ... ok
test platform_expr_strategy::tests::platform_strategy_uses_decision_date_for_next_bar_open_signals ... ok
test platform_expr_strategy::tests::platform_strategy_treats_missing_stock_rolling_window_as_filter_reject ... ok
test platform_expr_strategy::tests::platform_take_profit_default_uses_position_cost_basis ... ok
test platform_expr_strategy::tests::platform_take_profit_false_does_not_track_upper_limit_pending_sell ... ok
test platform_expr_strategy::tests::platform_take_profit_sells_position_at_upper_limit ... ok
test platform_expr_strategy::tests::platform_weak_exposure_carried_full_close_keeps_residual_slot ... ok
test platform_expr_strategy::tests::platform_weak_exposure_partial_stop_loss_keeps_residual_slot ... ok
test platform_expr_strategy::tests::portfolio_drawdown_control_is_idempotent_and_rearms_after_cooldown ... ok
test platform_expr_strategy::tests::platform_weak_market_skips_target_increase_before_intraday_stop_loss_sell_blocked ... ok
test platform_expr_strategy::tests::platform_weak_market_skips_target_adjust_before_actionable_stop_loss ... ok
test platform_expr_strategy::tests::selection_buffer_retains_held_symbols_within_buffer_rank ... ok
test platform_expr_strategy::tests::platform_weak_market_skips_target_increase_before_lower_limit_blocked_stop_loss ... ok
test platform_expr_strategy::tests::projected_historical_slippage_does_not_swallow_missing_calibration ... ok
test platform_expr_strategy::tests::selection_quote_plan_filters_daily_part_before_minute_candidates ... ok
test platform_expr_strategy::tests::selection_replacement_limit_caps_voluntary_daily_removals ... ok
test platform_expr_strategy::tests::session_event_uses_previous_bar_and_recomputes_at_each_minute_without_becoming_a_quote ... ok
test platform_expr_strategy::tests::selection_quote_plan_keeps_configured_static_selection_risk_for_intraday_fields ... ok
test platform_expr_strategy::tests::selection_limit_state_requires_intraday_selection_quotes ... ok
test platform_expr_strategy::tests::stock_pool_exit_weight_redistribution_is_explicit_and_uses_integer_bps ... ok
test platform_expr_strategy::tests::stock_pool_suspension_keeps_budget_and_exit_redistributes_only_available_weights ... ok
test platform_expr_strategy::tests::signal_date_explicit_actions_are_due_only_on_declared_snapshot_dates ... ok
test platform_expr_strategy::tests::portfolio_loss_observes_finalized_nav_after_fees_and_cash_flows ... ok
test platform_expr_strategy::tests::stock_pool_suspension_resolution_never_reads_the_future_execution_day ... ok
test platform_expr_strategy::tests::stock_pool_target_lifecycle_keeps_exits_for_next_day_reentry ... ok
test platform_expr_strategy::tests::stock_state_cache_resets_before_reusing_compact_keys_on_another_date ... ok
test platform_expr_strategy::tests::strategy_specific_labels_are_not_framework_rolling_factors ... ok
test platform_expr_strategy::tests::target_portfolio_smart_exposes_decision_and_execution_dates ... ok
test platform_expr_strategy::tests::stock_snapshot_fields_are_loaded_only_when_the_strategy_uses_them ... ok
test platform_expr_strategy::tests::target_portfolio_smart_respects_current_date_weight_conditions ... ok
test platform_expr_strategy::tests::target_scale_replenishment_preserves_strategy_cash_allocation ... ok
test platform_expr_strategy::tests::strategy105_numeric_expressions_compile_to_vm ... ok
test platform_expr_strategy::tests::typed_runtime_numbers_preserve_legacy_rhai_formatting ... ok
test platform_expr_strategy::tests::universe_exclude_matches_explicit_symbols_and_bjse_alias ... ok
test platform_runtime_schema::tests::runtime_schema_includes_known_identifiers ... ok
test platform_runtime_schema::tests::runtime_schema_serializes_to_json_object ... ok
test platform_strategy_spec::tests::accepts_equivalent_risk_policy_alias_values ... ok
test platform_strategy_spec::tests::accepts_scoped_blacklist_context_in_runtime_risk_policy ... ok
test platform_expr_strategy::tests::timed_rotation_runs_on_its_schedule_instead_of_the_untimed_day_hook ... ok
test platform_strategy_spec::tests::canonicalizes_duplicate_execution_aliases_without_changing_strategy_intent ... ok
test platform_strategy_spec::tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
test platform_strategy_spec::tests::engine_config_parses_weak_market_shrink_overweight_threshold ... ok
test platform_strategy_spec::tests::engine_stock_ma_filter_generates_price_and_volume_expr ... ok
test platform_strategy_spec::tests::explicit_actions_inherit_top_level_runtime_schedule ... ok
test platform_strategy_spec::tests::execution_limit_aliases_normalize_to_one_serde_field_without_touching_policy ... ok
test platform_strategy_spec::tests::explicit_delayed_limit_open_exit_false_overrides_trade_times ... ok
test platform_strategy_spec::tests::explicit_trading_schedule_overrides_rebalance_trade_times ... ok
test platform_strategy_spec::tests::index_throttle_uses_signal_ma_not_performance_benchmark_ma ... ok
test platform_strategy_spec::tests::frozen_candidate_order_is_explicit_and_preserves_source_positions ... ok
test platform_strategy_spec::tests::multiple_trade_times_do_not_imply_delayed_limit_exit ... ok
test platform_strategy_spec::tests::native_factor_bindings_declare_completed_session_fields ... ok
test platform_strategy_spec::tests::normalize_board_does_not_infer_kcb_from_security_code ... ok
test platform_strategy_spec::tests::numeric_strategy_aliases_accept_exact_zero_but_never_hide_rounding_or_conflicts ... ok
test platform_strategy_spec::tests::parses_and_rejects_invalid_position_target_rules ... ok
test platform_strategy_spec::tests::parses_buy_filter_as_a_separate_trading_condition ... ok
test platform_strategy_spec::tests::parses_daily_schedule_time_for_execution_quotes ... ok
test platform_strategy_spec::tests::parses_delayed_deposit_receiving_days_expression ... ok
test platform_strategy_spec::tests::parses_dynamic_model_score_dates_into_platform_config ... ok
test platform_strategy_spec::tests::parses_execution_cost_overrides_into_platform_config ... ok
test platform_strategy_spec::tests::parses_execution_slippage_overrides_into_platform_config ... ok
test platform_strategy_spec::tests::parses_explicit_delayed_limit_open_exit ... ok
test platform_strategy_spec::tests::parses_explicit_historical_slippage_into_platform_config ... ok
test platform_strategy_spec::tests::parses_lifecycle_target_portfolio_contract ... ok
test platform_strategy_spec::tests::parses_generic_futures_actions_and_rejects_incomplete_contracts ... ok
test platform_expr_strategy::tests::timed_current_close_stop_loss_reloads_state_after_quote_arrives ... ok
test platform_strategy_spec::tests::parses_max_holding_days_from_engine_and_runtime_trading ... ok
test platform_strategy_spec::tests::parses_percent_slippage_alias_into_platform_config ... ok
test platform_strategy_spec::tests::parses_minute_stage_schedule_and_initial_subscriptions ... ok
test platform_strategy_spec::tests::parses_portfolio_drawdown_control_into_platform_config ... ok
test platform_strategy_spec::tests::parses_position_average_entry_stop_take_reference_price_mode ... ok
test platform_strategy_spec::tests::parses_rebalance_cash_mode_and_forces_minute_to_actual_sequence ... ok
test platform_strategy_spec::tests::parses_risk_policy_into_platform_config ... ok
test platform_strategy_spec::tests::parses_runtime_expression_spec_into_platform_config ... ok
test platform_strategy_spec::tests::parses_signal_dates_rebalance_into_platform_config ... ok
test platform_strategy_spec::tests::parses_signal_day_post_adjusted_stop_take_reference_price_mode ... ok
test platform_strategy_spec::tests::parses_typed_time_in_force_for_explicit_orders ... ok
test platform_strategy_spec::tests::parses_st_and_star_st_risk_policy_switches_into_platform_config ... ok
test platform_strategy_spec::tests::rejects_conflicting_non_policy_strategy_aliases ... ok
test platform_strategy_spec::tests::rejects_conflicting_risk_policy_alias_values ... ok
test platform_strategy_spec::tests::rejects_invalid_explicit_volume_percent_in_platform_config ... ok
test platform_strategy_spec::tests::portfolio_loss_contract_is_explicit_and_validated ... ok
test platform_strategy_spec::tests::rejects_invalid_or_duplicate_static_universe_symbols ... ok
test platform_strategy_spec::tests::rejects_invalid_portfolio_drawdown_control ... ok
test platform_strategy_spec::tests::rejects_removed_compatibility_profile ... ok
test platform_strategy_spec::tests::rejects_removed_engine_profile_name ... ok
test platform_strategy_spec::tests::rejects_unknown_stop_take_reference_price_mode ... ok
test platform_strategy_spec::tests::rejects_non_positive_shared_order_limits ... ok
test platform_strategy_spec::tests::rejects_unknown_strategy_risk_policy_fields_before_deserialization ... ok
test platform_strategy_spec::tests::platform_spec_accepts_only_supported_matching_types ... ok
test platform_strategy_spec::tests::rejects_unknown_time_in_force_in_runtime_contract ... ok
test platform_strategy_spec::tests::runtime_expression_parser_does_not_inherit_microcap_template_defaults ... ok
test platform_strategy_spec::tests::runtime_expression_parses_dated_position_exposure_schedule ... ok
test platform_strategy_spec::tests::retired_or_unknown_slippage_models_do_not_fall_back_to_fixed_or_none ... ok
test platform_strategy_spec::tests::runtime_expressions_infer_ma_windows_from_literal_strategy_logic ... ok
test portfolio::tests::buy_after_partial_sell_continues_moving_average_cost_basis ... ok
test platform_strategy_spec::tests::volume_limit_does_not_enable_quote_quantity_limit_for_minute_last ... ok
test portfolio::tests::cash_dividend_can_preserve_avg_cost_for_aiquant_rules ... ok
test portfolio::tests::delayed_withdrawals_are_reserved_and_settled_atomically ... ok
test portfolio::tests::external_cash_flow_rebases_units_without_changing_nav ... ok
test portfolio::tests::fifo_fixed_pnl_conserves_value_while_alv_average_cost_stays_stable ... ok
test portfolio::tests::fixed_lot_allocation_rounds_nearest_and_conserves_total ... ok
test portfolio::tests::holdings_summary_reports_entry_price_pnl_excluding_buy_commission ... ok
test portfolio::tests::partial_sell_preserves_remaining_average_cost ... ok
test portfolio::tests::portfolio_exposes_engine_native_account_metrics ... ok
test portfolio::tests::portfolio_marks_same_day_buy_at_fill_until_next_trading_day ... ok
test portfolio::tests::portfolio_missing_market_requires_formal_suspension_before_carrying_price ... ok
test portfolio::tests::portfolio_tracks_dividend_receivable_and_day_pnl ... ok
test portfolio::tests::position_tracks_day_lifecycle_fields ... ok
test portfolio::tests::positions_preserve_insertion_order ... ok
test portfolio::tests::scoped_checkpoint_restores_order_flat_rows_and_progress_without_copying_untouched_lots ... ok
test portfolio::tests::strategy_entry_price_excludes_buy_commission_cost_basis ... ok
test portfolio_loss::tests::cash_sessions_preserve_continuity_without_inventing_returns ... ok
test platform_strategy_spec::tests::session_rotation_keeps_every_declared_clock_not_only_the_last_one ... ok
test portfolio_loss::tests::refuses_future_missing_corrected_and_incomplete_accounting ... ok
test portfolio_loss::tests::restart_is_exact_and_duplicate_decisions_do_not_consume_cooldown ... ok
test portfolio_loss::tests::restored_state_rejects_changed_policy_and_forged_cooldown ... ok
test portfolio_loss::tests::nav_serialization_preserves_float_bits ... ok
test position_exposure::tests::allocation_is_dated_and_any_later_scalar_or_restore_clears_it ... ok
test position_exposure::tests::explicit_equity_targets_and_buys_scale_but_sales_cashflows_and_prices_do_not ... ok
test position_exposure::tests::rejects_unknown_actions_duplicate_identity_and_invalid_bps ... ok
test position_exposure::tests::same_day_adjustments_restore_and_future_events_keep_their_own_times ... ok
test position_exposure::tests::scalar_preserves_strategy_risk_off_and_restore_keeps_original_exposure ... ok
test risk_control::tests::concrete_kcb_reason_wins_over_generic_missing_risk_state ... ok
test risk_control::tests::configurable_bjse_filter_can_be_disabled ... ok
test risk_control::tests::configurable_blacklist_rejects_selection_and_buy ... ok
test risk_control::tests::configurable_kcb_filter_can_be_disabled ... ok
test risk_control::tests::configurable_lower_limit_selection_filter_can_be_disabled ... ok
test risk_control::tests::configurable_upper_limit_buy_filter_can_be_disabled ... ok
test risk_control::tests::configurable_upper_limit_selection_filter_can_be_disabled ... ok
test risk_control::tests::delisting_period_status_is_not_terminal_without_delisted_date ... ok
test risk_control::tests::execution_quote_covers_missing_one_yuan_flag_but_not_other_risk_facts ... ok
test risk_control::tests::explicit_candidate_inactive_status_is_not_treated_as_missing_risk_state ... ok
test risk_control::tests::explicit_one_yuan_selection_policy_still_uses_selection_facts ... ok
test risk_control::tests::fund_identity_excludes_stock_one_yuan_rule_but_not_actual_price_and_pause_checks ... ok
test risk_control::tests::inactive_selection_checks_preserve_missing_facts_and_execution_rejections ... ok
test risk_control::tests::kcb_filter_uses_classification_instead_of_security_code ... ok
test risk_control::tests::missing_risk_state_can_be_relaxed_by_disabling_static_filters ... ok
test risk_control::tests::missing_risk_state_default_selection_ignores_allow_flags_but_buy_rejects ... ok
test risk_control::tests::missing_risk_state_rejects_sell_when_sell_risk_facts_are_missing ... ok
test risk_control::tests::missing_risk_state_respects_field_specific_buy_switches ... ok
test risk_control::tests::missing_risk_state_respects_field_specific_selection_switches ... ok
test risk_control::tests::missing_risk_state_selection_audit_keeps_missing_fields_in_reason ... ok
test risk_control::tests::missing_risk_state_selection_still_respects_configured_static_filters ... ok
test risk_control::tests::missing_risk_state_sell_can_be_relaxed_by_disabling_sell_static_filters ... ok
test risk_control::tests::one_yuan_buy_rule_uses_execution_price_not_later_close_or_earlier_open ... ok
test risk_control::tests::sell_rejection_blocks_execution_price_at_lower_limit ... ok
test risk_control::tests::sell_rejection_respects_allow_sell_policy_on_execution_day ... ok
test risk_control::tests::sell_rejects_explicit_candidate_inactive_status_without_instrument_row ... ok
test risk_control::tests::shared_order_size_limits_apply_to_both_sides_and_buy_position ... ok
test risk_control::tests::st_and_star_st_filters_are_independent ... ok
test scheduler::tests::date_due_is_independent_from_the_order_execution_clock ... ok
test scheduler::tests::scheduler_matches_before_trading_and_minute_level_rules ... ok
test scheduler::tests::scheduler_matches_daily_weekly_and_monthly_rules ... ok
test platform_expr_strategy::tests::shared_signal_reduction_and_stops_use_each_accounts_cost_quantity_and_sold_state ... ok
test portfolio::tests::cash_ledger_accumulates_micro_yuan_exactly ... ok
test session_events::tests::gaps_and_stale_last_bars_do_not_become_false_or_repeated_signals ... ok
test session_events::tests::opening_range_is_unavailable_before_the_range_has_completed ... ok
test signal_contract::tests::changed_valid_contents_must_not_reuse_a_version_hash ... ok
test signal_contract::tests::completed_daily_inputs_may_be_published_after_market_close ... ok
test signal_contract::tests::coverage_and_duplicate_actions_fail_closed ... ok
test signal_contract::tests::empty_complete_snapshot_clears_only_that_accounts_holdings ... ok
test signal_contract::tests::historical_reconstruction_is_not_online_publication ... ok
test signal_contract::tests::next_open_uses_decision_session_and_never_nearest_signal ... ok
test signal_contract::tests::observed_next_open_never_backdates_a_morning_publication_into_yesterdays_orders ... ok
test signal_contract::tests::platform_spec_consumes_book_without_running_another_selection ... ok
test signal_contract::tests::reconstruction_ignores_research_wall_clock_but_never_early_input_availability ... ok
test signal_contract::tests::reduction_is_resolved_from_each_accounts_actual_position ... ok
test signal_contract::tests::rejects_future_inputs_and_model_knowledge ... ok
test signal_contract::tests::rejects_overallocation_nonfinite_and_ambiguous_actions ... ok
test signal_contract::tests::rejects_quantities_prices_and_unknown_signal_fields ... ok
test stock_pool_candidates::tests::list_order_reuses_explicit_prefix_and_appends_new_candidates ... ok
test stock_pool_candidates::tests::missing_snapshot_duplicate_input_and_empty_filtered_scope_fail ... ok
test stock_pool_candidates::tests::mixed_sources_preserve_priority_and_both_provenances ... ok
test session_events::tests::decision_uses_the_previous_completed_label_and_future_prices_do_not_rewrite ... ok
test stock_pool_candidates::tests::zero_automatic_day_keeps_manual_members_without_inheriting_old_auto_targets ... ok
test stock_pool_execution::frozen::ratio_tests::legacy_paused_and_explicit_partial_budgets_are_not_reinterpreted ... ok
test stock_pool_execution::frozen::ratio_tests::precise_paused_budget_survives_replacement_and_zero_targets ... ok
test stock_pool_execution::frozen::ratio_tests::precision_is_checked_before_frozen_budget_is_allocated ... ok
test stock_pool_execution::tests::confirmed_holdings_not_unsettled_sells_determine_remaining_position_budget ... ok
test stock_pool_execution::tests::all_membership_and_weight_combinations_use_the_same_target_planner ... ok
test stock_pool_execution::tests::frozen_valuation_is_dated_and_cannot_turn_an_unknown_gap_into_a_pause ... ok
test stock_pool_execution::tests::full_stop_overrides_a_simultaneous_factor_reduction_without_a_second_target ... ok
test stock_pool_execution::tests::independent_exit_quote_priority_does_not_bypass_t_plus_one_or_price_validation ... ok
test stock_pool_execution::tests::independent_full_exit_has_no_ordinary_quote_dependency_but_partial_risk_does_not_fake_missing_facts ... ok
test stock_pool_execution::tests::independent_stop_does_not_require_unused_ordinary_sell_quote_facts ... ok
test stock_pool_execution::tests::index_cap_cannot_sell_a_paused_holding_or_use_its_budget_as_free_cash ... ok
test stock_pool_execution::tests::index_cap_keeps_non_sellable_quantity_and_defers_the_remaining_risk_target ... ok
test stock_pool_execution::tests::index_cap_reduces_existing_proportions_without_enabling_weight_rebalancing ... ok
test stock_pool_execution::tests::equal_thirty_seats_use_full_precision_at_a_board_lot_boundary ... ok
test stock_pool_execution::tests::index_cap_respects_ordinary_weight_changes_and_stronger_fill_protection ... ok
test stock_pool_execution::tests::instrument_price_tick_and_quantity_units_are_not_a_two_decimal_stock_assumption ... ok
test stock_pool_execution::tests::legacy_constraints_do_not_gain_an_empty_precision_field ... ok
test stock_pool_execution::tests::market_quantity_budget_includes_execution_slippage_and_fees ... ok
test stock_pool_execution::tests::index_exposure_and_reserved_seats_multiply_without_rounding_the_budget_early ... ok
test stock_pool_execution::tests::native_sell_and_quote_conditions_are_and_but_stop_and_protection_remain_independent ... ok
test stock_pool_execution::tests::optional_configuration_and_conflicting_aliases_are_explicit ... ok
test stock_pool_execution::tests::ordinary_sell_keeps_required_quote_failures_and_zero_stop_is_not_an_exit ... ok
test stock_pool_execution::tests::partial_sell_cooldown_restricts_increases_without_clearing_the_remainder ... ok
test stock_pool_execution::tests::ordinary_sell_scope_excludes_independent_exits_and_protected_positions ... ok
test stock_pool_execution::tests::paused_outside_holding_retains_prior_budget_without_an_execution_quote ... ok
test stock_pool_execution::tests::quote_condition_number_contract_rejects_invalid_facts_and_fractional_shares ... ok
test stock_pool_execution::tests::paused_held_weight_is_not_replenished_after_another_stock_exits ... ok
test stock_pool_execution::tests::quote_condition_scopes_are_evaluated_before_target_orders ... ok
test stock_pool_execution::tests::quote_only_exit_still_works_when_independent_risk_rules_are_configured ... ok
test session_events::tests::all_thirteen_events_return_native_boolean_series ... ok
test stock_pool_execution::tests::reserve_slots_keep_requested_and_effective_exposure_distinct ... ok
test stock_pool_execution::tests::retained_holdings_reserve_actual_value_and_zero_funding_still_clears ... ok
test stock_pool_execution::tests::risk_only_configuration_never_turns_into_an_unconditional_ordinary_exit ... ok
test stock_pool_execution::tests::sale_slippage_does_not_prevent_a_marked_value_board_lot_reduction ... ok
test stock_pool_execution::tests::sell_cooldown_does_not_turn_disabled_weight_rebalancing_back_on ... ok
test stock_pool_execution::tests::sell_quote_condition_creates_exit_and_unmet_holding_keeps_its_slot ... ok
test stock_pool_execution::tests::selection_excluding_paused_stock_keeps_the_same_24_stock_budget_as_broker_execution ... ok
test stock_pool_execution::tests::stop_reduction_merge_matrix_preserves_protection_t_plus_one_and_invalid_config_errors ... ok
test stock_pool_index_policy::tests::disabled_is_optional_but_enabled_parameters_are_not_invented ... ok
test stock_pool_index_policy::tests::missing_or_duplicate_or_future_rows_never_shorten_the_window ... ok
test stock_pool_index_policy::tests::trend_and_caps_match_the_declared_math_and_keep_zero_meaningful ... ok
test stock_pool_indicators::tests::kdj_uses_the_same_frozen_native_kernel_as_event_conditions ... ok
test stock_pool_indicators::tests::missing_inputs_require_source_admission_and_future_inputs_fail ... ok
test stock_pool_indicators::tests::uses_shared_ma_ema_and_preserves_recursive_prefix_before_output_window ... ok
test stock_pool_market_cap::tests::interpolates_declared_endpoints_and_clamps_without_business_defaults ... ok
test stock_pool_market_cap::tests::missing_duplicate_or_mismatched_index_inputs_do_not_shrink_the_calendar ... ok
test stock_pool_quote_facts::tests::lunch_and_post_close_gaps_follow_the_minute_feed_segments ... ok
test stock_pool_quote_facts::tests::real_source_session_prefix_matches_observed_checkpoints ... ignored, requires FIDC_SESSION_PREFIX_SOURCE_JSON from the frozen Source minute response
test stock_pool_quote_facts::tests::sparse_unverified_and_overflowing_quotes_cannot_be_called_session_totals ... ok
test stock_pool_quote_facts::tests::totals_use_only_the_complete_observed_prefix_and_keep_decimal_amounts ... ok
test strategy::decision_merge_tests::explicit_empty_complete_target_replaces_old_allocation_but_empty_callback_does_not ... ok
test strategy::decision_merge_tests::newer_complete_target_replaces_old_symbols_without_discarding_explicit_actions ... ok
test strategy::tests::omni_microcap_projection_uses_configured_trading_cost ... ok
test strategy::tests::omni_microcap_selection_uses_configured_risk_policy ... ok
test strategy::tests::order_time_in_force_parsing_and_order_type_contract_are_explicit ... ok
test strategy_ai::tests::generation_prompt_uses_explicit_performance_acceptance_contract ... ok
test strategy_ai::tests::manual_separates_explicit_business_selection_from_framework_risk_policy ... ok
test strategy_ai::tests::optimization_prompt_uses_explicit_performance_acceptance_contract ... ok
test universe::tests::selector_applies_configured_selection_risk_on_decision_date ... ok
test universe::tests::selector_can_defer_configured_selection_risk_without_losing_diagnostics ... ok
test universe::tests::selector_records_structured_selection_risk_decisions ... ok
test stock_pool_execution::tests::typed_exit_roles_merge_only_satisfied_ordinary_actions_with_independent_risk ... ok
test risk_control::tests::selection_check_activation_covers_every_configured_flag_and_blacklist_state ... ok
test stock_pool_execution::tests::quote_field_operator_side_and_scope_matrix_matches_the_configured_predicate ... ok
failures:
---- corporate_book::tests::unresolved_delisting_cannot_zero_a_pending_share_asset stdout ----
thread 'corporate_book::tests::unresolved_delisting_cannot_zero_a_pending_share_asset' (94174) panicked at crates/fidc-core/src/corporate_book.rs:814:94:
called `Result::unwrap()` on an `Err` value: MultipleBenchmarks
failures:
corporate_book::tests::unresolved_delisting_cannot_zero_a_pending_share_asset
test result: FAILED. 781 passed; 1 failed; 7 ignored; 0 measured; 0 filtered out; finished in 0.53s
error: test failed, to rerun pass `-p fidc-core --lib`

Some files were not shown because too many files have changed in this diff Show More