Compare commits

..

336 Commits

Author SHA1 Message Date
boris ad063264cf fix: borrow factor identifiers during lookup 2026-09-09 09:36:10 +08:00
boris 0108c91bae perf: preserve borrowed factor keys in stock state 2026-09-09 09:35:25 +08:00
boris ee2865829d Merge remote-tracking branch 'origin/main' 2026-09-09 09:31:46 +08:00
boris e66460c4e9 对齐次日交易信号基线与基准收益起点 2026-09-09 07:43:29 +08:00
boris 2811886a52 区分次日回放调度标签与实际日线可见时点 2026-09-09 07:21:13 +08:00
boris 3b5a7cd318 修复恒定小数价格累加误差产生虚假均线信号 2026-09-09 07:03:55 +08:00
boris 3fe2da3ee0 统一日线形态计算与次日分阶段信号 2026-09-09 06:41:15 +08:00
boris ee77028907 docs: record missing-value semantics and real replay gates 2026-09-09 06:07:01 +08:00
boris 1bcaa0b3d8 合并主线数值校验与买入阶段约束 2026-09-09 05:49:45 +08:00
boris 1703a7aa5e 保留已有行情和策略代码格式整理 2026-09-09 05:49:44 +08:00
boris e3f1028667 fix: reject missing numeric sizing and execution parameters 2026-09-09 04:34:09 +08:00
boris ea58ab2166 fix: enforce numeric guards in Rhai operator dispatch 2026-09-09 04:02:11 +08:00
boris 3b1aa2ebcb test: identify the dynamic missing-value comparison path 2026-09-09 03:58:49 +08:00
boris fda2e70456 fix: preserve unknown numeric conditions through boolean expressions 2026-09-09 03:56:44 +08:00
boris 3cea91467d test: provide dated amount facts in next-open selection fixture 2026-09-09 02:58:33 +08:00
boris dd6b37be16 fix: preserve missing numeric factors and reject nonfinite truthiness 2026-09-09 02:51:26 +08:00
boris 078839b0f3 fix: preserve authoritative STAR market classification in risk checks 2026-09-08 22:42:33 +08:00
boris 326438aac9 fix: evaluate buy quote conditions at the active schedule clock 2026-09-08 01:45:39 +08:00
boris 30da6eaead feat: evaluate trading buy filters into decision-scoped constraints 2026-09-08 01:27:42 +08:00
boris 3784246e6b docs: specify buy-constrained amendment behavior and validation scope 2026-09-08 01:09:14 +08:00
boris fa6f189cdd test: verify amendment rejection without order-state events 2026-09-08 01:04:51 +08:00
boris daa9d8d341 fix: apply decision buy denials to exposure-increasing amendments 2026-09-08 01:00:52 +08:00
boris c85afb59ab docs: define factor decision phase integration and acceptance gates 2026-09-08 00:53:48 +08:00
boris 7d293f092e test: cover next-open side flips and trim redundant default fields 2026-09-08 00:43:54 +08:00
boris da27204a71 test: assert broker fill events for scoped buy denials 2026-09-08 00:41:11 +08:00
boris bac721e593 feat: add decision-scoped buy denials to broker submission 2026-09-08 00:40:10 +08:00
boris bc666c6433 docs: record paper and live deployment of native factor timing fix 2026-09-07 22:30:14 +08:00
boris 4a71992752 docs: record intraday native factor and five-year next-open replays 2026-09-07 22:14:59 +08:00
boris 1b78186c4e docs: record scoped native daily factor visibility tests 2026-09-07 21:56:12 +08:00
boris cb97aa193d test: verify native daily values respect intraday availability 2026-09-07 21:54:26 +08:00
boris a02ac6e853 fix: gate bound daily indicator fields by completed session 2026-09-07 21:51:51 +08:00
boris f3cc790659 fix(data): reject normalized reserved adjustment keys 2026-09-07 19:38:28 +08:00
boris 5ffbf76565 docs(data): record typed adjustment snapshot acceptance 2026-09-07 18:13:31 +08:00
boris 04b45adf98 perf(data): type adjustment factor snapshots 2026-09-07 17:53:36 +08:00
boris 9714c051c5 精确预分配按股行情分组 2026-09-07 13:57:12 +08:00
boris fbf5a6d61a 按符号索引一次构建行情序列 2026-09-07 13:46:59 +08:00
boris 94632f42d6 拆分共享日线序列与日内字段 2026-09-07 13:34:38 +08:00
boris 3f6017d98b 保持日内覆盖与按股序列一致 2026-09-07 13:25:07 +08:00
boris d5af51c02b 支持复用只读日线基础面板 2026-09-07 12:54:49 +08:00
boris 1ec0bb65f7 记录行情计划冷路径验收 2026-09-07 12:34:50 +08:00
boris c934a948c6 流式构建总市值行情计划 2026-09-07 12:17:26 +08:00
boris 950bcaa7aa 记录通用行情覆盖层否决结论 2026-09-07 11:34:25 +08:00
boris 728ed7998d Revert "以运行覆盖层隔离补充行情"
This reverts commit 757b5665ca.
2026-09-07 11:28:33 +08:00
boris 757b5665ca 以运行覆盖层隔离补充行情 2026-09-07 11:22:20 +08:00
boris c280bbc1c3 记录分层行情索引否决结论 2026-09-07 11:15:38 +08:00
boris 68c186f649 Revert "分层共享执行行情索引"
This reverts commit f2de8b281a.
2026-09-07 11:09:02 +08:00
boris f2de8b281a 分层共享执行行情索引 2026-09-07 11:03:08 +08:00
boris 19f725dbaa 记录市值区间切片性能验收 2026-09-07 10:59:32 +08:00
boris df7a1ee382 按共享市值索引裁剪候选区间 2026-09-07 10:50:20 +08:00
boris 4fe1f0d77c 记录共享行情释放性能验收 2026-09-07 10:38:29 +08:00
boris f45b3a71fa 避免共享行情释放触发整图复制 2026-09-07 10:27:39 +08:00
boris 1aa7c28616 复用预计算行情证券范围 2026-09-07 09:41:10 +08:00
boris e542e52bdb 跳过无加载器的行情规划 2026-09-07 09:22:15 +08:00
boris 0afbdc2210 由执行风控处理无行情买单 2026-09-07 09:04:07 +08:00
boris fc6dea12eb 增加类型化静态股票池合同 2026-09-07 07:27:44 +08:00
boris c3f88ebf12 对齐持仓未实现盈亏口径 2026-09-07 06:26:34 +08:00
boris 1d1c93f8e2 补充持仓移动平均成交价 2026-09-07 05:53:30 +08:00
boris 929b105173 支持按持仓成交均价止盈止损 2026-09-07 05:36:23 +08:00
boris e00777ebc2 按完整目标集合约束持仓槽位 2026-09-07 05:18:57 +08:00
boris e4f6cdd025 对齐目标组合提交前过滤 2026-09-07 05:10:00 +08:00
boris 2a705a361a 说明目标组合退出语义 2026-09-07 05:04:34 +08:00
boris b4ec5da808 区分股票池退出后重新入场 2026-09-07 04:51:27 +08:00
boris 4d8761cc3c 阻止未完成退出反向补买 2026-09-07 04:44:12 +08:00
boris fdd0dd0525 修复目标生命周期退出后复活 2026-09-07 04:39:16 +08:00
boris df29c8d3ec 区分退出后权重重分配语义 2026-09-07 04:33:40 +08:00
boris ef24402747 保留策略目标资金比例 2026-09-07 04:19:15 +08:00
boris 78e872b609 修复盘中止盈止损行情缓存 2026-09-07 04:17:37 +08:00
boris 27e523a1dc 仅在成功清仓后释放目标权重 2026-09-07 04:01:25 +08:00
boris f9d9f06d3f 统一每日目标组合执行路径 2026-09-07 04:01:25 +08:00
boris d45f39f1bf 统一止盈退出后的目标权重重分配 2026-09-07 04:01:25 +08:00
boris 5fa3d3bf54 Revert "统一止盈退出后的目标权重重分配"
This reverts commit 3f14d9de54.
2026-09-07 03:29:34 +08:00
boris 7bc832f3c6 Revert "统一每日目标组合执行路径"
This reverts commit 0542a44afe.
2026-09-07 03:29:34 +08:00
boris 46c647d676 Merge remote-tracking branch 'origin/main' 2026-09-07 03:29:34 +08:00
boris 24b961ba61 Reapply "统一止盈退出后的目标权重重分配"
This reverts commit d2cf021194.
2026-09-07 03:29:34 +08:00
boris d2cf021194 Revert "统一止盈退出后的目标权重重分配"
This reverts commit 3f14d9de54.
2026-09-07 03:29:08 +08:00
boris 0542a44afe 统一每日目标组合执行路径 2026-09-07 03:27:50 +08:00
boris 3f14d9de54 统一止盈退出后的目标权重重分配 2026-09-07 03:15:05 +08:00
boris b8e0d3bf4c 修复盘中收盘撮合盘口限制覆盖 2026-09-07 02:28:59 +08:00
boris f1a6a2695d 修复日线盘中调度按分钟行情撮合 2026-09-07 01:29:18 +08:00
boris 54ccfe7e0a fix: retain scheduled decision diagnostics 2026-09-07 01:18:58 +08:00
boris ce041e0d16 修复定时轮动提前在日线阶段执行 2026-09-06 23:18:58 +08:00
boris 81f6b7d1a5 修复显式股票权重被截断 2026-09-06 22:39:01 +08:00
boris dd376e4b32 Revert "perf: build calendar series boundaries in one pass"
This reverts commit f927ef8c0f.
2026-09-06 22:24:18 +08:00
boris f927ef8c0f perf: build calendar series boundaries in one pass 2026-09-06 22:18:54 +08:00
boris 0d831c4ca6 Revert "perf: build price series by symbol id"
This reverts commit 96d0cc6fc4.
2026-09-06 21:08:15 +08:00
boris 96d0cc6fc4 perf: build price series by symbol id 2026-09-06 20:55:18 +08:00
boris c7d213bf35 Revert "perf: skip unused explicit-action stock state"
This reverts commit b5934085da.
2026-09-06 20:02:33 +08:00
boris b5934085da perf: skip unused explicit-action stock state 2026-09-06 19:58:01 +08:00
boris 3657d83833 test: cover signal-date target persistence 2026-09-06 19:35:27 +08:00
boris 94a1422a35 fix: honor signal dates for explicit actions 2026-09-06 19:32:57 +08:00
boris 5bc9753766 fix: align risk-free dates with engine schedule 2026-09-06 18:45:11 +08:00
boris a838732e5a fix: reject missing daily execution prices per order 2026-09-06 18:26:55 +08:00
boris 4b88defdab docs: record date numeric VM acceptance 2026-09-06 18:07:34 +08:00
boris bc228980af perf: compile date comparisons into numeric VM 2026-09-06 17:58:57 +08:00
boris e0b4a3f56c docs: record expression scope performance evidence 2026-09-06 17:45:42 +08:00
boris 840473362d perf: reuse expression scope identifiers and dates 2026-09-06 17:37:12 +08:00
boris 630a7a16c9 fix: value unavailable execution days without fills 2026-09-06 14:36:35 +08:00
boris f5de3a2c29 Skip market buys below one lot 2026-09-06 13:35:52 +08:00
boris c5767ca272 revert: reject neutral symbol-id series storage 2026-09-06 06:29:51 +08:00
boris a7f96c030f docs: record symbol-id series acceptance 2026-09-06 06:26:45 +08:00
boris 5a7c49a454 perf: build market series by symbol id 2026-09-06 06:18:21 +08:00
boris cda249e9b4 docs: map open-source engine designs to FIDC 2026-09-06 05:39:14 +08:00
boris 52c7831bf6 revert: reject marginal numeric VM slot reuse 2026-09-06 05:37:20 +08:00
boris 5122c73aa8 perf: reuse numeric VM slots by generation 2026-09-06 05:31:23 +08:00
boris 5f08978827 Revert "perf: bind numeric VM identifiers at compile time"
This reverts commit 2135a5bd03.
2026-09-06 05:24:11 +08:00
boris 2135a5bd03 perf: bind numeric VM identifiers at compile time 2026-09-06 05:16:48 +08:00
boris 199f988b2e feat: add dated candidate universe contracts 2026-09-06 03:57:33 +08:00
boris 0f1d49bf63 feat: execute factor position target rules 2026-09-06 02:45:39 +08:00
boris c7f5188354 docs: correct performance evidence medians 2026-09-05 13:54:36 +08:00
boris 6a1c60b2e2 docs: record signal rolling rejection 2026-09-05 13:35:00 +08:00
boris 2aa330786a revert: reject allocation-free signal rolling scan 2026-09-05 13:26:23 +08:00
boris b65b3ed8f1 perf: scan signal rolling aggregates without allocations 2026-09-05 13:19:04 +08:00
boris f9ec86436a docs: record selection benchmark decisions 2026-09-05 13:09:44 +08:00
boris 42999ffa2c revert: reject selection band precheck 2026-09-05 13:01:58 +08:00
boris e060af380e perf: reject selection bands before state construction 2026-09-05 12:55:11 +08:00
boris b55ac0bf81 revert: reject rolling boundary reuse 2026-09-05 12:46:42 +08:00
boris 6b5d57675e perf: reuse rolling window boundaries 2026-09-05 12:40:40 +08:00
boris c225d8484f perf: freeze rank expression presence 2026-09-05 06:26:49 +08:00
boris 15c8f1f403 docs: record symbol id rank benchmark 2026-09-05 06:25:27 +08:00
boris 52f9ee9d92 perf: use lexical symbol ids for rank ties 2026-09-05 06:14:49 +08:00
boris d7e11be01f perf: rank transient candidates by symbol id 2026-09-05 06:07:23 +08:00
boris 866fe32a8c test: record transient selection arena acceptance 2026-09-05 06:00:23 +08:00
boris d89dd24f0a perf: isolate ordered selection hot path 2026-09-05 05:47:11 +08:00
boris 6a304e2fc2 Revert "revert: benchmark generic transient selection"
This reverts commit b2da70897a.
2026-09-05 05:46:05 +08:00
boris b2da70897a revert: benchmark generic transient selection 2026-09-05 05:39:57 +08:00
boris 12ad2b163a perf: isolate generic selection ranking 2026-09-05 05:34:24 +08:00
boris 1e8e738eda perf: sort transient states by arena index 2026-09-05 05:26:24 +08:00
boris 7f7fce1fc3 perf: keep ranked candidate states transient 2026-09-05 05:19:33 +08:00
boris a2d9e910ff docs: record transient selection state benchmark 2026-09-05 05:08:02 +08:00
boris 29faf7932e perf: avoid caching transient selection states 2026-09-05 04:59:50 +08:00
boris 0af4cd7f68 docs: record symbol id selection benchmark 2026-09-05 04:54:45 +08:00
boris 0e3c2028d0 perf: stream selection candidates by symbol id 2026-09-05 04:44:53 +08:00
boris c2e9c11a9a docs: record rejected vm scratch generations 2026-09-05 04:35:37 +08:00
boris 95503d418c docs: record rejected adjusted series compaction 2026-09-05 04:29:53 +08:00
boris 0c2681e699 Revert "perf: compact adjusted close series values"
This reverts commit ab87e18ba5.
2026-09-05 04:24:43 +08:00
boris ab87e18ba5 perf: compact adjusted close series values 2026-09-05 04:19:58 +08:00
boris 33808d9ba9 docs: record current minute-mode regression 2026-09-05 04:15:58 +08:00
boris 1f8a0fdc44 docs: record rejected lazy expression scope 2026-09-05 04:06:46 +08:00
boris 229ca8332d Revert "perf: build expression scope values lazily"
This reverts commit 1b449287fd.
2026-09-05 04:01:40 +08:00
boris 1b449287fd perf: build expression scope values lazily 2026-09-05 03:56:51 +08:00
boris 05085b961b docs: record noalloc instrument rule validation 2026-09-05 03:50:23 +08:00
boris cfb19b5783 perf: avoid board normalization allocations 2026-09-05 03:42:21 +08:00
boris 6f1e40754d docs: record specialized snapshot source validation 2026-09-05 03:40:35 +08:00
boris d79678d850 perf: specialize stock snapshot sources 2026-09-05 03:32:38 +08:00
boris 224adf70d4 docs: record rejected symbol board cache 2026-09-05 03:28:06 +08:00
boris f2105399c5 Revert "perf: cache symbol board classification"
This reverts commit eb8b14602a.
2026-09-05 03:19:31 +08:00
boris eb8b14602a perf: cache symbol board classification 2026-09-05 03:09:15 +08:00
boris df52f90d46 docs: record rejected rolling lookback freeze 2026-09-05 03:05:29 +08:00
boris 05e67c73df Revert "perf: freeze standard rolling lookbacks"
This reverts commit 32e4030442.
2026-09-05 03:01:18 +08:00
boris 32e4030442 perf: freeze standard rolling lookbacks 2026-09-05 02:54:53 +08:00
boris be2f624e3c docs: record typed current rolling validation 2026-09-05 02:52:24 +08:00
boris 75ab0c06c6 perf: type static current rolling helpers 2026-09-05 02:42:47 +08:00
boris 1410aa588a docs: record rejected current rolling batch 2026-09-05 02:38:32 +08:00
boris 43b15b2098 Revert "perf: batch current rolling means per stock"
This reverts commit 004a46cb41.
2026-09-05 02:33:34 +08:00
boris 004a46cb41 perf: batch current rolling means per stock 2026-09-05 02:29:06 +08:00
boris 76b1d6c38b docs: record calendar-major boundary validation 2026-09-05 02:24:47 +08:00
boris abe4fed452 perf: transpose rolling boundary index by date 2026-09-05 02:17:24 +08:00
boris a35137ed1c docs: record stock snapshot field projection 2026-09-05 02:13:00 +08:00
boris 7f17fa1fb4 perf: project stock snapshot fields by strategy 2026-09-05 02:02:01 +08:00
boris 5f7321da58 docs: record interned stock symbol validation 2026-09-05 01:52:49 +08:00
boris e5646ef80c perf: intern stock-state symbols 2026-09-05 01:41:00 +08:00
boris 71b3517003 docs: record compact stock-state cache key validation 2026-09-05 01:20:20 +08:00
boris 6f81e1940a perf: compact daily stock-state cache keys 2026-09-05 01:11:34 +08:00
boris 98a74f7bb4 docs: record rejected duplicate rolling optimization 2026-09-05 01:06:30 +08:00
boris 5b2a03d416 revert: remove unproven duplicate rolling reuse 2026-09-05 01:00:33 +08:00
boris e469b0ddf4 test: use valid adjusted series in rolling benchmark 2026-09-05 00:54:10 +08:00
boris 1f02e78b24 perf: reuse duplicate rolling lookbacks 2026-09-05 00:48:12 +08:00
boris a235f46b6d docs: record shared market-cap order benchmark 2026-09-05 00:46:00 +08:00
boris 05953f857a perf: share immutable market-cap order index 2026-09-05 00:37:39 +08:00
boris 6538742dfa docs: record rejected daily snapshot optimization 2026-09-05 00:30:42 +08:00
boris 1f10a6bb3d Revert "perf: reuse daily snapshot views in stock selection"
This reverts commit 1df0081479.
2026-09-05 00:24:05 +08:00
boris 1df0081479 perf: reuse daily snapshot views in stock selection 2026-09-05 00:14:57 +08:00
boris db155e7ad0 docs: record stock-state calendar index benchmark 2026-09-05 00:03:53 +08:00
boris 6d458dbbc6 perf: reuse stock-state calendar index 2026-09-04 23:49:33 +08:00
boris f7708331d3 docs: add second strategy rolling regression 2026-09-04 23:22:05 +08:00
boris 873cdb9d31 docs: record current rolling boundary benchmark 2026-09-04 23:19:15 +08:00
boris 52b07be19b perf: reuse current rolling series boundary 2026-09-04 23:06:52 +08:00
boris 914820cc07 perf: skip unused standard rolling lookup 2026-09-04 22:55:03 +08:00
boris 47d1520d72 docs: record instrument symbol-id benchmark 2026-09-04 22:53:11 +08:00
boris 8ed22897ea perf: index instruments by symbol id 2026-09-04 22:46:14 +08:00
boris 3963648f1a perf: retain faster stable universe sort 2026-09-04 22:31:30 +08:00
boris 2c711871f5 perf: avoid stable universe sort allocation 2026-09-04 22:20:26 +08:00
boris 836f56af41 style: format merged metrics exports 2026-09-04 22:19:49 +08:00
boris e27375a204 Merge remote-tracking branch 'origin/main' 2026-09-04 22:18:41 +08:00
boris 92322349d4 style: normalize fidc core formatting 2026-09-04 22:18:35 +08:00
boris 1a79dc483c 补齐权威下行风险指标 2026-09-03 15:02:44 +08:00
boris a3a077fa87 统一每日PIT无风险收益指标 2026-09-03 14:04:51 +08:00
boris b15b93eec1 修复目标权重映射预校验 2026-09-02 19:05:28 +08:00
boris 1215a04b7d 支持日期化仓位调整回放 2026-09-02 18:15:07 +08:00
boris d014bb2fbd fix: fail closed on missing factor values 2026-08-31 14:30:33 +08:00
boris dff791b51f perf: index factor lookups by symbol 2026-08-31 14:27:30 +08:00
boris ce46e42ff7 Merge remote-tracking branch 'origin/main' 2026-08-31 09:44:52 +08:00
boris cf1b60c996 fix: accept scoped blacklist runtime context 2026-08-31 09:40:49 +08:00
boris d0ca09d4d8 test: normalize engine hook assertions 2026-08-31 08:51:06 +08:00
boris c1e66b31a5 预编译数值表达式助手参数 2026-08-31 06:52:44 +08:00
boris cb18a3f279 复用选股日快照视图 2026-08-31 05:03:20 +08:00
boris b634540047 固化定点金额与分钟流式验收 2026-08-31 02:44:20 +08:00
boris cd116bc3ae 减少选股状态热路径字符串分配 2026-08-30 19:02:40 +08:00
boris f839b16dbd Merge remote-tracking branch 'refs/remotes/bundle/main' 2026-08-29 15:14:11 +08:00
boris 70d72d5d02 Merge remote-tracking branch 'origin/main' 2026-08-29 14:52:08 +08:00
boris f3a37157fc 统一回测策略风控字段校验 2026-08-29 14:52:03 +08:00
boris ce5564408d 按策略引用投影额外因子字段 2026-08-29 08:16:58 +08:00
boris 41854fe5bd 按策略引用投影额外因子字段 2026-08-29 08:16:41 +08:00
boris d5265619f3 按有序市值流提前停止选股 2026-08-29 07:50:09 +08:00
boris e793a5fcc1 按有序市值流提前停止选股 2026-08-29 07:49:43 +08:00
boris 362d85773a 复用标准rolling的交易日边界索引 2026-08-29 06:15:28 +08:00
boris c55caaf79b 优化等价市值排序的选股路径 2026-08-29 06:01:02 +08:00
boris 257088d7d9 优化策略选股的索引查找路径 2026-08-29 05:44:06 +08:00
boris ff2844884d 修正退市候选卖出风控边界 2026-08-29 04:03:32 +08:00
boris 56a38accc8 为股票序列增加有界交易日位置索引 2026-08-28 17:02:35 +08:00
boris de1e65a642 Revert "复用当前时点标准rolling值"
This reverts commit 2d516cf1eb.
2026-08-28 16:54:01 +08:00
boris 2d516cf1eb 复用当前时点标准rolling值 2026-08-28 16:47:07 +08:00
boris e261d93ce5 跳过已排序快照的重复排序 2026-08-28 15:49:02 +08:00
boris c1e7fc91e4 跳过无需求的滚动计算 2026-08-28 15:15:18 +08:00
boris 56511f9d15 批量计算标准滚动均线 2026-08-28 14:53:28 +08:00
boris 8691076cef 加速按日股票快照查找 2026-08-28 14:39:12 +08:00
boris 2de84e88eb 优化按日快照数据集构造 2026-08-28 14:24:56 +08:00
boris 43184a7afe 记录多时点调度的实际时间 2026-08-28 12:54:15 +08:00
boris 4f647ef529 保留平台策略全部交易时点 2026-08-28 12:44:20 +08:00
boris 59be3b5dd5 修复多时点调度与触发价撮合 2026-08-28 11:44:31 +08:00
boris 5bbb093f47 Revert "perf(core): reuse aligned snapshots in universe scan"
This reverts commit 096115bd94.
2026-08-28 08:44:13 +08:00
boris 520409f50f Revert "perf(core): specialize market candidate snapshot lookup"
This reverts commit e677a73f95.
2026-08-28 08:44:13 +08:00
boris e677a73f95 perf(core): specialize market candidate snapshot lookup 2026-08-28 08:38:22 +08:00
boris 096115bd94 perf(core): reuse aligned snapshots in universe scan 2026-08-28 08:31:40 +08:00
boris cb2319cd22 Revert "perf(core): reuse rolling endpoints per stock state"
This reverts commit 732c3cfbf5.
2026-08-28 08:23:04 +08:00
boris 732c3cfbf5 perf(core): reuse rolling endpoints per stock state 2026-08-28 08:16:04 +08:00
boris 3d2ab17d72 perf(core): align market factor candidate lookups 2026-08-28 08:00:31 +08:00
boris 87c18574a8 Revert "perf(core): build sorted dataset components linearly"
This reverts commit c46dcf817b.
2026-08-28 07:20:20 +08:00
boris c46dcf817b perf(core): build sorted dataset components linearly 2026-08-28 07:11:19 +08:00
boris 3f67ee9134 严格按实际委托时间选择盘后撮合 2026-08-28 03:17:21 +08:00
boris 1a2e247c8d 合并分钟行情与策略定时事件时钟 2026-08-28 01:42:58 +08:00
boris 6c47c33cab 按实际委托时间选择盘后撮合阶段 2026-08-28 00:12:19 +08:00
boris a9511f9a4a 拆分调仓日期与执行时钟 2026-08-28 00:00:30 +08:00
boris 85c38b0756 移除策略级盘后撮合模式 2026-08-27 21:05:57 +08:00
boris dd08358f1c 限制盘后固定价格撮合生效日期 2026-08-27 19:20:37 +08:00
boris b6f4b05844 支持盘后固定价格撮合合同 2026-08-27 17:56:41 +08:00
boris c86a0e2339 让分钟行情按流式迭代器处理 2026-08-27 13:25:28 +08:00
boris ed126a3630 优化分钟历史窗口读取并移除滚动双口径 2026-08-27 13:14:41 +08:00
boris 45cafa5c96 Revert "恢复Source Lake滚动因子运行模式"
This reverts commit d0639558b3.
2026-08-27 13:13:50 +08:00
boris d0639558b3 恢复Source Lake滚动因子运行模式 2026-08-27 10:14:50 +08:00
boris 8dccf8414f Merge remote-tracking branch 'refs/remotes/177/latest'
# Conflicts:
#	crates/fidc-core/src/events.rs
2026-08-27 09:19:36 +08:00
boris d84fad721d Merge remote-tracking branch 'origin/main'
# Conflicts:
#	crates/fidc-core/src/events.rs
#	crates/fidc-core/src/lib.rs
2026-08-27 09:07:51 +08:00
boris ce4d17c293 规范化策略规格别名避免运行时重复字段 2026-08-27 09:06:37 +08:00
boris 97e9a83dd2 分离过程事件分发与结果保留 2026-08-27 09:05:15 +08:00
boris 9db2a9f79c 分离过程事件分发与结果保留 2026-08-27 09:04:48 +08:00
boris 801a27dace 让显式动作继承运行调度 2026-08-27 08:37:33 +08:00
boris 00ec7a6d55 让显式动作继承运行调度 2026-08-27 08:37:12 +08:00
boris 9b00a0777a 增加类型化开放订单改单能力 2026-08-27 08:08:39 +08:00
boris 5a765766e3 增加类型化开放订单改单能力 2026-08-27 08:07:07 +08:00
boris 6ee1835ca5 记录分钟成交精确时间 2026-08-27 02:47:19 +08:00
boris cdbd8a67de 记录分钟成交精确时间 2026-08-27 02:46:38 +08:00
boris 8d7bb60c30 共享回测分钟报价索引 2026-08-27 01:38:56 +08:00
boris 21cfa65af2 共享回测分钟报价索引 2026-08-27 01:37:18 +08:00
boris 01d1e5073d 修正跨调度撮合流动性重复消费 2026-08-27 00:57:34 +08:00
boris 5c300f8181 修正跨调度撮合流动性重复消费 2026-08-27 00:56:48 +08:00
boris 78c5b72ed3 完善统一策略规格元数据字段 2026-08-26 23:39:11 +08:00
boris 32b3122457 完善统一策略规格元数据字段 2026-08-26 23:37:42 +08:00
boris 50690540cd 导出统一基准调仓规格类型 2026-08-26 23:29:18 +08:00
boris 71b4ffcecf 导出统一基准调仓规格类型 2026-08-26 23:27:32 +08:00
boris 7f66bcfff7 统一基准与调仓规格字段 2026-08-26 23:26:24 +08:00
boris 422e5f1021 统一基准与调仓规格字段 2026-08-26 23:24:20 +08:00
boris 68bff3f661 统一引擎执行规格字段 2026-08-26 23:21:16 +08:00
boris b92a09b5ed 统一引擎执行规格字段 2026-08-26 23:18:43 +08:00
boris e867aea3b1 统一执行参数规格字段 2026-08-26 23:08:09 +08:00
boris 32693dad30 统一执行参数规格字段 2026-08-26 23:06:20 +08:00
boris 723ce93623 导出统一风控策略规格类型 2026-08-26 23:02:04 +08:00
boris b05bd3fc1b 导出统一风控策略规格类型 2026-08-26 23:01:33 +08:00
boris 48acd66c30 修正风控别名测试断言 2026-08-26 22:59:52 +08:00
boris 861ed483b5 修正风控别名测试断言 2026-08-26 22:59:05 +08:00
boris 3926ac2985 统一风控策略执行别名 2026-08-26 22:56:50 +08:00
boris a72a4518d3 统一风控策略执行别名 2026-08-26 22:54:11 +08:00
boris 255fc2b878 限定订单有效期运行模式能力 2026-08-26 22:14:18 +08:00
boris dbaf7b45af 限定订单有效期运行模式能力 2026-08-26 22:11:16 +08:00
boris 8e238f9131 实现类型化订单有效期合同 2026-08-26 21:00:22 +08:00
boris 88f5a1a0ae 实现类型化订单有效期合同 2026-08-26 19:48:21 +08:00
boris bc0f5f6089 修正回测部分成交终态合同 2026-08-26 18:28:42 +08:00
boris 0793473210 修正回测部分成交终态合同 2026-08-26 18:28:19 +08:00
boris 8303a6477b 允许运行态追加结算交易日历 2026-08-26 14:28:20 +08:00
boris 935dd47e34 允许运行态追加结算交易日历 2026-08-26 14:27:00 +08:00
boris 8fcf34b3a9 保留延迟资金到账表达式 2026-08-26 13:59:59 +08:00
boris c18306aed9 保留延迟资金到账表达式 2026-08-26 13:59:18 +08:00
boris 9399a61b46 增加回测结束边界状态审计 2026-08-26 13:37:36 +08:00
boris 33370fb694 增加回测结束边界状态审计 2026-08-26 13:34:36 +08:00
boris 82604481b6 下推分钟报价订阅过滤 2026-08-26 07:58:55 +08:00
boris 283bf56e9f 下推分钟报价订阅过滤 2026-08-26 07:57:23 +08:00
Boris d3bacffd8b 减少每日诊断文本临时分配 2026-08-26 06:43:59 +08:00
boris 670686681d 减少每日诊断文本临时分配 2026-08-26 06:43:36 +08:00
Boris 5929fedf91 跳过零管理费无效集合复制 2026-08-26 06:39:13 +08:00
boris 8b246a63f0 跳过零管理费无效集合复制 2026-08-26 06:38:19 +08:00
Boris 0867655d85 减少文本因子读取临时分配 2026-08-26 05:08:34 +08:00
boris 782bc640ff 减少文本因子读取临时分配 2026-08-26 05:07:19 +08:00
boris 77622e164c 减少日频数据读取临时分配 2026-08-26 04:48:22 +08:00
boris 6604afd24f 减少日频数据读取临时分配 2026-08-26 04:46:08 +08:00
boris bf2e3af4eb 优化数值表达式helper执行路径 2026-08-26 03:27:00 +08:00
boris d071a8a190 优化数值表达式helper执行路径 2026-08-26 03:25:41 +08:00
boris afef38e45e 跳过干净因子映射重复规范化 2026-08-25 22:59:30 +08:00
boris ac30d86b6a 线性构建数据集价格序列 2026-08-25 21:55:18 +08:00
boris 01cffb947c 共享固定数值因子字段名 2026-08-25 20:25:05 +08:00
boris fac5078dbf 减少数据集按股票分组字符串分配 2026-08-25 19:20:23 +08:00
boris 68ebe76f24 约束期货策略生成的数据可用性 2026-08-25 17:22:45 +08:00
boris c284cc191e 增加通用期货策略动作并修正组合净值 2026-08-25 17:10:17 +08:00
boris 90da7f8a21 将期货现金账本切换为定点并修正日度盈亏 2026-08-25 16:38:45 +08:00
boris 2b94d5148f 将股票持仓盈亏切换为定点批次账本 2026-08-25 15:59:37 +08:00
boris 2574b9375d 按顺序结算多笔现金应收 2026-08-25 15:25:18 +08:00
boris e368bad7e4 移除净值读取定点重复转换 2026-08-25 15:08:33 +08:00
boris 5b6b3682dd 冻结交易费率定点配置 2026-08-25 14:38:51 +08:00
boris 92724c6ab0 将股票执行资金切换为定点账本 2026-08-25 14:36:15 +08:00
boris c9ddff46dd 支持任意交易阶段调度时间 2026-08-25 09:32:34 +08:00
boris 5ff8ddca92 共享选股状态并合并风控扫描 2026-08-25 08:53:34 +08:00
boris 85cfdca14c 移除引擎历史事件重复复制 2026-08-25 08:34:06 +08:00
boris 5482c8a52d 合并177回测引擎运行历史
# Conflicts:
#	crates/fidc-core/src/data.rs
2026-08-25 05:36:43 +08:00
boris 2a6bbb82a6 支持原生回测事实存储 2026-08-25 05:32:18 +08:00
boris 24e4ac9284 线性合并分钟行情窗口 2026-08-25 04:17:53 +08:00
boris 81d70f18b3 跳过无业务分钟回调 2026-08-25 04:02:33 +08:00
boris 85c9d03b99 校验分钟订阅行情覆盖 2026-08-25 03:07:56 +08:00
boris a147c495af 重构分钟线事件流与订阅加载 2026-08-25 01:41:50 +08:00
boris 4cf0224d2d 移除DataSet行级Arc分配 2026-08-24 21:53:00 +08:00
boris 7503dc8517 共享回测只读数据索引 2026-08-24 19:48:14 +08:00
boris 1c04318ecf 增加定点金额精度验收模型 2026-08-24 17:25:53 +08:00
boris 4b577517a9 增加数值表达式字节码虚拟机 2026-08-24 13:46:45 +08:00
boris c52478708f 用快速哈希优化回测内部索引 2026-08-24 12:09:09 +08:00
boris 1d7ac19886 移除回测稠密索引性能回归 2026-08-24 11:53:12 +08:00
boris 0686532be0 用稠密行索引和滚动游标加速回测 2026-08-24 11:46:55 +08:00
boris 911074ae95 优化日线候选和成交量窗口索引 2026-08-24 11:34:20 +08:00
boris 555f2ab9bd 按证券索引优化表达式数据访问 2026-08-24 11:21:39 +08:00
boris a79077af17 按表达式依赖裁剪策略前置声明 2026-08-24 10:05:46 +08:00
boris 61a4172bd4 统一策略表达式执行与默认配置 2026-08-24 09:28:33 +08:00
boris 589f94e5b2 增加逐日紧凑证券索引 2026-08-24 04:04:03 +08:00
boris 8254ebbb47 压缩类型化因子并减少运行分配 2026-08-24 03:55:03 +08:00
boris ea79fdae46 减少滚动窗口重复索引开销 2026-08-24 03:36:07 +08:00
boris 2013314e4f 区分指数与股票滚动复权口径 2026-08-24 03:19:16 +08:00
boris 869c14e2b0 改用真实行情验证滚动风控 2026-08-24 02:57:42 +08:00
boris cea079a770 统一复权滚动因子计算口径 2026-08-24 02:51:58 +08:00
boris 9a7e5c7903 前置校验策略表达式语法 2026-08-23 22:58:33 +08:00
boris 279d6a100f 统一成交量滚动有效样本口径 2026-08-23 13:10:46 +08:00
boris 7afb72dca8 统一成交量滚动有效样本口径 2026-08-23 13:09:18 +08:00
85 changed files with 27709 additions and 2802 deletions
Generated
+1
View File
@@ -146,6 +146,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
name = "fidc-core"
version = "0.1.0"
dependencies = [
"ahash",
"chrono",
"indexmap",
"rayon",
+1
View File
@@ -11,6 +11,7 @@ version = "0.1.0"
authors = ["OpenAI Codex"]
[workspace.dependencies]
ahash = "=0.8.12"
chrono = { version = "=0.4.44", features = ["serde"] }
indexmap = { version = "=2.11.4", features = ["serde"] }
reqwest = { version = "=0.12.24", default-features = false, features = ["json", "rustls-tls"] }
+17 -1
View File
@@ -2,6 +2,16 @@
面向中国 A 股和期货策略的 Rust 回测核心。仓库目标是提供平台自有的策略 DSL、执行模型、撮合模型和结果分析能力,最终由 `fidc-backtest-service` 对外提供策略运行服务。
## Runtime position exposure schedule
`strategy_spec.runtimeExpressions.risk.positionExposureSchedule` accepts dated
`effectiveDate` plus `targetExposureBps` points. The platform expression strategy
uses the latest point whose date is not later than the current execution date and
otherwise keeps the strategy's normal `exposureExpr`. This contract is intended for
audited runtime controls replayed by paper/live shadow reconciliation; it is not a
market-data signal and does not change selection, pricing, fees, or execution-day
risk checks.
## 当前能力
- 日频和分钟执行价策略生命周期与确定性回放。
@@ -75,7 +85,7 @@
- `selection.market_cap_band(...)` 动态市值带。
- `filter.stock_expr(...)` 任意指标、因子和组合选股。
- `ordering.rank_by(...)``ordering.rank_expr(...)` 排序。
- `allocation.buy_scale(...)` 动态买入资金比例
- `allocation.buy_scale(...)` 相对等权槽位的个股资金倍率;显式权重可以大于 `1.0`,组合总仓位仍由 `risk.index_exposure(...)` 和严格资金预算控制
- `risk.stop_loss(...)``risk.take_profit(...)` 多条件止盈止损。
- `order.*``cancel.*``update_universe(...)``subscribe(...)` 显式交易动作。
@@ -83,8 +93,14 @@
Source Lake 日线成交量保留原始可用性合同:源 `volume=null` 与真实 `volume=0` 含义不同。依赖成交量的 rolling 窗口只要包含源空值就返回缺失,不得把空值补成 0;停牌日明确提供的 0 成交量仍是合法观测。该合同随 runner 快照版本冻结,旧快照不能跨版本复用。
盘后固定价格不是策略类型,也不是 `matchingType`。自 2026-07-06 起,只有实际同日提交时间落在 15:00–15:30 的普通委托才由 broker 进入盘后固定价格执行阶段;15:00–15:04 的委托等待到 15:0515:05–15:30 按官方收盘价和真实盘后成交量撮合,不叠加滑点,未成交余量不跨日。窗口外委托继续沿用连续竞价、当前收盘或下一交易日开盘合同;`next_bar_open` 策略即使在 15:00 生成信号,也不得被改写为同日盘后委托。缺失盘后行情时必须明确不成交,禁止回退全天成交量或 15:00 前分钟行情。
分钟回放使用行情时间戳与策略定时事件的有序合并时钟。`OnDay``Bar``Minute` 阶段只要声明显式 `physical_time`,就必须在各自真实分钟进入同一时间轴,即使该分钟没有预加载行情也必须触发,并由执行层按需查询该时点或之前最新有效价格;同一时间戳只形成一个事件,scheduler 回调先于 `on_minute`。日线 `current_bar_close` 无显式时间时使用官方日收盘,有显式时间时使用该触发点的 `Last` 行情,禁止读取下一分钟;多个时间点分别执行,禁止压成最后一个时间、把早盘单改成盘后单或依赖已有 BAR 才触发。`next_bar_open` 的 T 日信号时钟继续留在粗粒度决策阶段,不能延迟到 T+1 的同名分钟。
`holdUntilExit=true``stopTakeReferencePriceMode=signal_day_post_adjusted_close` 组合表示持久模型组合语义:股票进入模型目标后即记录信号日和后复权参考价,不以买单是否成交为前提。涨停、停牌或其他执行风控导致买单未成交时,模型成员仍占用目标槽位、每天累计模型持有日并继续生成目标仓位;达到止盈、止损或最大模型持有期后才从模型组合移除。实际订单仍由成交日风控独立决定,不得用实际持仓集合覆盖模型目标集合。
`targetPortfolioDaily=true` 时,每只股票的默认目标金额固定为 `target_budget / selection_limit * buy_scale`,候选不足、缺行情或风控拒绝产生的剩余资金保留为现金,不得自动归一到满仓。止盈、止损或最大持有期触发后,标的从活动目标顺序移除;卖出未完成时继续占用仓位槽且不得反向补买,成功释放的槽位只能由同一决策时点已排序且通过策略条件的后续候选补充。只有显式设置 `redistributeTargetWeightsAfterExit=true` 才在可用目标间重新分配权重,只有显式设置 `reenterExitedTargets=true` 才保留退出标的供后续重新入场;两个开关互相独立,默认都为 `false`
## 内置微盘策略
`OmniMicroCapStrategy` 是平台内置的微盘轮动策略,用于 demo、性能验证和策略迁移基线:
+1
View File
@@ -6,6 +6,7 @@ license.workspace = true
authors.workspace = true
[dependencies]
ahash.workspace = true
chrono.workspace = true
indexmap.workspace = true
rayon.workspace = true
File diff suppressed because it is too large Load Diff
+153 -61
View File
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use chrono::NaiveDate;
use crate::events::OrderSide;
use crate::fixed_point::{FixedChinaAShareCostModel, FixedMoney, FixedTradingCost};
use crate::risk_control::TradingConstraintConfig;
#[derive(Debug, Clone, Copy)]
@@ -14,7 +15,20 @@ pub struct TradingCost {
impl TradingCost {
pub fn total(self) -> f64 {
self.commission + self.stamp_tax + self.transfer_fee
self.fixed_total().to_f64()
}
pub fn fixed_total(self) -> FixedMoney {
FixedMoney::checked_sum_f64([self.commission, self.stamp_tax, self.transfer_fee])
.expect("trading costs must be finite fixed-point money")
}
fn from_fixed(value: FixedTradingCost) -> Self {
Self {
commission: value.commission.to_f64(),
stamp_tax: value.stamp_tax.to_f64(),
transfer_fee: value.transfer_fee.to_f64(),
}
}
}
@@ -35,12 +49,7 @@ pub trait CostModel {
#[derive(Debug, Clone, Copy)]
pub struct ChinaAShareCostModel {
pub commission_rate: f64,
pub stamp_tax_rate_before_change: f64,
pub stamp_tax_rate_after_change: f64,
pub stamp_tax_change_date: NaiveDate,
pub minimum_commission: f64,
pub transfer_fee_rate: f64,
fixed: FixedChinaAShareCostModel,
}
impl Default for ChinaAShareCostModel {
@@ -52,42 +61,121 @@ impl Default for ChinaAShareCostModel {
impl ChinaAShareCostModel {
pub fn from_trading_constraints(config: TradingConstraintConfig) -> Self {
Self {
commission_rate: config.commission_rate,
stamp_tax_rate_before_change: config.stamp_tax_rate_before_change,
stamp_tax_rate_after_change: config.stamp_tax_rate_after_change,
stamp_tax_change_date: config.stamp_tax_change_date,
minimum_commission: config.minimum_commission,
transfer_fee_rate: config.transfer_fee_rate,
fixed: FixedChinaAShareCostModel {
commission_rate: Self::fixed_money(config.commission_rate, "commission rate"),
stamp_tax_rate_before_change: Self::fixed_money(
config.stamp_tax_rate_before_change,
"stamp tax rate before change",
),
stamp_tax_rate_after_change: Self::fixed_money(
config.stamp_tax_rate_after_change,
"stamp tax rate after change",
),
stamp_tax_change_date: config.stamp_tax_change_date,
minimum_commission: Self::fixed_money(
config.minimum_commission,
"minimum commission",
),
transfer_fee_rate: Self::fixed_money(config.transfer_fee_rate, "transfer fee rate"),
},
}
}
pub fn set_commission_rate(&mut self, value: f64) {
self.fixed.commission_rate = Self::fixed_money(value, "commission rate");
}
pub fn set_minimum_commission(&mut self, value: f64) {
self.fixed.minimum_commission = Self::fixed_money(value, "minimum commission");
}
pub fn set_transfer_fee_rate(&mut self, value: f64) {
self.fixed.transfer_fee_rate = Self::fixed_money(value, "transfer fee rate");
}
pub fn set_stamp_tax_rate_before_change(&mut self, value: f64) {
self.fixed.stamp_tax_rate_before_change =
Self::fixed_money(value, "stamp tax rate before change");
}
pub fn set_stamp_tax_rate_after_change(&mut self, value: f64) {
self.fixed.stamp_tax_rate_after_change =
Self::fixed_money(value, "stamp tax rate after change");
}
pub fn set_stamp_tax_change_date(&mut self, value: NaiveDate) {
self.fixed.stamp_tax_change_date = value;
}
pub fn commission_rate(&self) -> f64 {
self.fixed.commission_rate.to_f64()
}
pub fn minimum_commission(&self) -> f64 {
self.fixed.minimum_commission.to_f64()
}
pub fn transfer_fee_rate(&self) -> f64 {
self.fixed.transfer_fee_rate.to_f64()
}
pub fn stamp_tax_rate_before_change(&self) -> f64 {
self.fixed.stamp_tax_rate_before_change.to_f64()
}
pub fn stamp_tax_rate_after_change(&self) -> f64 {
self.fixed.stamp_tax_rate_after_change.to_f64()
}
pub fn stamp_tax_change_date(&self) -> NaiveDate {
self.fixed.stamp_tax_change_date
}
pub fn with_commission_rate(mut self, value: f64) -> Self {
self.set_commission_rate(value);
self
}
pub fn with_minimum_commission(mut self, value: f64) -> Self {
self.set_minimum_commission(value);
self
}
pub fn with_stamp_tax_rates(mut self, before: f64, after: f64) -> Self {
self.set_stamp_tax_rate_before_change(before);
self.set_stamp_tax_rate_after_change(after);
self
}
pub fn commission_for(&self, gross_amount: f64) -> f64 {
if gross_amount <= 0.0 {
return 0.0;
}
(gross_amount * self.commission_rate).max(self.minimum_commission)
self.fixed_model()
.commission_for(Self::fixed_money(gross_amount, "gross amount"))
.to_f64()
}
pub fn stamp_tax_rate_for(&self, date: NaiveDate) -> f64 {
if date < self.stamp_tax_change_date {
self.stamp_tax_rate_before_change
} else {
self.stamp_tax_rate_after_change
}
self.fixed.stamp_tax_rate_for(date).to_f64()
}
pub fn stamp_tax_for(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> f64 {
if gross_amount <= 0.0 || side == OrderSide::Buy {
return 0.0;
}
gross_amount * self.stamp_tax_rate_for(date)
self.fixed_model()
.stamp_tax_for(date, side, Self::fixed_money(gross_amount, "gross amount"))
.to_f64()
}
pub fn transfer_fee_for(&self, gross_amount: f64) -> f64 {
if gross_amount <= 0.0 {
return 0.0;
}
gross_amount * self.transfer_fee_rate
self.fixed_model()
.transfer_fee_for(Self::fixed_money(gross_amount, "gross amount"))
.to_f64()
}
pub fn commission_for_order_fill(
@@ -100,31 +188,29 @@ impl ChinaAShareCostModel {
return 0.0;
}
let raw_commission = gross_amount * self.commission_rate;
let Some(order_id) = order_id else {
return raw_commission.max(self.minimum_commission);
return self.commission_for(gross_amount);
};
let remaining_minimum = commission_state
.entry(order_id)
.or_insert(self.minimum_commission);
if raw_commission > *remaining_minimum {
let charged = if (*remaining_minimum - self.minimum_commission).abs() < 1e-12 {
raw_commission
} else {
raw_commission - *remaining_minimum
};
*remaining_minimum = 0.0;
charged
} else {
let charged = if (*remaining_minimum - self.minimum_commission).abs() < 1e-12 {
self.minimum_commission
} else {
0.0
};
*remaining_minimum -= raw_commission;
charged
}
.or_insert(self.fixed.minimum_commission.to_f64());
let mut fixed_remaining = Self::fixed_money(*remaining_minimum, "remaining commission");
let charged = self.fixed_model().commission_for_order_fill_remaining(
Self::fixed_money(gross_amount, "gross amount"),
&mut fixed_remaining,
);
*remaining_minimum = fixed_remaining.to_f64();
charged.to_f64()
}
fn fixed_money(value: f64, label: &str) -> FixedMoney {
FixedMoney::from_f64(value)
.unwrap_or_else(|| panic!("{label} is not representable as fixed-point money: {value}"))
}
fn fixed_model(&self) -> FixedChinaAShareCostModel {
self.fixed
}
}
@@ -138,15 +224,11 @@ impl CostModel for ChinaAShareCostModel {
};
}
let commission = self.commission_for(gross_amount);
let stamp_tax = self.stamp_tax_for(date, side, gross_amount);
let transfer_fee = self.transfer_fee_for(gross_amount);
TradingCost {
commission,
stamp_tax,
transfer_fee,
}
TradingCost::from_fixed(self.fixed_model().calculate(
date,
side,
Self::fixed_money(gross_amount, "gross amount"),
))
}
fn calculate_with_order_state(
@@ -165,15 +247,25 @@ impl CostModel for ChinaAShareCostModel {
};
}
let commission = self.commission_for_order_fill(gross_amount, order_id, commission_state);
let stamp_tax = self.stamp_tax_for(date, side, gross_amount);
let transfer_fee = self.transfer_fee_for(gross_amount);
TradingCost {
let fixed_model = self.fixed_model();
let fixed_gross = Self::fixed_money(gross_amount, "gross amount");
let commission = if let Some(order_id) = order_id {
let remaining = commission_state
.entry(order_id)
.or_insert(self.fixed.minimum_commission.to_f64());
let mut fixed_remaining = Self::fixed_money(*remaining, "remaining commission");
let commission =
fixed_model.commission_for_order_fill_remaining(fixed_gross, &mut fixed_remaining);
*remaining = fixed_remaining.to_f64();
commission
} else {
fixed_model.commission_for(fixed_gross)
};
TradingCost::from_fixed(FixedTradingCost {
commission,
stamp_tax,
transfer_fee,
}
stamp_tax: fixed_model.stamp_tax_for(date, side, fixed_gross),
transfer_fee: fixed_model.transfer_fee_for(fixed_gross),
})
}
}
@@ -182,13 +274,13 @@ mod tests {
use super::*;
#[test]
fn default_matches_configurable_trading_constraints() {
fn default_quantizes_fees_to_micro_yuan() {
let model = ChinaAShareCostModel::default();
let date = NaiveDate::from_ymd_opt(2025, 11, 11).expect("valid date");
assert!((model.commission_for(248_059.812) - 74.4179436).abs() < 1e-9);
assert!((model.commission_for(248_059.812) - 74.417944).abs() < 1e-12);
assert!(
(model.stamp_tax_for(date, OrderSide::Sell, 245_747.007) - 122.8735035).abs() < 1e-9
(model.stamp_tax_for(date, OrderSide::Sell, 245_747.007) - 122.873504).abs() < 1e-12
);
}
+625
View File
@@ -0,0 +1,625 @@
//! Completed-session OHLCV rules shared by research and strategy execution.
use crate::DataSet;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
pub const CONTRACT: &str = "fidc_daily_ohlcv_pattern_v1";
pub fn catalog() -> Value {
json!({"contract":CONTRACT,"templates":{
"strength":{"label":"趋势强势","parameters":{"momentum_window":[25,5,120],"fast_window":[20,2,60],"slow_window":[60,20,252]},"stages":["selection","buy"],"method":"收盘价>短均线>长均线,按区间动量排序;不是当日金叉。"},
"breakout":{"label":"前高突破","parameters":{"high_window":[60,5,252],"volume_window":[10,2,60],"volume_multiple":[1.3,1,10],"max_upper_shadow":[0.1,0,1]},"stages":["selection","buy"],"method":"收盘突破此前N日最高价,量达到此前M日均量倍数,上影比例受限;参考窗口不含当日。"},
"volume_spike":{"label":"放量上涨","parameters":{"volume_window":[5,2,60],"volume_multiple":[3.0,1,10]},"stages":["selection","buy"],"method":"当日上涨且量达到此前N日最大量的指定倍数;不等同价格创新高。"},
"shrink_breakout":{"label":"缩量突破","parameters":{"spike_lookback":[5,2,30],"volume_window":[5,2,60],"volume_multiple":[3.0,1,10],"shrink_ratio":[0.5,0.01,1]},"stages":["selection","buy"],"method":"此前观察窗有放量日,今日收盘超过该日最高价,成交量不超过其指定比例。"},
"ma_below":{"label":"均线下方","parameters":{"ma_window":[20,2,252]},"stages":["sell"],"method":"完整收盘价低于含当日的N日均线;独立卖出条件。"},
"volume_down":{"label":"放量下跌","parameters":{"volume_window":[5,2,60],"volume_multiple":[3.0,1,10]},"stages":["sell"],"method":"当日下跌且量达到此前N日最大量的指定倍数。"}
},"data_frequency":"1d","execution_policies":["next_session_open"]})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PatternSpec {
pub template: String,
#[serde(default)]
pub parameters: BTreeMap<String, Value>,
}
impl PatternSpec {
pub fn validate(mut self) -> Result<Self, String> {
let catalog = catalog();
let definition = catalog["templates"]
.get(&self.template)
.ok_or("未登记的量价模板")?;
let parameters = definition["parameters"].as_object().unwrap();
if self.parameters.keys().any(|k| !parameters.contains_key(k)) {
return Err("模板包含未知参数".into());
}
for (key, bounds) in parameters {
let value = self.parameters.get(key).unwrap_or(&bounds[0]);
let number = value
.as_f64()
.filter(|v| v.is_finite())
.ok_or_else(|| format!("{key}必须为有限数值"))?;
if number < bounds[1].as_f64().unwrap() || number > bounds[2].as_f64().unwrap() {
return Err(format!("{key}超出允许范围"));
}
if key.ends_with("window") || key == "spike_lookback" {
if number.fract() != 0.0 {
return Err(format!("{key}必须是整数"));
}
self.parameters.insert(key.clone(), json!(number as usize));
} else {
self.parameters.insert(key.clone(), json!(number));
}
}
if self.template == "strength" && self.n("fast_window") >= self.n("slow_window") {
return Err("短均线必须小于长均线".into());
}
Ok(self)
}
pub fn n(&self, key: &str) -> usize {
self.parameters[key].as_u64().unwrap() as usize
}
pub fn v(&self, key: &str) -> f64 {
self.parameters[key].as_f64().unwrap()
}
pub fn history_len(&self) -> usize {
match self.template.as_str() {
"strength" => self.n("slow_window").max(self.n("momentum_window") + 1),
"breakout" => self.n("high_window").max(self.n("volume_window")) + 1,
"volume_spike" | "volume_down" => self.n("volume_window") + 1,
"ma_below" => self.n("ma_window").max(2),
"shrink_breakout" => self.n("spike_lookback") + self.n("volume_window") + 1,
_ => unreachable!(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PatternBar {
pub date: NaiveDate,
pub open: Option<f64>,
pub high: Option<f64>,
pub low: Option<f64>,
pub close: Option<f64>,
pub volume: Option<f64>,
pub adjustment_factor_backward1: Option<f64>,
pub paused: Option<bool>,
#[serde(default)]
pub source_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PatternSeries {
pub symbol: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub listed_at: Option<NaiveDate>,
pub bars: Vec<PatternBar>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternResult {
pub symbol: String,
pub name: Option<String>,
pub matched: bool,
pub score: Option<f64>,
pub checks: Vec<Value>,
pub values: Value,
pub anchor: Value,
pub exclusion: Option<Value>,
}
fn number(v: Option<f64>, symbol: &str, day: NaiveDate, field: &str) -> Result<f64, String> {
v.filter(|v|v.is_finite()).ok_or_else(||format!("pattern_input_invalid: symbol={symbol}, date={day}, field={field}, reason=missing_or_nonfinite"))
}
fn check(checks: &mut Vec<Value>, label: &str, actual: f64, operator: &str, threshold: f64) {
let passed = match operator {
">" => actual > threshold,
"<" => actual < threshold,
">=" => actual >= threshold,
"<=" => actual <= threshold,
_ => false,
};
checks.push(json!({"label":label,"actual":actual,"operator":operator,"threshold":threshold,"passed":passed}));
}
fn mean(mut values: impl ExactSizeIterator<Item = f64>) -> Result<f64, String> {
let count = values.len();
let first = values.next().ok_or("pattern_mean_empty")?;
// Center before summation so an unchanged decimal price stays exactly unchanged.
let result = first
+ values
.map(|value| (value - first) / count as f64)
.sum::<f64>();
if !result.is_finite() {
return Err("pattern_mean_nonfinite".into());
}
Ok(result)
}
/// No calendar compression, fill-forward prices or numerical substitutes.
pub fn evaluate(
spec: &PatternSpec,
days: &[NaiveDate],
series: &PatternSeries,
) -> Result<PatternResult, String> {
if days.len() != spec.history_len() || days.windows(2).any(|w| w[0] >= w[1]) {
return Err("pattern_calendar_incomplete: 需要完整、唯一且递增的真实交易日窗口".into());
}
let by_day = series
.bars
.iter()
.map(|b| (b.date, b))
.collect::<BTreeMap<_, _>>();
if by_day.len() != series.bars.len() || series.bars.iter().any(|b| !days.contains(&b.date)) {
return Err(format!(
"pattern_input_invalid: symbol={}, reason=duplicate_or_out_of_scope",
series.symbol
));
}
let mut unavailable = Vec::new();
let mut prices = Vec::new();
for &day in days {
let Some(b) = by_day.get(&day) else {
if series.listed_at.is_some_and(|listed| day < listed) {
unavailable.push(
json!({"date":day,"reason":"before_listing","listed_at":series.listed_at}),
);
continue;
}
return Err(format!(
"pattern_input_invalid: symbol={}, date={day}, reason=missing_market_row",
series.symbol
));
};
let factor = if b.close.is_some_and(|c| c.is_finite() && c > 0.0) {
let factor = number(
b.adjustment_factor_backward1,
&series.symbol,
day,
"adjustment_factor_backward1",
)?;
if factor <= 0.0 {
return Err(format!(
"pattern_input_invalid: symbol={}, date={day}, field=adjustment_factor_backward1, reason=nonpositive",
series.symbol
));
}
factor
} else {
1.0
};
let paused = b.paused.ok_or_else(|| {
format!(
"pattern_input_invalid: symbol={}, date={day}, field=paused",
series.symbol
)
})?;
if paused {
unavailable.push(
json!({"date":day,"reason":"confirmed_suspension","source_path":b.source_path}),
);
continue;
}
if series.listed_at.is_some_and(|listed| day < listed) {
return Err(format!(
"pattern_input_invalid: symbol={}, date={day}, reason=price_before_listing",
series.symbol
));
}
let o = number(b.open, &series.symbol, day, "open")?;
let h = number(b.high, &series.symbol, day, "high")?;
let l = number(b.low, &series.symbol, day, "low")?;
let c = number(b.close, &series.symbol, day, "close")?;
let v = number(b.volume, &series.symbol, day, "volume")?;
if o <= 0.0 || l <= 0.0 || c <= 0.0 || h < o.max(c) || l > o.min(c) || v < 0.0 {
return Err(format!(
"pattern_input_invalid: symbol={}, date={day}, reason=invalid_ohlcv",
series.symbol
));
}
prices.push((o * factor, h * factor, l * factor, c * factor, v));
}
let mut result = PatternResult {
symbol: series.symbol.clone(),
name: series.name.clone(),
matched: false,
score: None,
checks: vec![],
values: json!({}),
anchor: Value::Null,
exclusion: None,
};
if !unavailable.is_empty() {
result.exclusion = Some(
json!({"reason":"proven_incomplete_window","signal_date":days.last(),"evidence":unavailable}),
);
return Ok(result);
}
let len = prices.len();
let (o, h, l, c, v) = prices[len - 1];
let change = c / prices[len - 2].3 - 1.0;
result.values = json!({"close":by_day[&days[len-1]].close,"daily_return":change});
result.anchor = json!({"date":days[len-1],"raw_close":by_day[&days[len-1]].close,"factor":by_day[&days[len-1]].adjustment_factor_backward1});
let mut score = None;
match spec.template.as_str() {
"strength" => {
let fast = mean(prices[len - spec.n("fast_window")..].iter().map(|b| b.3))?;
let slow = mean(prices[len - spec.n("slow_window")..].iter().map(|b| b.3))?;
let momentum = c / prices[len - 1 - spec.n("momentum_window")].3 - 1.0;
score = Some(momentum);
result.values["momentum"] = json!(momentum);
result.values["fast_ma"] = json!(fast);
result.values["slow_ma"] = json!(slow);
check(&mut result.checks, "收盘高于短均线", c, ">", fast);
check(&mut result.checks, "短均线高于长均线", fast, ">", slow);
}
"breakout" => {
let prior_high = prices[len - 1 - spec.n("high_window")..len - 1]
.iter()
.map(|b| b.1)
.fold(f64::NEG_INFINITY, f64::max);
let avg = mean(
prices[len - 1 - spec.n("volume_window")..len - 1]
.iter()
.map(|b| b.4),
)?;
if avg <= 0.0 {
return Err(format!(
"pattern_input_invalid: symbol={}, reason=zero_reference_volume",
series.symbol
));
}
let shadow = if h > l { (h - o.max(c)) / (h - l) } else { 0.0 };
score = Some(c / prior_high - 1.0);
result.values["volume_ratio"] = json!(v / avg);
result.values["upper_shadow"] = json!(shadow);
check(&mut result.checks, "收盘突破前高", c, ">", prior_high);
check(
&mut result.checks,
"均量倍数",
v / avg,
">=",
spec.v("volume_multiple"),
);
check(
&mut result.checks,
"上影比例",
shadow,
"<=",
spec.v("max_upper_shadow"),
);
}
"volume_spike" | "volume_down" => {
let high = prices[len - 1 - spec.n("volume_window")..len - 1]
.iter()
.map(|b| b.4)
.fold(0.0, f64::max);
if high <= 0.0 {
return Err(format!(
"pattern_input_invalid: symbol={}, reason=zero_reference_volume",
series.symbol
));
}
score = Some(v / high);
result.values["volume_ratio"] = json!(v / high);
check(
&mut result.checks,
"最大量倍数",
v / high,
">=",
spec.v("volume_multiple"),
);
check(
&mut result.checks,
if spec.template == "volume_spike" {
"当日上涨"
} else {
"当日下跌"
},
change,
if spec.template == "volume_spike" {
">"
} else {
"<"
},
0.0,
);
}
"ma_below" => {
let avg = mean(prices[len - spec.n("ma_window")..].iter().map(|b| b.3))?;
score = Some(avg / c - 1.0);
result.values["ma"] = json!(avg);
check(&mut result.checks, "收盘低于均线", c, "<", avg);
}
"shrink_breakout" => {
let mut spikes = Vec::new();
let mut eligible = Vec::new();
for i in len - 1 - spec.n("spike_lookback")..len - 1 {
let prior = prices[i - spec.n("volume_window")..i]
.iter()
.map(|b| b.4)
.fold(0.0, f64::max);
if prior <= 0.0 {
return Err(format!(
"pattern_input_invalid: symbol={}, date={}, reason=zero_reference_volume",
series.symbol, days[i]
));
}
if prices[i].4 >= prior * spec.v("volume_multiple") {
spikes.push(i);
if c > prices[i].1 && v <= prices[i].4 * spec.v("shrink_ratio") {
eligible.push(i);
}
}
}
check(
&mut result.checks,
"观察窗存在放量日",
spikes.len() as f64,
">",
0.0,
);
if let Some(&i) = eligible.last().or_else(|| spikes.last()) {
score = Some(c / prices[i].1 - 1.0);
result.values["spike_date"] = json!(days[i]);
result.values["volume_ratio"] = json!(v / prices[i].4);
check(
&mut result.checks,
"收盘突破放量日高点",
c,
">",
prices[i].1,
);
check(
&mut result.checks,
"缩量比例",
v / prices[i].4,
"<=",
spec.v("shrink_ratio"),
);
}
}
_ => unreachable!(),
}
if score.is_some_and(|v| !v.is_finite()) {
return Err("pattern_result_nonfinite".into());
}
result.score = score;
result.matched = result.checks.iter().all(|c| c["passed"] == true);
Ok(result)
}
pub fn evaluate_dataset(
spec: &PatternSpec,
data: &DataSet,
date: NaiveDate,
symbol: &str,
) -> Result<PatternResult, String> {
let days = data.calendar().trailing_days(date, spec.history_len());
let bars = days
.iter()
.filter_map(|&d| {
data.market(d, symbol).map(|b| PatternBar {
date: d,
open: Some(b.open),
high: Some(b.high),
low: Some(b.low),
close: Some(b.close),
volume: Some(b.volume as f64),
adjustment_factor_backward1: data
.factor(d, symbol)
.and_then(|f| f.adjustment_factor_backward1),
paused: Some(b.paused),
source_path: None,
})
})
.collect();
evaluate(
spec,
&days,
&PatternSeries {
symbol: symbol.into(),
name: None,
listed_at: data.instrument(symbol).and_then(|i| i.listed_at),
bars,
},
)
}
pub fn evaluate_batch(
spec: PatternSpec,
days: &[NaiveDate],
series: &[PatternSeries],
) -> Result<Value, String> {
let spec = spec.validate()?;
if series.is_empty()
|| series.len() > 200
|| series
.iter()
.map(|s| &s.symbol)
.collect::<BTreeSet<_>>()
.len()
!= series.len()
{
return Err("pattern_batch_invalid: 需要1至200只唯一证券".into());
}
let rows = series
.iter()
.map(|s| evaluate(&spec, days, s))
.collect::<Result<Vec<_>, _>>()?;
Ok(
json!({"contract":CONTRACT,"spec":spec,"required_history":spec.history_len(),"rows":rows,"read_only":true}),
)
}
pub fn expression_specs(expression: &str) -> Result<Vec<PatternSpec>, String> {
let mut specs = Vec::new();
for helper in ["pattern_signal", "pattern_score"] {
for (index, _) in expression.match_indices(helper) {
if index > 0
&& expression[..index]
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_')
{
continue;
}
let rest = expression[index + helper.len()..].trim_start();
let Some(rest) = rest.strip_prefix('(') else {
continue;
};
let rest = rest.trim_start();
let mut stream = serde_json::Deserializer::from_str(rest).into_iter::<String>();
let text = stream
.next()
.ok_or("missing pattern JSON")?
.map_err(|e| e.to_string())?;
if !rest[stream.byte_offset()..].trim_start().starts_with(')') {
return Err("pattern helper takes one JSON string".into());
}
let spec: PatternSpec = serde_json::from_str(&text).map_err(|e| e.to_string())?;
specs.push(spec.validate()?);
}
}
Ok(specs)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(template: &str) -> (PatternSpec, Vec<NaiveDate>, PatternSeries) {
let spec = PatternSpec {
template: template.into(),
parameters: BTreeMap::new(),
}
.validate()
.unwrap();
let days = (0..spec.history_len())
.map(|n| {
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap() + chrono::Duration::days(n as i64)
})
.collect::<Vec<_>>();
let bars = days
.iter()
.enumerate()
.map(|(n, &date)| {
let c = 10.0 + n as f64;
PatternBar {
date,
open: Some(c),
high: Some(c),
low: Some(c),
close: Some(c),
volume: Some(1000.0),
adjustment_factor_backward1: Some(1.0),
paused: Some(false),
source_path: Some("fixture.parquet".into()),
}
})
.collect();
(
spec,
days,
PatternSeries {
symbol: "000001.SZ".into(),
name: None,
listed_at: Some(NaiveDate::from_ymd_opt(1991, 4, 3).unwrap()),
bars,
},
)
}
#[test]
fn daily_patterns_all_templates_and_score_absence() {
for template in [
"strength",
"breakout",
"volume_spike",
"shrink_breakout",
"ma_below",
"volume_down",
] {
let (spec, days, series) = fixture(template);
let result = evaluate(&spec, &days, &series).unwrap();
assert_eq!(result.matched, template == "strength");
assert_eq!(result.score.is_none(), template == "shrink_breakout");
}
}
#[test]
fn daily_patterns_adjusts_all_prices_not_volume() {
let (spec, days, series) = fixture("strength");
let a = evaluate(&spec, &days, &series).unwrap();
let mut split = series.clone();
for b in &mut split.bars {
b.open = b.open.map(|p| p / 2.0);
b.high = b.high.map(|p| p / 2.0);
b.low = b.low.map(|p| p / 2.0);
b.close = b.close.map(|p| p / 2.0);
b.adjustment_factor_backward1 = Some(2.0);
}
let b = evaluate(&spec, &days, &split).unwrap();
assert_eq!(a.score, b.score);
assert_eq!(a.checks, b.checks);
}
#[test]
fn daily_patterns_flat_decimal_prices_do_not_create_a_sell_signal() {
let (mut spec, _, mut series) = fixture("strength");
spec.template = "ma_below".into();
spec.parameters = BTreeMap::from([("ma_window".into(), json!(60))]);
for bar in &mut series.bars {
bar.open = Some(10.1);
bar.high = Some(10.1);
bar.low = Some(10.1);
bar.close = Some(10.1);
}
let days = series.bars.iter().map(|bar| bar.date).collect::<Vec<_>>();
let result = evaluate(&spec, &days, &series).unwrap();
assert!(
!result.matched,
"unchanged decimal prices must not trigger a below-MA sell: {:?}",
result.checks
);
assert_eq!(result.values["ma"], 10.1);
}
#[test]
fn daily_patterns_no_missing_data_fallback() {
let (spec, days, mut series) = fixture("strength");
series.bars[0].adjustment_factor_backward1 = None;
assert!(
evaluate(&spec, &days, &series)
.unwrap_err()
.contains("adjustment_factor")
);
series.bars[0].paused = Some(true);
assert!(evaluate(&spec, &days, &series).is_err());
series.bars[0].adjustment_factor_backward1 = Some(1.0);
let excluded = evaluate(&spec, &days, &series).unwrap();
assert!(excluded.exclusion.is_some());
assert!(!excluded.matched);
series.bars.remove(0);
assert!(evaluate(&spec, &days, &series).is_err());
series.listed_at = Some(days[1]);
assert!(evaluate(&spec, &days, &series).unwrap().exclusion.is_some());
}
#[test]
fn daily_patterns_rejects_future_and_duplicate_bars() {
let (spec, days, mut series) = fixture("strength");
series.bars.push(series.bars[0].clone());
assert!(evaluate(&spec, &days, &series).is_err());
series.bars.last_mut().unwrap().date = *days.last().unwrap() + chrono::Duration::days(1);
assert!(evaluate(&spec, &days, &series).is_err());
}
#[test]
fn daily_patterns_helper_literal_preserves_parameters() {
let text =
serde_json::to_string(&json!({"template":"breakout","parameters":{"high_window":252}}))
.unwrap();
let expression = format!("pattern_signal({})", serde_json::to_string(&text).unwrap());
assert_eq!(expression_specs(&expression).unwrap()[0].history_len(), 253);
assert!(expression_specs("pattern_signal(\"{}\")").is_err());
}
}
+3645 -471
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9
View File
@@ -125,6 +125,15 @@ impl ProcessEventBus {
loader.install_enabled(self, enabled_names)
}
pub fn has_listeners_for(&self, kinds: &[ProcessEventKind]) -> bool {
!self.any_listeners.is_empty()
|| kinds.iter().any(|kind| {
self.listeners
.get(kind)
.is_some_and(|listeners| !listeners.is_empty())
})
}
pub fn publish(&mut self, event: &ProcessEvent) {
if let Some(listeners) = self.listeners.get_mut(&event.kind) {
for listener in listeners {
+275 -1
View File
@@ -1,4 +1,4 @@
use chrono::NaiveDate;
use chrono::{NaiveDate, NaiveDateTime};
use serde::{Deserialize, Serialize};
mod date_format {
@@ -50,6 +50,35 @@ mod optional_date_format {
}
}
mod optional_datetime_format {
use chrono::NaiveDateTime;
use serde::{self, Deserialize, Deserializer, Serializer};
const FORMAT: &str = "%Y-%m-%d %H:%M:%S%.f";
pub fn serialize<S>(datetime: &Option<NaiveDateTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match datetime {
Some(datetime) => serializer.serialize_some(&datetime.format(FORMAT).to_string()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<NaiveDateTime>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<String>::deserialize(deserializer)?;
value
.map(|text| {
NaiveDateTime::parse_from_str(&text, FORMAT).map_err(serde::de::Error::custom)
})
.transpose()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum OrderSide {
Buy,
@@ -72,6 +101,7 @@ pub enum OrderStatus {
PartiallyFilled,
Canceled,
Rejected,
Expired,
}
impl OrderStatus {
@@ -82,6 +112,7 @@ impl OrderStatus {
Self::PartiallyFilled => "partially_filled",
Self::Canceled => "canceled",
Self::Rejected => "rejected",
Self::Expired => "expired",
}
}
}
@@ -106,6 +137,50 @@ pub struct OrderEvent {
pub reason: String,
}
impl OrderEvent {
pub fn validate(&self) -> Result<(), String> {
if self.symbol.trim().is_empty() || self.requested_quantity == 0 {
return Err(format!(
"invalid order identity/quantity order_id={:?} symbol={} requested={}",
self.order_id, self.symbol, self.requested_quantity
));
}
if self.filled_quantity > self.requested_quantity {
return Err(format!(
"order overfill order_id={:?} requested={} filled={}",
self.order_id, self.requested_quantity, self.filled_quantity
));
}
let quantity_valid = match self.status {
OrderStatus::Pending => self.filled_quantity < self.requested_quantity,
OrderStatus::Filled => self.filled_quantity == self.requested_quantity,
OrderStatus::PartiallyFilled => {
self.filled_quantity > 0 && self.filled_quantity < self.requested_quantity
}
OrderStatus::Canceled => self.filled_quantity < self.requested_quantity,
OrderStatus::Rejected => self.filled_quantity == 0,
OrderStatus::Expired => self.filled_quantity < self.requested_quantity,
};
if !quantity_valid {
return Err(format!(
"order status/quantity mismatch order_id={:?} status={} requested={} filled={}",
self.order_id,
self.status.as_str(),
self.requested_quantity,
self.filled_quantity
));
}
if self.reason.trim().is_empty() {
return Err(format!(
"order reason is empty order_id={:?} status={}",
self.order_id,
self.status.as_str()
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FillEvent {
#[serde(with = "date_format")]
@@ -116,6 +191,18 @@ pub struct FillEvent {
pub order_created_date: Option<NaiveDate>,
#[serde(default, with = "optional_date_format")]
pub execution_date: Option<NaiveDate>,
#[serde(
default,
with = "optional_datetime_format",
skip_serializing_if = "Option::is_none"
)]
pub execution_start_timestamp: Option<NaiveDateTime>,
#[serde(
default,
with = "optional_datetime_format",
skip_serializing_if = "Option::is_none"
)]
pub execution_timestamp: Option<NaiveDateTime>,
#[serde(default)]
pub order_id: Option<u64>,
pub symbol: String,
@@ -130,6 +217,42 @@ pub struct FillEvent {
pub reason: String,
}
impl FillEvent {
pub fn validate(&self) -> Result<(), String> {
if self.symbol.trim().is_empty()
|| self.quantity == 0
|| !self.price.is_finite()
|| self.price <= 0.0
{
return Err(format!(
"invalid fill identity/quantity/price order_id={:?} symbol={} quantity={} price={}",
self.order_id, self.symbol, self.quantity, self.price
));
}
if let (Some(start), Some(end)) = (self.execution_start_timestamp, self.execution_timestamp)
{
if start > end {
return Err(format!(
"fill execution timestamp order is invalid order_id={:?} start={} end={}",
self.order_id, start, end
));
}
if start.date() != self.date || end.date() != self.date {
return Err(format!(
"fill execution timestamp date mismatch order_id={:?} fill_date={} start={} end={}",
self.order_id, self.date, start, end
));
}
} else if self.execution_start_timestamp.is_some() || self.execution_timestamp.is_some() {
return Err(format!(
"fill execution timestamp range is incomplete order_id={:?}",
self.order_id
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PositionEvent {
#[serde(with = "date_format")]
@@ -183,6 +306,9 @@ pub enum ProcessEventKind {
OrderPendingCancel,
OrderCancellationPass,
OrderCancellationReject,
OrderPendingUpdate,
OrderUpdatePass,
OrderUpdateReject,
OrderUnsolicitedUpdate,
Trade,
UniverseUpdated,
@@ -225,6 +351,9 @@ impl ProcessEventKind {
Self::OrderPendingCancel => "order_pending_cancel",
Self::OrderCancellationPass => "order_cancellation_pass",
Self::OrderCancellationReject => "order_cancellation_reject",
Self::OrderPendingUpdate => "order_pending_update",
Self::OrderUpdatePass => "order_update_pass",
Self::OrderUpdateReject => "order_update_reject",
Self::OrderUnsolicitedUpdate => "order_unsolicited_update",
Self::Trade => "trade",
Self::UniverseUpdated => "universe_updated",
@@ -235,6 +364,38 @@ impl ProcessEventKind {
Self::AccountManagementFee => "account_management_fee",
}
}
/// Returns whether the event is part of the durable business lifecycle
/// audit. Phase boundary events are useful during interactive debugging,
/// but retaining every minute phase marker for a long run is unnecessary.
pub fn is_business_lifecycle(&self) -> bool {
matches!(
*self,
Self::PreScheduled
| Self::PostScheduled
| Self::PreOnDay
| Self::OnDay
| Self::PostOnDay
| Self::OrderPendingNew
| Self::OrderCreationPass
| Self::OrderCreationReject
| Self::OrderPendingCancel
| Self::OrderCancellationPass
| Self::OrderCancellationReject
| Self::OrderPendingUpdate
| Self::OrderUpdatePass
| Self::OrderUpdateReject
| Self::OrderUnsolicitedUpdate
| Self::Trade
| Self::UniverseUpdated
| Self::UniverseSubscribed
| Self::UniverseUnsubscribed
| Self::AccountDepositWithdraw
| Self::AccountFinanceRepay
| Self::AccountManagementFee
| Self::Settlement
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -250,3 +411,116 @@ pub struct ProcessEvent {
pub side: Option<OrderSide>,
pub detail: String,
}
#[cfg(test)]
mod tests {
use chrono::{NaiveDate, NaiveDateTime};
use super::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEventKind};
fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
OrderEvent {
date: NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
decision_date: None,
order_created_date: None,
execution_date: None,
order_id: Some(1),
symbol: "600000.SH".to_string(),
side: OrderSide::Buy,
requested_quantity: 100,
filled_quantity,
status,
reason: "test".to_string(),
}
}
#[test]
fn order_event_status_quantity_contract_is_explicit() {
assert!(order_event(OrderStatus::Pending, 0).validate().is_ok());
assert!(
order_event(OrderStatus::PartiallyFilled, 40)
.validate()
.is_ok()
);
assert!(order_event(OrderStatus::Filled, 100).validate().is_ok());
assert!(order_event(OrderStatus::Canceled, 40).validate().is_ok());
assert!(order_event(OrderStatus::Rejected, 0).validate().is_ok());
assert!(order_event(OrderStatus::Expired, 40).validate().is_ok());
assert!(
order_event(OrderStatus::PartiallyFilled, 0)
.validate()
.is_err()
);
assert!(order_event(OrderStatus::Filled, 99).validate().is_err());
assert!(order_event(OrderStatus::Canceled, 100).validate().is_err());
assert!(order_event(OrderStatus::Rejected, 1).validate().is_err());
assert!(order_event(OrderStatus::Expired, 100).validate().is_err());
}
fn fill_event(start: Option<NaiveDateTime>, end: Option<NaiveDateTime>) -> FillEvent {
FillEvent {
date: NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
decision_date: None,
order_created_date: None,
execution_date: None,
execution_start_timestamp: start,
execution_timestamp: end,
order_id: Some(1),
symbol: "600000.SH".to_string(),
side: OrderSide::Buy,
quantity: 100,
price: 10.0,
gross_amount: 1_000.0,
commission: 5.0,
stamp_tax: 0.0,
transfer_fee: 0.0,
net_cash_flow: -1_005.0,
reason: "test".to_string(),
}
}
#[test]
fn fill_execution_timestamp_range_is_explicit_and_backward_compatible() {
let start = NaiveDate::from_ymd_opt(2025, 1, 2)
.unwrap()
.and_hms_opt(10, 18, 0)
.unwrap();
let end = start + chrono::Duration::seconds(3);
assert!(fill_event(Some(start), Some(end)).validate().is_ok());
assert!(fill_event(Some(end), Some(start)).validate().is_err());
assert!(fill_event(Some(start), None).validate().is_err());
let next_day = start + chrono::Duration::days(1);
assert!(
fill_event(Some(next_day), Some(next_day))
.validate()
.is_err()
);
let legacy = fill_event(None, None);
let legacy_json = serde_json::to_value(&legacy).unwrap();
assert!(legacy_json.get("execution_start_timestamp").is_none());
assert!(legacy_json.get("execution_timestamp").is_none());
let decoded: FillEvent = serde_json::from_value(legacy_json).unwrap();
assert_eq!(decoded.execution_start_timestamp, None);
assert_eq!(decoded.execution_timestamp, None);
let timestamped_json = serde_json::to_value(fill_event(Some(start), Some(end))).unwrap();
assert_eq!(
timestamped_json["execution_start_timestamp"],
"2025-01-02 10:18:00"
);
assert_eq!(
timestamped_json["execution_timestamp"],
"2025-01-02 10:18:03"
);
}
#[test]
fn process_event_business_lifecycle_filter_keeps_audit_events_only() {
assert!(ProcessEventKind::OrderUpdateReject.is_business_lifecycle());
assert!(ProcessEventKind::Settlement.is_business_lifecycle());
assert!(!ProcessEventKind::PreMinute.is_business_lifecycle());
assert!(!ProcessEventKind::PostBar.is_business_lifecycle());
}
}
+566
View File
@@ -0,0 +1,566 @@
//! Fixed-point execution primitives for money and fee arithmetic.
//!
//! Market data and analytics remain floating point at their API boundaries.
//! The execution kernel quantizes monetary values to micro-yuan before fee,
//! budget and cash-ledger arithmetic so repeated fills and external cash flows
//! do not accumulate binary floating-point drift.
use std::collections::{BTreeMap, VecDeque};
use chrono::NaiveDate;
use crate::events::OrderSide;
pub const MONEY_SCALE: i128 = 1_000_000;
const MONEY_SCALE_F64: f64 = MONEY_SCALE as f64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct FixedMoney(i128);
impl FixedMoney {
pub const ZERO: Self = Self(0);
pub const fn from_raw(raw: i128) -> Self {
Self(raw)
}
pub const fn raw(self) -> i128 {
self.0
}
pub fn from_decimal_str(value: &str) -> Result<Self, String> {
let value = value.trim();
if value.is_empty() {
return Err("fixed money value is empty".to_string());
}
let (negative, unsigned) = match value.as_bytes()[0] {
b'-' => (true, &value[1..]),
b'+' => (false, &value[1..]),
_ => (false, value),
};
let mut parts = unsigned.split('.');
let whole = parts.next().unwrap_or_default();
let fractional = parts.next().unwrap_or_default();
if parts.next().is_some()
|| whole.is_empty()
|| !whole.bytes().all(|byte| byte.is_ascii_digit())
|| !fractional.bytes().all(|byte| byte.is_ascii_digit())
{
return Err(format!("invalid fixed money decimal: {value}"));
}
let whole = whole
.parse::<i128>()
.map_err(|_| format!("fixed money whole part is out of range: {value}"))?;
let mut fractional_digits = fractional.as_bytes().to_vec();
let round_up = fractional_digits.len() > 6 && fractional_digits[6] >= b'5';
fractional_digits.truncate(6);
while fractional_digits.len() < 6 {
fractional_digits.push(b'0');
}
let fractional = if fractional_digits.is_empty() {
0
} else {
std::str::from_utf8(&fractional_digits)
.expect("fractional digits are ASCII")
.parse::<i128>()
.map_err(|_| format!("fixed money fractional part is invalid: {value}"))?
};
let mut raw = whole
.checked_mul(MONEY_SCALE)
.and_then(|raw| raw.checked_add(fractional))
.ok_or_else(|| format!("fixed money value is out of range: {value}"))?;
if round_up {
raw = raw
.checked_add(1)
.ok_or_else(|| format!("fixed money value is out of range: {value}"))?;
}
Ok(Self(if negative { -raw } else { raw }))
}
pub fn from_f64(value: f64) -> Option<Self> {
if !value.is_finite() {
return None;
}
let raw = (value * MONEY_SCALE_F64).round();
if !raw.is_finite() || raw < i128::MIN as f64 || raw > i128::MAX as f64 {
return None;
}
Some(Self(raw as i128))
}
pub fn to_f64(self) -> f64 {
self.0 as f64 / MONEY_SCALE_F64
}
pub fn checked_add(self, other: Self) -> Option<Self> {
self.0.checked_add(other.0).map(Self)
}
pub fn checked_sub(self, other: Self) -> Option<Self> {
self.0.checked_sub(other.0).map(Self)
}
pub fn checked_mul_quantity(self, quantity: u64) -> Option<Self> {
self.0.checked_mul(i128::from(quantity)).map(Self)
}
pub fn checked_neg(self) -> Option<Self> {
self.0.checked_neg().map(Self)
}
pub fn checked_mul_rate(self, rate: Self) -> Option<Self> {
let product = self.0.checked_mul(rate.0)?;
let half = MONEY_SCALE / 2;
let rounded = if product >= 0 {
product.checked_add(half)? / MONEY_SCALE
} else {
product.checked_sub(half)? / MONEY_SCALE
};
Some(Self(rounded))
}
pub fn checked_sum_f64(values: impl IntoIterator<Item = f64>) -> Option<Self> {
values.into_iter().try_fold(Self::ZERO, |total, value| {
total.checked_add(Self::from_f64(value)?)
})
}
pub fn f64_fits_within(value: f64, limit: f64) -> Option<bool> {
let value = Self::from_f64(value)?;
if limit == f64::INFINITY {
return Some(true);
}
Some(value <= Self::from_f64(limit)?)
}
pub fn abs(self) -> Self {
Self(self.0.abs())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FixedTradingCost {
pub commission: FixedMoney,
pub stamp_tax: FixedMoney,
pub transfer_fee: FixedMoney,
}
impl FixedTradingCost {
pub fn total(self) -> FixedMoney {
FixedMoney::from_raw(self.commission.raw() + self.stamp_tax.raw() + self.transfer_fee.raw())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FixedChinaAShareCostModel {
pub commission_rate: FixedMoney,
pub stamp_tax_rate_before_change: FixedMoney,
pub stamp_tax_rate_after_change: FixedMoney,
pub stamp_tax_change_date: NaiveDate,
pub minimum_commission: FixedMoney,
pub transfer_fee_rate: FixedMoney,
}
impl FixedChinaAShareCostModel {
pub fn commission_for(self, gross_amount: FixedMoney) -> FixedMoney {
if gross_amount.raw() <= 0 {
return FixedMoney::ZERO;
}
let raw = gross_amount
.checked_mul_rate(self.commission_rate)
.expect("fixed commission multiplication overflow");
raw.max(self.minimum_commission)
}
pub fn stamp_tax_rate_for(self, date: NaiveDate) -> FixedMoney {
if date < self.stamp_tax_change_date {
self.stamp_tax_rate_before_change
} else {
self.stamp_tax_rate_after_change
}
}
pub fn stamp_tax_for(
self,
date: NaiveDate,
side: OrderSide,
gross_amount: FixedMoney,
) -> FixedMoney {
if gross_amount.raw() <= 0 || side == OrderSide::Buy {
return FixedMoney::ZERO;
}
gross_amount
.checked_mul_rate(self.stamp_tax_rate_for(date))
.expect("fixed stamp tax multiplication overflow")
}
pub fn transfer_fee_for(self, gross_amount: FixedMoney) -> FixedMoney {
if gross_amount.raw() <= 0 {
return FixedMoney::ZERO;
}
gross_amount
.checked_mul_rate(self.transfer_fee_rate)
.expect("fixed transfer fee multiplication overflow")
}
pub fn calculate(
self,
date: NaiveDate,
side: OrderSide,
gross_amount: FixedMoney,
) -> FixedTradingCost {
FixedTradingCost {
commission: self.commission_for(gross_amount),
stamp_tax: self.stamp_tax_for(date, side, gross_amount),
transfer_fee: self.transfer_fee_for(gross_amount),
}
}
pub fn commission_for_order_fill(
self,
gross_amount: FixedMoney,
order_id: Option<u64>,
commission_state: &mut BTreeMap<u64, FixedMoney>,
) -> FixedMoney {
if gross_amount.raw() <= 0 {
return FixedMoney::ZERO;
}
let raw = gross_amount
.checked_mul_rate(self.commission_rate)
.expect("fixed commission multiplication overflow");
let Some(order_id) = order_id else {
return raw.max(self.minimum_commission);
};
let remaining = commission_state
.entry(order_id)
.or_insert(self.minimum_commission);
self.commission_for_order_fill_remaining(gross_amount, remaining)
}
pub fn commission_for_order_fill_remaining(
self,
gross_amount: FixedMoney,
remaining: &mut FixedMoney,
) -> FixedMoney {
if gross_amount.raw() <= 0 {
return FixedMoney::ZERO;
}
let raw = gross_amount
.checked_mul_rate(self.commission_rate)
.expect("fixed commission multiplication overflow");
if raw > *remaining {
let charged = if *remaining == self.minimum_commission {
raw
} else {
raw.checked_sub(*remaining)
.expect("fixed remaining commission underflow")
};
*remaining = FixedMoney::ZERO;
charged
} else {
let charged = if *remaining == self.minimum_commission {
self.minimum_commission
} else {
FixedMoney::ZERO
};
*remaining = remaining
.checked_sub(raw)
.expect("fixed remaining commission underflow");
charged
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FixedLot {
pub acquired_date: NaiveDate,
pub quantity: u64,
pub entry_price: FixedMoney,
}
#[derive(Debug, Clone, Default)]
pub struct FixedLotBook {
lots: VecDeque<FixedLot>,
pub realized_pnl: FixedMoney,
pub quantity: u64,
}
impl FixedLotBook {
pub fn buy(&mut self, date: NaiveDate, quantity: u64, price: FixedMoney) {
if quantity == 0 {
return;
}
self.lots.push_back(FixedLot {
acquired_date: date,
quantity,
entry_price: price,
});
self.quantity = self.quantity.saturating_add(quantity);
}
pub fn sell(&mut self, quantity: u64, price: FixedMoney) -> Result<FixedMoney, String> {
if quantity > self.quantity {
return Err(format!(
"fixed sell quantity {} exceeds current quantity {}",
quantity, self.quantity
));
}
let mut remaining = quantity;
let mut realized = FixedMoney::ZERO;
while remaining > 0 {
let Some(mut lot) = self.lots.pop_front() else {
return Err("fixed lot book is empty while selling".to_string());
};
let sold = remaining.min(lot.quantity);
let price_delta = price
.checked_sub(lot.entry_price)
.and_then(|delta| delta.checked_mul_quantity(sold))
.ok_or_else(|| "fixed realized PnL overflow".to_string())?;
realized = realized
.checked_add(price_delta)
.ok_or_else(|| "fixed realized PnL overflow".to_string())?;
lot.quantity -= sold;
remaining -= sold;
if lot.quantity > 0 {
self.lots.push_front(lot);
}
}
self.quantity -= quantity;
self.realized_pnl = self
.realized_pnl
.checked_add(realized)
.ok_or_else(|| "fixed realized PnL overflow".to_string())?;
Ok(realized)
}
pub fn market_value(&self, mark_price: FixedMoney) -> FixedMoney {
mark_price
.checked_mul_quantity(self.quantity)
.expect("fixed market value overflow")
}
pub fn unrealized_pnl(&self, mark_price: FixedMoney) -> FixedMoney {
self.lots.iter().fold(FixedMoney::ZERO, |total, lot| {
let delta = mark_price
.checked_sub(lot.entry_price)
.and_then(|value| value.checked_mul_quantity(lot.quantity))
.expect("fixed unrealized PnL overflow");
total
.checked_add(delta)
.expect("fixed unrealized PnL overflow")
})
}
}
#[derive(Debug, Clone)]
pub struct FixedAccount {
pub cash: FixedMoney,
pub units: FixedMoney,
pub external_cash_flow_total: FixedMoney,
}
impl FixedAccount {
pub fn new(initial_cash: FixedMoney) -> Self {
Self {
cash: initial_cash,
units: initial_cash,
external_cash_flow_total: FixedMoney::ZERO,
}
}
pub fn apply_external_cash_flow(
&mut self,
amount: FixedMoney,
unit_nav: FixedMoney,
) -> Result<(), String> {
if unit_nav.raw() <= 0 {
return Err("fixed unit NAV must be positive".to_string());
}
let exact_units_raw = amount
.raw()
.checked_mul(MONEY_SCALE)
.and_then(|value| value.checked_div(unit_nav.raw()))
.ok_or_else(|| "fixed external flow unit conversion overflow".to_string())?;
self.cash = self
.cash
.checked_add(amount)
.ok_or_else(|| "fixed cash overflow".to_string())?;
self.units = self
.units
.checked_add(FixedMoney::from_raw(exact_units_raw))
.ok_or_else(|| "fixed units overflow".to_string())?;
self.external_cash_flow_total = self
.external_cash_flow_total
.checked_add(amount)
.ok_or_else(|| "fixed external flow overflow".to_string())?;
Ok(())
}
pub fn unit_nav(&self, total_equity: FixedMoney) -> Result<FixedMoney, String> {
if self.units.raw() <= 0 {
return Err("fixed account has no units".to_string());
}
let raw = total_equity
.raw()
.checked_mul(MONEY_SCALE)
.and_then(|value| value.checked_div(self.units.raw()))
.ok_or_else(|| "fixed unit NAV overflow".to_string())?;
Ok(FixedMoney::from_raw(raw))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cost::{ChinaAShareCostModel, CostModel};
use crate::risk_control::TradingConstraintConfig;
fn fixed_model() -> FixedChinaAShareCostModel {
let config = TradingConstraintConfig::default();
FixedChinaAShareCostModel {
commission_rate: FixedMoney::from_f64(config.commission_rate).unwrap(),
stamp_tax_rate_before_change: FixedMoney::from_f64(config.stamp_tax_rate_before_change)
.unwrap(),
stamp_tax_rate_after_change: FixedMoney::from_f64(config.stamp_tax_rate_after_change)
.unwrap(),
stamp_tax_change_date: config.stamp_tax_change_date,
minimum_commission: FixedMoney::from_f64(config.minimum_commission).unwrap(),
transfer_fee_rate: FixedMoney::from_f64(config.transfer_fee_rate).unwrap(),
}
}
#[test]
fn decimal_parser_rounds_only_beyond_money_scale() {
assert_eq!(
FixedMoney::from_decimal_str("1.234567").unwrap().raw(),
1_234_567
);
assert_eq!(
FixedMoney::from_decimal_str("1.2345675").unwrap().raw(),
1_234_568
);
assert_eq!(
FixedMoney::from_decimal_str("-0.0000014").unwrap().raw(),
-1
);
}
#[test]
fn runtime_cost_model_matches_fixed_execution_primitive() {
let fixed = fixed_model();
let float = ChinaAShareCostModel::default();
let dates = [
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
];
for gross in [0.01, 10.0, 16_666.67, 248_059.812, 1_000_000.01] {
let fixed_gross = FixedMoney::from_f64(gross).unwrap();
for date in dates {
for side in [OrderSide::Buy, OrderSide::Sell] {
let expected = float.calculate(date, side, gross);
let actual = fixed.calculate(date, side, fixed_gross);
for (actual, expected) in [
(actual.commission, expected.commission),
(actual.stamp_tax, expected.stamp_tax),
(actual.transfer_fee, expected.transfer_fee),
] {
assert_eq!(actual.to_f64(), expected);
}
}
}
}
}
#[test]
fn runtime_split_commission_matches_fixed_execution_primitive() {
let fixed = fixed_model();
let float = ChinaAShareCostModel::default();
let mut fixed_state = BTreeMap::new();
let mut float_state = BTreeMap::new();
let mut fixed_total = FixedMoney::ZERO;
let mut float_total = 0.0;
for gross in [1000.0, 2000.0, 4000.0, 40_000.0] {
let fixed_fee = fixed.commission_for_order_fill(
FixedMoney::from_f64(gross).unwrap(),
Some(42),
&mut fixed_state,
);
let float_fee = float.commission_for_order_fill(gross, Some(42), &mut float_state);
fixed_total = fixed_total.checked_add(fixed_fee).unwrap();
float_total += float_fee;
}
assert_eq!(fixed_total.to_f64(), float_total);
}
#[test]
fn fixed_budget_never_exceeds_cash_after_cost() {
let model = fixed_model();
let date = NaiveDate::from_ymd_opt(2025, 2, 3).unwrap();
let cash = FixedMoney::from_decimal_str("99880.00").unwrap();
let price = FixedMoney::from_decimal_str("19.9731").unwrap();
let mut quantity = 5_000u64;
while quantity > 0 {
let gross = price.checked_mul_quantity(quantity).unwrap();
if gross
.checked_add(model.calculate(date, OrderSide::Buy, gross).total())
.unwrap()
<= cash
{
break;
}
quantity -= 100;
}
let gross = price.checked_mul_quantity(quantity).unwrap();
let total = gross
.checked_add(model.calculate(date, OrderSide::Buy, gross).total())
.unwrap();
assert!(total <= cash);
assert!(quantity < 5_000);
}
#[test]
fn fixed_budget_comparison_rejects_one_micro_yuan_overrun() {
assert_eq!(FixedMoney::f64_fits_within(100.0, 100.0), Some(true));
assert_eq!(FixedMoney::f64_fits_within(100.000001, 100.0), Some(false));
assert_eq!(
FixedMoney::f64_fits_within(100.000001, f64::INFINITY),
Some(true)
);
}
#[test]
fn fixed_fifo_pnl_and_external_flow_are_deterministic() {
let day_one = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let day_two = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
let mut book = FixedLotBook::default();
book.buy(day_one, 100, FixedMoney::from_decimal_str("10.01").unwrap());
book.buy(day_two, 100, FixedMoney::from_decimal_str("10.03").unwrap());
let realized = book
.sell(150, FixedMoney::from_decimal_str("10.11").unwrap())
.unwrap();
assert_eq!(realized.raw(), 14_000_000);
assert_eq!(book.quantity, 50);
assert_eq!(
book.unrealized_pnl(FixedMoney::from_decimal_str("10.20").unwrap())
.raw(),
8_500_000
);
let mut account = FixedAccount::new(FixedMoney::from_decimal_str("100.00").unwrap());
account
.apply_external_cash_flow(
FixedMoney::from_decimal_str("50.00").unwrap(),
FixedMoney::from_decimal_str("1.00").unwrap(),
)
.unwrap();
assert_eq!(account.units.raw(), 150 * MONEY_SCALE);
assert_eq!(
account
.unit_nav(FixedMoney::from_decimal_str("150.00").unwrap())
.unwrap()
.raw(),
MONEY_SCALE
);
assert_eq!(account.external_cash_flow_total.raw(), 50 * MONEY_SCALE);
}
}
+308 -75
View File
@@ -7,6 +7,24 @@ use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
};
use crate::fixed_point::FixedMoney;
fn futures_money(value: f64, label: &str) -> Result<FixedMoney, String> {
FixedMoney::from_f64(value)
.ok_or_else(|| format!("{label} is not representable as fixed-point money: {value}"))
}
fn futures_money_or_panic(value: f64, label: &str) -> FixedMoney {
futures_money(value, label).unwrap_or_else(|error| panic!("{error}"))
}
fn sum_futures_money(values: impl IntoIterator<Item = FixedMoney>, label: &str) -> FixedMoney {
values.into_iter().fold(FixedMoney::ZERO, |total, value| {
total
.checked_add(value)
.unwrap_or_else(|| panic!("fixed-point {label} overflow"))
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FuturesDirection {
@@ -345,6 +363,14 @@ pub struct FuturesExecutionReport {
}
impl FuturesContractSpec {
pub fn unresolved() -> Self {
Self {
contract_multiplier: f64::NAN,
long_margin_rate: f64::NAN,
short_margin_rate: f64::NAN,
}
}
pub fn new(contract_multiplier: f64, long_margin_rate: f64, short_margin_rate: f64) -> Self {
Self {
contract_multiplier: contract_multiplier.max(1.0),
@@ -359,6 +385,15 @@ impl FuturesContractSpec {
FuturesDirection::Short => self.short_margin_rate,
}
}
pub fn is_resolved(&self) -> bool {
self.contract_multiplier.is_finite()
&& self.contract_multiplier > 0.0
&& self.long_margin_rate.is_finite()
&& self.long_margin_rate >= 0.0
&& self.short_margin_rate.is_finite()
&& self.short_margin_rate >= 0.0
}
}
#[derive(Debug, Clone)]
@@ -366,15 +401,16 @@ pub struct FuturesPosition {
pub symbol: String,
pub direction: FuturesDirection,
pub old_quantity: u32,
day_start_quantity: u32,
pub quantity: u32,
pub avg_price: f64,
pub last_price: f64,
pub prev_close: f64,
pub contract_multiplier: f64,
pub margin_rate: f64,
pub transaction_cost: f64,
transaction_cost: FixedMoney,
trade_quantity_delta: i32,
trade_cost: f64,
trade_value: FixedMoney,
}
impl FuturesPosition {
@@ -390,15 +426,16 @@ impl FuturesPosition {
symbol: symbol.into(),
direction,
old_quantity: init_quantity,
day_start_quantity: init_quantity,
quantity: init_quantity,
avg_price: init_price.max(0.0),
last_price: init_price.max(0.0),
prev_close: init_price.max(0.0),
contract_multiplier: spec.contract_multiplier,
margin_rate,
transaction_cost: 0.0,
transaction_cost: FixedMoney::ZERO,
trade_quantity_delta: 0,
trade_cost: 0.0,
trade_value: FixedMoney::ZERO,
}
}
@@ -407,18 +444,39 @@ impl FuturesPosition {
}
pub fn market_value(&self) -> f64 {
self.quantity as f64 * self.last_price * self.contract_multiplier
self.market_value_money().to_f64()
}
fn market_value_money(&self) -> FixedMoney {
futures_money_or_panic(
self.quantity as f64 * self.last_price * self.contract_multiplier,
"futures position market value",
)
}
pub fn margin(&self) -> f64 {
self.market_value() * self.margin_rate
self.margin_money().to_f64()
}
fn margin_money(&self) -> FixedMoney {
futures_money_or_panic(
self.market_value_money().to_f64() * self.margin_rate,
"futures position margin",
)
}
pub fn equity(&self) -> f64 {
(self.last_price - self.avg_price)
* self.quantity as f64
* self.contract_multiplier
* self.direction.factor()
self.equity_money().to_f64()
}
fn equity_money(&self) -> FixedMoney {
futures_money_or_panic(
(self.last_price - self.avg_price)
* self.quantity as f64
* self.contract_multiplier
* self.direction.factor(),
"futures position equity",
)
}
pub fn pnl(&self) -> f64 {
@@ -426,22 +484,47 @@ impl FuturesPosition {
}
pub fn trading_pnl(&self) -> f64 {
(self.trade_quantity_delta as f64 * self.last_price - self.trade_cost)
* self.contract_multiplier
* self.direction.factor()
self.trading_pnl_money().to_f64()
}
fn trading_pnl_money(&self) -> FixedMoney {
let marked_trade_value = futures_money_or_panic(
self.trade_quantity_delta as f64 * self.last_price * self.contract_multiplier,
"futures marked trade value",
);
let pnl = marked_trade_value
.checked_sub(self.trade_value)
.expect("fixed-point futures trading PnL overflow");
if self.direction == FuturesDirection::Short {
pnl.checked_neg()
.expect("fixed-point futures short trading PnL overflow")
} else {
pnl
}
}
pub fn position_pnl(&self) -> f64 {
if self.old_quantity == 0 {
0.0
self.position_pnl_money().to_f64()
}
fn position_pnl_money(&self) -> FixedMoney {
if self.day_start_quantity == 0 {
FixedMoney::ZERO
} else {
self.old_quantity as f64
* (self.last_price - self.prev_close)
* self.contract_multiplier
* self.direction.factor()
futures_money_or_panic(
self.day_start_quantity as f64
* (self.last_price - self.prev_close)
* self.contract_multiplier
* self.direction.factor(),
"futures position daily PnL",
)
}
}
pub fn transaction_cost(&self) -> f64 {
self.transaction_cost.to_f64()
}
pub fn open(&mut self, quantity: u32, price: f64, transaction_cost: f64) {
if quantity == 0 {
return;
@@ -450,9 +533,20 @@ impl FuturesPosition {
self.quantity += quantity;
self.avg_price = (old_value + price * quantity as f64) / self.quantity as f64;
self.last_price = price;
self.transaction_cost += transaction_cost.max(0.0);
let transaction_cost =
futures_money_or_panic(transaction_cost.max(0.0), "futures open transaction cost");
self.transaction_cost = self
.transaction_cost
.checked_add(transaction_cost)
.expect("fixed-point futures transaction cost overflow");
self.trade_quantity_delta += quantity as i32;
self.trade_cost += price * quantity as f64;
self.trade_value = self
.trade_value
.checked_add(futures_money_or_panic(
price * quantity as f64 * self.contract_multiplier,
"futures open trade value",
))
.expect("fixed-point futures trade value overflow");
}
pub fn close(
@@ -476,6 +570,17 @@ impl FuturesPosition {
transaction_cost: f64,
effect: FuturesPositionEffect,
) -> Result<f64, String> {
self.close_with_effect_money(quantity, price, transaction_cost, effect)
.map(FixedMoney::to_f64)
}
fn close_with_effect_money(
&mut self,
quantity: u32,
price: f64,
transaction_cost: f64,
effect: FuturesPositionEffect,
) -> Result<FixedMoney, String> {
if effect == FuturesPositionEffect::Open {
return Err("close_with_effect does not accept open effect".to_string());
}
@@ -489,7 +594,7 @@ impl FuturesPosition {
));
}
if quantity == 0 {
return Ok(0.0);
return Ok(FixedMoney::ZERO);
}
match effect {
FuturesPositionEffect::Open => unreachable!(),
@@ -523,19 +628,34 @@ impl FuturesPosition {
}
}
let realized = (price - self.avg_price)
* quantity as f64
* self.contract_multiplier
* self.direction.factor()
- transaction_cost.max(0.0);
let transaction_cost =
futures_money(transaction_cost.max(0.0), "futures close transaction cost")?;
let realized = futures_money(
(price - self.avg_price)
* quantity as f64
* self.contract_multiplier
* self.direction.factor(),
"futures realized PnL",
)?
.checked_sub(transaction_cost)
.ok_or_else(|| "fixed-point futures realized PnL overflow".to_string())?;
self.quantity -= quantity;
if self.quantity == 0 {
self.avg_price = 0.0;
}
self.last_price = price;
self.transaction_cost += transaction_cost.max(0.0);
self.transaction_cost = self
.transaction_cost
.checked_add(transaction_cost)
.ok_or_else(|| "fixed-point futures transaction cost overflow".to_string())?;
self.trade_quantity_delta -= quantity as i32;
self.trade_cost -= price * quantity as f64;
self.trade_value = self
.trade_value
.checked_sub(futures_money(
price * quantity as f64 * self.contract_multiplier,
"futures close trade value",
)?)
.ok_or_else(|| "fixed-point futures trade value overflow".to_string())?;
Ok(realized)
}
@@ -547,98 +667,163 @@ impl FuturesPosition {
pub fn begin_trading_day(&mut self) {
self.old_quantity = self.quantity;
self.day_start_quantity = self.quantity;
self.prev_close = self.last_price;
self.transaction_cost = 0.0;
self.transaction_cost = FixedMoney::ZERO;
self.trade_quantity_delta = 0;
self.trade_cost = 0.0;
self.trade_value = FixedMoney::ZERO;
}
pub fn settlement(&mut self, settlement_price: f64) -> f64 {
self.settlement_money(settlement_price).to_f64()
}
fn settlement_money(&mut self, settlement_price: f64) -> FixedMoney {
self.mark_price(settlement_price);
let cash_delta = self.equity();
let cash_delta = self.equity_money();
self.avg_price = self.last_price;
self.prev_close = self.last_price;
self.old_quantity = self.quantity;
cash_delta
}
}
#[derive(Debug, Clone)]
pub struct FuturesAccountState {
starting_cash: f64,
total_cash: f64,
frozen_cash: f64,
starting_cash: FixedMoney,
total_cash: FixedMoney,
frozen_cash: FixedMoney,
closed_day_trading_pnl: FixedMoney,
closed_day_position_pnl: FixedMoney,
closed_day_transaction_cost: FixedMoney,
positions: BTreeMap<(String, FuturesDirection), FuturesPosition>,
}
impl FuturesAccountState {
pub fn new(total_cash: f64) -> Self {
let total_cash = futures_money_or_panic(total_cash, "futures starting cash");
Self {
starting_cash: total_cash,
total_cash,
frozen_cash: 0.0,
frozen_cash: FixedMoney::ZERO,
closed_day_trading_pnl: FixedMoney::ZERO,
closed_day_position_pnl: FixedMoney::ZERO,
closed_day_transaction_cost: FixedMoney::ZERO,
positions: BTreeMap::new(),
}
}
pub fn starting_cash(&self) -> f64 {
self.starting_cash
self.starting_cash.to_f64()
}
pub fn total_cash(&self) -> f64 {
self.total_cash
self.total_cash.to_f64()
}
pub fn frozen_cash(&self) -> f64 {
self.frozen_cash
self.frozen_cash.to_f64()
}
pub fn cash(&self) -> f64 {
self.total_cash - self.margin() - self.frozen_cash
self.cash_money().to_f64()
}
fn cash_money(&self) -> FixedMoney {
self.total_cash
.checked_sub(self.margin_money())
.and_then(|cash| cash.checked_sub(self.frozen_cash))
.expect("fixed-point futures available cash overflow")
}
pub fn margin(&self) -> f64 {
self.positions.values().map(FuturesPosition::margin).sum()
self.margin_money().to_f64()
}
fn margin_money(&self) -> FixedMoney {
sum_futures_money(
self.positions.values().map(FuturesPosition::margin_money),
"futures account margin",
)
}
pub fn market_value(&self) -> f64 {
self.positions
.values()
.map(FuturesPosition::market_value)
.sum()
sum_futures_money(
self.positions
.values()
.map(FuturesPosition::market_value_money),
"futures account market value",
)
.to_f64()
}
pub fn position_equity(&self) -> f64 {
self.positions.values().map(FuturesPosition::equity).sum()
self.position_equity_money().to_f64()
}
fn position_equity_money(&self) -> FixedMoney {
sum_futures_money(
self.positions.values().map(FuturesPosition::equity_money),
"futures account position equity",
)
}
pub fn total_value(&self) -> f64 {
self.total_cash + self.position_equity()
self.total_cash
.checked_add(self.position_equity_money())
.expect("fixed-point futures total value overflow")
.to_f64()
}
pub fn daily_pnl(&self) -> f64 {
self.trading_pnl() + self.position_pnl() - self.transaction_cost()
self.trading_pnl_money()
.checked_add(self.position_pnl_money())
.and_then(|pnl| pnl.checked_sub(self.transaction_cost_money()))
.expect("fixed-point futures daily PnL overflow")
.to_f64()
}
pub fn trading_pnl(&self) -> f64 {
self.positions
.values()
.map(FuturesPosition::trading_pnl)
.sum()
self.trading_pnl_money().to_f64()
}
fn trading_pnl_money(&self) -> FixedMoney {
sum_futures_money(
std::iter::once(self.closed_day_trading_pnl).chain(
self.positions
.values()
.map(FuturesPosition::trading_pnl_money),
),
"futures account trading PnL",
)
}
pub fn position_pnl(&self) -> f64 {
self.positions
.values()
.map(FuturesPosition::position_pnl)
.sum()
self.position_pnl_money().to_f64()
}
fn position_pnl_money(&self) -> FixedMoney {
sum_futures_money(
std::iter::once(self.closed_day_position_pnl).chain(
self.positions
.values()
.map(FuturesPosition::position_pnl_money),
),
"futures account position PnL",
)
}
pub fn transaction_cost(&self) -> f64 {
self.positions
.values()
.map(|position| position.transaction_cost)
.sum()
self.transaction_cost_money().to_f64()
}
fn transaction_cost_money(&self) -> FixedMoney {
sum_futures_money(
std::iter::once(self.closed_day_transaction_cost).chain(
self.positions
.values()
.map(|position| position.transaction_cost),
),
"futures account transaction cost",
)
}
pub fn positions(&self) -> &BTreeMap<(String, FuturesDirection), FuturesPosition> {
@@ -667,7 +852,13 @@ impl FuturesAccountState {
.entry((symbol.clone(), direction))
.or_insert_with(|| FuturesPosition::new(symbol, direction, spec, 0, price));
position.open(quantity, price, transaction_cost);
self.total_cash -= transaction_cost.max(0.0);
self.total_cash = self
.total_cash
.checked_sub(futures_money_or_panic(
transaction_cost.max(0.0),
"futures open transaction cost",
))
.expect("fixed-point futures cash overflow");
}
pub fn close(
@@ -702,12 +893,30 @@ impl FuturesAccountState {
.positions
.get_mut(&key)
.ok_or_else(|| format!("missing futures position {symbol} {}", direction.as_str()))?;
let cash_delta = position.close_with_effect(quantity, price, transaction_cost, effect)?;
self.total_cash += cash_delta;
let cash_delta =
position.close_with_effect_money(quantity, price, transaction_cost, effect)?;
self.total_cash = self
.total_cash
.checked_add(cash_delta)
.ok_or_else(|| "fixed-point futures cash overflow".to_string())?;
if position.quantity == 0 {
self.closed_day_trading_pnl = self
.closed_day_trading_pnl
.checked_add(position.trading_pnl_money())
.ok_or_else(|| "fixed-point closed futures trading PnL overflow".to_string())?;
self.closed_day_position_pnl = self
.closed_day_position_pnl
.checked_add(position.position_pnl_money())
.ok_or_else(|| "fixed-point closed futures position PnL overflow".to_string())?;
self.closed_day_transaction_cost = self
.closed_day_transaction_cost
.checked_add(position.transaction_cost)
.ok_or_else(|| {
"fixed-point closed futures transaction cost overflow".to_string()
})?;
self.positions.remove(&key);
}
Ok(cash_delta)
Ok(cash_delta.to_f64())
}
pub fn execute_order(
@@ -782,7 +991,7 @@ impl FuturesAccountState {
intent.price,
intent.transaction_cost,
);
if projected.cash() < -1e-8 {
if projected.cash_money().raw() < 0 {
Err(format!(
"insufficient futures margin available_cash={:.2} required_margin_after={:.2}",
self.cash(),
@@ -797,7 +1006,13 @@ impl FuturesAccountState {
intent.price,
intent.transaction_cost,
);
Ok(-intent.transaction_cost.max(0.0))
Ok(futures_money_or_panic(
intent.transaction_cost.max(0.0),
"futures open transaction cost",
)
.checked_neg()
.expect("fixed-point futures open cash delta overflow")
.to_f64())
}
}
FuturesPositionEffect::Close
@@ -822,20 +1037,30 @@ impl FuturesAccountState {
.position(&intent.symbol, intent.direction)
.map(|position| position.avg_price)
.unwrap_or(0.0);
let notional =
intent.price * intent.quantity as f64 * intent.spec.contract_multiplier;
let notional = futures_money_or_panic(
intent.price * intent.quantity as f64 * intent.spec.contract_multiplier,
"futures fill notional",
)
.to_f64();
let transaction_cost = futures_money_or_panic(
intent.transaction_cost.max(0.0),
"futures fill transaction cost",
)
.to_f64();
report.fill_events.push(FillEvent {
date,
decision_date: None,
order_created_date: None,
execution_date: None,
execution_start_timestamp: None,
execution_timestamp: None,
order_id,
symbol: intent.symbol.clone(),
side,
quantity: intent.quantity,
price: intent.price,
gross_amount: notional,
commission: intent.transaction_cost.max(0.0),
commission: transaction_cost,
stamp_tax: 0.0,
transfer_fee: 0.0,
net_cash_flow: cash_delta,
@@ -1010,22 +1235,30 @@ impl FuturesAccountState {
}
pub fn begin_trading_day(&mut self) {
self.closed_day_trading_pnl = FixedMoney::ZERO;
self.closed_day_position_pnl = FixedMoney::ZERO;
self.closed_day_transaction_cost = FixedMoney::ZERO;
for position in self.positions.values_mut() {
position.begin_trading_day();
}
}
pub fn settle(&mut self, settlement_prices: &BTreeMap<String, f64>) -> f64 {
let mut cash_delta = 0.0;
let mut cash_delta = FixedMoney::ZERO;
for position in self.positions.values_mut() {
let price = settlement_prices
.get(&position.symbol)
.copied()
.unwrap_or(position.last_price);
cash_delta += position.settlement(price);
cash_delta = cash_delta
.checked_add(position.settlement_money(price))
.expect("fixed-point futures settlement overflow");
}
self.total_cash += cash_delta;
cash_delta
self.total_cash = self
.total_cash
.checked_add(cash_delta)
.expect("fixed-point futures cash settlement overflow");
cash_delta.to_f64()
}
}
+74 -7
View File
@@ -1,6 +1,17 @@
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
pub fn listed_sector_is_kcb(value: &str) -> Option<bool> {
match value.trim().to_ascii_uppercase().as_str() {
"科创板" | "KSH" | "STAR" | "STAR_MARKET" => Some(true),
"主板" | "沪市主板" | "深市主板" | "中小板" | "中小企业板" | "创业板"
| "北交所" | "北证" | "新三板" | "基础层" | "创新层" | "精选层"
| "MAIN" | "MAIN_BOARD" | "CHINEXT" | "GEM" | "BJ" | "BJS" | "BJSE"
| "BSE" => Some(false),
_ => None,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Instrument {
pub symbol: String,
@@ -21,17 +32,29 @@ impl Instrument {
}
pub fn minimum_order_quantity(&self) -> u32 {
match self.board.trim().to_ascii_uppercase().as_str() {
"KSH" => 200,
"BJS" | "BJ" | "BJSE" => 100,
_ => self.effective_round_lot(),
let board = self.board.trim();
if board.eq_ignore_ascii_case("KSH") {
200
} else if board.eq_ignore_ascii_case("BJS")
|| board.eq_ignore_ascii_case("BJ")
|| board.eq_ignore_ascii_case("BJSE")
{
100
} else {
self.effective_round_lot()
}
}
pub fn order_step_size(&self) -> u32 {
match self.board.trim().to_ascii_uppercase().as_str() {
"KSH" | "BJS" | "BJ" | "BJSE" => 1,
_ => self.effective_round_lot(),
let board = self.board.trim();
if board.eq_ignore_ascii_case("KSH")
|| board.eq_ignore_ascii_case("BJS")
|| board.eq_ignore_ascii_case("BJ")
|| board.eq_ignore_ascii_case("BJSE")
{
1
} else {
self.effective_round_lot()
}
}
@@ -56,6 +79,50 @@ fn default_status() -> String {
"active".to_string()
}
#[cfg(test)]
mod tests {
use super::{Instrument, listed_sector_is_kcb};
#[test]
fn listing_sector_is_explicit_and_unknown_stays_unknown() {
assert_eq!(listed_sector_is_kcb("科创板"), Some(true));
assert_eq!(listed_sector_is_kcb(" star "), Some(true));
assert_eq!(listed_sector_is_kcb("主板"), Some(false));
assert_eq!(listed_sector_is_kcb("创业板"), Some(false));
assert_eq!(listed_sector_is_kcb("北证"), Some(false));
for value in ["", "-", "SH", "688001.SH", "半导体"] {
assert_eq!(listed_sector_is_kcb(value), None);
}
}
fn instrument(board: &str, round_lot: u32) -> Instrument {
Instrument {
symbol: "000001.SZ".to_string(),
name: "test".to_string(),
board: board.to_string(),
round_lot,
listed_at: None,
delisted_at: None,
status: "active".to_string(),
}
}
#[test]
fn order_quantity_rules_are_case_insensitive_without_allocating_normalized_boards() {
let kcb = instrument(" kSh ", 100);
assert_eq!(kcb.minimum_order_quantity(), 200);
assert_eq!(kcb.order_step_size(), 1);
let bjse = instrument("bjse", 100);
assert_eq!(bjse.minimum_order_quantity(), 100);
assert_eq!(bjse.order_step_size(), 1);
let main_board = instrument("SZSE", 50);
assert_eq!(main_board.minimum_order_quantity(), 50);
assert_eq!(main_board.order_step_size(), 50);
}
}
mod optional_date_format {
use chrono::NaiveDate;
use serde::{self, Deserialize, Deserializer, Serializer};
+24 -12
View File
@@ -2,12 +2,15 @@ pub mod broker;
pub mod calendar;
pub mod cost;
pub mod data;
pub mod daily_patterns;
pub mod engine;
pub mod event_bus;
pub mod events;
pub mod fixed_point;
pub mod futures;
pub mod instrument;
pub mod metrics;
mod numeric_expr_vm;
pub mod platform_expr_strategy;
pub mod platform_runtime_schema;
pub mod platform_strategy_spec;
@@ -29,32 +32,40 @@ pub use data::{
BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot,
DailyMarketSnapshot, DailySnapshotBundle, DataSet, DataSetError, DividendRecord,
EligibleUniverseSnapshot, FactorTextValue, FactorValue, IntradayExecutionQuote,
IntradayOrderBookDepthLevel, PriceBar, PriceField, SecuritiesMarginRecord, SplitRecord,
YieldCurvePoint,
IntradayMarketSnapshotOverlay, IntradayOrderBookDepthLevel, NumericFactorMap, PriceBar,
PriceField, SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
};
pub use engine::{
AnalyzerMonthlyReturnRow, AnalyzerPositionRow, AnalyzerReport, AnalyzerRiskSummary,
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
BacktestResult, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
BacktestResult, BacktestTerminalAssetClass, BacktestTerminalAudit, BacktestTerminalOpenOrder,
BacktestTerminalStatus, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
ProcessEventRetention, backtest_execution_dates,
};
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
pub use events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
};
pub use fixed_point::{
FixedAccount, FixedChinaAShareCostModel, FixedLotBook, FixedMoney, FixedTradingCost,
MONEY_SCALE,
};
pub use futures::{
FuturesAccountState, FuturesCommissionType, FuturesContractSpec, FuturesDirection,
FuturesExecutionReport, FuturesOrderIntent, FuturesPosition, FuturesPositionEffect,
FuturesTradingParameter, FuturesTransactionCostModel,
};
pub use instrument::Instrument;
pub use metrics::{BacktestMetrics, compute_backtest_metrics};
pub use metrics::{
BacktestMetrics, RiskFreeRateContract, RiskFreeRateObservation, compute_backtest_metrics,
};
pub use platform_expr_strategy::{
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
PlatformPortfolioDrawdownControlConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency,
PlatformSelectionQuotePlan, PlatformStopTakeReferencePriceMode, PlatformTradeAction,
PlatformUniverseActionKind,
PlatformPortfolioDrawdownControlConfig, PlatformPositionTargetRule, PlatformRebalanceSchedule,
PlatformScheduleFrequency, PlatformSelectionQuotePlan, PlatformStopTakeReferencePriceMode,
PlatformTradeAction, PlatformUniverseActionKind,
};
pub use platform_runtime_schema::{
PLATFORM_RUNTIME_SCHEMA_VERSION, PlatformRuntimeSchema, reserved_scope_names,
@@ -67,9 +78,10 @@ pub use platform_strategy_spec::{
StrategyExpressionActionConfig, StrategyExpressionAllocationConfig,
StrategyExpressionOrderingConfig, StrategyExpressionRiskConfig,
StrategyExpressionScheduleConfig, StrategyExpressionSelectionConfig,
StrategyExpressionTradingConfig, StrategyPortfolioDrawdownControlConfig,
StrategyRuntimeEnvironment, StrategyRuntimeExpressions, StrategyRuntimeSpec,
platform_expr_config_from_spec, platform_expr_config_from_value,
StrategyExpressionTradingConfig, StrategyPortfolioDrawdownControlConfig, StrategyRebalanceSpec,
StrategyRiskPolicySpec, StrategyRuntimeEnvironment, StrategyRuntimeExpressions,
StrategyRuntimeSpec, StrategyUniverseSpec, platform_expr_config_from_spec,
platform_expr_config_from_value, validate_strategy_risk_policy_fields,
};
pub use portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position};
pub use risk_control::{
@@ -82,8 +94,8 @@ pub use scheduler::{
};
pub use strategy::{
AlgoOrderStyle, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, OmniMicroCapConfig,
OmniMicroCapStrategy, OpenOrderView, OrderIntent, OrderRuntimeView, PortfolioRuntimeView,
Strategy, StrategyContext, StrategyDecision, TargetPortfolioOrderPricing,
OmniMicroCapStrategy, OpenOrderView, OrderIntent, OrderRuntimeView, OrderTimeInForce,
PortfolioRuntimeView, Strategy, StrategyContext, StrategyDecision, TargetPortfolioOrderPricing,
};
pub use strategy_ai::{
ManualExample, ManualFactorSource, ManualField, ManualFieldGroup, ManualFunction,
+303 -48
View File
@@ -9,7 +9,28 @@ use crate::portfolio::HoldingSummary;
const TRADING_DAYS_PER_YEAR: f64 = 252.0;
const MONTHS_PER_YEAR: f64 = 12.0;
const DEFAULT_RISK_FREE_RATE: f64 = 0.022;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiskFreeRateObservation {
pub date: NaiveDate,
pub source_date: NaiveDate,
pub annual_rate: f64,
pub daily_rate: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiskFreeRateContract {
pub version: String,
pub source: String,
pub tenor: String,
pub periods_per_year: f64,
pub max_staleness_days: usize,
pub observed_max_staleness_days: usize,
pub sha256: String,
pub observations: Vec<RiskFreeRateObservation>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BacktestMetrics {
@@ -29,6 +50,7 @@ pub struct BacktestMetrics {
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,
@@ -55,6 +77,13 @@ pub struct BacktestMetrics {
pub excess_win_rate: f64,
pub monthly_sharpe: f64,
pub monthly_volatility: f64,
pub risk_free_rate_contract_version: String,
pub risk_free_rate_source: String,
pub risk_free_rate_tenor: String,
pub risk_free_rate_observation_count: usize,
pub risk_free_rate_max_staleness_days: usize,
pub risk_free_rate_observed_max_staleness_days: usize,
pub risk_free_rate_sha256: String,
}
pub fn compute_backtest_metrics(
@@ -63,30 +92,23 @@ pub fn compute_backtest_metrics(
daily_holdings: &[HoldingSummary],
account_events: &[AccountEvent],
initial_cash: f64,
) -> BacktestMetrics {
risk_free_contract: Option<&RiskFreeRateContract>,
) -> Result<BacktestMetrics, String> {
let Some(first_point) = equity_curve.first() else {
return BacktestMetrics {
risk_free_rate: DEFAULT_RISK_FREE_RATE,
return Ok(BacktestMetrics {
initial_cash,
..BacktestMetrics::default()
};
});
};
let Some(last_point) = equity_curve.last() else {
return BacktestMetrics {
risk_free_rate: DEFAULT_RISK_FREE_RATE,
return Ok(BacktestMetrics {
initial_cash,
..BacktestMetrics::default()
};
});
};
let trade_days = equity_curve.len();
let benchmark_start = if first_point.benchmark_prev_close.is_finite()
&& first_point.benchmark_prev_close > f64::EPSILON
{
first_point.benchmark_prev_close
} else {
first_point.benchmark_close
};
let benchmark_start = first_point.benchmark_reference_close();
let explicit_unit_nav = equity_curve.iter().any(|point| {
point.external_cash_flow.abs() > f64::EPSILON
|| (point.unit_nav.is_finite()
@@ -122,6 +144,7 @@ pub fn compute_backtest_metrics(
.zip(benchmark_returns.iter())
.map(|(lhs, rhs)| lhs - rhs)
.collect::<Vec<_>>();
let zero_risk_free_rates = vec![0.0; excess_returns.len()];
let benchmark_net_value = if benchmark_start.abs() < f64::EPSILON {
1.0
@@ -140,16 +163,28 @@ pub fn compute_backtest_metrics(
let annual_return = annualize_return(total_return, trade_days);
let excess_annual_return = annualize_return(excess_cumulative_return, trade_days);
let risk_free_rate = DEFAULT_RISK_FREE_RATE;
let daily_rf = risk_free_rate / TRADING_DAYS_PER_YEAR;
let sharpe = annualized_sharpe(&returns, daily_rf, TRADING_DAYS_PER_YEAR);
let sortino = annualized_sortino(&returns, daily_rf, TRADING_DAYS_PER_YEAR);
let information_ratio = annualized_sharpe(&excess_returns, 0.0, TRADING_DAYS_PER_YEAR);
let (daily_risk_free_rates, risk_free_metadata) =
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(
&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, 0.0, TRADING_DAYS_PER_YEAR);
let (alpha, beta) = alpha_beta(&returns, &benchmark_returns, daily_rf);
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);
let equity_nav = portfolio_nav;
let benchmark_nav_series = equity_curve
@@ -178,6 +213,8 @@ pub fn compute_backtest_metrics(
.zip(monthly_benchmark_returns.iter())
.map(|(lhs, rhs)| lhs - rhs)
.collect::<Vec<_>>();
let monthly_risk_free_returns =
group_monthly_risk_free_returns(equity_curve, &daily_risk_free_rates);
let monthly_excess_win_rate = ratio(
monthly_excess_returns
.iter()
@@ -187,7 +224,7 @@ pub fn compute_backtest_metrics(
);
let monthly_sharpe = annualized_sharpe(
&monthly_portfolio_returns,
risk_free_rate / MONTHS_PER_YEAR,
&monthly_risk_free_returns,
MONTHS_PER_YEAR,
);
let monthly_volatility = annualized_std(&monthly_portfolio_returns, MONTHS_PER_YEAR);
@@ -239,7 +276,7 @@ pub fn compute_backtest_metrics(
let total_trade_days = equity_by_date.len();
BacktestMetrics {
Ok(BacktestMetrics {
total_return,
annual_return,
sharpe,
@@ -256,6 +293,7 @@ pub fn compute_backtest_metrics(
max_drawdown_duration_days,
total_trade_days,
sortino,
downside_risk,
information_ratio,
tracking_error,
volatility,
@@ -285,7 +323,14 @@ pub fn compute_backtest_metrics(
excess_win_rate,
monthly_sharpe,
monthly_volatility,
}
risk_free_rate_contract_version: risk_free_metadata.version,
risk_free_rate_source: risk_free_metadata.source,
risk_free_rate_tenor: risk_free_metadata.tenor,
risk_free_rate_observation_count: daily_risk_free_rates.len(),
risk_free_rate_max_staleness_days: risk_free_metadata.max_staleness_days,
risk_free_rate_observed_max_staleness_days: risk_free_metadata.observed_max_staleness_days,
risk_free_rate_sha256: risk_free_metadata.sha256,
})
}
fn point_nav(point: &DailyEquityPoint, initial_cash: f64) -> f64 {
@@ -316,13 +361,106 @@ fn annualize_return(total_return: f64, periods: usize) -> f64 {
base.powf(TRADING_DAYS_PER_YEAR / periods) - 1.0
}
fn annualized_sharpe(returns: &[f64], daily_rf: f64, periods_per_year: f64) -> f64 {
if returns.len() < 2 {
fn aligned_daily_risk_free_rates(
equity_curve: &[DailyEquityPoint],
contract: Option<&RiskFreeRateContract>,
) -> Result<(Vec<f64>, RiskFreeRateContract), String> {
let Some(contract) = contract else {
return Ok((
vec![0.0; equity_curve.len()],
RiskFreeRateContract {
version: "not-configured".to_string(),
source: "not-configured".to_string(),
tenor: "NONE".to_string(),
periods_per_year: TRADING_DAYS_PER_YEAR,
..RiskFreeRateContract::default()
},
));
};
if contract.version.trim().is_empty()
|| contract.source.trim().is_empty()
|| contract.tenor.trim().is_empty()
|| contract.sha256.len() != 64
{
return Err("risk-free rate contract metadata is incomplete".to_string());
}
if contract.observations.len() != equity_curve.len() {
return Err(format!(
"risk-free rate observation count mismatch: expected={} actual={}",
equity_curve.len(),
contract.observations.len()
));
}
let mut rates = Vec::with_capacity(equity_curve.len());
for (point, observation) in equity_curve.iter().zip(&contract.observations) {
if observation.date != point.date {
return Err(format!(
"risk-free rate date mismatch: expected={} actual={}",
point.date, observation.date
));
}
if observation.source_date > observation.date {
return Err(format!(
"risk-free rate uses future observation: date={} source_date={}",
observation.date, observation.source_date
));
}
let staleness = observation
.date
.signed_duration_since(observation.source_date)
.num_days();
if staleness < 0 || staleness as usize > contract.max_staleness_days {
return Err(format!(
"risk-free rate observation is stale: date={} source_date={} days={}",
observation.date, observation.source_date, staleness
));
}
if !observation.annual_rate.is_finite()
|| observation.annual_rate <= -1.0
|| observation.annual_rate >= 1.0
|| !observation.daily_rate.is_finite()
|| observation.daily_rate <= -1.0
{
return Err(format!(
"risk-free rate observation is invalid: date={}",
observation.date
));
}
let periods_per_year =
if contract.periods_per_year.is_finite() && contract.periods_per_year > 0.0 {
contract.periods_per_year
} else {
TRADING_DAYS_PER_YEAR
};
let expected_daily = (1.0 + observation.annual_rate).powf(1.0 / periods_per_year) - 1.0;
if (expected_daily - observation.daily_rate).abs() > 1e-12 {
return Err(format!(
"risk-free daily conversion mismatch: date={} expected={} actual={}",
observation.date, expected_daily, observation.daily_rate
));
}
rates.push(observation.daily_rate);
}
Ok((rates, contract.clone()))
}
fn effective_annual_risk_free_rate(daily_rates: &[f64], periods_per_year: f64) -> f64 {
if daily_rates.is_empty() {
return 0.0;
}
let mean_log =
daily_rates.iter().map(|rate| rate.ln_1p()).sum::<f64>() / daily_rates.len() as 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()
.map(|value| value - daily_rf)
.zip(daily_risk_free_rates)
.map(|(value, risk_free)| value - risk_free)
.collect::<Vec<_>>();
let mean_ret = mean(&adjusted);
let std = std_dev(&adjusted);
@@ -333,23 +471,24 @@ fn annualized_sharpe(returns: &[f64], daily_rf: f64, periods_per_year: f64) -> f
}
}
fn annualized_sortino(returns: &[f64], daily_rf: f64, periods_per_year: f64) -> f64 {
if returns.is_empty() {
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()
.map(|value| value - daily_rf)
.zip(daily_risk_free_rates)
.map(|(value, risk_free)| value - risk_free)
.collect::<Vec<_>>();
let downside = adjusted
.iter()
.filter(|value| **value < 0.0)
.map(|value| value.powi(2))
.collect::<Vec<_>>();
if downside.is_empty() {
return 0.0;
}
let downside_dev = (downside.iter().sum::<f64>() / downside.len() as f64).sqrt();
.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 {
@@ -357,32 +496,60 @@ fn annualized_sortino(returns: &[f64], daily_rf: f64, periods_per_year: f64) ->
}
}
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 alpha_beta(returns: &[f64], benchmark_returns: &[f64], daily_rf: f64) -> (f64, f64) {
if returns.len() < 2 || returns.len() != benchmark_returns.len() {
fn alpha_beta(
returns: &[f64],
benchmark_returns: &[f64],
daily_risk_free_rates: &[f64],
) -> (f64, f64) {
if returns.len() < 2
|| returns.len() != benchmark_returns.len()
|| returns.len() != daily_risk_free_rates.len()
{
return (0.0, 0.0);
}
let strategy_excess = returns
.iter()
.map(|value| value - daily_rf)
.zip(daily_risk_free_rates)
.map(|(value, risk_free)| value - risk_free)
.collect::<Vec<_>>();
let benchmark_excess = benchmark_returns
.iter()
.map(|value| value - daily_rf)
.zip(daily_risk_free_rates)
.map(|(value, risk_free)| value - risk_free)
.collect::<Vec<_>>();
let mean_strategy = mean(&strategy_excess);
let mean_benchmark = mean(&benchmark_excess);
let variance_benchmark = variance(&benchmark_excess);
let mean_raw_strategy = mean(returns);
let mean_raw_benchmark = mean(benchmark_returns);
let variance_benchmark = variance(benchmark_returns);
if variance_benchmark <= f64::EPSILON {
return (0.0, 0.0);
}
let covariance = strategy_excess
let covariance = returns
.iter()
.zip(benchmark_excess.iter())
.map(|(lhs, rhs)| (lhs - mean_strategy) * (rhs - mean_benchmark))
.zip(benchmark_returns.iter())
.map(|(lhs, rhs)| (lhs - mean_raw_strategy) * (rhs - mean_raw_benchmark))
.sum::<f64>()
/ (strategy_excess.len() - 1) as f64;
let beta = covariance / variance_benchmark;
@@ -522,6 +689,26 @@ where
.collect()
}
fn group_monthly_risk_free_returns(
equity_curve: &[DailyEquityPoint],
daily_risk_free_rates: &[f64],
) -> Vec<f64> {
if equity_curve.len() != daily_risk_free_rates.len() {
return Vec::new();
}
let mut monthly_growth = BTreeMap::<(i32, u32), f64>::new();
for (point, daily_rate) in equity_curve.iter().zip(daily_risk_free_rates) {
let growth = monthly_growth
.entry((point.date.year(), point.date.month()))
.or_insert(1.0);
*growth *= 1.0 + daily_rate;
}
monthly_growth
.into_values()
.map(|growth| growth - 1.0)
.collect()
}
fn mean(values: &[f64]) -> f64 {
if values.is_empty() {
0.0
@@ -587,6 +774,7 @@ mod tests {
benchmark_prev_close: f64,
) -> DailyEquityPoint {
DailyEquityPoint {
signal_baseline: false,
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
cash: total_equity,
market_value: 0.0,
@@ -606,16 +794,26 @@ mod tests {
equity_point("2025-01-02", 100.0, 5797.089, 5957.717),
equity_point("2025-12-31", 120.0, 7595.285, 7597.299),
];
let metrics = compute_backtest_metrics(&curve, &[], &[], &[], 100.0);
let metrics = compute_backtest_metrics(&curve, &[], &[], &[], 100.0, None).unwrap();
let expected = 7595.285 / 5957.717 - 1.0;
assert!((metrics.benchmark_cumulative_return - expected).abs() < 1e-12);
}
#[test]
fn signal_baseline_uses_same_close_for_strategy_and_benchmark() {
let mut baseline=equity_point("2026-09-04",100.0,4548.0499,4552.5784);
baseline.signal_baseline=true;
let curve=vec![baseline,equity_point("2026-09-08",104.0,4558.7371,4575.0245)];
let metrics=compute_backtest_metrics(&curve,&[],&[],&[],100.0,None).unwrap();
assert!((metrics.benchmark_cumulative_return-(4558.7371/4548.0499-1.0)).abs()<1e-12);
}
#[test]
fn external_cash_flow_is_excluded_from_return_and_reported_separately() {
let curve = vec![
equity_point("2025-01-02", 100.0, 100.0, 100.0),
DailyEquityPoint {
signal_baseline: false,
date: NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(),
cash: 220.0,
market_value: 0.0,
@@ -635,9 +833,66 @@ mod tests {
total_equity: 200.0,
note: "deposit_withdraw amount=100.00 reason=test".to_string(),
}];
let metrics = compute_backtest_metrics(&curve, &[], &[], &events, 100.0);
let metrics = compute_backtest_metrics(&curve, &[], &[], &events, 100.0, None).unwrap();
assert!((metrics.total_return - 0.1).abs() < 1e-12);
assert!((metrics.unit_nav - 1.1).abs() < 1e-12);
assert!((metrics.external_cash_flow_total - 100.0).abs() < 1e-12);
}
#[test]
fn risk_adjusted_metrics_use_daily_pit_rates_and_all_period_downside() {
let curve = vec![
equity_point("2026-01-02", 101.0, 100.0, 100.0),
equity_point("2026-01-05", 98.98, 100.0, 100.0),
equity_point("2026-01-06", 100.4647, 100.0, 100.0),
equity_point("2026-01-07", 99.9623765, 100.0, 100.0),
];
let annual_rates = [0.012, 0.012, 0.013, 0.013];
let observations = curve
.iter()
.zip(annual_rates)
.map(|(point, annual_rate)| RiskFreeRateObservation {
date: point.date,
source_date: point.date,
annual_rate,
daily_rate: (1.0 + annual_rate).powf(1.0 / TRADING_DAYS_PER_YEAR) - 1.0,
})
.collect();
let contract = RiskFreeRateContract {
version: "cn-government-bond-3m-pit-daily/v1".to_string(),
source: "test".to_string(),
tenor: "3M".to_string(),
periods_per_year: TRADING_DAYS_PER_YEAR,
max_staleness_days: 15,
observed_max_staleness_days: 0,
sha256: "a".repeat(64),
observations,
};
let metrics =
compute_backtest_metrics(&curve, &[], &[], &[], 100.0, Some(&contract)).unwrap();
let returns = [0.01, -0.02, 0.015, -0.005];
let daily_rates = annual_rates
.map(|annual_rate| (1.0 + annual_rate).powf(1.0 / TRADING_DAYS_PER_YEAR) - 1.0);
let adjusted = returns
.iter()
.zip(daily_rates)
.map(|(value, risk_free)| value - risk_free)
.collect::<Vec<_>>();
let expected_sharpe = mean(&adjusted) / std_dev(&adjusted) * TRADING_DAYS_PER_YEAR.sqrt();
let downside = (adjusted
.iter()
.map(|value| value.min(0.0).powi(2))
.sum::<f64>()
/ 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_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);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -227,6 +227,8 @@ const RUNTIME_HELPER_FUNCTIONS: &[&str] = &[
"factor",
"day_factor",
"rolling_mean",
"pattern_signal",
"pattern_score",
"rolling_mean_current",
"rolling_max_current",
"rolling_return_stddev_current",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+146 -7
View File
@@ -3,6 +3,7 @@ use std::collections::BTreeSet;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::OrderSide;
use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField};
use crate::instrument::Instrument;
use crate::portfolio::Position;
@@ -77,6 +78,13 @@ impl Default for StaticRiskRuleConfig {
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TradingConstraintConfig {
/// Shared execution limits. These fields intentionally use the same
/// names and defaults as the FIDC trading-core RiskLimits contract so a
/// strategy cannot appear protected in paper/live while being unlimited
/// in a backtest.
pub max_order_quantity: f64,
pub max_order_notional: f64,
pub max_symbol_position: f64,
pub volume_limit_enabled: bool,
pub volume_percent: f64,
pub liquidity_limit_enabled: bool,
@@ -91,6 +99,9 @@ pub struct TradingConstraintConfig {
impl Default for TradingConstraintConfig {
fn default() -> Self {
Self {
max_order_quantity: 1_000_000.0,
max_order_notional: 100_000_000.0,
max_symbol_position: 10_000_000.0,
volume_limit_enabled: true,
volume_percent: 0.25,
liquidity_limit_enabled: true,
@@ -386,7 +397,7 @@ impl ChinaAShareRiskControl {
RiskCheckScope::Buy => config.static_rules.reject_kcb_buy,
RiskCheckScope::Sell => false,
};
if reject_kcb && (candidate.is_kcb || symbol_is_kcb(&candidate.symbol)) {
if reject_kcb && candidate.is_kcb {
return Some("kcb");
}
let reject_bjse = match scope {
@@ -402,7 +413,10 @@ impl ChinaAShareRiskControl {
RiskCheckScope::Buy => config.static_rules.reject_one_yuan_buy,
RiskCheckScope::Sell => false,
};
if reject_one_yuan && (candidate.is_one_yuan || market.day_open <= 1.0) {
if reject_one_yuan
&& (candidate.is_one_yuan
|| (market.day_open.is_finite() && market.day_open > 0.0 && market.day_open <= 1.0))
{
return Some("one_yuan");
}
if Self::missing_risk_state_rejected(candidate, config, scope) {
@@ -479,6 +493,36 @@ impl ChinaAShareRiskControl {
None
}
/// Apply the shared quantity/notional/position limits at the same stage
/// as paper/live `RiskLimits`. Static instrument rules remain in the
/// side-specific methods above; this helper only checks order sizing and
/// never changes selection semantics.
pub fn order_size_rejection_reason_with_config(
side: OrderSide,
requested_quantity: u32,
current_position_quantity: u32,
check_price: f64,
config: &FidcRiskControlConfig,
) -> Option<&'static str> {
let limits = &config.trading_constraints;
if (requested_quantity as f64) > limits.max_order_quantity {
return Some("quantity exceeds max_order_quantity");
}
if check_price.is_finite()
&& check_price > 0.0
&& (requested_quantity as f64) * check_price > limits.max_order_notional
{
return Some("notional exceeds max_order_notional");
}
if side == OrderSide::Buy
&& (current_position_quantity as f64) + (requested_quantity as f64)
> limits.max_symbol_position
{
return Some("position exceeds max_symbol_position");
}
None
}
pub fn sell_rejection_reason(
date: NaiveDate,
candidate: &CandidateEligibility,
@@ -515,6 +559,15 @@ impl ChinaAShareRiskControl {
) {
return Some(reason);
}
// When instrument metadata is unavailable, an explicit candidate
// lifecycle fact must still protect the sell path. Otherwise a
// `inactive_or_delisted` candidate could fall through to a synthetic
// sell price and violate the unresolved-delisted holding contract.
if let Some(reason) =
candidate_active_status_rejection(candidate, config, RiskCheckScope::Sell)
{
return Some(reason);
}
if config.static_rules.reject_paused_sell && (market.paused || candidate.is_paused) {
return Some("paused");
}
@@ -547,11 +600,6 @@ impl ChinaAShareRiskControl {
}
}
fn symbol_is_kcb(symbol: &str) -> bool {
let normalized = symbol.trim().to_ascii_uppercase();
(normalized.starts_with("688") || normalized.starts_with("689")) && normalized.ends_with(".SH")
}
fn symbol_is_bjse(symbol: &str) -> bool {
let normalized = symbol.trim().to_ascii_uppercase();
normalized.ends_with(".BJ") || normalized.ends_with(".BSE") || normalized.ends_with(".BE")
@@ -956,6 +1004,24 @@ mod tests {
assert_eq!(configured_reason, None);
}
#[test]
fn kcb_filter_uses_classification_instead_of_security_code() {
let date = d(2025, 1, 2);
let market = market(date, 6.27, 5.63);
let mut candidate = candidate(date);
let config = FidcRiskControlConfig::default();
for symbol in ["688001.SH", "689001.SH", "000001.SZ"] {
candidate.symbol = symbol.to_string();
for is_kcb in [false, true] {
candidate.is_kcb = is_kcb;
let reason = ChinaAShareRiskControl::buy_rejection_reason_with_config(
date, &candidate, &market, None, 6.27, &config,
);
assert_eq!(reason, is_kcb.then_some("kcb"), "{symbol}");
}
}
}
#[test]
fn st_and_star_st_filters_are_independent() {
let date = d(2025, 1, 2);
@@ -1086,6 +1152,7 @@ mod tests {
let date = d(2025, 1, 2);
let mut candidate = candidate(date);
candidate.symbol = "688506.SH".to_string();
candidate.is_kcb = true;
candidate.risk_level_code = Some("missing_risk_state".to_string());
let market = market(date, 6.27, 5.63);
let mut config = FidcRiskControlConfig::default();
@@ -1138,6 +1205,29 @@ mod tests {
);
}
#[test]
fn sell_rejects_explicit_candidate_inactive_status_without_instrument_row() {
let date = d(2025, 1, 2);
let mut candidate = candidate(date);
candidate.allow_sell = true;
candidate.risk_level_code = Some("inactive_or_delisted".to_string());
let market = market(date, 6.27, 5.63);
let position = position(d(2024, 12, 31));
assert_eq!(
ChinaAShareRiskControl::sell_rejection_reason_with_config(
date,
&candidate,
&market,
None,
Some(&position),
6.27,
&FidcRiskControlConfig::default(),
),
Some("inactive_or_delisted")
);
}
#[test]
fn missing_risk_state_default_selection_ignores_allow_flags_but_buy_rejects() {
let date = d(2025, 1, 2);
@@ -1396,4 +1486,53 @@ mod tests {
assert_eq!(enabled_reason, Some("lower_limit"));
assert_eq!(configured_reason, None);
}
#[test]
fn shared_order_size_limits_apply_to_both_sides_and_buy_position() {
let mut config = FidcRiskControlConfig::default();
config.trading_constraints.max_order_quantity = 500.0;
config.trading_constraints.max_order_notional = 5_000.0;
config.trading_constraints.max_symbol_position = 800.0;
assert_eq!(
ChinaAShareRiskControl::order_size_rejection_reason_with_config(
OrderSide::Buy,
600,
0,
5.0,
&config,
),
Some("quantity exceeds max_order_quantity")
);
assert_eq!(
ChinaAShareRiskControl::order_size_rejection_reason_with_config(
OrderSide::Sell,
400,
10_000,
20.0,
&config,
),
Some("notional exceeds max_order_notional")
);
assert_eq!(
ChinaAShareRiskControl::order_size_rejection_reason_with_config(
OrderSide::Buy,
300,
600,
5.0,
&config,
),
Some("position exceeds max_symbol_position")
);
assert_eq!(
ChinaAShareRiskControl::order_size_rejection_reason_with_config(
OrderSide::Sell,
200,
10_000,
5.0,
&config,
),
None
);
}
}
+42
View File
@@ -164,6 +164,16 @@ impl<'a> Scheduler<'a> {
.collect()
}
/// Evaluate only the trading-calendar frequency of a rule.
///
/// Strategy callbacks and order execution clocks are separate contracts:
/// a 15:00 schedule is still due on the same daily/weekly/monthly trading
/// date even when the engine's coarse `on_day` callback runs at another
/// default time. Exact clock matching remains in `triggered_rules_at`.
pub fn is_due_on(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
self.matches(date, rule)
}
fn matches(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
match &rule.frequency {
ScheduleFrequency::Daily => true,
@@ -265,6 +275,38 @@ mod tests {
])
}
#[test]
fn date_due_is_independent_from_the_order_execution_clock() {
let calendar = sample_calendar();
let scheduler = Scheduler::new(&calendar);
let daily = ScheduleRule::daily("close_signal", ScheduleStage::OnDay)
.with_time_rule(ScheduleTimeRule::physical_time(15, 0));
assert!(scheduler.is_due_on(d(2025, 1, 30), &daily));
assert!(scheduler.is_due_on(d(2025, 1, 31), &daily));
assert!(
scheduler
.triggered_rules_at(
d(2025, 1, 30),
ScheduleStage::OnDay,
Some(NaiveTime::from_hms_opt(15, 0, 0).unwrap()),
std::slice::from_ref(&daily),
)
.len()
== 1
);
assert!(
scheduler
.triggered_rules_at(
d(2025, 1, 30),
ScheduleStage::OnDay,
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
std::slice::from_ref(&daily),
)
.is_empty()
);
}
#[test]
fn scheduler_matches_daily_weekly_and_monthly_rules() {
let calendar = sample_calendar();
+321 -27
View File
@@ -10,6 +10,7 @@ use crate::data::{
};
use crate::engine::BacktestError;
use crate::events::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEvent};
use crate::fixed_point::FixedMoney;
use crate::futures::{FuturesAccountState, FuturesOrderIntent};
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
@@ -19,6 +20,12 @@ use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSe
pub trait Strategy {
fn name(&self) -> &str;
fn initial_subscriptions(&self) -> BTreeSet<String> {
BTreeSet::new()
}
fn requires_minute_callbacks(&self) -> bool {
true
}
fn management_fee(
&mut self,
_ctx: &StrategyContext<'_>,
@@ -82,7 +89,7 @@ pub trait Strategy {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct OpenOrderView {
pub order_id: u64,
pub symbol: String,
@@ -970,6 +977,7 @@ fn safe_ratio(numerator: f64, denominator: f64) -> f64 {
#[derive(Debug, Clone, Default)]
pub struct StrategyDecision {
pub buy_denials: BTreeMap<String, String>,
pub rebalance: bool,
pub target_weights: BTreeMap<String, f64>,
pub exit_symbols: BTreeSet<String>,
@@ -980,7 +988,20 @@ pub struct StrategyDecision {
}
impl StrategyDecision {
pub fn potential_buy_symbols(&self, open_orders: &[OpenOrderView]) -> BTreeSet<String> {
let mut symbols = BTreeSet::new();
if self.rebalance {
symbols.extend(self.target_weights.iter().filter(|(_, weight)| **weight > 0.0).map(|(symbol, _)| symbol.clone()));
}
for intent in &self.order_intents {
intent.collect_potential_buy_symbols(open_orders, &mut symbols);
}
symbols.retain(|symbol| !symbol.trim().is_empty());
symbols
}
pub fn merge_from(&mut self, mut other: StrategyDecision) {
self.buy_denials.append(&mut other.buy_denials);
self.rebalance |= other.rebalance;
self.target_weights.append(&mut other.target_weights);
self.exit_symbols.append(&mut other.exit_symbols);
@@ -991,7 +1012,8 @@ impl StrategyDecision {
}
pub fn is_empty(&self) -> bool {
!self.rebalance
self.buy_denials.is_empty()
&& !self.rebalance
&& self.target_weights.is_empty()
&& self.exit_symbols.is_empty()
&& self.order_intents.is_empty()
@@ -1007,6 +1029,35 @@ pub enum AlgoOrderStyle {
Twap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderTimeInForce {
Day,
Ioc,
Fok,
Gtc,
}
impl OrderTimeInForce {
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"day" => Some(Self::Day),
"ioc" | "immediate_or_cancel" | "immediate-or-cancel" => Some(Self::Ioc),
"fok" | "fill_or_kill" | "fill-or-kill" => Some(Self::Fok),
"gtc" | "good_til_canceled" | "good-til-canceled" => Some(Self::Gtc),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Day => "day",
Self::Ioc => "ioc",
Self::Fok => "fok",
Self::Gtc => "gtc",
}
}
}
#[derive(Debug, Clone)]
pub enum TargetPortfolioOrderPricing {
LimitPrices(BTreeMap<String, f64>),
@@ -1019,6 +1070,10 @@ pub enum TargetPortfolioOrderPricing {
#[derive(Debug, Clone)]
pub enum OrderIntent {
WithTimeInForce {
intent: Box<OrderIntent>,
time_in_force: OrderTimeInForce,
},
Shares {
symbol: String,
quantity: i32,
@@ -1130,6 +1185,12 @@ pub enum OrderIntent {
order_id: u64,
reason: String,
},
ModifyOrder {
order_id: u64,
new_total_quantity: Option<u32>,
new_limit_price: Option<f64>,
reason: String,
},
CancelSymbol {
symbol: String,
reason: String,
@@ -1167,6 +1228,137 @@ pub enum OrderIntent {
},
}
impl OrderIntent {
fn collect_potential_buy_symbols(&self, open_orders: &[OpenOrderView], symbols: &mut BTreeSet<String>) {
match self.unwrapped() {
Self::Shares { symbol, quantity, .. } | Self::LimitShares { symbol, quantity, .. } if *quantity > 0 => { symbols.insert(symbol.clone()); }
Self::Lots { symbol, lots, .. } | Self::LimitLots { symbol, lots, .. } if *lots > 0 => { symbols.insert(symbol.clone()); }
Self::TargetShares { symbol, target_quantity, .. } | Self::LimitTargetShares { symbol, target_quantity, .. } if *target_quantity > 0 => { symbols.insert(symbol.clone()); }
Self::Value { symbol, value, .. } | Self::LimitValue { symbol, value, .. } | Self::AlgoValue { symbol, value, .. } if *value > 0.0 => { symbols.insert(symbol.clone()); }
Self::Percent { symbol, percent, .. } | Self::LimitPercent { symbol, percent, .. } | Self::AlgoPercent { symbol, percent, .. } if *percent > 0.0 => { symbols.insert(symbol.clone()); }
Self::TargetValue { symbol, target_value, .. } | Self::LimitTargetValue { symbol, target_value, .. } | Self::TimedTargetValue { symbol, target_value, .. } if *target_value > 0.0 => { symbols.insert(symbol.clone()); }
Self::TargetPercent { symbol, target_percent, .. } | Self::LimitTargetPercent { symbol, target_percent, .. } if *target_percent > 0.0 => { symbols.insert(symbol.clone()); }
Self::TargetPortfolioSmart { target_weights, .. } => {
symbols.extend(target_weights.iter().filter(|(_, weight)| **weight > 0.0).map(|(symbol, _)| symbol.clone()));
}
Self::ModifyOrder { order_id, new_total_quantity, new_limit_price, .. } => {
if let Some(order) = open_orders.iter().find(|order| order.order_id == *order_id)
&& order.side == OrderSide::Buy
&& (new_total_quantity.is_some_and(|value| value > order.requested_quantity)
|| new_limit_price.is_some_and(|value| value > order.limit_price))
{
symbols.insert(order.symbol.clone());
}
}
Self::Shares { .. } | Self::LimitShares { .. }
| Self::Lots { .. } | Self::LimitLots { .. }
| Self::TargetShares { .. } | Self::LimitTargetShares { .. }
| Self::Value { .. } | Self::LimitValue { .. } | Self::AlgoValue { .. }
| Self::Percent { .. } | Self::LimitPercent { .. } | Self::AlgoPercent { .. }
| Self::TargetValue { .. } | Self::LimitTargetValue { .. } | Self::TimedTargetValue { .. }
| Self::TargetPercent { .. } | Self::LimitTargetPercent { .. }
| Self::CancelOrder { .. } | Self::CancelSymbol { .. } | Self::CancelAll { .. }
| Self::UpdateUniverse { .. } | Self::Subscribe { .. } | Self::Unsubscribe { .. }
| Self::DepositWithdraw { .. } | Self::FinanceRepay { .. } | Self::SetManagementFeeRate { .. }
| Self::Futures { .. } => {}
Self::WithTimeInForce { .. } => unreachable!("intent is unwrapped"),
}
}
pub fn with_time_in_force(self, time_in_force: OrderTimeInForce) -> Self {
match self {
Self::WithTimeInForce { intent, .. } => Self::WithTimeInForce {
intent,
time_in_force,
},
intent => Self::WithTimeInForce {
intent: Box::new(intent),
time_in_force,
},
}
}
pub fn time_in_force(&self) -> Option<OrderTimeInForce> {
match self {
Self::WithTimeInForce { time_in_force, .. } => Some(*time_in_force),
_ => None,
}
}
pub fn into_time_in_force_parts(self) -> (Self, Option<OrderTimeInForce>) {
match self {
Self::WithTimeInForce {
intent,
time_in_force,
} => (*intent, Some(time_in_force)),
intent => (intent, None),
}
}
pub fn apply_time_in_force(self, time_in_force: Option<OrderTimeInForce>) -> Self {
match time_in_force {
Some(time_in_force) => self.with_time_in_force(time_in_force),
None => self,
}
}
pub fn unwrapped(&self) -> &Self {
match self {
Self::WithTimeInForce { intent, .. } => intent.unwrapped(),
_ => self,
}
}
pub fn supports_time_in_force(&self, time_in_force: OrderTimeInForce) -> bool {
let intent = self.unwrapped();
if matches!(
intent,
Self::CancelOrder { .. }
| Self::ModifyOrder { .. }
| Self::CancelSymbol { .. }
| Self::CancelAll { .. }
| Self::UpdateUniverse { .. }
| Self::Subscribe { .. }
| Self::Unsubscribe { .. }
| Self::DepositWithdraw { .. }
| Self::FinanceRepay { .. }
| Self::SetManagementFeeRate { .. }
| Self::Futures { .. }
) {
return false;
}
let is_algo = matches!(
intent,
Self::AlgoValue { .. } | Self::AlgoPercent { .. } | Self::TimedTargetValue { .. }
) || matches!(
intent,
Self::TargetPortfolioSmart {
order_prices: Some(TargetPortfolioOrderPricing::AlgoOrder { .. }),
..
}
);
let is_limit = matches!(
intent,
Self::LimitShares { .. }
| Self::LimitLots { .. }
| Self::LimitTargetShares { .. }
| Self::LimitTargetValue { .. }
| Self::LimitValue { .. }
| Self::LimitPercent { .. }
| Self::LimitTargetPercent { .. }
| Self::TargetPortfolioSmart {
order_prices: Some(TargetPortfolioOrderPricing::LimitPrices(_)),
..
}
);
match time_in_force {
OrderTimeInForce::Day | OrderTimeInForce::Ioc => true,
OrderTimeInForce::Fok => !is_algo,
OrderTimeInForce::Gtc => is_limit,
}
}
}
#[derive(Debug, Clone)]
pub struct CnSmallCapRotationConfig {
pub strategy_name: String,
@@ -1428,6 +1620,7 @@ impl Strategy for CnSmallCapRotationStrategy {
if self.config.in_skip_window(ctx.decision_date) {
self.last_gross_exposure = Some(0.0);
return Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: true,
target_weights: BTreeMap::new(),
exit_symbols: ctx.portfolio.positions().keys().cloned().collect(),
@@ -1449,6 +1642,7 @@ impl Strategy for CnSmallCapRotationStrategy {
if message.contains("signal series insufficient") =>
{
return Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -1624,6 +1818,7 @@ impl Strategy for CnSmallCapRotationStrategy {
self.last_gross_exposure = Some(gross_exposure);
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance,
target_weights,
exit_symbols,
@@ -1744,14 +1939,47 @@ impl OmniMicroCapStrategy {
ChinaAShareCostModel::from_trading_constraints(self.config.risk_config.trading_constraints)
}
fn buy_commission(&self, gross_amount: f64) -> f64 {
self.cost_model().commission_for(gross_amount)
fn buy_cost(&self, gross_amount: f64) -> f64 {
let model = self.cost_model();
FixedMoney::checked_sum_f64([
model.commission_for(gross_amount),
model.transfer_fee_for(gross_amount),
])
.expect("projected buy costs must be finite fixed-point money")
.to_f64()
}
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
let model = self.cost_model();
model.commission_for(gross_amount)
+ model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
FixedMoney::checked_sum_f64([
model.commission_for(gross_amount),
model.stamp_tax_for(date, OrderSide::Sell, gross_amount),
model.transfer_fee_for(gross_amount),
])
.expect("projected sell costs must be finite fixed-point money")
.to_f64()
}
fn buy_cash_out(&self, gross_amount: f64) -> f64 {
FixedMoney::checked_sum_f64([gross_amount, self.buy_cost(gross_amount)])
.expect("projected buy cash must be finite fixed-point money")
.to_f64()
}
fn sell_net_cash(&self, date: NaiveDate, gross_amount: f64) -> f64 {
let gross = FixedMoney::from_f64(gross_amount)
.expect("projected sell gross must be finite fixed-point money");
gross
.checked_sub(
FixedMoney::from_f64(self.sell_cost(date, gross.to_f64()))
.expect("projected sell costs must be finite fixed-point money"),
)
.expect("projected sell proceeds underflow")
.to_f64()
}
fn fixed_cash_fits(value: f64, limit: f64) -> bool {
FixedMoney::f64_fits_within(value, limit).unwrap_or(false)
}
fn round_lot_quantity(
@@ -1820,7 +2048,7 @@ impl OmniMicroCapStrategy {
let mut quantity = self.round_lot_quantity((cash / sizing_price).floor() as u32, 100, 100);
while quantity > 0 {
let gross_amount = execution_price * quantity as f64;
if gross_amount + self.buy_commission(gross_amount) <= cash + 1e-6 {
if Self::fixed_cash_fits(self.buy_cash_out(gross_amount), cash) {
return quantity;
}
quantity = self.decrement_order_quantity(quantity, 100, 100);
@@ -1868,8 +2096,10 @@ impl OmniMicroCapStrategy {
);
while snapshot_requested_qty > 0 {
let gross_amount = sizing_price * snapshot_requested_qty as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out <= order_value + 1e-6 && cash_out <= projected.cash() + 1e-6 {
let cash_out = self.buy_cash_out(gross_amount);
if Self::fixed_cash_fits(cash_out, order_value)
&& Self::fixed_cash_fits(cash_out, projected.cash())
{
break;
}
snapshot_requested_qty = self.decrement_order_quantity(
@@ -1896,8 +2126,10 @@ impl OmniMicroCapStrategy {
let mut quantity = snapshot_requested_qty;
while quantity > 0 {
let gross_amount = projected_execution_price * quantity as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out <= order_value + 1e-6 && cash_out <= projected.cash() + 1e-6 {
let cash_out = self.buy_cash_out(gross_amount);
if Self::fixed_cash_fits(cash_out, order_value)
&& Self::fixed_cash_fits(cash_out, projected.cash())
{
break;
}
quantity =
@@ -1912,8 +2144,10 @@ impl OmniMicroCapStrategy {
.unwrap_or(projected_execution_price);
while quantity > 0 {
let gross_amount = execution_price * quantity as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out <= order_value + 1e-6 && cash_out <= projected.cash() + 1e-6 {
let cash_out = self.buy_cash_out(gross_amount);
if Self::fixed_cash_fits(cash_out, order_value)
&& Self::fixed_cash_fits(cash_out, projected.cash())
{
break;
}
quantity =
@@ -1928,11 +2162,15 @@ impl OmniMicroCapStrategy {
next_cursor: date.and_time(self.intraday_execution_start_time()) + Duration::seconds(1),
};
let gross_amount = fill.price * fill.quantity as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out > projected.cash() + 1e-6 || cash_out > order_value + 1e-6 {
let cash_out = self.buy_cash_out(gross_amount);
if !Self::fixed_cash_fits(cash_out, projected.cash())
|| !Self::fixed_cash_fits(cash_out, order_value)
{
return 0;
}
projected.apply_cash_delta(-cash_out);
projected
.apply_cash_delta(-cash_out)
.expect("projected buy cash must fit fixed-point ledger");
projected
.position_mut(symbol)
.buy(date, fill.quantity, fill.price);
@@ -1988,12 +2226,14 @@ impl OmniMicroCapStrategy {
+ Duration::seconds(1),
});
let gross_amount = fill.price * fill.quantity as f64;
let net_cash = gross_amount - self.sell_cost(date, gross_amount);
let net_cash = self.sell_net_cash(date, gross_amount);
projected
.position_mut(symbol)
.sell(fill.quantity, fill.price)
.ok()?;
projected.apply_cash_delta(net_cash);
projected
.apply_cash_delta(net_cash)
.expect("projected sell cash must fit fixed-point ledger");
*execution_state
.intraday_turnover
.entry(symbol.to_string())
@@ -2138,7 +2378,9 @@ impl OmniMicroCapStrategy {
);
while take_qty > 0 {
let candidate_gross = execution_price * take_qty as f64;
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
if gross_limit
.is_some_and(|limit| !Self::fixed_cash_fits(candidate_gross, limit))
{
take_qty = self.decrement_order_quantity(
take_qty,
minimum_order_quantity,
@@ -2146,9 +2388,8 @@ impl OmniMicroCapStrategy {
);
continue;
}
let candidate_cash =
candidate_gross + self.buy_commission(candidate_gross);
if candidate_cash <= cash + 1e-6 {
let candidate_cash = self.buy_cash_out(candidate_gross);
if Self::fixed_cash_fits(candidate_cash, cash) {
break;
}
take_qty = self.decrement_order_quantity(
@@ -2248,7 +2489,9 @@ impl OmniMicroCapStrategy {
if let Some(cash) = cash_limit {
while take_qty > 0 {
let candidate_gross = gross_amount + quote_price * take_qty as f64;
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
if gross_limit
.is_some_and(|limit| !Self::fixed_cash_fits(candidate_gross, limit))
{
take_qty = self.decrement_order_quantity(
take_qty,
minimum_order_quantity,
@@ -2256,7 +2499,7 @@ impl OmniMicroCapStrategy {
);
continue;
}
if candidate_gross + self.buy_commission(candidate_gross) <= cash + 1e-6 {
if Self::fixed_cash_fits(self.buy_cash_out(candidate_gross), cash) {
break;
}
take_qty = self.decrement_order_quantity(
@@ -2486,7 +2729,7 @@ impl OmniMicroCapStrategy {
date: NaiveDate,
) -> Vec<FidcRiskDecisionAudit> {
let mut decisions = Vec::new();
for factor in ctx.data.factor_snapshots_on(date) {
for factor in ctx.data.factor_snapshot_rows_on(date) {
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
continue;
}
@@ -2584,6 +2827,7 @@ impl Strategy for OmniMicroCapStrategy {
let lagged_execution = ctx.is_lagged_execution();
if self.config.in_skip_window(signal_date) {
return Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: ctx.portfolio.positions().keys().cloned().collect(),
@@ -2614,6 +2858,7 @@ impl Strategy for OmniMicroCapStrategy {
if message.contains("insufficient benchmark") =>
{
return Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -2824,6 +3069,7 @@ impl Strategy for OmniMicroCapStrategy {
];
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols,
@@ -2854,6 +3100,53 @@ mod tests {
use super::*;
use crate::{BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot};
#[test]
fn order_time_in_force_parsing_and_order_type_contract_are_explicit() {
assert_eq!(OrderTimeInForce::parse("DAY"), Some(OrderTimeInForce::Day));
assert_eq!(
OrderTimeInForce::parse("immediate_or_cancel"),
Some(OrderTimeInForce::Ioc)
);
assert_eq!(
OrderTimeInForce::parse("fill-or-kill"),
Some(OrderTimeInForce::Fok)
);
assert_eq!(
OrderTimeInForce::parse("good_til_canceled"),
Some(OrderTimeInForce::Gtc)
);
assert_eq!(OrderTimeInForce::parse("unknown"), None);
let market = OrderIntent::Shares {
symbol: "000001.SZ".to_string(),
quantity: 100,
reason: "market".to_string(),
};
assert!(market.supports_time_in_force(OrderTimeInForce::Day));
assert!(market.supports_time_in_force(OrderTimeInForce::Ioc));
assert!(market.supports_time_in_force(OrderTimeInForce::Fok));
assert!(!market.supports_time_in_force(OrderTimeInForce::Gtc));
let limit = OrderIntent::LimitShares {
symbol: "000001.SZ".to_string(),
quantity: 100,
limit_price: 10.0,
reason: "limit".to_string(),
};
assert!(limit.supports_time_in_force(OrderTimeInForce::Gtc));
let algo = OrderIntent::AlgoValue {
symbol: "000001.SZ".to_string(),
value: 10_000.0,
style: AlgoOrderStyle::Vwap,
start_time: None,
end_time: None,
reason: "algo".to_string(),
};
assert!(!algo.supports_time_in_force(OrderTimeInForce::Fok));
assert!(!algo.supports_time_in_force(OrderTimeInForce::Gtc));
}
#[test]
fn omni_microcap_projection_uses_configured_trading_cost() {
let mut cfg = OmniMicroCapConfig::omni_microcap();
@@ -2864,8 +3157,8 @@ mod tests {
.stamp_tax_rate_after_change = 0.0005;
let strategy = OmniMicroCapStrategy::new(cfg);
assert!((strategy.buy_commission(100_000.0) - 30.0).abs() < 1e-9);
assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9);
assert!((strategy.buy_cost(100_000.0) - 30.0).abs() < 1e-9);
assert!((strategy.buy_cost(1_000.0) - 5.0).abs() < 1e-9);
assert!(
(strategy.sell_cost(NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(), 100_000.0) - 80.0)
.abs()
@@ -2918,6 +3211,7 @@ mod tests {
pe_ttm: 12.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
})
.collect::<Vec<_>>();
+21 -8
View File
@@ -119,8 +119,8 @@ pub struct StrategyAiOptimizeRequest {
}
const PERFORMANCE_ACCEPTANCE_CONTRACT_PROMPT: &str = "收益验收合同:收益、回撤、年度收益、样本外区间及比较运算符只能来自用户目标、请求约束或不可变 candidate/promotion contract;不得注入 120% 或其他默认数值,也不得提高、降低或替换已经明确的门槛。没有明确数值合同时只做策略有效性、数据时序和风险审计,禁止声称收益已经达标;存在冻结合同时必须逐项按原运算符验证,不能只看总收益。";
const DEFAULT_RISK_POLICY_DSL_PROMPT: &str = "reject_st_selection=false、reject_st_buy=true、reject_star_st_selection=false、reject_star_st_buy=true、reject_paused_selection=false、reject_paused_buy=true、reject_paused_sell=true、reject_inactive_selection=false、reject_inactive_buy=true、reject_inactive_sell=true、reject_new_listing_selection=false、reject_new_listing_buy=true、reject_kcb_selection=false、reject_kcb_buy=true、reject_bjse_selection=false、reject_bjse_buy=true、reject_one_yuan_selection=false、reject_one_yuan_buy=true、respect_allow_buy_sell=true、reject_upper_limit_selection=false、reject_lower_limit_selection=false、reject_upper_limit_buy=true、reject_lower_limit_sell=true、forbid_same_day_rebuy_after_sell=true、blacklist_enabled=true、allow_market_orders=true、live_trading_enabled=false、volume_limit_enabled=true、liquidity_limit_enabled=true、volume_percent=0.25、commission_rate=0.0003、minimum_commission=5、stamp_tax_rate_before_change=0.001、stamp_tax_rate_after_change=0.0005、stamp_tax_change_date=\"2023-08-28\"";
const DEFAULT_RISK_POLICY_DSL_CODE: &str = "reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=true, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=true, allow_market_orders=true, live_trading_enabled=false, volume_limit_enabled=true, liquidity_limit_enabled=true, volume_percent=0.25, commission_rate=0.0003, minimum_commission=5, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\"";
const DEFAULT_RISK_POLICY_DSL_PROMPT: &str = "max_order_quantity=1000000、max_order_notional=100000000、max_symbol_position=10000000、reject_st_selection=false、reject_st_buy=true、reject_star_st_selection=false、reject_star_st_buy=true、reject_paused_selection=false、reject_paused_buy=true、reject_paused_sell=true、reject_inactive_selection=false、reject_inactive_buy=true、reject_inactive_sell=true、reject_new_listing_selection=false、reject_new_listing_buy=true、reject_kcb_selection=false、reject_kcb_buy=true、reject_bjse_selection=false、reject_bjse_buy=true、reject_one_yuan_selection=false、reject_one_yuan_buy=true、respect_allow_buy_sell=true、reject_upper_limit_selection=false、reject_lower_limit_selection=false、reject_upper_limit_buy=true、reject_lower_limit_sell=true、forbid_same_day_rebuy_after_sell=true、blacklist_enabled=true、allow_market_orders=true、live_trading_enabled=false、volume_limit_enabled=true、liquidity_limit_enabled=true、volume_percent=0.25、commission_rate=0.0003、minimum_commission=5、stamp_tax_rate_before_change=0.001、stamp_tax_rate_after_change=0.0005、stamp_tax_change_date=\"2023-08-28\"";
const DEFAULT_RISK_POLICY_DSL_CODE: &str = "max_order_quantity=1000000, max_order_notional=100000000, max_symbol_position=10000000, reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=true, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=true, allow_market_orders=true, live_trading_enabled=false, volume_limit_enabled=true, liquidity_limit_enabled=true, volume_percent=0.25, commission_rate=0.0003, minimum_commission=5, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\"";
pub fn built_in_strategy_manual() -> StrategyAiManual {
StrategyAiManual {
@@ -250,7 +250,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
},
ManualSection {
title: "risk.policy / risk.blacklist".to_string(),
detail: "统一配置 FIDC 基础风控。risk.policy(...) 支持 reject_st_selection、reject_st_buy、reject_star_st_selection、reject_star_st_buy、reject_paused_selection、reject_paused_buy、reject_paused_sell、reject_inactive_selection、reject_inactive_buy、reject_inactive_sell、reject_new_listing_selection、reject_new_listing_buy、reject_kcb_selection、reject_kcb_buy、reject_bjse_selection、reject_bjse_buy、reject_one_yuan_selection、reject_one_yuan_buy、respect_allow_buy_sell、reject_upper_limit_selection、reject_lower_limit_selection、reject_upper_limit_buy、reject_lower_limit_sell、forbid_same_day_rebuy_after_sell、blacklist_enabled、allow_market_orders、live_trading_enabled、blacklisted_symbols、volume_limit_enabled、liquidity_limit_enabled、volume_percent、commission_rate、minimum_commission、stamp_tax_rate_before_change、stamp_tax_rate_after_change、stamp_tax_change_date 等命名参数;risk.blacklist([\"600000.SH\"]) 写策略级黑名单。框架默认的 ST、*ST、停牌、退市、科创、北交所、一元、涨跌停、同日卖出禁买、黑名单、成交量和费用等基础风控必须走 risk.policy 或运行态 RiskLimits,不能被转换器隐式写进 universe.exclude 或 filter.stock_expr源策略明确写出的业务选股排除属于策略本身,必须原样保留在 filter.stock_expr,并且不能反向修改冻结的 reject_*_selection 开关。PG/Source Lake 是真相源,Redis 只可做当日锁、热配置缓存和配置变更通知。".to_string(),
detail: "统一配置 FIDC 基础风控。risk.policy(...) 支持 max_order_quantity、max_order_notional、max_symbol_position,以及 ST/*ST、停牌、退市、新股、科创、北交所、一元、涨跌停、同日卖出禁买、黑名单、成交量、流动性和交易成本等命名参数;risk.blacklist([\"600000.SH\"]) 写策略级黑名单。框架默认基础风控必须走 risk.policy 或运行态 RiskLimits,不能被转换器隐式写进 universe.exclude 或 filter.stock_expr;源策略明确写出的业务选股排除属于策略本身,必须原样保留不能反向修改冻结的 reject_*_selection 开关;冻结的 `reject_*_selection` 值不得改变。PG/Source Lake 是真相源,Redis 只可做当日锁、热配置缓存和配置变更通知。".to_string(),
},
ManualSection {
title: "corporate_actions.dividend_reinvestment".to_string(),
@@ -261,12 +261,20 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
detail: "设置回测全局撮合模式和滑点。日线回测只允许 execution.matching_type(\"current_bar_close\") 或 execution.matching_type(\"next_bar_open\")current_bar_close 使用决策日当日 closenext_bar_open 在 T 日收盘冻结目标金额或目标权益,并在下一可交易日按实际 open、滑点、手续费和证券数量步长重算股数,保证执行金额加手续费不超过分配金额;禁止把执行日 open/high/low/close 解释为下单前已知数据,也禁止用 T+1 prev_close 或 T 日估算股数直接成交;next_bar_open 的涨停买入和跌停卖出判断必须比较实际 open 成交价与涨跌停价,不能用执行日 close/last 或 next-close。金额预算始终严格,execution.strict_value_budget(false) 会被拒绝。分钟线回测使用当前分钟价格成交,只能写 execution.matching_type(\"minute_last\");不要把 vwap、twap、open_auction、minute_best_own、minute_best_counterparty 写成全局 matching_type,这些只属于显式订单或内部撮合能力。日线调仓现金口径由 execution.rebalance_cash_mode(\"sell_then_buy\" | \"same_point_net\" | \"pre_open_cash\") 或页面/API 参数控制,默认 sell_then_buysell_then_buy_delay_slippage_rate 只来自页面/API 执行参数,默认 0,不要写进策略表达式。滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 请求滑点率,例如 0.002) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(),
},
ManualSection {
title: "期货提交校验".to_string(),
detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、限价最小价位、price limit 校验,但默认全部开启".to_string(),
title: "期货 runtime action 与提交校验".to_string(),
detail: "runtimeExpressions.trading.actions 支持 futures_order、futures_open、futures_close、futures_close_today、futures_close_yesterday;字段包括 symbol、direction=long|short、quantityExpr/amountExpr、可选 limitPriceExpr、transactionCostExpr、whenExpr 和 reason。期货-only 策略把请求初始资金分配给期货账户且股票账户为0;股票+期货混合策略必须显式声明 futuresInitialCash,可选 stockInitialCash。合约必须先由 Source Lake 发布 futures_contract_daily、futures_contract_spec_history、futures_cost_margin_history 三张真实数据集;缺任一张时生成/回测必须失败,禁止手写默认乘数、保证金、费用或价格。订单进入撮合前继续检查上市/退市日期、停牌、trading_phase、限价 tick、涨跌停、反向挂单自成交、保证金和可平今昨仓".to_string(),
},
ManualSection {
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
detail: "支持显式下单、撤单、AlgoOrder、动态 universe 和账户资金动作。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 平台内核 的日内订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截,TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule,然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99)、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices={\"600000.SH\": open * 0.99}, valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])、account.deposit_withdraw(100000, receiving_days=0)、account.finance_repay(50000)、account.set_management_fee_rate(0.001)。其中 order.target_shares(...) 对应 平台内核 的 order_toorder.target_portfolio_smart(...) 对应 平台内核 的 order_target_portfolio_smart 批量目标权重语义;account.deposit_withdraw(...) 和 account.finance_repay(...) 对应 平台内核 账户出入金与融资/还款语义;order_prices 既可以是逐标的限价映射,也可以是 VWAPOrder/TWAPOrder 这类全局 AlgoOrderorder.vwap_* / order.twap_* 对应 平台内核 的 AlgoOrder 时间窗订单风格,而 update_universe/subscribe/unsubscribe 对应 平台内核 的动态 universe 与订阅接口。symbol 使用标准证券代码;数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(),
title: "trading.rotation / order.* / order.modify / cancel.* / update_universe / subscribe".to_string(),
detail: "支持股票显式下单、期货 runtime action、撤单、AlgoOrder、动态 universe 和账户资金动作。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 平台内核 的日内订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截,TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule,然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99, time_in_force=\"gtc\")、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])、account.deposit_withdraw(100000, receiving_days=0)、account.finance_repay(50000)、account.set_management_fee_rate(0.001)。股票订单和 target_portfolio_smart 支持可选关键字 time_in_force=\"day|ioc|fok|gtc\",编译后写入 runtimeExpressions.trading.actions[].timeInForceDAY 日内保留并在收盘 Expired,IOC 立即撤销未成交余量,FOK 必须全量可成交否则零成交,GTC 仅支持限价单并跨交易日保留;VWAP/TWAP 不接受 FOK/GTC。期货 action 必须由编译器写入结构化 runtimeExpressions,不得让策略源码直接构造 FuturesOrderIntent 或硬编码合约参数。symbol 使用标准证券/合约代码;数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(),
},
ManualSection {
title: "order.time_in_force target runtime scope".to_string(),
detail: "回测支持 DAY/IOC/FOK/GTCpaper/live 当前只支持 DAY/IOC/FOK。GTC 需要持久化跨交易日 parent/child 重挂账本和券商适配器能力,在该合同实现前只允许回测,paper/live 必须明确拒绝并禁止降级为 DAY。生成策略前必须按目标运行模式选择能力。".to_string(),
},
ManualSection {
title: "order.modify".to_string(),
detail: "回测中可用 order.modify(order_id, total_quantity=?, limit_price=?) 原位修改仍未完成的限价单。total_quantity 是新的总委托量而不是增量,不能低于已成交量;改价或增量会重置盘口队列优先级,减少总量且不改价保留优先级,同时保留 order_id、有效期、累计成交和费用状态。paper/live 在适配器提供持久且确认的 cancel-replace 合同前必须拒绝该动作,不得静默转换为撤单加新订单。".to_string(),
},
ManualSection {
title: "when / unless / else".to_string(),
@@ -404,7 +412,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
},
ManualFactorSource {
table: "期货交易参数".to_string(),
detail: "字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价最小价位校验".to_string(),
detail: "来自 futures_contract_spec_history 与 futures_cost_margin_history字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测按交易日选择不晚于当前日期的最新参数。schema catalog 未同时发布 futures_contract_daily、futures_contract_spec_history、futures_cost_margin_history 时,该能力视为不可用".to_string(),
fields: vec![],
},
],
@@ -672,6 +680,11 @@ mod tests {
assert!(markdown.contains("源策略明确写出的业务选股排除属于策略本身"));
assert!(markdown.contains("不能反向修改冻结的 reject_*_selection 开关"));
assert!(markdown.contains("冻结的 `reject_*_selection` 值不得改变"));
assert!(markdown.contains("time_in_force=\"day|ioc|fok|gtc\""));
assert!(markdown.contains("FOK 必须全量可成交否则零成交"));
assert!(markdown.contains("GTC 仅支持限价单并跨交易日保留"));
assert!(markdown.contains("paper/live 当前只支持 DAY/IOC/FOK"));
assert!(markdown.contains("paper/live 必须明确拒绝并禁止降级为 DAY"));
}
#[test]
+3 -2
View File
@@ -80,7 +80,7 @@ impl SelectionContext<'_> {
}
};
let mut decisions = Vec::new();
for factor in self.data.factor_snapshots_on(self.decision_date) {
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))
@@ -213,7 +213,7 @@ impl UniverseSelector for DynamicMarketCapBandSelector {
risk_decisions: Vec::new(),
};
diagnostics.factor_total = ctx.data.factor_snapshots_on(ctx.decision_date).len();
diagnostics.factor_total = ctx.data.factor_snapshot_rows_on(ctx.decision_date).len();
diagnostics.risk_decisions = ctx.selection_risk_decisions();
diagnostics.not_eligible_count = diagnostics.risk_decisions.len();
diagnostics.paused_count = diagnostics
@@ -339,6 +339,7 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: Some(0.01),
effective_turnover_ratio: Some(0.01),
adjustment_factor_backward1: None,
extra_factors: Default::default(),
}
}
+29 -10
View File
@@ -49,14 +49,30 @@ fn portfolio_settles_cash_receivable_on_payable_date() {
amount: 500.0,
reason: "cash_dividend 0.5".to_string(),
});
portfolio.add_cash_receivable(CashReceivable {
symbol: "000002.SZ".to_string(),
ex_date: d(2025, 1, 2),
payable_date: d(2025, 1, 5),
amount: 250.0,
reason: "cash_dividend 0.25".to_string(),
});
let settled_early = portfolio.settle_cash_receivables(d(2025, 1, 4));
assert!(settled_early.is_empty());
let due_early = portfolio.take_due_cash_receivables(d(2025, 1, 4));
assert!(due_early.is_empty());
assert!((portfolio.cash() - 1_000_000.0).abs() < 1e-9);
let settled = portfolio.settle_cash_receivables(d(2025, 1, 5));
assert_eq!(settled.len(), 1);
assert!((portfolio.cash() - 1_000_500.0).abs() < 1e-9);
let due = portfolio.take_due_cash_receivables(d(2025, 1, 5));
assert_eq!(due.len(), 2);
let mut cash_chain = Vec::new();
for receivable in &due {
let cash_before = portfolio.cash();
portfolio.settle_cash_receivable(receivable).unwrap();
cash_chain.push((cash_before, portfolio.cash()));
}
assert_eq!(
cash_chain,
vec![(1_000_000.0, 1_000_500.0), (1_000_500.0, 1_000_750.0)]
);
assert!(portfolio.cash_receivables().is_empty());
}
@@ -74,6 +90,7 @@ impl Strategy for BuyAndHoldStrategy {
ctx: &StrategyContext<'_>,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -128,6 +145,7 @@ fn stock_factor_snapshot(date: NaiveDate) -> DailyFactorSnapshot {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}
}
@@ -254,6 +272,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -264,6 +283,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -274,6 +294,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
],
@@ -368,11 +389,9 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
first_date: buy_date,
},
BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel {
commission_rate: 0.0008,
minimum_commission: 0.0,
..ChinaAShareCostModel::default()
},
ChinaAShareCostModel::default()
.with_commission_rate(0.0008)
.with_minimum_commission(0.0),
ChinaEquityRuleHooks::default(),
PriceField::Open,
),
@@ -5,6 +5,7 @@ use fidc_core::{
IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext,
StrategyDecision,
};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, Mutex};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -62,6 +63,191 @@ impl Strategy for DecisionQuoteReader {
}
}
struct NoLoaderDecisionQuoteStrategy {
symbol_plan_calls: Arc<Mutex<usize>>,
}
impl Strategy for NoLoaderDecisionQuoteStrategy {
fn name(&self) -> &str {
"no_loader_decision_quote_strategy"
}
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> {
*self
.symbol_plan_calls
.lock()
.expect("symbol plan counter mutex") += 1;
Ok(BTreeSet::new())
}
}
fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
DataSet::from_components(
Vec::new(),
vec![DailyMarketSnapshot {
date,
symbol: "000001.SZ".to_string(),
timestamp: Some(format!("{date} 15:00:00")),
day_open: 10.0,
open: 10.0,
high: 10.2,
low: 9.9,
close: 10.0,
last_price: 10.0,
bid1: 10.0,
ask1: 10.0,
prev_close: 9.8,
volume: 10_000,
minute_volume: 1_000,
bid1_volume: 10_000,
ask1_volume: 10_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 10.78,
lower_limit: 8.82,
price_tick: 0.01,
}],
vec![DailyFactorSnapshot {
date,
symbol: "000001.SZ".to_string(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: Default::default(),
}],
vec![CandidateEligibility {
date,
symbol: "000001.SZ".to_string(),
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,
}],
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1001.0,
prev_close: 999.0,
volume: 1_000_000,
}],
)
.expect("dataset")
}
#[test]
fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
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_matching_type(MatchingType::CurrentBarClose);
let config = BacktestConfig {
initial_cash: 10_000.0,
benchmark_code: "000852.SH".to_string(),
start_date: Some(date),
end_date: Some(date),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Close,
};
let symbol_plan_calls = Arc::new(Mutex::new(0usize));
let loader_calls = Arc::new(Mutex::new(0usize));
let strategy = NoLoaderDecisionQuoteStrategy {
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 {
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".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"
);
assert_eq!(
*loader_calls.lock().expect("loader counter mutex"),
1,
"the supplied symbols must still pass through the normal quote loader"
);
}
#[test]
fn engine_skips_decision_quote_symbol_plan_without_loader() {
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_matching_type(MatchingType::CurrentBarClose);
let config = BacktestConfig {
initial_cash: 10_000.0,
benchmark_code: "000852.SH".to_string(),
start_date: Some(date),
end_date: Some(date),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Close,
};
let symbol_plan_calls = Arc::new(Mutex::new(0usize));
let strategy = NoLoaderDecisionQuoteStrategy {
symbol_plan_calls: Arc::clone(&symbol_plan_calls),
};
let mut engine = BacktestEngine::new(data, strategy, broker, config);
engine.run().expect("backtest should run");
assert_eq!(
*symbol_plan_calls.lock().expect("symbol plan counter mutex"),
0,
"a preloaded/no-loader engine cannot use a newly computed quote symbol plan"
);
}
#[test]
fn engine_preloads_declared_decision_quotes_for_current_positions() {
let first = d(2026, 1, 5);
@@ -125,6 +311,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: Default::default(),
},
DailyFactorSnapshot {
@@ -135,6 +322,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: Default::default(),
},
],
@@ -293,6 +481,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: Default::default(),
},
DailyFactorSnapshot {
@@ -303,6 +492,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: Default::default(),
},
],
@@ -526,6 +716,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: Default::default(),
},
DailyFactorSnapshot {
@@ -536,6 +727,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: Default::default(),
},
],
+8
View File
@@ -24,6 +24,7 @@ impl Strategy for BuyThenHoldStrategy {
) -> Result<StrategyDecision, fidc_core::BacktestError> {
if ctx.decision_date == d(2025, 1, 2) && ctx.portfolio.position("000001.SZ").is_none() {
return Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -170,6 +171,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -180,6 +182,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -190,6 +193,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -200,6 +204,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
],
@@ -436,6 +441,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -446,6 +452,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -456,6 +463,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
],
+378 -27
View File
@@ -1,18 +1,21 @@
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use chrono::{NaiveDate, NaiveDateTime};
use fidc_core::{
BacktestConfig, BacktestEngine, BacktestProcessMod, BacktestProcessModLoader,
BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FuturesAccountState,
BacktestTerminalAssetClass, BacktestTerminalStatus, BenchmarkSnapshot, BrokerSimulator,
CandidateEligibility, ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot,
DailyMarketSnapshot, DataSet, ExecutionQuoteRequest, FuturesAccountState,
FuturesCommissionType, FuturesContractSpec, FuturesDirection, FuturesOrderIntent,
FuturesTradingParameter, FuturesValidationConfig, Instrument, IntradayExecutionQuote,
IntradayOrderBookDepthLevel, MatchingType, OpenOrderView, OrderIntent, OrderSide, OrderStatus,
PlatformExprStrategy, PlatformExprStrategyConfig, PortfolioState, PriceField, ProcessEvent,
ProcessEventBus, ProcessEventKind, ScheduleRule, ScheduleStage, ScheduleTimeRule, Strategy,
StrategyContext, StrategyDecision,
FuturesPositionEffect, FuturesTradingParameter, FuturesValidationConfig, Instrument,
IntradayExecutionQuote, IntradayOrderBookDepthLevel, MatchingType, NumericFactorMap,
OpenOrderView, OrderIntent, OrderSide, OrderStatus, PlatformExprStrategy,
PlatformExprStrategyConfig, PlatformTradeAction, PortfolioState, PriceField, ProcessEvent,
ProcessEventBus, ProcessEventKind, ProcessEventRetention, ScheduleRule, ScheduleStage,
ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision,
};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -75,6 +78,7 @@ fn single_day_anchor_data(date: NaiveDate) -> DataSet {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}],
vec![CandidateEligibility {
@@ -131,7 +135,7 @@ fn market_row(date: NaiveDate, symbol: &str, open: f64, close: f64) -> DailyMark
fn factor_row(
date: NaiveDate,
symbol: &str,
extra_factors: BTreeMap<String, f64>,
extra_factors: NumericFactorMap,
) -> DailyFactorSnapshot {
DailyFactorSnapshot {
date,
@@ -141,6 +145,7 @@ fn factor_row(
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors,
}
}
@@ -207,26 +212,26 @@ fn two_day_futures_data() -> DataSet {
d1,
"000001.SZ",
BTreeMap::from([
("custom_alpha".to_string(), 7.0),
("margin_all".to_string(), 1.0),
("yield_curve_1y".to_string(), 0.02),
("total_shares".to_string(), 123.0),
("stock_connect_north_bound".to_string(), 1.0),
("industry_citics_l1".to_string(), 10.0),
("fundamental_net_profit".to_string(), 99.0),
("custom_alpha".into(), 7.0),
("margin_all".into(), 1.0),
("yield_curve_1y".into(), 0.02),
("total_shares".into(), 123.0),
("stock_connect_north_bound".into(), 1.0),
("industry_citics_l1".into(), 10.0),
("fundamental_net_profit".into(), 99.0),
]),
),
factor_row(
d2,
"000001.SZ",
BTreeMap::from([
("custom_alpha".to_string(), 8.0),
("margin_all".to_string(), 1.0),
("yield_curve_1y".to_string(), 0.021),
("total_shares".to_string(), 124.0),
("stock_connect_north_bound".to_string(), 1.0),
("industry_citics_l1".to_string(), 10.0),
("fundamental_net_profit".to_string(), 101.0),
("custom_alpha".into(), 8.0),
("margin_all".into(), 1.0),
("yield_curve_1y".into(), 0.021),
("total_shares".into(), 124.0),
("stock_connect_north_bound".into(), 1.0),
("industry_citics_l1".into(), 10.0),
("fundamental_net_profit".into(), 101.0),
]),
),
],
@@ -290,6 +295,7 @@ impl Strategy for HookProbeStrategy {
.borrow_mut()
.push(format!("on_day:{}", ctx.execution_date));
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -329,6 +335,7 @@ impl Strategy for AuctionOrderStrategy {
_ctx: &StrategyContext<'_>,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -372,6 +379,7 @@ impl Strategy for FuturesOrderStrategy {
return Ok(StrategyDecision::default());
}
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -634,6 +642,9 @@ struct UniverseDirectiveStrategy {
struct MinuteProbeStrategy {
seen_ticks: Rc<RefCell<Vec<String>>>,
scheduled_count: Rc<RefCell<usize>>,
subscribe_symbols: BTreeSet<String>,
minute_callbacks: bool,
ordered: bool,
}
@@ -674,6 +685,8 @@ impl Strategy for ScheduledProbeStrategy {
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
ScheduleRule::monthly("first_trading_day_on_day", 1, ScheduleStage::OnDay)
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
ScheduleRule::daily("daily_on_day_close", ScheduleStage::OnDay)
.with_time_rule(ScheduleTimeRule::physical_time(15, 0)),
]
}
@@ -710,6 +723,7 @@ impl Strategy for LimitCarryStrategy {
}
self.issued = true;
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -793,6 +807,7 @@ impl Strategy for UniverseDirectiveStrategy {
_ => Vec::new(),
};
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -809,16 +824,37 @@ impl Strategy for MinuteProbeStrategy {
"minute-probe"
}
fn requires_minute_callbacks(&self) -> bool {
self.minute_callbacks
}
fn schedule_rules(&self) -> Vec<ScheduleRule> {
vec![
ScheduleRule::daily("minute_barrier", ScheduleStage::Minute)
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
]
}
fn on_scheduled(
&mut self,
_ctx: &StrategyContext<'_>,
_rule: &ScheduleRule,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
*self.scheduled_count.borrow_mut() += 1;
Ok(StrategyDecision::default())
}
fn on_day(
&mut self,
_ctx: &StrategyContext<'_>,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
order_intents: vec![OrderIntent::Subscribe {
symbols: BTreeSet::from(["000001.SZ".to_string()]),
symbols: self.subscribe_symbols.clone(),
reason: "subscribe_minute_probe".to_string(),
}],
notes: Vec::new(),
@@ -855,6 +891,7 @@ impl Strategy for MinuteProbeStrategy {
}
self.ordered = true;
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -957,6 +994,7 @@ impl Strategy for OrderInspectionStrategy {
_ctx: &StrategyContext<'_>,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -1000,6 +1038,7 @@ impl Strategy for AccountFlowStrategy {
return Ok(StrategyDecision::default());
}
Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
@@ -1109,6 +1148,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -1119,6 +1159,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
],
@@ -1171,6 +1212,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
)
.expect("dataset");
let compact_data = data.clone();
let log = Rc::new(RefCell::new(Vec::new()));
let strategy = HookProbeStrategy { log: log.clone() };
let broker = BrokerSimulator::new_with_execution_price(
@@ -1210,6 +1252,48 @@ fn engine_runs_strategy_hooks_in_daily_order() {
]
);
assert_eq!(result.process_events.len(), 36);
let compact_strategy = HookProbeStrategy {
log: Rc::new(RefCell::new(Vec::new())),
};
let compact_broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
let mut compact_engine = BacktestEngine::new(
compact_data,
compact_strategy,
compact_broker,
BacktestConfig {
initial_cash: 100_000.0,
benchmark_code: "000300.SH".to_string(),
start_date: Some(date1),
end_date: Some(date2),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Open,
},
)
.with_process_event_retention(ProcessEventRetention::Business);
let compact_result = compact_engine.run().expect("compact backtest succeeds");
assert!(
compact_result
.process_events
.iter()
.all(|event| event.kind.is_business_lifecycle())
);
assert!(
compact_result
.process_events
.iter()
.any(|event| event.kind == ProcessEventKind::OnDay)
);
assert!(
!compact_result
.process_events
.iter()
.any(|event| event.kind == ProcessEventKind::PreBeforeTrading)
);
assert_eq!(
result.process_events[..18]
.iter()
@@ -1282,6 +1366,7 @@ fn engine_executes_open_auction_decisions_before_on_day() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}],
vec![CandidateEligibility {
@@ -1382,6 +1467,7 @@ fn engine_executes_futures_order_intents_against_future_account() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}],
vec![CandidateEligibility {
@@ -1449,6 +1535,73 @@ fn engine_executes_futures_order_intents_against_future_account() {
assert!((futures_account.cash() - 355_988.0).abs() < 1e-6);
}
#[test]
fn platform_runtime_actions_execute_generic_futures_open_and_close() {
let mut cfg = PlatformExprStrategyConfig::generic();
cfg.signal_symbol = "000001.SZ".to_string();
cfg.benchmark_symbol = "000300.SH".to_string();
cfg.rotation_enabled = false;
cfg.benchmark_short_ma_days = 1;
cfg.benchmark_long_ma_days = 1;
cfg.explicit_actions = vec![
PlatformTradeAction::Futures {
symbol: "IF2501".to_string(),
direction: FuturesDirection::Long,
effect: FuturesPositionEffect::Open,
quantity_expr: "1".to_string(),
limit_price_expr: None,
transaction_cost_expr: None,
when_expr: Some("decision_date == \"2025-01-02\"".to_string()),
reason: "generic futures open".to_string(),
},
PlatformTradeAction::Futures {
symbol: "IF2501".to_string(),
direction: FuturesDirection::Long,
effect: FuturesPositionEffect::Close,
quantity_expr: "1".to_string(),
limit_price_expr: None,
transaction_cost_expr: None,
when_expr: Some("decision_date == \"2025-01-03\"".to_string()),
reason: "generic futures close".to_string(),
},
];
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
let mut engine = BacktestEngine::new(
two_day_futures_data(),
PlatformExprStrategy::new(cfg),
broker,
BacktestConfig {
initial_cash: 100_000.0,
benchmark_code: "000300.SH".to_string(),
start_date: Some(d(2025, 1, 2)),
end_date: Some(d(2025, 1, 3)),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Open,
},
)
.with_futures_initial_cash(500_000.0);
let result = engine.run().expect("generic futures actions execute");
let futures_fills = result
.fills
.iter()
.filter(|fill| fill.symbol == "IF2501")
.collect::<Vec<_>>();
assert_eq!(futures_fills.len(), 2);
assert!((futures_fills[0].price - 4000.0).abs() < 1e-12);
assert!((futures_fills[0].commission - 2.5).abs() < 1e-12);
assert!((futures_fills[1].price - 3988.0).abs() < 1e-12);
assert!((futures_fills[1].commission - 2.0).abs() < 1e-12);
let futures_account = engine.futures_account().expect("future account");
assert!(futures_account.positions().is_empty());
assert!((futures_account.total_cash() - 496_395.5).abs() < 1e-12);
}
#[test]
fn engine_settles_configured_futures_expiration_at_settlement() {
let date = d(2025, 1, 2);
@@ -1524,7 +1677,9 @@ fn engine_aggregates_futures_account_into_nav_and_metrics() {
assert_eq!(result.metrics.initial_cash, 600_000.0);
assert!((result.equity_curve[0].total_equity - 599_988.0).abs() < 1e-6);
assert!((result.equity_curve[0].unit_nav - 0.99998).abs() < 1e-12);
assert!((result.metrics.total_assets - 599_988.0).abs() < 1e-6);
assert!((result.metrics.total_return + 0.00002).abs() < 1e-12);
assert_eq!(result.analyzer_report().trades.len(), result.fills.len());
assert_eq!(result.analyzer_report().monthly_returns.len(), 1);
assert_eq!(
@@ -1587,6 +1742,61 @@ fn engine_matches_pending_futures_limit_order_with_data_driven_costs() {
.expect("long futures position");
assert_eq!(position.quantity, 2);
assert!((position.contract_multiplier - 300.0).abs() < 1e-6);
assert_eq!(result.terminal_audit.status, BacktestTerminalStatus::Clean);
assert_eq!(result.terminal_audit.open_order_count(), 0);
}
#[test]
fn engine_reports_pending_futures_order_at_backtest_boundary() {
let date = d(2025, 1, 2);
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Open,
);
let mut engine = BacktestEngine::new(
two_day_futures_data(),
FuturesLimitOrderStrategy,
broker,
BacktestConfig {
initial_cash: 100_000.0,
benchmark_code: "000300.SH".to_string(),
start_date: Some(date),
end_date: Some(date),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Open,
},
)
.with_futures_initial_cash(1_000_000.0);
let result = engine.run().expect("backtest succeeds");
assert!(result.fills.is_empty());
assert_eq!(
result.terminal_audit.status,
BacktestTerminalStatus::CompletedWithPendingState
);
assert_eq!(result.terminal_audit.last_execution_date, Some(date));
assert_eq!(result.terminal_audit.stock_open_order_count, 0);
assert_eq!(result.terminal_audit.futures_open_order_count, 1);
assert_eq!(result.terminal_audit.open_order_count(), 1);
assert_eq!(result.terminal_audit.omitted_open_order_count, 0);
assert_eq!(result.terminal_audit.open_order_samples.len(), 1);
assert_eq!(
result.terminal_audit.open_order_samples[0].asset_class,
BacktestTerminalAssetClass::Futures
);
assert_eq!(result.terminal_audit.open_order_samples[0].symbol, "IF2501");
assert_eq!(
result.terminal_audit.open_order_samples[0].remaining_quantity,
2
);
assert!(
result
.order_events
.iter()
.any(|event| { event.symbol == "IF2501" && event.status == OrderStatus::Pending })
);
}
#[test]
@@ -1973,6 +2183,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}],
vec![CandidateEligibility {
@@ -2011,6 +2222,19 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
amount_delta: 10_200.0,
trading_phase: Some("continuous".to_string()),
},
IntradayExecutionQuote {
date,
symbol: "000002.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 18, 0),
last_price: 20.4,
bid1: 20.3,
ask1: 20.4,
bid1_volume: 1_000,
ask1_volume: 1_000,
volume_delta: 1_000,
amount_delta: 20_400.0,
trading_phase: Some("continuous".to_string()),
},
IntradayExecutionQuote {
date,
symbol: "000001.SZ".to_string(),
@@ -2029,8 +2253,12 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
.expect("dataset");
let seen_ticks = Rc::new(RefCell::new(Vec::new()));
let scheduled_count = Rc::new(RefCell::new(0usize));
let strategy = MinuteProbeStrategy {
seen_ticks: seen_ticks.clone(),
scheduled_count: scheduled_count.clone(),
subscribe_symbols: BTreeSet::from(["000001.SZ".to_string(), "000002.SZ".to_string()]),
minute_callbacks: true,
ordered: false,
};
let broker = BrokerSimulator::new_with_execution_price(
@@ -2038,6 +2266,8 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
ChinaEquityRuleHooks::default(),
PriceField::Last,
);
let loader_requests = Arc::new(Mutex::new(Vec::<ExecutionQuoteRequest>::new()));
let loader_requests_for_callback = Arc::clone(&loader_requests);
let mut engine = BacktestEngine::new(
data,
strategy,
@@ -2050,7 +2280,11 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
decision_lag_trading_days: 0,
execution_price_field: PriceField::Last,
},
);
)
.with_execution_quote_loader(move |request| {
loader_requests_for_callback.lock().unwrap().push(request);
Ok(Vec::new())
});
let result = engine.run().expect("backtest run");
@@ -2058,9 +2292,19 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
seen_ticks.borrow().as_slice(),
[
"000001.SZ:10:18:00:true:visible=10.20:previous=",
"000002.SZ:10:18:00:true:visible=20.40:previous=",
"000001.SZ:10:19:00:true:visible=10.20,10.30:previous=10.20"
]
);
assert_eq!(*scheduled_count.borrow(), 1);
let loader_requests = loader_requests.lock().unwrap();
assert_eq!(loader_requests.len(), 1);
assert_eq!(loader_requests[0].start_time, None);
assert_eq!(loader_requests[0].end_time, None);
assert_eq!(
loader_requests[0].symbols,
BTreeSet::from(["000001.SZ".to_string(), "000002.SZ".to_string()])
);
assert_eq!(result.fills.len(), 1);
assert_eq!(result.fills[0].reason, "minute_buy");
assert_eq!(result.fills[0].quantity, 100);
@@ -2082,6 +2326,90 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
.iter()
.any(|event| event.kind == ProcessEventKind::PostMinute)
);
assert_eq!(
result
.process_events
.iter()
.filter(|event| event.kind == ProcessEventKind::PreMinute)
.count(),
2
);
}
#[test]
fn engine_skips_empty_platform_style_minute_callbacks_between_schedule_times() {
let date = d(2025, 1, 2);
let mut data = single_day_anchor_data(date);
data.add_execution_quotes(vec![
IntradayExecutionQuote {
date,
symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 18, 0),
last_price: 10.2,
bid1: 10.1,
ask1: 10.2,
bid1_volume: 1_000,
ask1_volume: 1_000,
volume_delta: 1_000,
amount_delta: 10_200.0,
trading_phase: Some("continuous".to_string()),
},
IntradayExecutionQuote {
date,
symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 19, 0),
last_price: 10.3,
bid1: 10.2,
ask1: 10.3,
bid1_volume: 1_000,
ask1_volume: 1_000,
volume_delta: 1_000,
amount_delta: 10_300.0,
trading_phase: Some("continuous".to_string()),
},
]);
let seen_ticks = Rc::new(RefCell::new(Vec::new()));
let scheduled_count = Rc::new(RefCell::new(0usize));
let strategy = MinuteProbeStrategy {
seen_ticks: seen_ticks.clone(),
scheduled_count: scheduled_count.clone(),
subscribe_symbols: BTreeSet::from(["000001.SZ".to_string()]),
minute_callbacks: false,
ordered: false,
};
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(),
PriceField::Last,
);
let mut engine = BacktestEngine::new(
data,
strategy,
broker,
BacktestConfig {
initial_cash: 10_000.0,
benchmark_code: "000300.SH".to_string(),
start_date: Some(date),
end_date: Some(date),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Last,
},
)
.with_execution_quote_loader(|_| Ok(Vec::new()));
let result = engine.run().expect("scheduled-only minute run");
assert!(seen_ticks.borrow().is_empty());
assert_eq!(*scheduled_count.borrow(), 1);
assert!(result.fills.is_empty());
assert_eq!(
result
.process_events
.iter()
.filter(|event| event.kind == ProcessEventKind::PreMinute)
.count(),
1
);
}
#[test]
@@ -2151,6 +2479,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
})
.collect::<Vec<_>>();
@@ -2319,6 +2648,7 @@ fn strategy_context_exposes_final_order_runtime_view() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}],
vec![CandidateEligibility {
@@ -2562,6 +2892,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -2572,6 +2903,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
],
@@ -2682,10 +3014,12 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
assert!(result.process_events.iter().any(|event| {
event.kind == ProcessEventKind::AccountManagementFee && event.detail.contains("fee=42.00")
}));
assert_eq!(result.terminal_audit.status, BacktestTerminalStatus::Clean);
assert_eq!(result.terminal_audit.pending_cash_flow_count, 0);
}
#[test]
fn engine_rejects_pending_limit_orders_at_market_close() {
fn engine_expires_pending_day_limit_orders_at_market_close() {
let date1 = d(2025, 1, 2);
let date2 = d(2025, 1, 3);
let data = DataSet::from_components(
@@ -2755,6 +3089,7 @@ fn engine_rejects_pending_limit_orders_at_market_close() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -2765,6 +3100,7 @@ fn engine_rejects_pending_limit_orders_at_market_close() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
],
@@ -2847,12 +3183,14 @@ fn engine_rejects_pending_limit_orders_at_market_close() {
);
assert!(result.order_events.iter().any(|event| {
event.date == date1
&& event.status == fidc_core::OrderStatus::Rejected
&& event.reason.contains("Market close")
&& event.status == fidc_core::OrderStatus::Expired
&& event.reason.contains("DAY order expired at market close")
}));
assert!(result.process_events.iter().any(|event| {
event.date == date1 && event.kind == ProcessEventKind::OrderUnsolicitedUpdate
}));
assert_eq!(result.terminal_audit.status, BacktestTerminalStatus::Clean);
assert_eq!(result.terminal_audit.stock_open_order_count, 0);
}
#[test]
@@ -2950,6 +3288,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -2960,6 +3299,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -2970,6 +3310,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
],
@@ -3076,12 +3417,15 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
"scheduled:daily_before_trading:2025-01-30",
"scheduled:daily_market_open:2025-01-30",
"scheduled:first_trading_day_on_day:2025-01-30",
"scheduled:daily_on_day_close:2025-01-30",
"scheduled:daily_before_trading:2025-01-31",
"scheduled:daily_market_open:2025-01-31",
"scheduled:friday_on_day:2025-01-31",
"scheduled:daily_on_day_close:2025-01-31",
"scheduled:daily_before_trading:2025-02-03",
"scheduled:daily_market_open:2025-02-03",
"scheduled:first_trading_day_on_day:2025-02-03",
"scheduled:daily_on_day_close:2025-02-03",
]
);
let process_log = process_log.borrow();
@@ -3202,6 +3546,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -3212,6 +3557,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -3222,6 +3568,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
],
@@ -3567,6 +3914,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
DailyFactorSnapshot {
@@ -3577,6 +3925,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
pe_ttm: 12.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
},
]
@@ -3721,6 +4070,7 @@ fn engine_exposes_current_process_context_to_strategies() {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}],
vec![CandidateEligibility {
@@ -3793,6 +4143,7 @@ impl Strategy for BuyMissingRowThenHoldStrategy {
) -> Result<StrategyDecision, fidc_core::BacktestError> {
if ctx.execution_date == d(2025, 5, 26) {
return Ok(StrategyDecision {
buy_denials: Default::default(),
rebalance: false,
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
File diff suppressed because it is too large Load Diff
+131
View File
@@ -208,3 +208,134 @@ fn futures_expiration_settlement_closes_all_contract_directions() {
);
assert!((account.total_cash() - 1_003_000.0).abs() < 1e-6);
}
#[test]
fn futures_full_close_preserves_closed_position_daily_metrics() {
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
let mut account = FuturesAccountState::new(100_000.0);
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 1.0);
account.begin_trading_day();
let realized = account
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 2.0)
.expect("close overnight position");
assert!(account.positions().is_empty());
assert!((realized - 98.0).abs() < 1e-12);
assert!((account.position_pnl() - 100.0).abs() < 1e-12);
assert!(account.trading_pnl().abs() < 1e-12);
assert!((account.transaction_cost() - 2.0).abs() < 1e-12);
assert!((account.daily_pnl() - 98.0).abs() < 1e-12);
assert!((account.total_cash() - 100_097.0).abs() < 1e-12);
account.begin_trading_day();
assert!(account.daily_pnl().abs() < 1e-12);
assert!(account.transaction_cost().abs() < 1e-12);
}
#[test]
fn futures_intraday_roundtrip_preserves_closed_trading_pnl() {
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
let mut account = FuturesAccountState::new(100_000.0);
account.begin_trading_day();
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 1.0);
account
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 2.0)
.expect("close intraday position");
assert!(account.positions().is_empty());
assert!((account.trading_pnl() - 100.0).abs() < 1e-12);
assert!(account.position_pnl().abs() < 1e-12);
assert!((account.transaction_cost() - 3.0).abs() < 1e-12);
assert!((account.daily_pnl() - 97.0).abs() < 1e-12);
assert!((account.total_cash() - 100_097.0).abs() < 1e-12);
}
#[test]
fn futures_partial_close_offsets_later_mark_with_trading_pnl() {
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
let mut account = FuturesAccountState::new(100_000.0);
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 2, 100.0, 0.0);
account.begin_trading_day();
account
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 0.0)
.expect("partially close overnight position");
account.mark_price("IF2506.CCFX", FuturesDirection::Long, 120.0);
assert!((account.position_pnl() - 400.0).abs() < 1e-12);
assert!((account.trading_pnl() + 100.0).abs() < 1e-12);
assert!((account.daily_pnl() - 300.0).abs() < 1e-12);
assert!((account.total_value() - 100_300.0).abs() < 1e-12);
}
#[test]
fn futures_settlement_keeps_same_day_pnl_visible_until_next_day() {
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
let mut account = FuturesAccountState::new(100_000.0);
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 0.0);
account.begin_trading_day();
account.mark_price("IF2506.CCFX", FuturesDirection::Long, 110.0);
let settled = account.settle(&BTreeMap::from([("IF2506.CCFX".to_string(), 110.0)]));
assert!((settled - 100.0).abs() < 1e-12);
assert!((account.daily_pnl() - 100.0).abs() < 1e-12);
assert!((account.total_cash() - 100_100.0).abs() < 1e-12);
assert!((account.total_value() - 100_100.0).abs() < 1e-12);
account.begin_trading_day();
assert!(account.daily_pnl().abs() < 1e-12);
}
#[test]
fn futures_cash_and_closed_cost_accumulate_micro_yuan_exactly() {
let spec = FuturesContractSpec::new(1.0, 0.0, 0.0);
let mut account = FuturesAccountState::new(1_000_000.0);
account.begin_trading_day();
for _ in 0..10_000 {
account.open(
"IF2506.CCFX",
FuturesDirection::Long,
spec,
1,
100.0,
0.000001,
);
account
.close("IF2506.CCFX", FuturesDirection::Long, 1, 100.0, 0.000001)
.expect("close micro-cost position");
}
assert!((account.total_cash() - 999_999.98).abs() < 1e-12);
assert!((account.transaction_cost() - 0.02).abs() < 1e-12);
assert!((account.daily_pnl() + 0.02).abs() < 1e-12);
}
#[test]
fn futures_margin_gate_and_fill_cash_use_exact_micro_yuan() {
let date = d(2025, 1, 2);
let spec = FuturesContractSpec::new(1.0, 1.0, 1.0);
let intent = FuturesOrderIntent::open(
"IF2506.CCFX",
FuturesDirection::Long,
spec,
1,
100.0,
0.000001,
"micro margin boundary",
);
let mut insufficient = FuturesAccountState::new(100.0);
let rejected = insufficient.execute_order(date, Some(1), intent.clone());
assert_eq!(rejected.order_events[0].status, OrderStatus::Rejected);
assert!((insufficient.total_cash() - 100.0).abs() < 1e-12);
let mut exact = FuturesAccountState::new(100.000001);
let filled = exact.execute_order(date, Some(2), intent);
assert_eq!(filled.order_events[0].status, OrderStatus::Filled);
assert_eq!(filled.fill_events.len(), 1);
assert!((filled.fill_events[0].gross_amount - 100.0).abs() < 1e-12);
assert!((filled.fill_events[0].commission - 0.000001).abs() < 1e-12);
assert!((filled.fill_events[0].net_cash_flow + 0.000001).abs() < 1e-12);
assert!(exact.cash().abs() < 1e-12);
}
@@ -0,0 +1,220 @@
use std::hint::black_box;
use std::time::Instant;
use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
use fidc_core::{
BenchmarkSnapshot, DailyMarketSnapshot, DataSet, Instrument, IntradayExecutionQuote,
};
const SYMBOL: &str = "000001.SZ";
fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
let start = NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid start date");
let dates = (0..day_count)
.map(|offset| start + Duration::days(offset as i64))
.collect::<Vec<_>>();
let markets = dates
.iter()
.map(|date| DailyMarketSnapshot {
date: *date,
symbol: SYMBOL.to_string(),
timestamp: None,
day_open: 10.0,
open: 10.0,
high: 10.5,
low: 9.5,
close: 10.0,
last_price: 10.0,
bid1: 9.99,
ask1: 10.01,
prev_close: 10.0,
volume: 1_000_000,
minute_volume: 1_000,
bid1_volume: 10_000,
ask1_volume: 10_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 11.0,
lower_limit: 9.0,
price_tick: 0.01,
})
.collect::<Vec<_>>();
let benchmarks = dates
.iter()
.map(|date| BenchmarkSnapshot {
date: *date,
benchmark: "000852.SH".to_string(),
open: 1_000.0,
close: 1_000.0,
prev_close: 1_000.0,
volume: 10_000_000,
})
.collect::<Vec<_>>();
let mut quotes = Vec::with_capacity(day_count * bars_per_day);
for date in &dates {
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 {
date: *date,
symbol: SYMBOL.to_string(),
timestamp,
last_price: 10.0 + offset as f64 / 10_000.0,
bid1: 9.99,
ask1: 10.01,
bid1_volume: 10_000,
ask1_volume: 10_000,
volume_delta: 1_000,
amount_delta: 10_000.0,
trading_phase: Some("continuous".to_string()),
});
}
}
let data = DataSet::from_components_with_actions_and_quotes(
vec![Instrument {
symbol: SYMBOL.to_string(),
name: "平安银行".to_string(),
board: "SZ".to_string(),
round_lot: 100,
listed_at: Some(start - Duration::days(1_000)),
delisted_at: None,
status: "active".to_string(),
}],
markets,
Vec::new(),
Vec::new(),
benchmarks,
Vec::new(),
quotes,
)
.expect("build intraday history dataset");
(data, dates)
}
fn timestamp(date: NaiveDate, time: &str) -> NaiveDateTime {
let time = NaiveTime::parse_from_str(time, "%H:%M:%S").expect("valid time");
date.and_time(time)
}
#[test]
fn intraday_history_is_bounded_by_visibility_and_preserves_order() {
let (data, dates) = dataset(3, 4);
let rows = data.history_intraday_quotes_at(
dates[1],
Some(timestamp(dates[1], "09:32:00")),
SYMBOL,
3,
false,
);
assert_eq!(
rows.iter().map(|row| row.timestamp).collect::<Vec<_>>(),
vec![
timestamp(dates[0], "09:33:00"),
timestamp(dates[1], "09:30:00"),
timestamp(dates[1], "09:31:00"),
]
);
let including_now = data.history_intraday_quotes_at(
dates[1],
Some(timestamp(dates[1], "09:32:00")),
SYMBOL,
3,
true,
);
assert_eq!(
including_now
.iter()
.map(|row| row.timestamp)
.collect::<Vec<_>>(),
vec![
timestamp(dates[1], "09:30:00"),
timestamp(dates[1], "09:31:00"),
timestamp(dates[1], "09:32:00"),
]
);
}
#[test]
#[ignore = "manual release-mode intraday history benchmark"]
fn benchmark_bounded_intraday_history() {
let (data, dates) = dataset(250, 240);
let active_datetime = timestamp(*dates.last().expect("last date"), "13:29:00");
for _ in 0..5 {
black_box(data.history_intraday_quotes_at(
active_datetime.date(),
Some(active_datetime),
SYMBOL,
30,
true,
));
}
let started = Instant::now();
let mut checksum = 0_i64;
for _ in 0..200 {
let rows = data.history_intraday_quotes_at(
active_datetime.date(),
Some(active_datetime),
SYMBOL,
30,
true,
);
checksum += rows
.last()
.expect("history row")
.timestamp
.and_utc()
.timestamp();
black_box(&rows);
}
let elapsed = started.elapsed();
eprintln!(
"intraday_history_benchmark iterations=200 rows_per_dataset=60000 elapsed_seconds={:.6} checksum={checksum}",
elapsed.as_secs_f64(),
);
}
#[test]
#[ignore = "manual release-mode quote-stream benchmark"]
fn benchmark_borrowed_execution_quote_stream() {
let (data, dates) = dataset(250, 240);
let date = *dates.last().expect("last date");
let symbols = std::collections::BTreeSet::from([SYMBOL.to_string()]);
for _ in 0..5 {
black_box(data.execution_quotes_on_date_for_symbols(date, Some(&symbols)));
black_box(
data.execution_quotes_iter_on_date_for_symbols(date, Some(&symbols))
.count(),
);
}
let materialized_started = Instant::now();
let mut materialized_checksum = 0_i64;
for _ in 0..5_000 {
let rows = data.execution_quotes_on_date_for_symbols(date, Some(&symbols));
materialized_checksum += rows
.iter()
.map(|quote| quote.timestamp.and_utc().timestamp())
.sum::<i64>();
black_box(rows);
}
let materialized_seconds = materialized_started.elapsed().as_secs_f64();
let streamed_started = Instant::now();
let mut streamed_checksum = 0_i64;
for _ in 0..5_000 {
let count = data
.execution_quotes_iter_on_date_for_symbols(date, Some(&symbols))
.map(|quote| quote.timestamp.and_utc().timestamp())
.sum::<i64>();
streamed_checksum += count;
black_box(count);
}
let streamed_seconds = streamed_started.elapsed().as_secs_f64();
eprintln!(
"quote_stream_benchmark iterations=5000 rows_per_day=240 materialized_seconds={materialized_seconds:.6} streamed_seconds={streamed_seconds:.6} materialized_checksum={materialized_checksum} streamed_checksum={streamed_checksum}"
);
}
@@ -72,6 +72,7 @@ fn factor(
pe_ttm: 18.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: Default::default(),
}
}
+24
View File
@@ -0,0 +1,24 @@
# 完成日线形态与次日信号
`fidc_daily_ohlcv_pattern_v1``fidc-core::daily_patterns` 单一计算核实现。Source Lake 只读取、核验及传输真实 OHLCV;研究服务和策略表达式不分别维护数值算法。
四种量价条件为趋势强势、前高突破、放量上涨、缩量突破;额外提供独立的均线下方、放量下跌卖出条件。前三者名称不暗示当日金叉或价格突破等未实际检验的事实。
## 应用阶段
- `filter.stock_expr(pattern_signal("<模板 JSON>"))`:选择候选,再按既有顺序和 Top N 取目标。
- `filter.buy_expr(pattern_signal("<模板 JSON>"))`:只限制正向仓位增量,不移除目标、不反向清仓,正常减仓不受影响。
- `risk.stop_loss(pattern_signal("<独立卖出模板 JSON>"))`:独立退出条件,不使用买入条件的反值。
- `pattern_score` 只可用于已通过形态条件的对象;没有放量参照或合法排除对象不伪造零分。
参数是 JSON 字符串,例如 `pattern_signal("{\"template\":\"ma_below\",\"parameters\":{\"ma_window\":20}}")`
新规则必须显式 `execution.matching_type("next_bar_open")`。信号日 D 的完整日线不能用于 D 日盘前或盘中;历史回放按 D 决策、下一真实交易日执行,实时上下文使用已完成 D 日窗口。实际委托仍需要执行日行情、资金、可卖数量、交易许可和风控。不得用研究结果开启交易路由。
## 数据与预热
所有价格统一用真实 backward1 因子,成交量不复权。缺失、非有限值、无效 OHLC、重复、未来行、未声明停牌状态均拒绝。仅按明确上市日期证明的上市前窗口或正式停牌记录可以返回结构化排除;不补价、不跳过日期压缩窗口。有效价格但缺复权因子即使停牌也报错。回测和运行态须从表达式提取真实窗口需求,冻结完整日历预热。
研究选择的范围及日期、上市/停牌排除证据、源查询和哈希需保留。固定候选的后续规则回测不等于历史全市场动态选股。CAPM 全区间拟合属于解释性诊断;要成为次日条件,必须另行使用截至 D 日的滚动估计并验证样本外表现,不得回填到拟合区间内。
旧任务默认撮合、历史筛选记录和策略源码不变;用户显式创建新规则后才采用此合同。
+12 -1
View File
@@ -10,7 +10,7 @@ The roadmap focuses on making the engine complete enough for editable platform
strategies, long-range A-share backtests, futures strategies, intraday order
simulation, AI-generated strategy code, and service-level result downloads.
## Re-Audit Findings (2026-04-24)
## Re-Audit Findings (2026-08-31)
The latest re-audit focused on the engine's execution model, account model,
order lifecycle, data helper surface, analyzer output, extension hooks, and
@@ -39,6 +39,8 @@ futures path. Confirmed aligned areas:
| P0 | Futures intraday matching | Closed for daily/open/close, tick-price futures fills, and true multi-level order-book sweeping when optional `order_book_depth` data exists. L1-only data still uses the existing L1 matcher and is not inflated into fake depth. | Extend depth fields only if production vendors expose more levels or exchange-specific fields. |
| P0 | Futures open-order lifecycle | Closed for futures pending limit orders, cross-day rematching, cancellation by id/symbol/all, and merged open-order runtime views. | Add more order status transitions only if UI requires extra intermediate event names. |
| P0 | Combined multi-account NAV | Closed. `DailyEquityPoint`, progress events, and metrics use aggregate stock + futures initial cash and total equity. | None. |
| P0 | Fixed-point execution money | Closed. Stock execution freezes fee rates once and uses signed micro-yuan `i128` for gross amount, commission, stamp tax, transfer fee, strict budget checks, cash, liabilities, management fees, external flows, account units, position lot cost and realized PnL. The standalone futures account uses the same fixed-point money boundary for cash, margin, transaction cost and daily PnL. Market indicators and return statistics remain `f64` outside the execution boundary. | None. |
| P0 | Bounded minute-data processing | Closed for the engine data model. Intraday history uses a sorted date index and scans backward only until the requested bar count is satisfied. Daily minute processing consumes a borrowed timestamp-ordered k-way merge and does not clone/materialize the full selected quote day before event dispatch. | Keep Source Lake and service clients batch-streamed; do not reintroduce whole-window row materialization. |
| P1 | Futures trading parameter data source | Closed for engine-side trading-parameter ingestion/resolution via `futures_trading_parameters.csv` or component data. | Add more exchange metadata columns only when source data exposes them. |
| P1 | Futures transaction cost decider | Closed. `FuturesTransactionCostModel` calculates by-money/by-volume open/close/close-today costs from trading parameters. | None. |
| P1 | Futures settlement price mode | Closed. Engine supports configurable settlement price mode and resolves settlement/prev-settlement from factor fields with close/prev_close fallback. | Add dedicated settlement columns if the storage layer later separates them from factors. |
@@ -55,6 +57,12 @@ futures path. Confirmed aligned areas:
- [x] Minute-level `time_rule` semantics including market-open, market-close,
and physical-time style schedules.
- [x] Fine-grained daily and minute execution quote strategy entrypoints.
- [x] Stock broker fee, budget and cash-ledger arithmetic uses a micro-yuan
fixed-point execution primitive; one-micro over-budget orders fail.
- [x] Stock position lots, realized/unrealized PnL, dividends and external cash
flows preserve fixed-point value conservation.
- [x] Futures cash, margin, transaction cost and daily realized/position PnL use
the fixed-point ledger.
- [x] Scheduled actions evaluated against explicit intraday times.
- [x] `update_universe`, `subscribe`, and `unsubscribe`.
- [x] Intraday subscription guards at strategy API level; intraday execution uses minute quote semantics.
@@ -70,6 +78,9 @@ futures path. Confirmed aligned areas:
- [x] Trading-date range, previous-date, and next-date helpers.
- [x] Phase-aware minute history cursor semantics matching the active bar or
intraday execution quote callback.
- [x] Bounded intraday history lookup and borrowed minute quote streaming avoid
full-history scans and full-day quote clones while preserving timestamp
order and visibility boundaries.
- [x] Suspension, ST, date-range price, active instrument, and instrument
history helpers.
- [x] Open-order status, unfilled quantity, final order lookup, average fill
@@ -0,0 +1,43 @@
{
"schemaVersion": "fidc-batched-current-rolling-rejection/v1",
"measuredAt": "2026-09-05T02:38:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "004a46c",
"revertCommit": "43b15b2098c427869a4a582b4b24325155b1370e",
"restoredRunnerBinarySha256": "a4135986b69625a0f3443e9091754874b3f9d65e9913424298c5d8fedf733985",
"candidate": {
"description": "collect static current rolling windows at strategy construction, batch them per stock, and store fixed current close/volume arrays in StockExpressionState",
"processColdEngineSeconds": 6.412,
"processHotEngineSeconds": [6.046, 6.497, 6.035, 6.309, 6.074],
"processHotMedianEngineSeconds": 6.074
},
"rollback": {
"processColdEngineSeconds": 5.18,
"processHotEngineSeconds": [5.48, 5.47, 4.602],
"processHotMedianEngineSeconds": 5.47
},
"observedCandidateRegressionPercent": 11.04204753199269,
"businessContract": {
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"testGate": {
"coreUnitTotal": 421,
"corePassed": 415,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/batched-current-rolling-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/batched-current-rolling-rollback-20260905.json"
],
"acceptance": {
"status": "rejected_and_removed",
"reason": "the larger per-stock state and eager batch work cost more than the repeated scalar helper calls on the real five-year workload"
}
}
@@ -0,0 +1,54 @@
{
"schemaVersion": "fidc-cached-symbol-board-rejection/v1",
"measuredAt": "2026-09-05T03:22:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "eb8b146",
"revertCommit": "f210539",
"candidate": {
"description": "precompute a symbol-id-aligned BJSE bit vector and share one suffix classifier between platform expressions and risk control",
"primaryHotEngineSeconds": [4.656, 4.759, 4.669, 4.698, 4.614],
"primaryHotMedianEngineSeconds": 4.669,
"acceptedPrimaryBaselineMedianEngineSeconds": 4.729,
"primaryObservedImprovementPercent": 1.2687661249735674,
"secondaryLowContentionEngineSeconds": [4.404, 4.327, 4.346],
"secondaryLowContentionMedianEngineSeconds": 4.346,
"acceptedSecondaryBaselineMedianEngineSeconds": 4.049,
"secondaryObservedRegressionPercent": 7.33514448011855,
"highContentionSecondaryEngineSecondsExcluded": [6.398]
},
"rollbackComparison": {
"primaryEngineSecondsExcluded": [12.736, 12.795, 12.899, 13.845],
"hostLoadAverage": 44.84,
"reason": "managed symbolic workers entered a roughly 30-core phase, so the rollback batch cannot serve as a same-load wall-time comparison"
},
"businessContract": {
"primaryTotalReturn": 0.9219861819172002,
"primaryTradeCount": 26088,
"primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"primaryResultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"secondaryTotalReturn": 1.1342962298106998,
"secondaryTradeCount": 19404,
"secondaryCanonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"secondaryResultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"profile": {
"allThreadRunId": "btr_1788549525151_3404548_13",
"allThreadRunEngineSeconds": 4.957,
"trimMatchesPercent": 2.0,
"typedBaselineSingleWorkerProfileTrimMatchesPercent": 2.85,
"comparisonLimited": true,
"reason": "the two profiles used different thread attachment sets and cannot establish an end-to-end speedup"
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/cached-symbol-board-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/cached-symbol-board-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/cached-symbol-board-rollback-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/cached-symbol-board-all-threads-profile-20260905/perf.data"
],
"acceptance": {
"status": "rejected_and_removed",
"reason": "the candidate preserved correctness but did not improve both five-year strategies, and the later rollback batch was too heavily contended to overturn the cross-strategy regression"
}
}
@@ -0,0 +1,85 @@
{
"schemaVersion": "fidc-calendar-major-series-boundary-index/v1",
"measuredAt": "2026-09-05T02:24:00+08:00",
"host": "192.168.31.177",
"engineCommit": "abe4fed4527e07ad7ae4464e574fa582150e306e",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "a4135986b69625a0f3443e9091754874b3f9d65e9913424298c5d8fedf733985",
"implementation": {
"description": "transpose immutable decision/current market-series boundary indexes from symbol-major vectors to calendar-major contiguous symbol rows",
"logicalEntryCountChanged": false,
"entryType": "u32",
"missingSentinel": "u32::MAX",
"factorValuesCached": false,
"selectionResultsCached": false,
"pitSemanticsChanged": false
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"processCold": {
"totalSeconds": 18.605,
"dataSeconds": 12.849,
"dataSetConstructSeconds": 4.052,
"marketIndexBuildSeconds": 2.396,
"engineSeconds": 5.06
},
"processHotEngineSeconds": [5.297, 5.227, 5.029, 5.012, 5.202],
"processHotMedianEngineSeconds": 5.202,
"fieldProjectionBaselineMedianEngineSeconds": 5.356,
"observedMedianImprovementPercent": 2.875280059746078,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [4.646, 4.87, 4.886, 5.056],
"medianEngineSeconds": 4.878,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"profile": {
"runId": "btr_1788546155152_3120069_10",
"engineSeconds": 5.211,
"eventCycles": 14556215580,
"seriesEndIndexPresentInTopProfile": false,
"fieldProjectionBaselineSeriesEndIndexPercent": 4.74,
"stockStateSelfPercent": 15.19,
"adjustedCloseMovingAveragePercent": 6.24,
"numericVmPercent": 6.77
},
"memory": {
"serviceCgroupCurrentBytes": 11493711872,
"serviceCgroupPeakBytes": 11495387136,
"processRssKiB": 11212504,
"processAnonymousKiB": 11196148,
"fieldProjectionBaselineCgroupCurrentBytes": 11485425664,
"observedCgroupIncreaseBytes": 8286208
},
"testGate": {
"coreUnitTotal": 421,
"corePassed": 415,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-major-series-boundary-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-major-series-boundary-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-major-series-boundary-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-major-series-boundary-profile-20260905/perf-report.txt"
],
"acceptance": {
"status": "accepted_generic_calendar_major_boundary_index",
"reason": "both five-year contracts preserve exact outputs and clean terminal audits, the primary hot median improves, the former boundary lookup leaves the top profile, and steady-state memory remains effectively unchanged"
}
}
@@ -0,0 +1,46 @@
{
"schemaVersion": "fidc-compact-adjusted-close-rejection/v1",
"measuredAt": "2026-09-05T04:29:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "ab87e18",
"revertCommit": "0c2681e6996800eae5f3b881e75a01e7a078863f",
"restoredRunnerBinarySha256": "3e69af42e41321d31c69b552cf22d7033ce1ea8d94305a32e32461148cdbfc60",
"candidate": {
"description": "replace two Vec<Option<f64>> adjusted-close arrays with f64 arrays using NaN as the internal missing sentinel",
"theoreticalSteadyStateMemoryReductionBytesPerMarketRow": 16,
"hotEngineSeconds": [5.119, 5.168, 5.516, 5.332, 4.69],
"hotMedianEngineSeconds": 5.168
},
"acceptedBaseline": {
"historicalMedianEngineSeconds": 3.896,
"sameWindowRollbackEngineSeconds": [4.144, 4.169],
"sameWindowRollbackMedianEngineSeconds": 4.169
},
"observed": {
"regressionVersusHistoricalBaselinePercent": 32.64887063655031,
"regressionVersusSameWindowRollbackPercent": 23.962580954665402
},
"businessContract": {
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"testGate": {
"coreUnitTotal": 422,
"corePassed": 416,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/compact-adjusted-close-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/compact-adjusted-close-rollback-primary-20260905.json"
],
"acceptance": {
"status": "rejected_and_removed",
"reason": "the memory-dense NaN representation materially slowed the hottest moving-average path despite preserving exact business results"
}
}
@@ -0,0 +1,79 @@
{
"schemaVersion": "fidc-compact-daily-stock-state-cache-key/v1",
"measuredAt": "2026-09-05T01:20:00+08:00",
"host": "192.168.31.177",
"engineCommit": "6f81e1940ad4ddd4eefbe12c056be28f2b64d127",
"runnerBinarySha256": "c2d9101a89b71b51051b8762375476518e40d40e29a8ee0f9615ac4c55fcdb0a",
"implementation": {
"description": "remove the redundant execution date from stock-state cache keys because the complete cache is already cleared before the active date changes",
"keyBefore": "(execution_date, factor_date, symbol_id, execution_time, use_intraday_quote)",
"keyAfter": "(factor_date, symbol_id, execution_time, use_intraday_quote)",
"crossDateResetTested": true,
"cachedFactorValues": false,
"cachedSelectionResults": false,
"pitSemanticsChanged": false
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"hotEngineSeconds": [5.262, 5.734, 5.89, 5.612, 6.301],
"hotMedianEngineSeconds": 5.734,
"sharedOrderBaselineMedianEngineSeconds": 5.742,
"observedMedianImprovementPercent": 0.13932427725531174,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [5.889, 5.853, 5.342],
"medianEngineSeconds": 5.853,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"coldDataObservation": {
"runId": "btr_1788542108713_2936456_0",
"totalSeconds": 54.502,
"dataSeconds": 46.584,
"engineSeconds": 7.011,
"sourceQuerySeconds": 17.585,
"datasetConstructSeconds": 12.913,
"marketIndexBuildSeconds": 7.671,
"adjustmentValidationSeconds": 1.855,
"hotDataSecondsRange": [0.002, 0.006],
"performanceComparisonExcluded": true
},
"memoryObservation": {
"processRssKiB": 11136672,
"processAnonymousKiB": 11120428,
"cgroupCurrentBytes": 12445237248,
"cgroupFileCacheBytes": 1029632000,
"cgroupActiveFileBytes": 1009012736,
"duplicateDataSetLeakObserved": false,
"note": "the gap between process RSS and cgroup memory is reclaimable file-page cache rather than a second resident DataSet"
},
"testGate": {
"coreUnitTotal": 420,
"corePassed": 414,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/compact-stock-cache-key-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/compact-stock-cache-key-secondary-20260905.json"
],
"acceptance": {
"status": "accepted_structural_compaction_no_regression",
"endToEndSpeedupClaimed": false,
"reason": "the key is smaller, the cross-date isolation test passes, both five-year strategies retain exact results, and observed wall time is neutral to slightly better"
}
}
@@ -0,0 +1,74 @@
{
"schemaVersion": "fidc-current-rolling-boundary-reuse-ab/v1",
"measuredAt": "2026-09-04T15:17:49Z",
"host": "192.168.31.177",
"engineCommit": "52b07be19bdd1597e5b412b5a549e57640efa415",
"implementation": {
"description": "resolve the current market-series end once per stock state and reuse the integer boundary for literal stock rolling_mean_current calls",
"cachedFactorValues": false,
"cachedStrategyResults": false,
"additionalHeapCollectionsPerStock": 0,
"fallback": "dynamic or non-market fields retain the original date and factor-series lookup",
"pitSemanticsChanged": false,
"adjustmentSemanticsChanged": false
},
"componentBenchmark": {
"iterations": 100000,
"helperCallsPerIteration": 5,
"repeatedLookupSeconds": 0.009142143,
"reusedBoundarySeconds": 0.006728638,
"speedup": 1.3586914617787433,
"equal": true
},
"testGate": {
"total": 418,
"passed": 413,
"ignoredManualBenchmarks": 5,
"failed": 0
},
"backtestEvidence": {
"runId": "btr_1788535029326_2441112_5",
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalSeconds": 7.509,
"engineSeconds": 6.494,
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54"
},
"secondaryStrategyEvidence": {
"description": "same frozen bundle and execution contract with target positions changed from 30 to 20",
"runId": "btr_1788535290995_2441112_6",
"totalSeconds": 9.081,
"engineSeconds": 8.367,
"dataSeconds": 0.003,
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"tradeDateCount": 1216,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"matchesPreOptimizationBusinessResult": true
},
"artifacts": [
{
"path": "/srv/fidc/canonical/run/fidc-private/evidence/current-rolling-boundary-profile-20260904/perf.data",
"sha256": "be9f6e04c01a98cb834bc3f207359dc17555e0e660e178d552b525722a13873a"
},
{
"path": "/srv/fidc/canonical/run/fidc-private/evidence/current-rolling-boundary-profile-20260904/perf-report.txt",
"sha256": "6f4e1e4370ce996fcf7308a7a723469b417d8c03156eb43b5a26a5fb42c42511"
},
{
"path": "/srv/fidc/canonical/run/fidc-private/evidence/current-rolling-boundary-profile-20260904/run.json",
"sha256": "fee9f104d72d8918cdcd7660d7cd40dced9a84edcabcd68e89588ce5690523a5"
}
],
"acceptance": {
"status": "accepted_component_improvement_end_to_end_no_regression",
"endToEndSpeedupClaimed": false,
"reason": "the component result is positive and exact, while concurrent non-FIDC tan load and symbolic phases make cross-build wall-time medians non-comparable"
}
}
@@ -0,0 +1,71 @@
{
"schemaVersion": "fidc-daily-snapshot-view-stock-state-rejection/v1",
"measuredAt": "2026-09-05T00:30:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "1df0081479b93a051f9a40d2acf7060d0eb6929d",
"revertCommit": "1f10a6bb3decdd40c77397a7596c6f23a9f90a18",
"candidate": {
"description": "carry symbol_id in EligibleUniverseSnapshot and pass pre-resolved execution/factor DailySnapshotView values into stock-state construction",
"componentBenchmark": {
"symbols": 6000,
"rounds": 200,
"dateMapLookupSeconds": 0.020211,
"dailyViewLookupSeconds": 0.006410,
"speedup": 3.1530421216848675,
"equal": true
},
"fiveYearRuns": [
{"runId": "btr_1788538826285_2693431_0", "engineSeconds": 8.075},
{"runId": "btr_1788538854544_2693431_1", "engineSeconds": 8.488},
{"runId": "btr_1788538868210_2693431_2", "engineSeconds": 10.455},
{"runId": "btr_1788538887159_2693431_3", "engineSeconds": 10.672},
{"runId": "btr_1788538903432_2693431_4", "engineSeconds": 11.284},
{"runId": "btr_1788538920072_2693431_5", "engineSeconds": 9.985}
],
"hotMedianEngineSeconds": 10.455,
"runnerBinarySha256": "cbe20d1bd5c5e02372bd310107a1f4e41aaa9f256b3c31f79c6ad000648d1854"
},
"restored": {
"fiveYearRuns": [
{"runId": "btr_1788539286281_2739751_0", "engineSeconds": 7.118},
{"runId": "btr_1788539313845_2739751_1", "engineSeconds": 6.702},
{"runId": "btr_1788539325631_2739751_2", "engineSeconds": 7.093},
{"runId": "btr_1788539338757_2739751_3", "engineSeconds": 6.509}
],
"hotMedianEngineSeconds": 6.702,
"candidateRegressionPercent": 55.99820948970456,
"runnerBinarySha256": "e33dd75aa7b481975c0991cf634d07900cdefa51756802e28e64f18b980a83dc",
"matchesPreCandidateRunnerSha256": true
},
"businessContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"allRunsConsistent": true
},
"profile": {
"candidateRunId": "btr_1788538966659_2693431_6",
"candidateEngineSeconds": 10.566,
"stockStatePercent": 28.14,
"adjustedCloseMovingAveragePercent": 8.27,
"stableSortPercent": 5.46,
"mallocPercent": 5.12,
"note": "The removed map lookup symbols disappeared, but their component savings were too small to offset the larger universe row and generalized optional-view state path."
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/daily-view-after-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/daily-view-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/daily-view-profile-20260905/perf-report.txt",
"/srv/fidc/canonical/run/fidc-private/evidence/daily-view-reverted-20260905.json"
],
"decision": {
"status": "rejected_and_removed",
"reason": "a positive lookup microbenchmark did not translate to end-to-end performance; the same frozen five-year workload became about 56 percent slower",
"productionEngineCommit": "1f10a6bb3decdd40c77397a7596c6f23a9f90a18"
}
}
@@ -0,0 +1,90 @@
{
"schemaVersion": "fidc.date-numeric-vm-performance/v1",
"generatedAt": "2026-09-06T18:08:00+08:00",
"host": "192.168.31.177",
"change": {
"engineCommit": "bc228980afc02cde483b1a6fe69da4048120cbc4",
"serviceCommit": "bac07c208c652a717d312e332d44f90218cc1247",
"runnerBinarySha256": "46684dea71273466f8ba0f6351eb6f6adf694393ff23ee278054e3b6b71acbe5",
"description": "Valid ISO date literals directly compared with trade_date, current_date, date, decision_date or execution_date compile to the numeric bytecode VM as YYYYMMDD keys. All other strings remain on the Rhai path. Numeric VM floating comparisons now match Rhai relative epsilon semantics."
},
"correctnessBug": {
"previousBehavior": "Numeric VM used exact f64 comparisons while Rhai used relative f64::EPSILON comparisons, so an expression could change result depending on whether it compiled to VM or fell back to Rhai.",
"capturedExample": "11.699999999999998 == 11.7",
"previousVmResult": false,
"rhaiAndFixedVmResult": true,
"operatorsAligned": [
"==",
"!=",
"<",
"<=",
">",
">="
]
},
"request": {
"sourceRunId": "btr_1788681250851_3170500_1",
"startDate": "2023-01-03",
"endDate": "2025-12-31",
"frequency": "1d",
"matchingType": "next_bar_open",
"initialCash": 10000000.0,
"benchmark": "000852.SH",
"frozenBundleReused": true
},
"baseline": {
"engineCommit": "840473362d7032b80d551ba5935723ee823dffa1",
"hotEngineMedianSeconds": 5.049,
"hotTotalMedianSeconds": 5.796
},
"candidate": {
"coldRun": {
"runId": "btr_1788688964051_3295756_0",
"totalSeconds": 21.341,
"dataSeconds": 7.904,
"engineSeconds": 12.519,
"classification": "post-deploy process cold and concurrent host-load observation; retained explicitly and excluded from the hot median"
},
"hotRunIds": [
"btr_1788688990229_3295756_1",
"btr_1788689001276_3295756_2",
"btr_1788689030863_3295756_3",
"btr_1788689059131_3295756_4"
],
"hotEngineSeconds": [
4.963,
4.934,
4.854,
4.859
],
"hotEngineMedianSeconds": 4.8965,
"hotTotalSeconds": [
5.901,
5.619,
5.738,
5.539
],
"hotTotalMedianSeconds": 5.6785,
"incrementalHotEngineImprovementPercent": 3.0204,
"incrementalHotTotalImprovementPercent": 2.0273
},
"resultParity": {
"totalReturn": -0.615167693429,
"tradeCount": 4961,
"canonicalResultSha256": "f9c26271d4f2096e51429b6106740f600757823789bf6447cd0c524f702f0836",
"resultStoreSha256": "56042bd25bd4261e41e8237c9f5ead580b782d897baae58c8e4436f331a1d91f",
"allFiveRunsEqualBaseline": true,
"returnDelta": 0.0,
"tradeCountDelta": 0
},
"validation": {
"numericVmTests": "4 passed, 1 ignored",
"capturedRegressionTest": "platform_helpers_support_generic_rolling_stats_and_normalized_factors",
"workspaceTests": "423 passed, 6 ignored plus all integration tests passed",
"invalidDateLiteralFallsBack": true,
"nonDateStringFallsBack": true,
"helperDateArgumentPreserved": true,
"runtimeUser": "boris",
"serviceHealth": "ok"
}
}
@@ -0,0 +1,18 @@
{
"date": "2026-09-08",
"host": "192.168.31.177",
"candidateCommit": "fa6f189",
"unitTestsPassed": 453,
"integrationTestsPassed": 122,
"ignoredManualBenchmarks": 8,
"failed": 0,
"backtestRunnerCargoCheck": "passed",
"covered": ["top-up rejection", "sell permission preserved", "decision context restored", "existing pending order not rewritten", "next-open target direction determined by actual price"],
"deployed": false,
"factorCompilerConnected": false,
"paperLivePlanConnected": false,
"orderAmendmentAcceptanceComplete": false,
"brokerAmendmentTestsPassed": true,
"amendmentPolicy": "Deny buy quantity increases or limit-price increases; allow validated reductions; preserve original state and queue order on rejection.",
"realStrategyAcceptanceComplete": false
}
@@ -0,0 +1,78 @@
{
"schemaVersion": "fidc-duplicate-rolling-lookback-rejection/v1",
"measuredAt": "2026-09-05T01:05:00+08:00",
"host": "192.168.31.177",
"candidateCommits": [
"1f02e78b2400c73c5f05351b0beff16d6c724e2a",
"e469b0ddf4aa3bb9fe69b785b51ce6c7cf960b90"
],
"revertCommit": "5b2a03d416a7bd709680a1adf52013e3eadc199a",
"candidate": {
"description": "reuse equal lookback values only inside one AdjustedCloseSeries moving_averages_at_end call",
"validComponentBenchmark": {
"samples": 6,
"iterationsPerSample": 3000000,
"lookbacks": [1, 2, 3, 1, 2, 3, 1],
"repeatedSeconds": 1.40643611,
"reusedSeconds": 0.853281932,
"speedup": 1.6482666012902287,
"checksum": 1458000000.0
},
"hardwareCounters": {
"runId": "btr_1788541184367_2862338_11",
"engineSeconds": 8.423,
"taskClockMilliseconds": 9030.56,
"cycles": 22931608591,
"instructions": 28199702874,
"branches": 5010114942,
"branchMisses": 23586244,
"cacheReferences": 268428800,
"cacheMisses": 146632717
}
},
"restoredBaseline": {
"warmupRunId": "btr_1788541416661_2890172_0",
"warmupEngineSeconds": 5.581,
"hardwareCounters": {
"runId": "btr_1788541455567_2890172_1",
"engineSeconds": 6.078,
"taskClockMilliseconds": 6643.43,
"cycles": 16773013609,
"instructions": 28218847364,
"branches": 5024236708,
"branchMisses": 23209586,
"cacheReferences": 266316407,
"cacheMisses": 81226408
},
"runnerBinarySha256": "188269bf0c2ee65d6c11ed37ddddfdaf86695941e8123f69f950054bea39796e"
},
"comparison": {
"candidateInstructionReductionPercent": 0.06784229696471904,
"candidateBranchReductionPercent": 0.2810720756590066,
"materialSystemImprovementProven": false,
"wallTimeComparable": false,
"reason": "host-wide thermal and external workload changes produced materially different cache-miss and clock conditions; the instruction reduction itself is below one tenth of one percent"
},
"businessContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"allRunsConsistent": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/duplicate-lookback-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/duplicate-lookback-low-contention-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/duplicate-lookback-perf-stat-candidate-20260905/perf-stat.csv",
"/srv/fidc/canonical/run/fidc-private/evidence/duplicate-lookback-perf-stat-baseline-20260905/perf-stat.csv"
],
"decision": {
"status": "rejected_and_removed",
"reason": "the valid microbenchmark improves, but the real five-year workload saves only about 0.068 percent instructions and provides no measurable system-level benefit",
"productionEngineCommit": "5b2a03d416a7bd709680a1adf52013e3eadc199a"
}
}
@@ -0,0 +1,47 @@
{
"schemaVersion": "fidc-engine-optimization-rejection/v1",
"generatedDate": "2026-09-07",
"host": "192.168.31.177",
"candidate": {
"engineCommit": "757b5665ca01bceb2bf8afe1ae2fe2dfc9c52c5c",
"implementationIdentitySha256": "a60aed1a405c1dc4713fdf72a379dcd07bc493998ec92c0aa0573c21ee863fc8",
"description": "Keep the flat immutable quote base and merge a run-local lazy quote overlay through every DataSet quote API."
},
"revert": {
"engineCommit": "728ed795b0c3b9bac9b7e79cb90e469c106e3629",
"status": "deployed_and_healthy"
},
"measurements": {
"currentCloseOneYear": {
"acceptedHotMedianEngineSeconds": 0.722,
"candidateHotMedianEngineSeconds": 0.858,
"candidateHotMedianTotalSeconds": 1.56,
"engineRegressionPercent": 18.836565
},
"nextBarOpenFiveYear": {
"acceptedHotMedianEngineSeconds": 2.223,
"candidateHotMedianEngineSeconds": 3.319,
"candidateHotMedianTotalSeconds": 4.032,
"engineRegressionPercent": 49.302744
}
},
"correctness": {
"totalReturnEqual": true,
"tradeCountEqual": true,
"canonicalResultDigestEqual": true,
"resultStoreDigestEqual": true,
"terminalAuditEqual": true,
"candidateWorkspaceCoreTestsPassed": 442,
"revertWorkspaceCoreTestsPassed": 441,
"workspaceFailures": 0
},
"decision": {
"status": "rejected_and_removed",
"reason": "Public DataSet reads paid overlay branching and enlarged code paths even when no overlay existed; end-to-end regressions outweighed the avoided lazy-write clone.",
"replacementDirection": "Build immutable shared Base Panels before engine execution. Any supplemental intraday store must be private to the broker/loader and must not alter generic DataSet read APIs."
},
"supportingEvidence": [
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-run-overlay-rejection-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-run-overlay-rejection-20260907.json"
]
}
@@ -0,0 +1,108 @@
{
"schemaVersion": "fidc.engine-expression-scope-performance/v1",
"generatedAt": "2026-09-06T17:45:00+08:00",
"host": "192.168.31.177",
"profile": {
"artifact": "/srv/fidc/canonical/run/fidc-private/evidence/profiles/fidc-hot-engine-d324d284-20260906.data",
"event": "cpu-clock",
"frequencyHz": 499,
"samples": 3501,
"lostSamples": 0,
"scope": "system.slice/fidc-backtest-service-highmem177.service",
"observedSelfOverheadPercent": {
"chronoDelayedFormat": 6.31,
"chronoStrftimeIterator": 5.0,
"selectiveScopePushDynamic": 5.2,
"selectiveScopePushString": 3.48,
"selectiveScopePushInteger": 3.03,
"selectiveScopePushBoolean": 1.09,
"marketBySymbolId": 6.2,
"malloc": 4.74,
"free": 2.43
}
},
"change": {
"baselineEngineCommit": "630a7a1",
"candidateEngineCommit": "840473362d7032b80d551ba5935723ee823dffa1",
"serviceCommit": "bac07c208c652a717d312e332d44f90218cc1247",
"behavior": "Expression scope identifiers are passed directly to Rhai without an intermediate heap String. Trade, decision and execution date strings are formatted once per DayExpressionState and reused as ImmutableString values by every expression evaluation on that day."
},
"request": {
"sourceRunId": "btr_1788681250851_3170500_1",
"startDate": "2023-01-03",
"endDate": "2025-12-31",
"frequency": "1d",
"matchingType": "next_bar_open",
"initialCash": 10000000.0,
"benchmark": "000852.SH",
"frozenBundleReused": true
},
"baseline": {
"runIds": [
"btr_1788686878533_3170500_3",
"btr_1788686907702_3170500_4",
"btr_1788687153269_3170500_5"
],
"engineSeconds": [
6.582,
6.442,
6.352
],
"engineMedianSeconds": 6.442,
"totalSeconds": [
7.22,
7.04,
7.388
],
"totalMedianSeconds": 7.22
},
"candidate": {
"coldRun": {
"runId": "btr_1788687746484_3277755_0",
"totalSeconds": 13.369,
"dataSeconds": 7.827,
"engineSeconds": 4.925,
"classification": "runner_process_cold"
},
"hotRunIds": [
"btr_1788687763649_3277755_1",
"btr_1788687774308_3277755_2",
"btr_1788687798961_3277755_3",
"btr_1788687813292_3277755_4"
],
"hotEngineSeconds": [
5.021,
5.077,
5.18,
4.951
],
"hotEngineMedianSeconds": 5.049,
"hotTotalSeconds": [
5.659,
5.756,
5.836,
5.84
],
"hotTotalMedianSeconds": 5.796,
"engineMedianImprovementPercent": 21.624,
"totalMedianImprovementPercent": 19.723
},
"correctness": {
"totalReturn": -0.615167693429,
"tradeCount": 4961,
"canonicalResultSha256": "f9c26271d4f2096e51429b6106740f600757823789bf6447cd0c524f702f0836",
"resultStoreSha256": "56042bd25bd4261e41e8237c9f5ead580b782d897baae58c8e4436f331a1d91f",
"allFiveCandidateRunsEqualBaseline": true,
"returnDelta": 0.0,
"tradeCountDelta": 0
},
"validation": {
"cargoFmt": "passed",
"platformExpressionTests": "180 passed, 1 ignored",
"workspaceTests": "421 passed, 6 ignored plus all integration tests passed",
"runtimeUser": "boris",
"serviceHealth": "ok",
"buildManifest": "/srv/fidc/canonical/run/build/fidc-backtest-service.json",
"runnerBinarySha256": "909cb47ef20128b4990bf355d0c63e10d7d74aac128067fa19c971698c5d85bc"
}
}
@@ -0,0 +1,51 @@
{
"schemaVersion": "fidc-frozen-standard-rolling-lookbacks-rejection/v1",
"measuredAt": "2026-09-05T03:03:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "32e4030",
"revertCommit": "05e67c73df0c002efec15711dd5ef066d817abec",
"restoredRunnerBinarySha256": "1071852a374620029398a967d485b827ed2defbb6ecbcc3f1ee6ffd3e2db76b6",
"candidate": {
"description": "replace per-stock rolling requirement-set lookups with strategy-level fixed standard lookback arrays",
"primaryHotEngineSeconds": [4.329, 4.455, 4.751, 4.822, 4.847],
"primaryHotMedianEngineSeconds": 4.751,
"secondaryEngineSeconds": [4.774, 5.473, 6.161, 5.491],
"hardwareRunId": "btr_1788548442183_3221468_10",
"hardwareRunEngineSeconds": 10.522,
"hardwareRunHostLoadAverage": 42.93,
"instructions": 26384933876,
"branches": 4682344116
},
"acceptedBaseline": {
"engineCommit": "75ab0c0",
"primaryHotMedianEngineSeconds": 4.729,
"instructions": 26049740736,
"branches": 4601262679
},
"observed": {
"primaryMedianRegressionPercent": 0.4652146331148285,
"instructionRegressionPercent": 1.2867427104054538,
"branchRegressionPercent": 1.7621562309418415
},
"businessContract": {
"primaryTotalReturn": 0.9219861819172002,
"primaryTradeCount": 26088,
"primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"primaryResultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"secondaryTotalReturn": 1.1342962298106998,
"secondaryTradeCount": 19404,
"secondaryCanonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"secondaryResultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/frozen-standard-rolling-lookbacks-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/frozen-standard-rolling-lookbacks-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/frozen-standard-rolling-lookbacks-perf-stat-20260905/perf-stat.csv"
],
"acceptance": {
"status": "rejected_and_removed",
"reason": "the compiler layout change increased retired instructions and branches while the primary end-to-end median did not improve"
}
}
@@ -0,0 +1,88 @@
{
"schemaVersion": "fidc-instrument-symbol-id-index-ab/v1",
"measuredAt": "2026-09-04T14:31:25Z",
"host": "192.168.31.177",
"engineCommit": "8ed22897eac506297c9ab4023140fbb6cfa0cc26",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"serviceBinarySha256": "24b53421e8724a29963e3254d9dea33c0a2259a81567caa853b98369df7cf704",
"runnerBinarySha256": "0d314809675544fed19ba0888ae22e0098f87da1672ac692fe38f3dbd3b55c70",
"implementation": {
"description": "build one immutable Instrument vector aligned to the existing symbol-id index and use it only at call sites that already own a validated symbol id",
"stringInstrumentApiPreserved": true,
"mutableStateShared": false,
"riskSemanticsChanged": false,
"executionSemanticsChanged": false
},
"componentBenchmark": {
"rows": 6000,
"iterations": 1000,
"hashMapSeconds": 0.09067713,
"denseVectorSeconds": 0.010526148,
"speedup": 8.614464664566752,
"checksum": 600000000,
"equal": true
},
"testGate": {
"total": 417,
"passed": 413,
"ignoredManualBenchmarks": 4,
"failed": 0
},
"backtestContract": {
"bundleId": "bt_bundle_5baf27b98346123e1a7b",
"bundleHash": "68571cc2c3082cacf1774e681d6ff82d1beddb4241cebc2179f9235001cd730f",
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"tradeDateCount": 1216,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54"
},
"before": {
"runtime": "5dec29a0d6ee",
"lowOverheadEngineSeconds": [7.363, 6.922, 6.917],
"medianEngineSeconds": 6.922
},
"after": {
"processColdRun": {
"runId": "btr_1788533379797_2361496_0",
"totalSeconds": 20.534,
"dataSeconds": 13.02,
"engineSeconds": 6.377
},
"hotRuns": [
{"runId": "btr_1788533414497_2361496_1", "totalSeconds": 7.182, "engineSeconds": 6.517},
{"runId": "btr_1788533424201_2361496_2", "totalSeconds": 7.164, "engineSeconds": 6.482},
{"runId": "btr_1788533436894_2361496_3", "totalSeconds": 7.289, "engineSeconds": 6.48},
{"runId": "btr_1788533447093_2361496_4", "totalSeconds": 7.367, "engineSeconds": 6.477}
],
"medianHotEngineSeconds": 6.481,
"medianEngineImprovementPercent": 6.37,
"lowOverheadProfileRun": {
"runId": "btr_1788533478319_2361496_5",
"totalSeconds": 6.917,
"engineSeconds": 6.251,
"sampleCount": 332,
"lostSamples": 0
},
"serviceMemoryCurrentBytes": 11426889728,
"serviceMemoryPeakBytes": 11434971136
},
"artifacts": [
{
"path": "/srv/fidc/canonical/run/fidc-private/evidence/instrument-symbol-id-profile-20260904/hot-five-year-perf.data",
"sha256": "a3d788f8e6ad799604b83019a4403fee509d6c955606f46bd721960e3d38433b"
},
{
"path": "/srv/fidc/canonical/run/fidc-private/evidence/instrument-symbol-id-profile-20260904/hot-five-year-perf-report.txt",
"sha256": "dbd3004b6ab2719e67b34ca4c6d98f908053f6f28f617bd1c1969552b0f14291"
},
{
"path": "/srv/fidc/canonical/run/fidc-private/evidence/instrument-symbol-id-profile-20260904/hot-five-year-run.json",
"sha256": "092276999525532005a815ab9d2588ee8865b4652c11367ca9ffe52b6a601557"
}
]
}
@@ -0,0 +1,95 @@
{
"schemaVersion": "fidc-interned-stock-state-symbols/v1",
"measuredAt": "2026-09-05T01:50:00+08:00",
"host": "192.168.31.177",
"engineCommit": "e5646ef80c9a5c6d2d3a1b9b6f330fbbc27aa4ab",
"runnerBinarySha256": "c2f6f0b8cc194a5e3f668bcc0d54b3712cf037267ccc6c04d5f850d8ccbd6314",
"implementation": {
"description": "intern each canonical symbol once in DataSet and clone Arc<str> into per-date StockExpressionState objects instead of allocating a String for every state",
"rhaiStringBoundaryPreserved": true,
"crossDateSharedSymbolTested": true,
"cachedFactorValues": false,
"cachedSelectionResults": false,
"pitSemanticsChanged": false
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"lowContentionEngineSeconds": [5.505, 5.748, 5.67, 6.005, 6.497],
"medianEngineSeconds": 5.748,
"compactKeyBaselineMedianEngineSeconds": 5.734,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [5.648, 5.35, 5.003],
"medianEngineSeconds": 5.35,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"hardwareCounters": {
"candidateRunId": "btr_1788544043073_3041540_6",
"candidateEngineSeconds": 8.227,
"candidateInstructions": 27997441984,
"candidateBranches": 4947191270,
"earlierSharedIndexBaselineInstructions": 28218847364,
"earlierSharedIndexBaselineBranches": 5024236708,
"observedInstructionReductionPercent": 0.7846044523842924,
"observedBranchReductionPercent": 1.533451728719079,
"attributionNote": "the earlier hardware baseline predates the compact cache-key change, so counters prove the combined structural reduction rather than symbol interning alone"
},
"profile": {
"runId": "btr_1788544253262_3041540_15",
"totalSeconds": 6.136,
"engineSeconds": 5.509,
"eventCycles": 15296376746,
"stockStatePercent": 16.25,
"preInternStockStatePercent": 23.86,
"mallocPercent": 4.22,
"numericVmPercent": 6.02,
"adjustedCloseMovingAveragePercent": 5.10
},
"excludedHighContentionBatch": {
"hostLoadAveragePeak": 69.23,
"engineSeconds": [7.247, 7.094, 7.481, 7.967, 37.981, 44.366],
"resultConsistent": true,
"performanceComparisonExcluded": true,
"reason": "independent tan child processes executed on backtest-reserved CPUs; no external process was modified"
},
"memory": {
"serviceCgroupCurrentBytes": 11485425664,
"serviceCgroupPeakBytes": 11488174080,
"processRssKiB": 11196756,
"processAnonymousKiB": 11180328
},
"testGate": {
"coreUnitTotal": 420,
"corePassed": 414,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-low-contention-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-perf-stat-20260905/perf-stat.csv",
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/interned-stock-symbol-profile-20260905/perf-report.txt"
],
"acceptance": {
"status": "accepted_structural_allocation_reduction_no_regression",
"endToEndSpeedupClaimed": false,
"reason": "both five-year strategies preserve exact outputs, low-contention wall time is neutral, and the stock-state profile plus hardware counters show less allocation-related work"
}
}
@@ -0,0 +1,62 @@
{
"schemaVersion": "fidc-engine-optimization-rejection/v1",
"generatedDate": "2026-09-07",
"host": "192.168.31.177",
"candidate": {
"engineCommit": "f2de8b281a7f9537665d86895f8c51fb33386d9e",
"implementationIdentitySha256": "1ea0d2834901504e79d76d9df722f009a2ced3c2a02ddf247eae8722be135516",
"description": "Store execution quote dates, symbol maps and quote vectors as nested Arc values so a lazy write clones only one path."
},
"revert": {
"engineCommit": "68c186f649965118da63ff0f0dc2272005538a55",
"implementationIdentitySha256": "4f81eb707baeb76375a03eb8989cb043089b1a7c1d07a998570861eb7dc6fa95",
"description": "Restore the flat read-optimized execution quote index."
},
"measurements": {
"currentCloseOneYear": {
"candidateHotMedianTotalSeconds": 1.19,
"candidateHotMedianEngineSeconds": 0.652,
"revertHotMedianTotalSeconds": 1.183,
"revertHotMedianEngineSeconds": 0.722,
"candidateEngineImprovementPercent": 9.695291
},
"nextBarOpenFiveYear": {
"candidateHotMedianTotalSeconds": 2.896,
"candidateHotMedianEngineSeconds": 2.25,
"revertHotMedianTotalSeconds": 2.848,
"revertHotMedianEngineSeconds": 2.223,
"candidateEngineRegressionPercent": 1.214575
},
"staticCurrentClose": {
"candidateHotMedianEngineSeconds": 0.324,
"acceptedFlatIndexHotMedianEngineSeconds": 0.322
},
"fullMinute": {
"candidateHotMedianEngineSeconds": 0.225,
"acceptedFlatIndexHotMedianEngineSeconds": 0.226
}
},
"correctness": {
"totalReturnEqual": true,
"tradeCountEqual": true,
"canonicalResultDigestEqual": true,
"resultStoreDigestEqual": true,
"terminalAuditEqual": true,
"candidateWorkspaceCoreTestsPassed": 442,
"revertWorkspaceCoreTestsPassed": 441,
"workspaceFailures": 0
},
"decision": {
"status": "rejected_and_removed",
"reason": "Nested Arc indirection improves the lazy-write strategy but causes a repeatable next_bar_open regression and has no static/full-minute read benefit.",
"replacementDirection": "Keep the flat immutable base index and isolate lazy additions in a run-local overlay without changing the base iterator representation."
},
"supportingEvidence": [
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-layered-quote-index-rejection-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/static-pool-layered-quote-index-rejection-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/full-minute-layered-quote-index-rejection-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-layered-quote-index-rejection-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-after-layered-index-revert-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-after-layered-index-revert-20260907.json"
]
}
@@ -0,0 +1,56 @@
{
"schemaVersion": "fidc-lazy-expression-scope-rejection/v1",
"measuredAt": "2026-09-05T04:06:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "1b44928",
"revertCommit": "229ca8332df050c6961f73bd1a17de0dab583cf0",
"restoredRunnerBinarySha256": "3e69af42e41321d31c69b552cf22d7033ce1ea8d94305a32e32461148cdbfc60",
"candidate": {
"description": "add closure-based lazy SelectiveExpressionScope bindings for dates, order/process state, stock strings, and position availability",
"primaryHotEngineSeconds": [3.838, 3.867, 4.017, 3.931, 3.866],
"primaryHotMedianEngineSeconds": 3.867,
"secondaryEngineSeconds": [3.664, 3.649, 3.693, 3.767],
"secondaryMedianEngineSeconds": 3.693
},
"acceptedBaseline": {
"primaryMedianEngineSeconds": 3.896,
"secondaryMedianEngineSeconds": 3.582
},
"observed": {
"primaryImprovementPercent": 0.744353182751538,
"secondaryRegressionPercent": 3.098827470686773
},
"rollbackBatchExcluded": {
"secondaryEngineSeconds": [5.343, 5.352, 6.996],
"hostLoadAverage": 28.38,
"reason": "managed factor workload changed phase after deployment; this batch is retained for correctness only"
},
"businessContract": {
"primaryTotalReturn": 0.9219861819172002,
"primaryTradeCount": 26088,
"primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"primaryResultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"secondaryTotalReturn": 1.1342962298106998,
"secondaryTradeCount": 19404,
"secondaryCanonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"secondaryResultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"testGate": {
"coreUnitTotal": 423,
"corePassed": 417,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/lazy-expression-scope-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/lazy-expression-scope-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/lazy-expression-scope-rollback-secondary-20260905.json"
],
"acceptance": {
"status": "rejected_and_removed",
"reason": "closure-based lazy bindings marginally helped one strategy but regressed the second stable five-year contract, so the implementation was removed"
}
}
@@ -0,0 +1,95 @@
{
"schemaVersion": "fidc-engine-optimization-ab/v1",
"generatedDate": "2026-09-07",
"host": "192.168.31.177",
"change": {
"engineCommit": "df7a1ee382843b22d145718576d24e78fdd6c804",
"description": "Use binary partition points on the shared ascending market-cap symbol index before constructing per-stock expression state.",
"activation": "Only ascending market-cap rank with a market-cap selection field and finite inclusive bounds.",
"fallback": "Free-float, custom field, custom expression, descending rank and non-finite bounds keep the original full candidate path.",
"cachedBusinessResults": false
},
"deployment": {
"serviceCommit": "e4ed9467b9a4ca3ed56c64724623f0ba0d005b91",
"implementationIdentitySha256": "b38dc2bb32d46197eb2c48460757b7e4e1c1a3ea80dee1447a169d35af4e759e",
"serviceBinarySha256": "31b97e2ff8d9dca00c685ab7e70c239cb038ba467ed3ec669a237de7b8d0ba27",
"runnerBinarySha256": "932d531a222df936c82ca38c3ddad1f940c386e315c906cd3da4f428348114d8",
"serviceUser": "boris",
"sourceLakeRestarted": false,
"health": "ok"
},
"currentCloseOneYear": {
"before": {
"processHotMedianTotalSeconds": 1.238,
"processHotMedianEngineSeconds": 0.773
},
"after": {
"runs": 5,
"processHotMedianTotalSeconds": 1.154,
"processHotMedianEngineSeconds": 0.714,
"totalReturn": 0.3201517861398,
"tradeCount": 5351,
"canonicalResultDigest": "7204c6f41b8e39fbf1af7fc55cd601b80f3427a7aa058394ccd8b0b14ca48eed",
"resultStoreDigest": "0b4d24ed5ec2b27cc4135707b4c51c78eb2c3e35a20da8108610778c30c72c73",
"terminalAuditStatus": "clean"
},
"improvementPercent": {
"total": 6.785137,
"engine": 7.6326
}
},
"crossModeRegression": {
"staticCurrentClose": {
"runs": 6,
"processHotMedianTotalSeconds": 0.822,
"processHotMedianEngineSeconds": 0.322,
"totalReturn": 0.13228843240310018,
"tradeCount": 4445,
"canonicalResultDigest": "fdfa855295c0b55bdbe6f39952ead1515e844bf033ced974d3c3ddc037a5d0b1",
"resultStoreDigest": "697566645116c76ff837cd36f7f9bbd7ad3eb30510a5b95012fb730d5072d511"
},
"nextBarOpenFiveYear": {
"runs": 5,
"processHotMedianTotalSeconds": 2.811,
"processHotMedianEngineSeconds": 2.188,
"totalReturn": 0.9922618879291,
"tradeCount": 25827,
"canonicalResultDigest": "ac1d167cb1e1073e1d1ecb01e914f94d7560081c1d238e6b4418d86233250719",
"resultStoreDigest": "79570e0ae6b07badc1b693dc897dd1381647d259a4fe44ed3e50bc215e2fd088"
},
"fullMinute": {
"runs": 6,
"processHotMedianTotalSeconds": 0.598,
"processHotMedianEngineSeconds": 0.226,
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"canonicalResultDigest": "7dae3a618932b90d36e9968c027f08a94d69b8fc6a56b1ea0cc1a2cb771d85b8",
"resultStoreDigest": "bbbd7080b8fd7f6e8c3a8132499842bd0e4d644dbfdc3e8dbca7d2c0c1381e93"
}
},
"profile": {
"beforePath": "/srv/fidc/canonical/run/fidc-private/evidence/current-close-shared-quote-profile-20260907",
"afterPath": "/srv/fidc/canonical/run/fidc-private/evidence/current-close-market-cap-range-profile-20260907",
"beforeSamples": 408,
"afterSamples": 375,
"lostSamples": 0,
"stockStatePercentBefore": 6.8,
"stockStatePercentAfter": 3.02,
"marketCapSelectionPercentBefore": 2.91,
"marketCapSelectionPercentAfter": 1.7
},
"verification": {
"workspaceCoreTestsPassed": 441,
"workspaceCoreTestsIgnored": 6,
"workspaceFailures": 0,
"allCanonicalDigestsMatched": true,
"allResultStoreDigestsMatched": true,
"allTerminalAuditsClean": true
},
"supportingEvidence": [
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-market-cap-range-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/static-pool-market-cap-range-control-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-market-cap-range-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/full-minute-market-cap-range-control-20260907.json"
]
}
@@ -0,0 +1,467 @@
{
"schemaVersion": "fidc-backtest-benchmark/v5",
"label": "native-pit-intraday",
"generatedAtUnixSeconds": 1788790043,
"requestSha256": "9dd420ae606878b90ff660fcd4efe520e04a237ec758ed974b87b6d9a72cab73",
"requestOrigin": {
"kind": "request_file"
},
"baseline": null,
"baselineComparable": true,
"baselineComparison": null,
"bundleRefresh": null,
"baseUrl": "http://127.0.0.1:8081",
"runs": [
{
"slot": 0,
"cacheClass": "restart_or_cold",
"runId": "btr_1788790021780_1150210_0",
"status": "succeeded",
"queueWaitSeconds": 0.0,
"executionSeconds": 8.994,
"totalSeconds": 8.994,
"runnerSeconds": 7.904,
"bundleValidationSeconds": 0.166,
"sourceEpochSeconds": 0.0,
"strategyContractSeconds": 0.001,
"terminalCalendarSeconds": 0.0,
"riskFreeRateSeconds": 0.031,
"riskFreeRateCacheHit": false,
"engineSeconds": 0.012,
"dataSeconds": 7.662,
"resultSeconds": 0.03,
"unattributedSeconds": 0.0,
"canonicalSeconds": 0.001,
"resultFinalization": {
"canonicalSeconds": 0.001,
"processEventStoreSeconds": 0.001,
"storePublishSeconds": 0.027,
"summarySeconds": 0.0
},
"dataPreparationTimings": {
"adjustmentValidationSeconds": 0.0,
"availableStockStDates": 0,
"benchmarkFetchSeconds": 0.344,
"candidatePlanBasePanelSeconds": 0.006,
"candidatePlanSelectionSeconds": 0.004,
"corporateActionSeconds": 0.605,
"datasetConstructSeconds": 0.0,
"externalFetchSeconds": 0.344,
"historyDateSeconds": 0.0,
"instrumentIndexBuildSeconds": 0.0,
"instrumentMetadataSeconds": 0.016,
"loopSeconds": 0.0,
"marketIndexBuildSeconds": 0.001,
"quotePlanSeconds": 0.935,
"riskSnapshotSeconds": 0.0,
"sourceQuerySeconds": 5.752,
"stockStLookupSeconds": 0.0,
"supplementalExecutionFetchSeconds": 0.0,
"totalSeconds": 7.659
},
"finalizationSeconds": 1.089,
"finalizationTimings": {
"artifactVerificationSeconds": 0.0,
"cacheRegistrationSeconds": 0.0,
"postgresClientAcquireSeconds": 0.216,
"postgresPersistSeconds": 0.872,
"preTerminalUpdateSeconds": 1.089,
"schemaCheckSeconds": 0.0,
"timingSource": "fidc-backtest-service-monotonic"
},
"totalReturn": 0.011211658838299776,
"tradeCount": 104,
"riskDecisionCount": 11,
"resultStoreDigest": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
"canonicalResultDigest": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
"terminalAudit": {
"cashReceivableCount": 0,
"cashReceivableTotalAmount": 0.0,
"earliestDeferredCashDate": null,
"futuresOpenOrderCount": 0,
"lastExecutionDate": "2025-09-12",
"omittedOpenOrderCount": 0,
"openOrderSamples": [],
"pendingCashFlowCount": 0,
"pendingCashFlowNetAmount": 0.0,
"status": "clean",
"stockOpenOrderCount": 0
},
"terminalAuditVerified": true,
"terminalAuditSha256": "5836794e260aa2cfd1a06d4568ae6577a37c9cbe09ec0f00995121ea5b2522d9",
"implementation": {
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
"builtAt": "2026-09-07T22:06:18+08:00",
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
"schemaVersion": "fidc-backtest-implementation/v1",
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
"status": "verified"
},
"implementationVerified": true,
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"artifactManifest": {
"canonicalResult": {
"algorithm": "sha256",
"ordering": "engine_fact_order_v2",
"schemaVersion": "fidc-canonical-backtest-result/v2",
"sections": {
"accountEvents": {
"rowCount": 104,
"sha256": "ea6bbbd55df3da0692dfd712f88ef6f8af510ada318641ede71884f5c5d14e7e"
},
"equityFacts": {
"rowCount": 5,
"sha256": "96004e235e4603c0a7d1a5629a2f0d2b364258d562e1d397cdd6925718a7d060"
},
"fillEvents": {
"rowCount": 104,
"sha256": "2c37f626b7de014f5638dc8edf33f3ca8d23126ab2e62f26f01fbcb33e8c4e48"
},
"holdingSnapshots": {
"rowCount": 112,
"sha256": "50abab8a304d2e2a7de344538aff604c8bf96ef00283330d107916cc2e234f67"
},
"orderEvents": {
"rowCount": 108,
"sha256": "f327f21151a5d7dbe53d2dcde9202be699f9672f758373eeb9c67985ca3826f5"
},
"riskAudits": {
"rowCount": 11,
"sha256": "428a8d7395b54ec5a9523eb745d5007c64b9d9867615e870d95ea9ea176f536e"
}
},
"sha256": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
"totalRows": 444
},
"processEventStore": {
"bytes": 44302,
"identitySha256": "1444075abea626e8d7d1b8c9c9cda984a2a687ff2b9465db5648e0f4525130e7",
"manifestBytes": 461,
"manifestSha256": "b43a818096d09648b809a46d12ad9eae39638d57ea52f7696906359036a80c52",
"schemaVersion": "fidc-backtest-process-event-manifest/v1",
"sections": {
"processEvents": {
"blockCount": 1,
"bytes": 43841,
"rowCount": 370,
"sha256": "8edb6dad95777e3eef5946acbbdfc4b9125df34a79ac716d8c3ff7835b552bf6"
}
},
"sha256": "661d84b7b430ac25fb5c959df8e7fb309133784d85a7524375cd803e29e52094",
"totalEvents": 370
},
"resultStore": {
"bytes": 148006,
"identitySha256": "484b7a4a8d5cc77c6222ce01f2fe452c890995a476579f209d1d4ac43885d60a",
"manifestBytes": 2999,
"manifestSha256": "a0a7072f176db79d781846cc124d2e4794c5084c48c525876863751f709c4e7f",
"schemaVersion": "fidc-backtest-fact-manifest/v1",
"sections": {
"accountEvents": {
"blockCount": 1,
"bytes": 13313,
"rowCount": 104,
"sha256": "ab2f936c9072f80d58487b62a6f64553d279582ddfe6af0299afe20447e83424"
},
"equityFacts": {
"blockCount": 1,
"bytes": 11009,
"rowCount": 5,
"sha256": "c40650f5b27cfff873194572ffe28a5412fe77730fe75492c7b5a575f8f5caae"
},
"fillEvents": {
"blockCount": 1,
"bytes": 40541,
"rowCount": 104,
"sha256": "80a3f1d3a9c23db7dec2dc70f029280c0844370be6d87703cd396a234068f708"
},
"holdingSnapshots": {
"blockCount": 1,
"bytes": 51338,
"rowCount": 112,
"sha256": "7684244dc8e7788f777309169be61b14fcb532abdd89fe93f0a7f2980a615ba9"
},
"orderEvents": {
"blockCount": 1,
"bytes": 25768,
"rowCount": 108,
"sha256": "31ff15d2cf79b1eb9c333d19d8fb400c9c5a52c36b0f662330ec39aa9d0f805c"
},
"riskAudits": {
"blockCount": 1,
"bytes": 3038,
"rowCount": 11,
"sha256": "5a7269219a21cf14c23d788a6a96ddf1d1ddf0ef1e3b57a8db4ae4b1da789aff"
}
},
"sha256": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
"totalEvents": 444
},
"schemaVersion": "fidc-backtest-artifacts/v4"
}
},
{
"slot": 1,
"cacheClass": "process_hot",
"runId": "btr_1788790036494_1150210_1",
"status": "succeeded",
"queueWaitSeconds": 0.0,
"executionSeconds": 0.596,
"totalSeconds": 0.596,
"runnerSeconds": 0.026,
"bundleValidationSeconds": 0.0,
"sourceEpochSeconds": 0.0,
"strategyContractSeconds": 0.003,
"terminalCalendarSeconds": 0.0,
"riskFreeRateSeconds": 0.0,
"riskFreeRateCacheHit": true,
"engineSeconds": 0.012,
"dataSeconds": 0.007,
"resultSeconds": 0.002,
"unattributedSeconds": 0.0,
"canonicalSeconds": 0.001,
"resultFinalization": {
"canonicalSeconds": 0.001,
"processEventStoreSeconds": 0.001,
"storePublishSeconds": 0.0,
"summarySeconds": 0.0
},
"dataPreparationTimings": {
"adjustmentValidationSeconds": 0.0,
"availableStockStDates": 0,
"benchmarkFetchSeconds": 0.0,
"candidatePlanBasePanelSeconds": 0.0,
"candidatePlanSelectionSeconds": 0.0,
"corporateActionSeconds": 0.0,
"datasetConstructSeconds": 0.0,
"externalFetchSeconds": 0.0,
"historyDateSeconds": 0.0,
"instrumentIndexBuildSeconds": 0.0,
"instrumentMetadataSeconds": 0.0,
"loopSeconds": 0.0,
"marketIndexBuildSeconds": 0.0,
"quotePlanSeconds": 0.0,
"riskSnapshotSeconds": 0.0,
"sourceQuerySeconds": 0.0,
"stockStLookupSeconds": 0.0,
"supplementalExecutionFetchSeconds": 0.0,
"totalSeconds": 0.006
},
"finalizationSeconds": 0.569,
"finalizationTimings": {
"artifactVerificationSeconds": 0.001,
"cacheRegistrationSeconds": 0.0,
"postgresClientAcquireSeconds": 0.27,
"postgresPersistSeconds": 0.298,
"preTerminalUpdateSeconds": 0.569,
"schemaCheckSeconds": 0.0,
"timingSource": "fidc-backtest-service-monotonic"
},
"totalReturn": 0.011211658838299776,
"tradeCount": 104,
"riskDecisionCount": 11,
"resultStoreDigest": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
"canonicalResultDigest": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
"terminalAudit": {
"cashReceivableCount": 0,
"cashReceivableTotalAmount": 0.0,
"earliestDeferredCashDate": null,
"futuresOpenOrderCount": 0,
"lastExecutionDate": "2025-09-12",
"omittedOpenOrderCount": 0,
"openOrderSamples": [],
"pendingCashFlowCount": 0,
"pendingCashFlowNetAmount": 0.0,
"status": "clean",
"stockOpenOrderCount": 0
},
"terminalAuditVerified": true,
"terminalAuditSha256": "5836794e260aa2cfd1a06d4568ae6577a37c9cbe09ec0f00995121ea5b2522d9",
"implementation": {
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
"builtAt": "2026-09-07T22:06:18+08:00",
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
"schemaVersion": "fidc-backtest-implementation/v1",
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
"status": "verified"
},
"implementationVerified": true,
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"artifactManifest": {
"canonicalResult": {
"algorithm": "sha256",
"ordering": "engine_fact_order_v2",
"schemaVersion": "fidc-canonical-backtest-result/v2",
"sections": {
"accountEvents": {
"rowCount": 104,
"sha256": "ea6bbbd55df3da0692dfd712f88ef6f8af510ada318641ede71884f5c5d14e7e"
},
"equityFacts": {
"rowCount": 5,
"sha256": "96004e235e4603c0a7d1a5629a2f0d2b364258d562e1d397cdd6925718a7d060"
},
"fillEvents": {
"rowCount": 104,
"sha256": "2c37f626b7de014f5638dc8edf33f3ca8d23126ab2e62f26f01fbcb33e8c4e48"
},
"holdingSnapshots": {
"rowCount": 112,
"sha256": "50abab8a304d2e2a7de344538aff604c8bf96ef00283330d107916cc2e234f67"
},
"orderEvents": {
"rowCount": 108,
"sha256": "f327f21151a5d7dbe53d2dcde9202be699f9672f758373eeb9c67985ca3826f5"
},
"riskAudits": {
"rowCount": 11,
"sha256": "428a8d7395b54ec5a9523eb745d5007c64b9d9867615e870d95ea9ea176f536e"
}
},
"sha256": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
"totalRows": 444
},
"processEventStore": {
"bytes": 44302,
"identitySha256": "1444075abea626e8d7d1b8c9c9cda984a2a687ff2b9465db5648e0f4525130e7",
"manifestBytes": 461,
"manifestSha256": "b43a818096d09648b809a46d12ad9eae39638d57ea52f7696906359036a80c52",
"schemaVersion": "fidc-backtest-process-event-manifest/v1",
"sections": {
"processEvents": {
"blockCount": 1,
"bytes": 43841,
"rowCount": 370,
"sha256": "8edb6dad95777e3eef5946acbbdfc4b9125df34a79ac716d8c3ff7835b552bf6"
}
},
"sha256": "661d84b7b430ac25fb5c959df8e7fb309133784d85a7524375cd803e29e52094",
"totalEvents": 370
},
"resultStore": {
"bytes": 148006,
"identitySha256": "484b7a4a8d5cc77c6222ce01f2fe452c890995a476579f209d1d4ac43885d60a",
"manifestBytes": 2999,
"manifestSha256": "a0a7072f176db79d781846cc124d2e4794c5084c48c525876863751f709c4e7f",
"schemaVersion": "fidc-backtest-fact-manifest/v1",
"sections": {
"accountEvents": {
"blockCount": 1,
"bytes": 13313,
"rowCount": 104,
"sha256": "ab2f936c9072f80d58487b62a6f64553d279582ddfe6af0299afe20447e83424"
},
"equityFacts": {
"blockCount": 1,
"bytes": 11009,
"rowCount": 5,
"sha256": "c40650f5b27cfff873194572ffe28a5412fe77730fe75492c7b5a575f8f5caae"
},
"fillEvents": {
"blockCount": 1,
"bytes": 40541,
"rowCount": 104,
"sha256": "80a3f1d3a9c23db7dec2dc70f029280c0844370be6d87703cd396a234068f708"
},
"holdingSnapshots": {
"blockCount": 1,
"bytes": 51338,
"rowCount": 112,
"sha256": "7684244dc8e7788f777309169be61b14fcb532abdd89fe93f0a7f2980a615ba9"
},
"orderEvents": {
"blockCount": 1,
"bytes": 25768,
"rowCount": 108,
"sha256": "31ff15d2cf79b1eb9c333d19d8fb400c9c5a52c36b0f662330ec39aa9d0f805c"
},
"riskAudits": {
"blockCount": 1,
"bytes": 3038,
"rowCount": 11,
"sha256": "5a7269219a21cf14c23d788a6a96ddf1d1ddf0ef1e3b57a8db4ae4b1da789aff"
}
},
"sha256": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
"totalEvents": 444
},
"schemaVersion": "fidc-backtest-artifacts/v4"
}
}
],
"summary": {
"resultConsistent": true,
"resultStoreDigest": "5ed069a1f5999542e6da78b10797aa21aabe71e13dd79649cd11157cbc57d3b4",
"canonicalResultDigest": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
"totalReturn": 0.011211658838299776,
"tradeCount": 104,
"terminalAudit": {
"cashReceivableCount": 0,
"cashReceivableTotalAmount": 0.0,
"earliestDeferredCashDate": null,
"futuresOpenOrderCount": 0,
"lastExecutionDate": "2025-09-12",
"omittedOpenOrderCount": 0,
"openOrderSamples": [],
"pendingCashFlowCount": 0,
"pendingCashFlowNetAmount": 0.0,
"status": "clean",
"stockOpenOrderCount": 0
},
"terminalAuditSha256": "5836794e260aa2cfd1a06d4568ae6577a37c9cbe09ec0f00995121ea5b2522d9",
"implementation": {
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
"builtAt": "2026-09-07T22:06:18+08:00",
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
"schemaVersion": "fidc-backtest-implementation/v1",
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
"status": "verified"
},
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"restartOrCold": {
"queueWaitSeconds": 0.0,
"executionSeconds": 8.994,
"totalSeconds": 8.994,
"runnerSeconds": 7.904,
"bundleValidationSeconds": 0.166,
"sourceEpochSeconds": 0.0,
"strategyContractSeconds": 0.001,
"terminalCalendarSeconds": 0.0,
"riskFreeRateSeconds": 0.031,
"riskFreeRateCacheHit": false,
"engineSeconds": 0.012,
"dataSeconds": 7.662,
"resultSeconds": 0.03,
"unattributedSeconds": 0.0,
"canonicalSeconds": 0.001,
"finalizationSeconds": 1.089
},
"processHotMedian": {
"queueWaitSeconds": 0.0,
"executionSeconds": 0.596,
"totalSeconds": 0.596,
"runnerSeconds": 0.026,
"bundleValidationSeconds": 0.0,
"sourceEpochSeconds": 0.0,
"strategyContractSeconds": 0.003,
"terminalCalendarSeconds": 0.0,
"riskFreeRateSeconds": 0.0,
"riskFreeRateCacheHit": true,
"engineSeconds": 0.012,
"dataSeconds": 0.007,
"resultSeconds": 0.002,
"unattributedSeconds": 0.0,
"canonicalSeconds": 0.001,
"finalizationSeconds": 0.569
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,276 @@
{
"schemaVersion": "fidc-backtest-benchmark/v5",
"label": "native-pit-nextopen",
"generatedAtUnixSeconds": 1788790370,
"requestSha256": "7948497bfa966eb118e56438e7c261869a26ab70a62186b92f761719f85e91c7",
"requestOrigin": {
"kind": "request_file"
},
"baseline": null,
"baselineComparable": true,
"baselineComparison": null,
"bundleRefresh": null,
"baseUrl": "http://127.0.0.1:8081",
"runs": [
{
"slot": 0,
"cacheClass": "restart_or_cold",
"runId": "btr_1788790344805_1150210_2",
"status": "succeeded",
"queueWaitSeconds": 0.0,
"executionSeconds": 21.61,
"totalSeconds": 21.61,
"runnerSeconds": 21.294,
"bundleValidationSeconds": 0.004,
"sourceEpochSeconds": 0.0,
"strategyContractSeconds": 0.003,
"terminalCalendarSeconds": 0.0,
"riskFreeRateSeconds": 0.017,
"riskFreeRateCacheHit": false,
"engineSeconds": 5.566,
"dataSeconds": 15.305,
"resultSeconds": 0.395,
"unattributedSeconds": 0.0,
"canonicalSeconds": 0.206,
"resultFinalization": {
"canonicalSeconds": 0.206,
"processEventStoreSeconds": 0.168,
"storePublishSeconds": 0.005,
"summarySeconds": 0.003
},
"dataPreparationTimings": {
"adjustmentValidationSeconds": 0.0,
"availableStockStDates": 0,
"benchmarkFetchSeconds": 0.023,
"candidatePlanBasePanelSeconds": 0.0,
"candidatePlanSelectionSeconds": 0.0,
"corporateActionSeconds": 0.017,
"datasetConstructSeconds": 2.943,
"externalFetchSeconds": 0.025,
"historyDateSeconds": 0.0,
"instrumentIndexBuildSeconds": 0.269,
"instrumentMetadataSeconds": 0.006,
"loopSeconds": 0.925,
"marketIndexBuildSeconds": 1.846,
"quotePlanSeconds": 0.0,
"riskSnapshotSeconds": 0.0,
"sourceQuerySeconds": 9.0,
"stockStLookupSeconds": 0.0,
"supplementalExecutionFetchSeconds": 0.0,
"totalSeconds": 15.3
},
"finalizationSeconds": 0.307,
"finalizationTimings": {
"artifactVerificationSeconds": 0.008,
"cacheRegistrationSeconds": 0.0,
"postgresClientAcquireSeconds": 0.0,
"postgresPersistSeconds": 0.299,
"preTerminalUpdateSeconds": 0.307,
"schemaCheckSeconds": 0.0,
"timingSource": "fidc-backtest-service-monotonic"
},
"totalReturn": 0.9857987122635,
"tradeCount": 25408,
"riskDecisionCount": 113,
"resultStoreDigest": "1eab5a5ffe59efeaba6b2048a33760aab31d42efa0fc10e35a4a7048db853235",
"canonicalResultDigest": "b29b085d43bcc0f8f1712767421781c70570a24112933623d4bbbef46508d710",
"terminalAudit": {
"cashReceivableCount": 0,
"cashReceivableTotalAmount": 0.0,
"earliestDeferredCashDate": null,
"futuresOpenOrderCount": 0,
"lastExecutionDate": "2026-08-28",
"omittedOpenOrderCount": 0,
"openOrderSamples": [],
"pendingCashFlowCount": 0,
"pendingCashFlowNetAmount": 0.0,
"status": "clean",
"stockOpenOrderCount": 0
},
"terminalAuditVerified": true,
"terminalAuditSha256": "0e8503c2bfe9ecfd93bab3a95f79461830886eb693cfe6f477279908c0d95bd5",
"implementation": {
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
"builtAt": "2026-09-07T22:06:18+08:00",
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
"schemaVersion": "fidc-backtest-implementation/v1",
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
"status": "verified"
},
"implementationVerified": true,
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"artifactManifest": {
"canonicalResult": {
"algorithm": "sha256",
"ordering": "engine_fact_order_v2",
"schemaVersion": "fidc-canonical-backtest-result/v2",
"sections": {
"accountEvents": {
"rowCount": 25572,
"sha256": "da7576c1cbade9b48f1a154d07e870f1472924f4e0054cba0ef06739585ce380"
},
"equityFacts": {
"rowCount": 1216,
"sha256": "13db0a6d484cdf871e4f1dc1a4bc8db1e809d2808b21cf4fcab3961e6ad9e6fe"
},
"fillEvents": {
"rowCount": 25408,
"sha256": "92e0d103bbb487af45d27b720cdc92ef398d16e11655a6d0bcff8046013335db"
},
"holdingSnapshots": {
"rowCount": 33164,
"sha256": "97e3d60230a167a960bd2f615adfa33a9171527b62f1a181e2d5af694090e492"
},
"orderEvents": {
"rowCount": 25541,
"sha256": "635fe79eab9b1db0c116da2860ded66c350c051f44751d36306d158777039744"
},
"riskAudits": {
"rowCount": 113,
"sha256": "17bc940bcac18b050235ae3f17195f12586d1e29cd7bc64b6189edf2e07d1406"
}
},
"sha256": "b29b085d43bcc0f8f1712767421781c70570a24112933623d4bbbef46508d710",
"totalRows": 111014
},
"processEventStore": {
"bytes": 10115947,
"identitySha256": "e89d00ea1c2525c2ab1b57a29f2447047f2523083a953869ea2e25a488986565",
"manifestBytes": 9757,
"manifestSha256": "9bff5b97488c8804275ee65a717f01dca9ac68443b9766d4aab8d36189b1d8a1",
"schemaVersion": "fidc-backtest-process-event-manifest/v1",
"sections": {
"processEvents": {
"blockCount": 61,
"bytes": 10106190,
"rowCount": 83859,
"sha256": "f436a32403aaa74b3068a0d8c74bfb7c429523d3f78617234eec82a009ab3da2"
}
},
"sha256": "aa7488614a85f1527713332039b0fa212d1807f11b03e0e701480bf5d8ede1a4",
"totalEvents": 83859
},
"resultStore": {
"bytes": 35831197,
"identitySha256": "a238986d82354e802d2c73526d035021b38e1eed0a2b0b0f1eb6ce41ed4f321b",
"manifestBytes": 54237,
"manifestSha256": "0991f096db161fd16da00a7529ece54a8cf68643d88aeec8f4d3fedc6c740d03",
"schemaVersion": "fidc-backtest-fact-manifest/v1",
"sections": {
"accountEvents": {
"blockCount": 61,
"bytes": 3303444,
"rowCount": 25572,
"sha256": "276fd3edd7e60afbb05fa32c111db3e1df2025637887509b6353b24c7f127a48"
},
"equityFacts": {
"blockCount": 61,
"bytes": 3761920,
"rowCount": 1216,
"sha256": "42c22c70bad16b01d8f70fa80977028b2789381772e4eff7bb231701e18293a1"
},
"fillEvents": {
"blockCount": 61,
"bytes": 7731746,
"rowCount": 25408,
"sha256": "c59a4a1e70195bd8a2421941e11b7fbb138d658796ef4d42466c44505b8353d8"
},
"holdingSnapshots": {
"blockCount": 61,
"bytes": 15176388,
"rowCount": 33164,
"sha256": "6e40d3f1d43005dc4f0c492662c4fdf600ca96e9abe874db4b56b9bd51e89cd4"
},
"orderEvents": {
"blockCount": 61,
"bytes": 5775840,
"rowCount": 25541,
"sha256": "14fa27db9328c4c6337157af4fc1efc1c4bc9f239a4605bf92439b0f3f426200"
},
"riskAudits": {
"blockCount": 36,
"bytes": 27622,
"rowCount": 113,
"sha256": "2f6d9a01ec568ed13943b9590e21d73bf614ce2c777302764533d2da0168b2d5"
}
},
"sha256": "1eab5a5ffe59efeaba6b2048a33760aab31d42efa0fc10e35a4a7048db853235",
"totalEvents": 111014
},
"schemaVersion": "fidc-backtest-artifacts/v4"
}
}
],
"summary": {
"resultConsistent": true,
"resultStoreDigest": "1eab5a5ffe59efeaba6b2048a33760aab31d42efa0fc10e35a4a7048db853235",
"canonicalResultDigest": "b29b085d43bcc0f8f1712767421781c70570a24112933623d4bbbef46508d710",
"totalReturn": 0.9857987122635,
"tradeCount": 25408,
"terminalAudit": {
"cashReceivableCount": 0,
"cashReceivableTotalAmount": 0.0,
"earliestDeferredCashDate": null,
"futuresOpenOrderCount": 0,
"lastExecutionDate": "2026-08-28",
"omittedOpenOrderCount": 0,
"openOrderSamples": [],
"pendingCashFlowCount": 0,
"pendingCashFlowNetAmount": 0.0,
"status": "clean",
"stockOpenOrderCount": 0
},
"terminalAuditSha256": "0e8503c2bfe9ecfd93bab3a95f79461830886eb693cfe6f477279908c0d95bd5",
"implementation": {
"buildManifestSha256": "fb4f7ac191c8ab8d8d39fc48014f84ce01d4c0731ffe986e7d5cb0107a3e5539",
"builtAt": "2026-09-07T22:06:18+08:00",
"engineCommit": "1b78186c4e37273c64c1f9e208c2e47021ee4fff",
"identitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"runnerBinarySha256": "7cddebf9ab5cbbd5d6f9bd5c7bf006c3895ee26967d23d80dc86b47c50fb761d",
"schemaVersion": "fidc-backtest-implementation/v1",
"serviceBinarySha256": "f09d121d73c34ad3cd5e2e0c24b0bdaf1974309c9ec3b93229cfb824b1e9f11a",
"serviceCommit": "c80a38b91a398fd713f1f1faaa19a581ea85efad",
"status": "verified"
},
"implementationIdentitySha256": "14e7ff10848d64ffcec232dea8cff23a032702301b81ae036bb0b26b4b0b5e33",
"restartOrCold": {
"queueWaitSeconds": 0.0,
"executionSeconds": 21.61,
"totalSeconds": 21.61,
"runnerSeconds": 21.294,
"bundleValidationSeconds": 0.004,
"sourceEpochSeconds": 0.0,
"strategyContractSeconds": 0.003,
"terminalCalendarSeconds": 0.0,
"riskFreeRateSeconds": 0.017,
"riskFreeRateCacheHit": false,
"engineSeconds": 5.566,
"dataSeconds": 15.305,
"resultSeconds": 0.395,
"unattributedSeconds": 0.0,
"canonicalSeconds": 0.206,
"finalizationSeconds": 0.307
},
"processHotMedian": {
"queueWaitSeconds": 0.0,
"executionSeconds": 21.61,
"totalSeconds": 21.61,
"runnerSeconds": 21.294,
"bundleValidationSeconds": 0.004,
"sourceEpochSeconds": 0.0,
"strategyContractSeconds": 0.003,
"terminalCalendarSeconds": 0.0,
"riskFreeRateSeconds": 0.017,
"riskFreeRateCacheHit": false,
"engineSeconds": 5.566,
"dataSeconds": 15.305,
"resultSeconds": 0.395,
"unattributedSeconds": 0.0,
"canonicalSeconds": 0.206,
"finalizationSeconds": 0.307
}
}
}
@@ -0,0 +1,424 @@
{
"strategy_id": "benchmark-native-factor-overlay",
"strategy_version_id": "goal-five-year-semantics-v1",
"user_id": "boris",
"runtime": {
"start_date": "2021-08-23",
"end_date": "2026-08-28",
"frequency": "1d",
"source_table": "strategy_factory_source_lake.daily_source_rows_v1",
"signal_symbol": "000852.SH",
"benchmark_symbol": "000852.SH",
"initial_cash": 10000000.0,
"backtestDataBundleId": "bt_bundle_b44e03990c76064f54a9",
"backtestDataBundleHash": "d7c1461131edaecb5981e207852782d92e636dbfee9fd7c44063605d96eb2b4f"
},
"execution": {
"matchingType": "next_bar_open",
"rebalanceCashMode": "same_point_net",
"slippageModel": "price_ratio",
"slippageValue": 0.0001,
"commissionRate": 0.0001,
"minimumCommission": 5.0,
"stampTaxRateBeforeChange": 0.001,
"stampTaxRateAfterChange": 0.0005,
"stampTaxChangeDate": "2023-08-28",
"volumeLimit": true,
"liquidityLimit": false,
"volumePercent": 0.25,
"riskPolicy": {
"allowMarketOrders": true,
"blacklistEnabled": false,
"blacklistedSymbols": [],
"commissionRate": 0.0001,
"forbidSameDayRebuyAfterSell": true,
"liquidityLimitEnabled": false,
"liveTradingEnabled": false,
"minimumCommission": 5.0,
"rejectBjseBuy": false,
"rejectBjseSelection": false,
"rejectInactiveBuy": true,
"rejectInactiveSelection": false,
"rejectInactiveSell": true,
"rejectKcbBuy": true,
"rejectKcbSelection": false,
"rejectLowerLimitSelection": false,
"rejectLowerLimitSell": true,
"rejectNewListingBuy": true,
"rejectNewListingSelection": false,
"rejectOneYuanBuy": true,
"rejectOneYuanSelection": false,
"rejectPausedBuy": true,
"rejectPausedSelection": false,
"rejectPausedSell": true,
"rejectStBuy": true,
"rejectStSelection": false,
"rejectStarStBuy": true,
"rejectStarStSelection": false,
"rejectUpperLimitBuy": true,
"rejectUpperLimitSelection": false,
"respectAllowBuySell": true,
"stampTaxChangeDate": "2023-08-28",
"stampTaxRateAfterChange": 0.0005,
"stampTaxRateBeforeChange": 0.001,
"volumeLimitEnabled": true,
"volumePercent": 0.25
}
},
"strategy_source": {
"source_type": "platform-strategy",
"language": "engine-script",
"parser": "omniquant-engine-script-v2",
"source_code": "strategy(\"xiaoshizhi_1_06_dynamic_small_cap_csi2000_signal_day_exposure\") {\n market(\"CN_A\");\n benchmark(\"000300.SH\");\n signal(\"932000.CSI\");\n\n let stocknum = 30;\n let candidate_pool_size = 50;\n let position_denominator_extra = 1;\n let signal_close_t = rolling_mean_current(\"signal_close\", 1);\n let signal_ma10_t = rolling_mean_current(\"signal_close\", 10);\n let signal_ma30_t = rolling_mean_current(\"signal_close\", 30);\n let signal_vol20_t = rolling_return_stddev_current(\"signal_close\", 20);\n let signal_high60_t = rolling_max_current(\"signal_close\", 60);\n let signal_drawdown60_t = 1.0 - safe_div(signal_close_t, signal_high60_t);\n let signal_range_t = safe_div(clamp(signal_close_t, 2000.0, 3000.0) - 2000.0, 1000.0);\n let market_cap_lower_t = 12.0 + signal_range_t * 5.0;\n let market_cap_upper_t = 40.0 + signal_range_t * 5.0;\n let base_exposure_t = signal_ma10_t > signal_ma30_t ? 1.0 : 0.3;\n let volatility_exposure_t = signal_vol20_t >= 0.025 ? 0.3 : 1.0;\n let drawdown_exposure_t = signal_drawdown60_t >= 0.08 ? 0.2 : 1.0;\n let final_exposure_t =\n signal_close_t > 0.0 &&\n signal_ma10_t > 0.0 &&\n signal_ma30_t > 0.0 &&\n signal_high60_t > 0.0\n ? min(min(base_exposure_t, volatility_exposure_t), drawdown_exposure_t)\n : 0.0;\n\n rebalance.every_days(1).at([\"15:00\"]);\n\n selection.market_cap_band(\n field=\"market_cap\",\n lower=market_cap_lower_t,\n upper=market_cap_upper_t\n );\n\n filter.stock_expr(((!is_star_st && !is_kcb && !is_bjse && rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10) && rolling_mean_current(\"close\", 10) > rolling_mean_current(\"close\", 30) && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)) && (!is_st))) && (ths_up_days_stock >= 1);\n\n ordering.rank_by(\"market_cap\", \"asc\");\n selection.candidate_limit(50);\n selection.limit(stocknum);\n\n allocation.buy_scale(30.0 / 31.0);\n execution.strict_value_budget(true)\n\n trading.hold_until_exit(true);\n trading.max_holding_days(90);\n trading.daily_top_up(true);\n trading.daily_position_target_adjust(true);\n trading.target_portfolio_daily(true);\n trading.rebalance_existing_positions(true);\n trading.retry_empty_rebalance(true);\n trading.release_slot_on_exit_signal(true);\n\n risk.stop_loss(0.08);\n risk.take_profit(0.16);\n risk.reference_price_mode(\"signal_day_post_adjusted_close\");\n risk.index_exposure(final_exposure_t);\n\n risk.policy(reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=false, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=false, blacklisted_symbols=[], allow_market_orders=true, live_trading_enabled=false, volume_limit_enabled=true, liquidity_limit_enabled=false, volume_percent=0.25, commission_rate=0.0001, minimum_commission=5.0, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\");\n\n execution.matching_type(\"next_bar_open\");\n execution.slippage(\"price_ratio\", 0.0001);\n execution.rebalance_cash_mode(\"same_point_net\");\n}"
},
"strategy_spec": {
"benchmark": {
"fallbackInstrumentId": "000852.SH",
"instrumentId": "000852.SH"
},
"engineConfig": {
"benchmarkSymbol": "000852.SH",
"commissionRate": 0.0001,
"dividendReinvestment": false,
"dynamicRange": {
"baseCapFloor": 7,
"baseIndexLevel": 2000,
"capSpan": 10,
"xs": 0.008
},
"frequency": "1d",
"indexThrottle": {
"defensiveExposure": 0.5,
"fullExposure": 1,
"longDays": 130,
"rsiRate": 1.0001,
"shortDays": 1
},
"liquidityLimit": false,
"matchingType": "next_bar_open",
"minimumCommission": 5.0,
"rankLimit": 30,
"rebalanceCashMode": "same_point_net",
"rebalanceSchedule": {
"frequency": "daily",
"time": "15:00"
},
"refreshRate": 1,
"riskPolicy": {
"allowMarketOrders": true,
"blacklistEnabled": false,
"blacklistedSymbols": [],
"commissionRate": 0.0001,
"forbidSameDayRebuyAfterSell": true,
"liquidityLimitEnabled": false,
"liveTradingEnabled": false,
"minimumCommission": 5.0,
"rejectBjseBuy": false,
"rejectBjseSelection": false,
"rejectInactiveBuy": true,
"rejectInactiveSelection": false,
"rejectInactiveSell": true,
"rejectKcbBuy": true,
"rejectKcbSelection": false,
"rejectLowerLimitSelection": false,
"rejectLowerLimitSell": true,
"rejectNewListingBuy": true,
"rejectNewListingSelection": false,
"rejectOneYuanBuy": true,
"rejectOneYuanSelection": false,
"rejectPausedBuy": true,
"rejectPausedSelection": false,
"rejectPausedSell": true,
"rejectStBuy": true,
"rejectStSelection": false,
"rejectStarStBuy": true,
"rejectStarStSelection": false,
"rejectUpperLimitBuy": true,
"rejectUpperLimitSelection": false,
"respectAllowBuySell": true,
"stampTaxChangeDate": "2023-08-28",
"stampTaxRateAfterChange": 0.0005,
"stampTaxRateBeforeChange": 0.001,
"volumeLimitEnabled": true,
"volumePercent": 0.25
},
"rsiRate": 1.0001,
"signalSymbol": "000852.SH",
"skipWindows": [],
"slippageModel": "price_ratio",
"slippageValue": 0.0001,
"stampTaxChangeDate": "2023-08-28",
"stampTaxRateAfterChange": 0.0005,
"stampTaxRateBeforeChange": 0.001,
"stockMaFilter": {
"longDays": 30,
"midDays": 10,
"rsiRate": 1.0001,
"shortDays": 5,
"volumeLongDays": 100,
"volumeShortDays": 5
},
"stopLossMultiplier": 0.08,
"strictValueBudget": true,
"takeProfitMultiplier": 0.16,
"templateId": "xiaoshizhi_1_06_dynamic_small_cap_csi2000_signal_day_exposure",
"volumeLimit": true,
"volumePercent": 0.25
},
"execution": {
"commissionRate": 0.0001,
"executionGranularity": "daily_or_minute_bar",
"extractor": "omniquant-engine-script-v2",
"frequency": "1d",
"liquidityLimit": false,
"matchingType": "next_bar_open",
"minimumCommission": 5.0,
"priceSource": "current_bar_close_or_next_bar_open_or_minute_bar",
"rebalanceCashMode": "same_point_net",
"riskPolicy": {
"allowMarketOrders": true,
"blacklistEnabled": false,
"blacklistedSymbols": [],
"commissionRate": 0.0001,
"forbidSameDayRebuyAfterSell": true,
"liquidityLimitEnabled": false,
"liveTradingEnabled": false,
"minimumCommission": 5.0,
"rejectBjseBuy": false,
"rejectBjseSelection": false,
"rejectInactiveBuy": true,
"rejectInactiveSelection": false,
"rejectInactiveSell": true,
"rejectKcbBuy": true,
"rejectKcbSelection": false,
"rejectLowerLimitSelection": false,
"rejectLowerLimitSell": true,
"rejectNewListingBuy": true,
"rejectNewListingSelection": false,
"rejectOneYuanBuy": true,
"rejectOneYuanSelection": false,
"rejectPausedBuy": true,
"rejectPausedSelection": false,
"rejectPausedSell": true,
"rejectStBuy": true,
"rejectStSelection": false,
"rejectStarStBuy": true,
"rejectStarStSelection": false,
"rejectUpperLimitBuy": true,
"rejectUpperLimitSelection": false,
"respectAllowBuySell": true,
"stampTaxChangeDate": "2023-08-28",
"stampTaxRateAfterChange": 0.0005,
"stampTaxRateBeforeChange": 0.001,
"volumeLimitEnabled": true,
"volumePercent": 0.25
},
"selectionGranularity": "strategy_factory_source_lake.daily_source_rows_v1",
"slippageModel": "price_ratio",
"slippageValue": 0.0001,
"sourceKind": "platform-strategy",
"sourceLanguage": "engine-script",
"stampTaxChangeDate": "2023-08-28",
"stampTaxRateAfterChange": 0.0005,
"stampTaxRateBeforeChange": 0.001,
"strictValueBudget": true,
"volumeLimit": true,
"volumePercent": 0.25
},
"factorRefs": [
"market_cap",
"ths_up_days_stock"
],
"market": "CN_A",
"metadata": {
"backtestDataBundle": {
"sourceTable": "strategy_factory_source_lake.daily_source_rows_v1",
"backtestDataBundleId": "bt_bundle_b44e03990c76064f54a9",
"backtestDataBundleHash": "d7c1461131edaecb5981e207852782d92e636dbfee9fd7c44063605d96eb2b4f"
},
"backtestDataBundleHash": "d7c1461131edaecb5981e207852782d92e636dbfee9fd7c44063605d96eb2b4f",
"backtestDataBundleId": "bt_bundle_b44e03990c76064f54a9",
"sourceTable": "strategy_factory_source_lake.daily_source_rows_v1"
},
"rebalance": {
"dailyApproximation": "日线回测按 matching_type 撮合;分钟线回测按交易时刻分钟价格撮合",
"frequencyDays": 1,
"schedule": {
"frequency": "daily",
"time": "15:00"
},
"tradeTimes": [
"15:00"
]
},
"risk": {
"indexThrottleExpr": "final_exposure_t",
"stopLossExpr": "0.08",
"stopTakeReferencePriceMode": "signal_day_post_adjusted_close",
"takeProfitExpr": "0.16"
},
"runtimeExpressions": {
"allocation": {
"buyScaleExpr": "30.0 / 31.0"
},
"ordering": {
"rankBy": "market_cap",
"rankExpr": "",
"rankOrder": "asc"
},
"prelude": "let stocknum = 30;\nlet candidate_pool_size = 50;\nlet position_denominator_extra = 1;\nlet signal_close_t = rolling_mean_current(\"signal_close\", 1);\nlet signal_ma10_t = rolling_mean_current(\"signal_close\", 10);\nlet signal_ma30_t = rolling_mean_current(\"signal_close\", 30);\nlet signal_vol20_t = rolling_return_stddev_current(\"signal_close\", 20);\nlet signal_high60_t = rolling_max_current(\"signal_close\", 60);\nlet signal_drawdown60_t = 1.0 - safe_div(signal_close_t, signal_high60_t);\nlet signal_range_t = safe_div(clamp(signal_close_t, 2000.0, 3000.0) - 2000.0, 1000.0);\nlet market_cap_lower_t = 12.0 + signal_range_t * 5.0;\nlet market_cap_upper_t = 40.0 + signal_range_t * 5.0;\nlet base_exposure_t = signal_ma10_t > signal_ma30_t ? 1.0 : 0.3;\nlet volatility_exposure_t = signal_vol20_t >= 0.025 ? 0.3 : 1.0;\nlet drawdown_exposure_t = signal_drawdown60_t >= 0.08 ? 0.2 : 1.0;\nlet final_exposure_t = signal_close_t > 0.0 && signal_ma10_t > 0.0 && signal_ma30_t > 0.0 && signal_high60_t > 0.0 ? min(min(base_exposure_t, volatility_exposure_t), drawdown_exposure_t) : 0.0;\nlet warmup_probe = rolling_sum(\"amount\", 125);",
"risk": {
"exposureExpr": "final_exposure_t",
"stopLossExpr": "0.08",
"stopTakeReferencePriceMode": "signal_day_post_adjusted_close",
"takeProfitExpr": "0.16"
},
"schedule": {
"frequency": "daily",
"time": "15:00"
},
"selection": {
"candidateLimitExpr": "50",
"limitExpr": "stocknum",
"marketCapField": "market_cap",
"marketCapLowerExpr": "market_cap_lower_t",
"marketCapUpperExpr": "market_cap_upper_t",
"stockFilterExpr": "(((!is_star_st && !is_kcb && !is_bjse && rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10) && rolling_mean_current(\"close\", 10) > rolling_mean_current(\"close\", 30) && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)) && (!is_st))) && (ths_up_days_stock >= 1)"
},
"trading": {
"actions": [],
"dailyPositionTargetAdjust": true,
"dailyTopUp": true,
"holdUntilExit": true,
"maxHoldingDays": 90,
"rebalanceExistingPositions": true,
"releaseSlotOnExitSignal": true,
"retryEmptyRebalance": true,
"rotationEnabled": true,
"stage": "on_day",
"subscriptionGuardRequired": false,
"targetPortfolioDaily": true
}
},
"seasonality": {
"skipWindows": []
},
"selectors": [
{
"field": "market_cap",
"lowerExpr": "market_cap_lower_t",
"mapping": "market_cap -> strategy_factory_source_lake.runtime_fields.market_cap",
"type": "dynamicRange",
"upperExpr": "market_cap_upper_t"
},
{
"expr": "(((!is_star_st && !is_kcb && !is_bjse && rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10) && rolling_mean_current(\"close\", 10) > rolling_mean_current(\"close\", 30) && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)) && (!is_st))) && (ths_up_days_stock >= 1)",
"type": "filter"
},
{
"limitExpr": "stocknum",
"orderBy": [
"market_cap asc"
],
"type": "rank"
}
],
"signalSymbol": "000852.SH",
"sourceCode": "strategy(\"xiaoshizhi_1_06_dynamic_small_cap_csi2000_signal_day_exposure\") {\n market(\"CN_A\");\n benchmark(\"000300.SH\");\n signal(\"932000.CSI\");\n\n let stocknum = 30;\n let candidate_pool_size = 50;\n let position_denominator_extra = 1;\n let signal_close_t = rolling_mean_current(\"signal_close\", 1);\n let signal_ma10_t = rolling_mean_current(\"signal_close\", 10);\n let signal_ma30_t = rolling_mean_current(\"signal_close\", 30);\n let signal_vol20_t = rolling_return_stddev_current(\"signal_close\", 20);\n let signal_high60_t = rolling_max_current(\"signal_close\", 60);\n let signal_drawdown60_t = 1.0 - safe_div(signal_close_t, signal_high60_t);\n let signal_range_t = safe_div(clamp(signal_close_t, 2000.0, 3000.0) - 2000.0, 1000.0);\n let market_cap_lower_t = 12.0 + signal_range_t * 5.0;\n let market_cap_upper_t = 40.0 + signal_range_t * 5.0;\n let base_exposure_t = signal_ma10_t > signal_ma30_t ? 1.0 : 0.3;\n let volatility_exposure_t = signal_vol20_t >= 0.025 ? 0.3 : 1.0;\n let drawdown_exposure_t = signal_drawdown60_t >= 0.08 ? 0.2 : 1.0;\n let final_exposure_t =\n signal_close_t > 0.0 &&\n signal_ma10_t > 0.0 &&\n signal_ma30_t > 0.0 &&\n signal_high60_t > 0.0\n ? min(min(base_exposure_t, volatility_exposure_t), drawdown_exposure_t)\n : 0.0;\n\n rebalance.every_days(1).at([\"15:00\"]);\n\n selection.market_cap_band(\n field=\"market_cap\",\n lower=market_cap_lower_t,\n upper=market_cap_upper_t\n );\n\n filter.stock_expr(((!is_star_st && !is_kcb && !is_bjse && rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10) && rolling_mean_current(\"close\", 10) > rolling_mean_current(\"close\", 30) && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)) && (!is_st))) && (ths_up_days_stock >= 1);\n\n ordering.rank_by(\"market_cap\", \"asc\");\n selection.candidate_limit(50);\n selection.limit(stocknum);\n\n allocation.buy_scale(30.0 / 31.0);\n execution.strict_value_budget(true)\n\n trading.hold_until_exit(true);\n trading.max_holding_days(90);\n trading.daily_top_up(true);\n trading.daily_position_target_adjust(true);\n trading.target_portfolio_daily(true);\n trading.rebalance_existing_positions(true);\n trading.retry_empty_rebalance(true);\n trading.release_slot_on_exit_signal(true);\n\n risk.stop_loss(0.08);\n risk.take_profit(0.16);\n risk.reference_price_mode(\"signal_day_post_adjusted_close\");\n risk.index_exposure(final_exposure_t);\n\n risk.policy(reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=false, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=false, blacklisted_symbols=[], allow_market_orders=true, live_trading_enabled=false, volume_limit_enabled=true, liquidity_limit_enabled=false, volume_percent=0.25, commission_rate=0.0001, minimum_commission=5.0, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\");\n\n execution.matching_type(\"next_bar_open\");\n execution.slippage(\"price_ratio\", 0.0001);\n execution.rebalance_cash_mode(\"same_point_net\");\n}",
"strategyId": "warmup-expression-contract-acceptance",
"universe": {
"exclude": [],
"implementationNotes": [
"ST、停牌、退市、新股、科创、一元、涨跌停、同日卖出禁买、成交量和费用由 riskPolicy / RiskLimits 统一执行",
"上市日期与退市日期取自 instrument 结构化字段,不再使用股票名称做 ST/退市判断",
"盘中 current_price / last_price 由策略交易时刻批量 tick 查询驱动"
]
},
"version": "1.0.0",
"stockPoolFactorContract": {
"schemaVersion": 1,
"entryLogic": "all",
"exitLogic": "any",
"conditions": [
{
"factorRef": "up_days_stock",
"label": "连涨天数",
"role": "selection",
"registryRole": "selection_feature",
"roleRegistrySha256": "1d0b307c168feda08d5fbe20f0e88964230553f8ceb2017b66aec48dbd5a5b57",
"roleEvidence": {
"role": "selection_feature",
"polarity": "trend_persistence_positive",
"signalShape": "state",
"holdingStates": [
"flat"
],
"requiredConfirmations": [],
"cooldownTradingDays": 0,
"windowTradingDays": 1,
"recommendedParameters": {
"inputUnit": "days",
"minimum": 0
}
},
"operator": ">=",
"threshold": 1,
"semantic": {
"ref": "up_days_stock",
"label": "连涨天数",
"status": "available",
"queryable": true,
"source": "strategy-factory-source-lake:indicator",
"schema": "strategy-factory.value-semantics/v1",
"valueType": "integer",
"semanticType": "count",
"comparisonGroup": "count",
"storageUnit": "days",
"inputUnit": "days",
"inputScale": 1.0,
"allowedOperators": [
">",
">=",
"<",
"<=",
"==",
"!=",
"between",
"in"
],
"nullable": true,
"declared": true,
"metadataStatus": "declared",
"semanticProvenance": "explicit_manifest",
"businessSemanticDeclared": true,
"minimum": 0,
"backtestBinding": {
"field": "ths_up_days_stock",
"sourceDataset": "indicators_up_days_stock"
},
"tradingRoles": [
{
"role": "selection_feature",
"polarity": "trend_persistence_positive",
"signalShape": "state",
"holdingStates": [
"flat"
],
"requiredConfirmations": [],
"cooldownTradingDays": 0,
"windowTradingDays": 1,
"recommendedParameters": {
"inputUnit": "days",
"minimum": 0
}
}
],
"tradingRoleTradable": true,
"tradingRoleEvidenceStatus": "source_lake_registered_indicator",
"tradingRoleRegistrySha256": "1d0b307c168feda08d5fbe20f0e88964230553f8ceb2017b66aec48dbd5a5b57"
}
}
]
}
}
}
@@ -0,0 +1,58 @@
{
"date": "2026-09-07",
"host": "192.168.31.177",
"identity": "boris",
"implementationCommit": "a02ac6e",
"valueRegressionCommit": "cb97aa1",
"scope": "Native daily indicator fields explicitly bound in stockPoolFactorContract; other factor fields and pricing are unchanged.",
"targetedTests": {"passed": 3, "failed": 0},
"fullLibraryTestsBeforeAdditionalValueCase": {"passed": 447, "ignored": 6, "failed": 0},
"provenCases": [
"09:30, 10:30 and 14:30 resolve to the preceding trading date",
"15:00 resolves to the completed decision day",
"active intraday datetime applies when no explicit execution time exists",
"next-open retains the completed decision day",
"no previous trading date does not fall back to the current day",
"stock state with prior value 2 and current value 999 reads 2 intraday and 999 at close",
"unbound factor value remains unchanged"
],
"backtestServiceDeployed": true,
"paperLiveRuntimeDeployed": true,
"paperLiveDeploymentEvidence": "/Users/boris/WorkSpace/fidc-trading-platform/docs/evidence/trading-engine-revision-deployment-20260907.json",
"realBacktestAcceptanceComplete": false,
"scopedBacktestEvidence": {
"intraday": {
"range": "2025-09-08..2025-09-12",
"time": "09:30",
"runIds": ["btr_1788790021780_1150210_0", "btr_1788790036494_1150210_1"],
"seconds": [8.994, 0.596],
"tradeCount": 104,
"riskDecisionCount": 11,
"canonical": "5c8a110cc6f285b9d569e818a0472a8b5c76f14df853a1c2c42d1b5222c39b3a",
"identical": true,
"persistedFactorBindingVerified": true,
"rawParquetAudit": {
"buyFills": 60,
"priorPassCurrentFailExamples": 21,
"existingPositionTopUpsBelowCurrentSelectionThreshold": 23,
"retainedTargetReentryBelowCurrentSelectionThreshold": {"symbol": "600276.SH", "date": "2025-09-12", "priorExit": "2025-09-11 stop_loss_exit", "configuration": "reenterExitedTargets=true", "reason": "model_target_portfolio_daily"},
"note": "Selection-only conditions are not an execution-time buy veto. Position adjustment and explicit retained-target reentry must be audited separately from fresh candidate selection."
}
},
"nextOpen": {
"range": "2021-08-23..2026-08-28",
"runId": "btr_1788790344805_1150210_2",
"seconds": 21.610,
"tradeCount": 25408,
"canonical": "b29b085d43bcc0f8f1712767421781c70570a24112933623d4bbbef46508d710",
"matchesPreFixBaseline": true
},
"terminalAudits": "clean",
"rawEvidenceDirectory": "native-daily-factor-replays-20260907"
},
"limitations": [
"This is not a generic per-field publication-timestamp model for all factor datasets.",
"Raw dynamic fields used without a stock-pool native binding need separate availability-contract review.",
"Broader factor/PIT and actual trading acceptance remain required; these replays use isolated API research fixtures. Browser draft handoff is separately recorded in OmniQuant documentation."
]
}
@@ -0,0 +1,76 @@
{
"schemaVersion": "fidc-noalloc-instrument-board-rules/v1",
"measuredAt": "2026-09-05T03:48:00+08:00",
"host": "192.168.31.177",
"engineCommit": "cfb19b5783cb446099cb3e4aff70cc39beec2e88",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "3e69af42e41321d31c69b552cf22d7033ce1ea8d94305a32e32461148cdbfc60",
"implementation": {
"description": "evaluate KSH and BJSE order quantity rules with borrowed case-insensitive comparisons instead of allocating normalized board strings",
"rulesChanged": false,
"cacheAdded": false,
"pitSemanticsChanged": false,
"coveredBoards": ["KSH", "BJS", "BJ", "BJSE", "default"]
},
"primaryFiveYearContract": {
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"processCold": {
"totalSeconds": 17.395,
"dataSeconds": 12.879,
"engineSeconds": 3.873
},
"processHotEngineSeconds": [3.861, 3.812, 3.896, 3.978, 3.924],
"processHotMedianEngineSeconds": 3.896,
"snapshotSourceBaselineMedianEngineSeconds": 3.91,
"observedMedianImprovementPercent": 0.3580562659846607,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [3.519, 3.501, 3.582, 3.597],
"medianEngineSeconds": 3.582,
"snapshotSourceBaselineMedianEngineSeconds": 3.676,
"observedMedianImprovementPercent": 2.5571273122959823,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"profile": {
"runId": "btr_1788551290580_3858934_10",
"engineSeconds": 4.283,
"minimumOrderQuantityPercent": 0.21,
"orderStepSizePercent": 0.11,
"snapshotSourceBaselineMinimumOrderQuantityPercent": 1.28,
"minimumOrderQuantityRelativeReductionPercent": 83.59375
},
"memory": {
"serviceCgroupCurrentBytes": 11477078016,
"serviceCgroupPeakBytes": 11478847488,
"processRssKiB": 11195964,
"processAnonymousKiB": 11179928,
"cacheMemoryAddedBytes": 0
},
"testGate": {
"coreUnitTotal": 422,
"corePassed": 416,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/noalloc-instrument-board-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/noalloc-instrument-board-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/noalloc-instrument-board-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/noalloc-instrument-board-profile-20260905/perf-report.txt"
],
"acceptance": {
"status": "accepted_generic_no_allocation_board_rules",
"reason": "both five-year contracts preserve exact outputs, the primary does not regress, the secondary improves, the targeted profile hotspot falls, and no cache memory is added"
}
}
@@ -0,0 +1,74 @@
{
"schemaVersion": "fidc-numeric-vm-binding-generation-rejection/v1",
"measuredAt": "2026-09-06T05:36:00+08:00",
"host": "192.168.31.177",
"baseline": {
"engineCommit": "5f08978",
"primaryFiveYearHotMedianEngineSeconds": 2.629,
"primaryFiveYearTotalReturn": 0.9219861819172002,
"primaryFiveYearTradeCount": 26088,
"primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"primaryResultStoreSha256": "9ccf0c0fc6f5d72e974381ad4cd09a80241d7de99f2649f2b01736f7c80dc2c7",
"profileInstructions": 16600366059,
"profileBranches": 2592214949
},
"compileTimeIdentifierBinding": {
"engineCommit": "2135a5b",
"implementation": "map every numeric VM identifier to a typed runtime enum during expression plan compilation",
"primaryFiveYearHotEngineSeconds": [2.608, 2.624, 2.651, 2.619, 2.781],
"primaryFiveYearHotMedianEngineSeconds": 2.624,
"observedMedianImprovementPercent": 0.190186,
"profileInstructions": 16360894880,
"instructionReductionPercent": 1.442566,
"secondaryFiveYearResultConsistent": true,
"secondaryFiveYearPerformanceExcluded": true,
"secondaryFiveYearExclusionReason": "the symbolic campaign entered a high-memory-bandwidth phase between the primary and secondary batches",
"netCodeLinesAdded": 562,
"retained": false
},
"generationStampedScratchSlots": {
"engineCommit": "5122c73",
"implementation": "invalidate numeric VM variable and local slots with a generation counter instead of clearing Option arrays for each evaluation",
"localReleaseBenchmark": {
"baselineVmNanosecondsPerEvaluation": 86.992,
"candidateSamples": [83.284, 94.166, 86.275, 84.138, 85.453],
"candidateMedianNanosecondsPerEvaluation": 85.453,
"componentImprovementPercent": 1.769136,
"comparisonStrength": "weak because the baseline contains one sample"
},
"primaryFiveYearHotEngineSeconds": [2.634, 2.636, 2.608, 2.642, 2.710],
"primaryFiveYearHotMedianEngineSeconds": 2.636,
"observedMedianRegressionPercent": 0.266261,
"profileInstructions": 16521146700,
"instructionReductionPercent": 0.477215,
"netCodeLinesAdded": 39,
"retained": false
},
"hostLoad": {
"symbolicWorkersObserved": 3,
"symbolicWorkerCpuPercentApproximate": [720, 718, 698],
"symbolicWorkerRssBytesApproximate": [54479982592, 53353455616, 53941170176],
"wallTimeComparisonsAcrossPhasesExcluded": true
},
"testGate": {
"workspacePassedBeforeFirstCandidateRejection": 539,
"workspacePassedForGenerationCandidate": 540,
"failed": 0,
"ignoredManualBenchmarks": 8
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/goal-primary-five-year-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-primary-five-year-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-secondary-five-year-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-candidate-perf-stat-20260906.csv",
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-rollback-primary-five-year-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-rollback-perf-stat-20260906.csv",
"/srv/fidc/canonical/run/fidc-private/evidence/vm-generation-primary-five-year-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/vm-generation-candidate-perf-stat-20260906.csv"
],
"decision": {
"status": "rejected_and_removed",
"reason": "both candidates preserved exact business results but failed to produce a material, stable end-to-end improvement; the typed binding added disproportionate code and the generation slots slightly regressed the five-year median",
"nextTarget": "profile and specialize the numeric VM instruction dispatch or runtime helper execution without changing expression, PIT, or lazy short-circuit semantics"
}
}
@@ -0,0 +1,24 @@
{
"schemaVersion": "fidc-numeric-vm-generation-scratch-rejection/v1",
"measuredAt": "2026-09-05T04:34:00+08:00",
"host": "local-macos",
"candidateCommitted": false,
"candidateDeployed": false,
"candidate": {
"description": "replace per-evaluation Option slot clearing with value arrays and u64 generation stamps",
"iterations": 2000000,
"vmNanosecondsPerEvaluation": 86.405,
"rhaiNanosecondsPerEvaluation": 364.356
},
"baseline": {
"iterations": 2000000,
"vmNanosecondsPerEvaluation": 86.384,
"rhaiNanosecondsPerEvaluation": 374.938
},
"observedVmRegressionPercent": 0.024310531,
"targetedTestsPassed": 7,
"acceptance": {
"status": "rejected_and_removed_before_commit",
"reason": "generation checks exactly offset slot initialization savings in the existing representative VM benchmark"
}
}
@@ -0,0 +1,91 @@
{
"schemaVersion": "fidc-post-hotpath-full-minute-regression/v1",
"measuredAt": "2026-09-05T04:12:00+08:00",
"host": "192.168.31.177",
"engineCommit": "1f8a0fd",
"runtimeCodeCommit": "cfb19b5783cb446099cb3e4aff70cc39beec2e88",
"runnerBinarySha256": "3e69af42e41321d31c69b552cf22d7033ce1ea8d94305a32e32461148cdbfc60",
"contract": {
"startDate": "2025-01-02",
"endDate": "2025-11-17",
"frequency": "1m",
"matchingType": "minute_last",
"scheduleTime": "10:18:00",
"slippageModel": "price_ratio",
"slippageValue": 0.001,
"commissionRate": 0.0001,
"minimumCommission": 5.0,
"stampTaxRate": 0.0005,
"volumeLimit": false,
"configuredVolumePercentInactive": 0.25
},
"bundle": {
"oldBundleRejectedAsStale": true,
"oldBundleId": "bt_bundle_d3109a7220681b850f31",
"newBundleId": "bt_bundle_daadb1059454b30a9a6d",
"newBundleHash": "14024f3774efb800575c7bd47b6a594b7b7fd7731493a1785c119ee7533378d2",
"newDataEpoch": "strategy-factory-source-lake:scope-v1:3556e7dbadbdc94176d23a92acc78c84c50e2ec2a6d16b9dfd25978b1a98b2ae"
},
"result": {
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"buyTradeCount": 79,
"sellTradeCount": 77,
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
"oldCanonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"canonicalEqualAcrossDataEpoch": true,
"terminalAuditStatus": "clean",
"warnings": []
},
"performance": {
"coldTotalSeconds": 106.497,
"coldDataSeconds": 37.21,
"coldEngineSeconds": 65.519,
"coldRunContended": true,
"hotTotalSeconds": 0.584,
"hotDataSeconds": 0.004,
"hotEngineSeconds": 0.162,
"hotResultSeconds": 0.005,
"hotFinalizationSeconds": 0.397
},
"whiteBoxAudit": {
"status": "ok",
"sourceRowsFormat": "arrow",
"queriedSymbols": 1,
"requiredPairs": 156,
"queriedDailyBars": 156,
"queriedMinuteBars": 156,
"slippagePriceChecks": 156,
"missingExecutionTimestamps": 0,
"executionTimestamp": "each trade date at 10:18:00",
"failureCounts": {},
"warningCounts": {},
"firstBuy": {
"symbol": "000001.SZ",
"rawMinuteClose": 11.55,
"fillPrice": 11.56155,
"quantity": 43200,
"grossAmount": 499458.96,
"commission": 49.945896,
"stampTax": 0.0
},
"lastSell": {
"symbol": "000001.SZ",
"rawMinuteClose": 11.77,
"fillPrice": 11.75823,
"quantity": 400,
"grossAmount": 4703.292,
"commission": 5.0,
"stampTax": 2.351646
}
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/post-daily-hotpath-full-minute-current-bundle-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/post-daily-hotpath-full-minute-audit-20260905/audit.json"
],
"acceptance": {
"status": "passed",
"reason": "the current Source Lake generation reproduces the historical canonical result and every fill passes minute timestamp, raw-price, slippage, fee, tax, and terminal-state audit"
}
}
@@ -0,0 +1,80 @@
{
"schemaVersion": "fidc-rank-expression-presence/v1",
"measuredAt": "2026-09-05T12:50:00+08:00",
"host": "192.168.31.177",
"engineCommit": "42999ffa2cb6967315dc4c97220561c0611cda22",
"featureCommit": "c225d8484f0493a473b68c11389513d5167ba4cc",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "3060dd5afe720bccd1d2f959f6af04af6325766f8c6829cd80e5af9785ec2346",
"implementation": {
"description": "freeze rank expression presence at strategy construction and remove repeated rank_expr trim checks in selection dispatch",
"strategyCodeChangeRecompilesRequirement": true,
"rankValuesCached": false,
"selectionResultsCached": false,
"pitSemanticsChanged": false
},
"primaryFiveYearContract": {
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"previousAcceptedEngineMedianSeconds": 2.611,
"engineSeconds": [2.439, 2.447, 2.435, 2.442, 2.441, 2.432],
"processHotMedianEngineSeconds": 2.441,
"observedImprovementPercent": 6.510915,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"previousAcceptedEngineMedianSeconds": 2.346,
"engineSeconds": [2.191, 2.165, 2.249, 2.17, 2.159, 2.18],
"processHotMedianEngineSeconds": 2.17,
"observedImprovementPercent": 7.502131,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"genericRankFiveYearContract": {
"rankBy": "free_float_cap",
"totalReturn": 0.7140315244542004,
"tradeCount": 21876,
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
"resultStoreSha256": "e2c7401bb9950da6fdfc0b516428061651e3e5b874a5f80c4b03ab8a81e90191",
"lowLoadEngineSeconds": [3.529, 3.472, 3.514, 3.465, 3.455, 3.459],
"processHotMedianEngineSeconds": 3.465,
"sameLoadCausalComparison": "not_claimed",
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"fullMinuteContract": {
"scheduleTime": "10:18",
"matchingType": "minute_last",
"slippageModel": "price_ratio",
"slippageValue": 0.001,
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
"engineSeconds": [0.157, 0.16],
"processHotMedianEngineSeconds": 0.16,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"testGate": {
"workspaceTotal": 544,
"passed": 536,
"ignoredManualBenchmarks": 8,
"failed": 0
},
"memory": {
"persistentCacheAddedBytes": 0,
"currentServiceCgroupBytesAfterRestart": 23715840
},
"acceptance": {
"status": "accepted_compile_time_rank_presence",
"reason": "both independent five-year ordered contracts improved in the low-load runs, generic rank stayed business-identical without a causal wall-time claim, and full-minute execution stayed identical"
}
}
@@ -0,0 +1,33 @@
{
"schemaVersion": "fidc-rolling-boundary-reuse-rejection/v1",
"measuredAt": "2026-09-05T12:40:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "6b5d576",
"rollbackCommit": "b55ac0bf814641ec09f61f4ec10d52b311ee8aab",
"finalRemovalCommit": "b55ac0bf814641ec09f61f4ec10d52b311ee8aab",
"implementation": {
"description": "share one adjusted-close base factor and one valid-volume count across batched rolling windows",
"businessResultsChanged": false,
"retainedInProduction": false
},
"primaryFiveYearContract": {
"candidateEngineSeconds": [2.6, 2.557, 2.564, 2.603, 2.574, 2.614],
"candidateProcessHotMedianEngineSeconds": 2.574,
"acceptedBaselineEngineSeconds": 2.441,
"observedRegressionPercent": 5.448587,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"tradeCount": 26088
},
"secondaryFiveYearContract": {
"candidateEngineSeconds": [2.381, 2.323, 2.344, 2.377, 2.353, 2.348],
"candidateProcessHotMedianEngineSeconds": 2.348,
"acceptedBaselineEngineSeconds": 2.17,
"observedRegressionPercent": 8.202765,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"tradeCount": 19404
},
"acceptance": {
"status": "rejected_cross_strategy_regression",
"reason": "both real five-year controls became slower despite exact result parity; candidate code and production binary were removed"
}
}
@@ -0,0 +1,45 @@
{
"schemaVersion": "fidc-selection-band-precheck-rejection/v1",
"measuredAt": "2026-09-05T12:55:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "e060af380ebfce6d9e802bb6e9677c11835fc0d0",
"removalCommit": "42999ffa2cb6967315dc4c97220561c0611cda22",
"implementation": {
"description": "evaluate factor-backed market-cap band bounds before constructing StockExpressionState",
"genericRankBenefitObserved": true,
"orderedControlRegressionObserved": true,
"retainedInProduction": false
},
"genericRankFiveYearContract": {
"rankBy": "free_float_cap",
"candidateProcessHotMedianEngineSeconds": 2.835,
"acceptedBaselineProcessHotMedianEngineSeconds": 3.465,
"observedImprovementPercent": 18.181818,
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
"tradeCount": 21876
},
"primaryFiveYearContract": {
"candidateProcessHotMedianEngineSeconds": 2.565,
"acceptedBaselineProcessHotMedianEngineSeconds": 2.441,
"observedRegressionPercent": 5.079148,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"tradeCount": 26088
},
"secondaryFiveYearContract": {
"candidateProcessHotMedianEngineSeconds": 2.3465,
"acceptedBaselineProcessHotMedianEngineSeconds": 2.17,
"observedRegressionPercent": 8.133641,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"tradeCount": 19404
},
"fullMinuteRollbackContract": {
"tradeCount": 156,
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
"terminalAuditStatus": "clean"
},
"acceptance": {
"status": "rejected_cross_strategy_regression",
"reason": "single generic-rank improvement cannot justify regressions in both standard ordered controls; candidate was removed from local, remote and deployed code"
}
}
@@ -0,0 +1,89 @@
{
"schemaVersion": "fidc-engine-optimization-ab/v1",
"generatedDate": "2026-09-07",
"host": "192.168.31.177",
"change": {
"engineCommit": "f45b3a71fa78ecb95b0fa80687130ae7385549e8",
"description": "Do not clone the shared multi-date execution quote map when a run reaches the end of one execution date.",
"dataContract": "The prepared-data cache remains immutable and shared. A uniquely owned run-local quote map still removes completed dates.",
"cachedBusinessResults": false
},
"deployment": {
"serviceCommit": "11cfe1b8d09c1da4807400f3d903199ea6f1e711",
"implementationIdentitySha256": "7dbc839acb26fed98fc84a0bc221c316bf370046780c6ae75a79171fd52e5535",
"serviceBinarySha256": "bc9385ef340b8e0b3d0d17a9251e159806107443865361271ee0ccd8dbe1a768",
"runnerBinarySha256": "55c321f39518b5821e35ee1ec5320e35f94590be3aeeeac0827df07670e623fc",
"serviceUser": "boris",
"sourceLakeRestarted": false,
"health": "ok"
},
"currentCloseOneYear": {
"before": {
"processHotMedianTotalSeconds": 1.548,
"processHotMedianEngineSeconds": 1.09
},
"after": {
"runs": 5,
"processHotMedianTotalSeconds": 1.238,
"processHotMedianEngineSeconds": 0.773,
"totalReturn": 0.3201517861398,
"tradeCount": 5351,
"canonicalResultDigest": "7204c6f41b8e39fbf1af7fc55cd601b80f3427a7aa058394ccd8b0b14ca48eed",
"resultStoreDigest": "0b4d24ed5ec2b27cc4135707b4c51c78eb2c3e35a20da8108610778c30c72c73",
"terminalAuditStatus": "clean"
},
"improvementPercent": {
"total": 20.02584,
"engine": 29.082569
}
},
"crossModeRegression": {
"staticCurrentClose": {
"processHotTotalSeconds": 0.866,
"processHotEngineSeconds": 0.326,
"totalReturn": 0.13228843240310018,
"tradeCount": 4445,
"canonicalResultDigest": "fdfa855295c0b55bdbe6f39952ead1515e844bf033ced974d3c3ddc037a5d0b1",
"resultStoreDigest": "697566645116c76ff837cd36f7f9bbd7ad3eb30510a5b95012fb730d5072d511"
},
"nextBarOpenFiveYear": {
"processHotTotalSeconds": 3.212,
"processHotEngineSeconds": 2.597,
"totalReturn": 0.9922618879291,
"tradeCount": 25827,
"canonicalResultDigest": "ac1d167cb1e1073e1d1ecb01e914f94d7560081c1d238e6b4418d86233250719",
"resultStoreDigest": "79570e0ae6b07badc1b693dc897dd1381647d259a4fe44ed3e50bc215e2fd088"
},
"fullMinute": {
"processHotTotalSeconds": 0.973,
"processHotEngineSeconds": 0.231,
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"canonicalResultDigest": "7dae3a618932b90d36e9968c027f08a94d69b8fc6a56b1ea0cc1a2cb771d85b8",
"resultStoreDigest": "bbbd7080b8fd7f6e8c3a8132499842bd0e4d644dbfdc3e8dbca7d2c0c1381e93"
}
},
"profile": {
"beforePath": "/srv/fidc/canonical/run/fidc-private/evidence/current-close-post-preplan-profile-20260907",
"afterPath": "/srv/fidc/canonical/run/fidc-private/evidence/current-close-shared-quote-profile-20260907",
"beforeSamples": 564,
"afterSamples": 408,
"lostSamples": 0,
"beforeFinding": "Arc::make_mut was reached from remove_execution_quotes_on_date and cloned the shared quote map.",
"afterFinding": "The remove_execution_quotes_on_date call chain is absent; release_execution_quotes_on_date accounts for 0.27 percent of sampled cycles."
},
"verification": {
"workspaceCoreTestsPassed": 440,
"workspaceCoreTestsIgnored": 6,
"workspaceFailures": 0,
"allCanonicalDigestsMatched": true,
"allResultStoreDigestsMatched": true,
"allTerminalAuditsClean": true
},
"supportingEvidence": [
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/current-close-shared-quote-release-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/static-pool-shared-quote-release-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/next-open-shared-quote-release-20260907.json",
"/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/full-minute-shared-quote-release-20260907.json"
]
}
@@ -0,0 +1,93 @@
{
"schemaVersion": "fidc-shared-market-cap-order-index-ab/v1",
"measuredAt": "2026-09-05T00:43:00+08:00",
"host": "192.168.31.177",
"engineCommit": "05953f857a3e3844595bb63a684c34a2e44cbaaf",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "188269bf0c2ee65d6c11ed37ddddfdaf86695941e8123f69f950054bea39796e",
"implementation": {
"description": "build an immutable per-date u32 symbol-id order by market cap in parallel with DataSet construction and reuse it across strategy runs",
"riskEvaluationOrderPreserved": true,
"riskDiagnosticsOrderPreserved": true,
"selectionOrderPreserved": true,
"cachedFactorValues": false,
"cachedSelectionResults": false,
"cachedAccountOrOrderState": false,
"pitSemanticsChanged": false,
"adjustmentSemanticsChanged": false
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"firstRun": {
"runId": "btr_1788540070941_2799354_0",
"totalSeconds": 19.825,
"dataSeconds": 13.141,
"engineSeconds": 6.024
},
"hotRuns": [
{"runId": "btr_1788540095588_2799354_1", "totalSeconds": 6.664, "engineSeconds": 5.528},
{"runId": "btr_1788540107127_2799354_2", "totalSeconds": 6.546, "engineSeconds": 5.742},
{"runId": "btr_1788540118939_2799354_3", "totalSeconds": 6.586, "engineSeconds": 5.643},
{"runId": "btr_1788540129894_2799354_4", "totalSeconds": 6.591, "engineSeconds": 5.929},
{"runId": "btr_1788540141992_2799354_5", "totalSeconds": 6.794, "engineSeconds": 6.145}
],
"hotMedianEngineSeconds": 5.742,
"restoredBaselineHotMedianEngineSeconds": 6.702,
"observedMedianImprovementPercent": 14.324082363473588,
"terminalAuditStatus": "clean",
"resultConsistent": true
},
"secondaryFiveYearContract": {
"description": "same frozen source and execution contract with target positions changed from 30 to 20",
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [5.699, 5.545, 5.604, 5.23],
"processHotMedianEngineSeconds": 5.545,
"terminalAuditStatus": "clean",
"resultConsistent": true
},
"memory": {
"baselineCurrentBytes": 11426254848,
"candidateCurrentBytes": 11433934848,
"candidatePeakBytes": 11435282432,
"observedIncreaseBytes": 7680000
},
"profile": {
"runId": "btr_1788540249539_2799354_10",
"totalSeconds": 6.35,
"engineSeconds": 5.696,
"eventCycles": 15734772698,
"stockStatePercent": 23.86,
"stableUniverseSortPresentInTopProfile": false,
"numericVmPercent": 5.29,
"adjustedCloseMovingAveragePercent": 4.71,
"mallocPercent": 4.33
},
"testGate": {
"coreUnitTotal": 419,
"corePassed": 413,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-profile-20260905/perf-report.txt",
"/srv/fidc/canonical/run/fidc-private/evidence/shared-market-cap-order-profile-20260905/run.json"
],
"acceptance": {
"status": "accepted_shared_immutable_index_end_to_end_no_regression",
"reason": "both five-year strategies preserve exact business outputs, the primary stable hot median improves by about 14.3 percent, first-build data time remains at the prior process-cold baseline, and resident memory increases by only about 7.7 MB"
}
}
@@ -0,0 +1,47 @@
{
"schemaVersion": "fidc-signal-rolling-scan-rejection/v1",
"measuredAt": "2026-09-05T13:45:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "b65b3ed8f1739f7df84c50ae7993ca2ed3d5857e",
"removalCommit": "2aa330786aa45ccdfa88c2dc8a65821d8d7a9606",
"implementation": {
"description": "replace materialized signal_close values and return values with direct current-series scans for rolling max and return sample standard deviation",
"businessResultsChanged": false,
"retainedInProduction": false
},
"genericRankFiveYearContract": {
"rankBy": "free_float_cap",
"candidateEngineSeconds": [3.707, 3.704, 3.7, 3.736, 3.699, 3.721],
"candidateProcessHotMedianEngineSeconds": 3.704,
"acceptedBaselineProcessHotMedianEngineSeconds": 3.465,
"observedRegressionPercent": 6.897547,
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
"tradeCount": 21876
},
"primaryFiveYearContract": {
"candidateEngineSeconds": [2.624, 2.607, 2.604, 2.601, 2.61],
"candidateProcessHotMedianEngineSeconds": 2.6055,
"acceptedBaselineProcessHotMedianEngineSeconds": 2.441,
"observedRegressionPercent": 6.739041,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"tradeCount": 26088
},
"secondaryFiveYearContract": {
"candidateEngineSeconds": [2.349, 2.336, 2.39, 2.365, 2.353],
"candidateProcessHotMedianEngineSeconds": 2.359,
"acceptedBaselineProcessHotMedianEngineSeconds": 2.17,
"observedRegressionPercent": 8.709677,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"tradeCount": 19404
},
"rollbackFullMinuteContract": {
"tradeCount": 156,
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
"terminalAuditStatus": "clean"
},
"acceptance": {
"status": "rejected_cross_strategy_regression",
"reason": "direct scans were slower for generic rank and both daily controls despite exact output parity; candidate and deployed binary were removed"
}
}
@@ -0,0 +1,94 @@
{
"schemaVersion": "fidc-specialized-stock-snapshot-source/v1",
"measuredAt": "2026-09-05T03:38:00+08:00",
"host": "192.168.31.177",
"engineCommit": "d79678d8509b40ec085f2fa0a7c1e7cb90c95810",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "3f9ce258d3b055097b0cb9e4ae4df423b3d584420befae4d85cdd7ccc51e38bf",
"implementation": {
"description": "monomorphize indexed and daily-view stock snapshot sources, reuse execution/factor DailySnapshotView in selection loops, and keep the shared stock-state builder free of runtime Optional lookup branches",
"eligibleUniverseRowChanged": false,
"runtimeOptionalViewBranch": false,
"selectionResultCached": false,
"factorValueCached": false,
"pitSemanticsChanged": false,
"componentBenchmark": {
"rows": 6000,
"rounds": 200,
"indexedSeconds": 0.008542,
"dailyViewSeconds": 0.002821,
"speedup": 3.027294
}
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"processCold": {
"totalSeconds": 17.62,
"dataSeconds": 13.03,
"engineSeconds": 3.976
},
"processHotEngineSeconds": [4.085, 3.91, 3.893, 4.125, 3.874],
"processHotMedianEngineSeconds": 3.91,
"typedHelperBaselineMedianEngineSeconds": 4.729,
"observedMedianImprovementPercent": 17.318672023683654,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [3.672, 3.655, 3.676, 3.689],
"medianEngineSeconds": 3.676,
"typedHelperBaselineMedianEngineSeconds": 4.049,
"observedMedianImprovementPercent": 9.212151148431715,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"profile": {
"runId": "btr_1788550694645_3730056_10",
"engineSeconds": 4.278,
"allThreadEventCycles": 95711324146,
"comparisonAllThreadEventCycles": 110359308122,
"observedCycleReductionPercent": 13.272993665207602,
"marketBySymbolIdPercent": 1.69,
"factorBySymbolIdPercent": 0.13,
"candidateBySymbolIdPercent": 0.32,
"comparisonMarketBySymbolIdPercent": 3.46,
"comparisonFactorBySymbolIdPercent": 2.66,
"comparisonCandidateBySymbolIdPercent": 2.51
},
"memory": {
"serviceCgroupCurrentBytes": 11498143744,
"serviceCgroupPeakBytes": 11501256704,
"processRssKiB": 11216788,
"processAnonymousKiB": 11200492,
"typedHelperBaselineCgroupCurrentBytes": 11495362560,
"observedCgroupIncreaseBytes": 2781184
},
"testGate": {
"coreUnitTotal": 421,
"corePassed": 415,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/specialized-stock-snapshot-source-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/specialized-stock-snapshot-source-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/specialized-stock-snapshot-source-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/specialized-stock-snapshot-source-profile-20260905/perf-report.txt"
],
"acceptance": {
"status": "accepted_generic_monomorphized_snapshot_source",
"reason": "both five-year strategies preserve exact outputs and improve stable medians, lookup hotspots fall materially, and the implementation avoids the enlarged row and runtime Optional branches that caused the prior rejected design"
}
}
@@ -0,0 +1,87 @@
{
"schemaVersion": "fidc-stock-snapshot-field-projection/v1",
"measuredAt": "2026-09-05T02:11:00+08:00",
"host": "192.168.31.177",
"engineCommit": "7f17fa1fb427fefe5c70c9170fe98e2a8761e3ac",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "0c0ea8629afa789024a6212bcb7ac0e8291f6a88943dd9f7bee16d4902c96be1",
"implementation": {
"description": "compile per-strategy requirements for amount and upper/lower limit-touch fields, then skip their factor-map lookups and fallback calculations when no expression consumes them",
"dynamicFactorMapStillLoadsAllFields": true,
"explicitActionsStillLoadAllFields": true,
"factorValuesCached": false,
"selectionResultsCached": false,
"pitSemanticsChanged": false,
"executionSemanticsChanged": false
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"processCold": {
"totalSeconds": 18.368,
"dataSeconds": 12.691,
"engineSeconds": 4.952
},
"processHotEngineSeconds": [5.434, 4.762, 5.269, 5.366, 5.356],
"processHotMedianEngineSeconds": 5.356,
"compactKeyBaselineMedianEngineSeconds": 5.734,
"observedMedianImprovementPercent": 6.592256714335544,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"lowContentionEngineSeconds": [4.867, 4.36],
"highContentionEngineSecondsExcluded": [32.561, 24.001],
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"hardwareCounters": {
"candidateRunId": "btr_1788545432236_3086752_11",
"candidateEngineSeconds": 5.291,
"candidateInstructions": 26663937176,
"candidateBranches": 4751846020,
"candidateBranchMisses": 21422590,
"internedSymbolBaselineInstructions": 27997441984,
"internedSymbolBaselineBranches": 4947191270,
"observedInstructionReductionPercent": 4.762952303864304,
"observedBranchReductionPercent": 3.9486092075029067
},
"profile": {
"runId": "btr_1788545382848_3086752_10",
"engineSeconds": 5.009,
"eventCycles": 13976422355,
"stockStateSelfPercent": 12.52,
"internedSymbolBaselineStockStatePercent": 16.25,
"stockStateRelativeReductionPercent": 22.953846153846154,
"factorMapBTreeGetBaselinePercent": 4.0,
"factorMapBTreeGetPresentInCandidateTopProfile": false
},
"testGate": {
"coreUnitTotal": 421,
"corePassed": 415,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-profile-20260905/perf-report.txt",
"/srv/fidc/canonical/run/fidc-private/evidence/stock-snapshot-field-projection-perf-stat-20260905/perf-stat.csv"
],
"acceptance": {
"status": "accepted_generic_compiled_field_projection",
"reason": "two distinct five-year contracts preserve exact business outputs and terminal audits, while the primary low-contention median, hardware counters, and profile all show less work without adding a shared mutable cache"
}
}
@@ -0,0 +1,92 @@
{
"schemaVersion": "fidc-stock-state-calendar-index-reuse-ab/v1",
"measuredAt": "2026-09-05T00:02:00+08:00",
"host": "192.168.31.177",
"engineCommit": "6d458dbbc6fa3acb1bff0824307281c82170031a",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"serviceBinarySha256": "bcd918407de2aa3fdc230d8e9976be5cbf5d3b53185f2a528d55beb10f5f1552",
"runnerBinarySha256": "e33dd75aa7b481975c0991cf634d07900cdefa51756802e28e64f18b980a83dc",
"implementation": {
"description": "resolve the trading-calendar index once when the per-day stock-state cache advances, then reuse it for decision and current rolling-series boundaries",
"cachedFactorValues": false,
"cachedSelectionResults": false,
"cachedOrderState": false,
"pitSemanticsChanged": false,
"adjustmentSemanticsChanged": false,
"fallback": "dates outside the indexed calendar retain the existing date-based rolling fallback"
},
"componentBenchmark": {
"samples": 6,
"iterationsPerSample": 5000000,
"dateLookupSeconds": 0.400282218,
"reusedCalendarIndexSeconds": 0.070751718,
"speedup": 5.657561813552005,
"checksum": 90000000,
"equal": true
},
"testGate": {
"coreUnitTotal": 419,
"corePassed": 413,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"beforeEngineSeconds": [6.764, 6.391, 12.387],
"beforeMedianEngineSeconds": 6.764,
"afterHotEngineSeconds": [6.224, 6.497, 6.405, 6.539, 6.386],
"afterMedianEngineSeconds": 6.405,
"observedMedianImprovementPercent": 5.307510348905976,
"benchmarkSummaryProcessHotMedianSeconds": {
"before": 9.389,
"after": 6.451
},
"note": "The explicit all-run medians are reported above. The benchmark tool excludes its first run when calculating processHotMedian; concurrent non-FIDC load makes the tool summary less comparable than the complete sample list."
},
"secondaryFiveYearContract": {
"description": "same frozen source and execution contract with target positions changed from 30 to 20",
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [6.217, 9.279, 10.771],
"resultConsistent": true,
"performanceClaimed": false,
"reason": "two runs overlapped high external host contention; the strategy is retained as cross-strategy semantic evidence only"
},
"excludedHighContentionBatch": {
"hostLoadAverageObserved": [65.08, 44.49, 35.56],
"engineSeconds": [11.601, 12.061, 33.904, 23.149, 7.391],
"resultConsistent": true,
"performanceComparisonExcluded": true,
"reason": "independent tan processes and active FIDC symbolic workers caused material host scheduling contention; no external process was modified"
},
"runtime": {
"servicePid": 2548679,
"serviceUser": "boris",
"serviceState": "active/running",
"allowedCpus": "0 2 4 6 8 10 12 14 48 50 52 54 56 58 60 62",
"memoryCurrentBytes": 11426254848,
"memoryPeakBytes": 11427790848,
"maxConcurrentRuns": 1,
"runnerThreadsPerRun": 8
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-index-before-20260904.json",
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-index-after-20260904.json",
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-index-after-hot-20260904.json",
"/srv/fidc/canonical/run/fidc-private/evidence/calendar-index-secondary-20260905.json"
],
"acceptance": {
"status": "accepted_component_and_primary_end_to_end_no_regression",
"reason": "the component path is materially faster, the stable primary five-year batch improves, and both frozen business contracts preserve exact returns, trade counts, canonical digests, result-store digests, and clean terminal audits"
}
}
@@ -0,0 +1,76 @@
{
"schemaVersion": "fidc-quote-plan-optimization-ab/v1",
"generatedDate": "2026-09-07",
"host": "192.168.31.177",
"implementation": {
"engineCommit": "c934a948c6b3adc0d38b99cf5826481a9a7c3ceb",
"serviceCommit": "178cd2a76961b7e687424cc47107c2c1c3f4f74e",
"implementationIdentitySha256": "192392bd7c6bbb7d9a8601aca51a55de4a518357d2c3014b4a09e644e7caccc6",
"engineChange": "Stream ascending market-cap quote candidates from the shared ordered symbol index without a candidate-state arena or redundant sort.",
"runnerChange": "Build the preliminary selection DataSet from daily bundles instead of flattening and regrouping component vectors."
},
"quotePlanSeconds": {
"baselineSamples": [
7.143,
6.848,
7.404
],
"baselineMedian": 7.143,
"candidateSamples": [
7.092,
6.115,
5.931,
6.179,
6.129,
6.096
],
"candidateMedian": 6.122,
"improvementPercent": 14.293714
},
"dynamicCurrentClose": {
"totalReturn": 0.3201517861398,
"tradeCount": 5351,
"riskDecisionCount": 2661,
"canonicalResultDigest": "7204c6f41b8e39fbf1af7fc55cd601b80f3427a7aa058394ccd8b0b14ca48eed",
"resultStoreDigest": "0b4d24ed5ec2b27cc4135707b4c51c78eb2c3e35a20da8108610778c30c72c73",
"terminalAuditStatus": "clean"
},
"crossModeRegression": {
"staticCurrentClose": {
"totalReturn": 0.13228843240310018,
"tradeCount": 4445,
"canonicalResultDigest": "fdfa855295c0b55bdbe6f39952ead1515e844bf033ced974d3c3ddc037a5d0b1",
"resultStoreDigest": "697566645116c76ff837cd36f7f9bbd7ad3eb30510a5b95012fb730d5072d511"
},
"fullMinute": {
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"canonicalResultDigest": "7dae3a618932b90d36e9968c027f08a94d69b8fc6a56b1ea0cc1a2cb771d85b8",
"resultStoreDigest": "bbbd7080b8fd7f6e8c3a8132499842bd0e4d644dbfdc3e8dbca7d2c0c1381e93"
},
"nextBarOpenFiveYear": {
"totalReturn": 0.9922618879291,
"tradeCount": 25827,
"canonicalResultDigest": "ac1d167cb1e1073e1d1ecb01e914f94d7560081c1d238e6b4418d86233250719",
"resultStoreDigest": "79570e0ae6b07badc1b693dc897dd1381647d259a4fe44ed3e50bc215e2fd088"
}
},
"testGate": {
"engineCorePassed": 441,
"engineIgnoredBenchmarks": 6,
"runnerPassed": 347,
"runnerIgnoredBenchmarks": 3,
"apiPassed": 80,
"failures": 0,
"fastPathGenericPathParityTest": true
},
"remainingBottleneck": {
"profilePath": "/srv/fidc/canonical/run/fidc-private/evidence/daily-bundle-quote-plan-cold-profile-20260907",
"dataSeconds": 11.215,
"quotePlanSeconds": 6.096,
"datasetConstructSeconds": 1.677,
"loopSeconds": 2.28,
"finding": "SymbolPriceSeries, adjusted-close series and DataSet indexes are still built once for quote planning and again for the final run DataSet.",
"next": "Replace SourceRowRecord and duplicate preliminary/final DataSet construction with one epoch-scoped typed Base Panel and lightweight run views."
}
}
@@ -0,0 +1,111 @@
{
"schemaVersion": "fidc-symbol-id-ranked-candidates/v1",
"measuredAt": "2026-09-05T06:23:00+08:00",
"host": "192.168.31.177",
"engineCommit": "52f9ee9d92781c1367ba4f01cbc3b1676fdd241e",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "c567c6da3d3625839a1d0127f755ccd278f31473924bf81ab837158370c08667",
"implementation": {
"description": "rank custom and quote-plan candidates as (symbol_id, rank, state_index), borrow symbols only at diagnostics and output boundaries, and use the lexical symbol-id order as the exact equal-rank tie breaker",
"symbolIdContract": "DataSet assigns u32 ids after sorting the complete normalized symbol set lexicographically",
"candidateSymbolClonesRemoved": true,
"selectionResultCached": false,
"persistentCacheAdded": false,
"rankTieSemanticsChanged": false,
"pitSemanticsChanged": false
},
"rejectedIntermediate": {
"commit": "d7e11be",
"problem": "looked up both factor-row strings inside every equal-rank comparator call even though symbol ids already encode lexical order",
"engineSeconds": [6.056, 6.274, 5.298, 6.071, 6.066, 5.957],
"retainedInFinalCode": false
},
"genericRankFiveYearContract": {
"rankBy": "free_float_cap",
"totalReturn": 0.7140315244542004,
"tradeCount": 21876,
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
"resultStoreSha256": "e2c7401bb9950da6fdfc0b516428061651e3e5b874a5f80c4b03ab8a81e90191",
"arenaBaselineMedianEngineSeconds": 4.222,
"engineSeconds": [3.809, 3.685, 3.739, 3.743, 3.76, 3.752],
"processHotMedianEngineSeconds": 3.743,
"incrementalImprovementPercent": 11.345334,
"originalCachedBaselineMedianEngineSeconds": 5.934,
"cumulativeImprovementPercent": 36.922818,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"primaryOrderedFiveYearContract": {
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"previousProcessHotMedianEngineSeconds": 2.73,
"processHotMedianEngineSeconds": 2.611,
"observedImprovementPercent": 4.358974,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryOrderedFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"previousProcessHotMedianEngineSeconds": 2.372,
"processHotMedianEngineSeconds": 2.346,
"observedImprovementPercent": 1.096121,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"fullMinuteContract": {
"scheduleTime": "10:18",
"matchingType": "minute_last",
"slippageModel": "price_ratio",
"slippageValue": 0.001,
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
"totalSeconds": [0.608, 0.608],
"engineSeconds": [0.161, 0.167],
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"profile": {
"runId": "btr_1788560495042_846890_19",
"engineSeconds": 4.007,
"allThreadEventCycles": 86456699303,
"transientStockStateBuilderPercent": 18.97,
"numericVmEvaluatePercent": 8.0,
"rankedSelectionPercent": 7.6,
"adjustedCloseMovingAveragePercent": 5.38,
"stableSortPercent": 3.09,
"mallocPercent": 1.19,
"arenaBaselineMallocPercent": 3.34,
"lostSamples": 0
},
"memory": {
"serviceCgroupCurrentBytes": 11537428480,
"serviceCgroupPeakBytes": 11538923520,
"persistentCacheAddedBytes": 0
},
"testGate": {
"workspaceTotal": 544,
"passed": 536,
"ignoredManualBenchmarks": 8,
"failed": 0,
"lexicalSymbolIdRegression": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-tie-fixed-generic-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-tie-fixed-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-tie-fixed-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-tie-fixed-full-minute-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-rank-profile-20260905/perf-report.txt"
],
"acceptance": {
"status": "accepted_symbol_id_ranked_candidates",
"reason": "the final implementation preserves exact rank ties and all business outputs, improves the generic five-year median by 11.35 percent over the arena baseline and 36.92 percent over the old cached path, improves both ordered controls, and lowers malloc share without persistent memory"
}
}
@@ -0,0 +1,85 @@
{
"schemaVersion": "fidc-symbol-id-selection-stream/v1",
"measuredAt": "2026-09-05T04:53:13+08:00",
"host": "192.168.31.177",
"engineCommit": "0e3c2028d0b0a26a2d3b8e2fb9dd0765c60809b1",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "386e5542d66bc6850f9569dc25d061f043103df279d3aba5e86343a72d5113f9",
"implementation": {
"description": "scan signal-day selection risk into a market-cap-ordered u32 symbol-id stream and materialize EligibleUniverseSnapshot rows only for strategies that require the generic ranking path",
"fastPathContract": "ascending market_cap or market_cap_bn rank with daily_replacement_limit=0",
"genericRankingChanged": false,
"selectionResultCached": false,
"factorValueCached": false,
"riskDecisionOrderChanged": false,
"pitSemanticsChanged": false
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"restartOrCold": {
"totalSeconds": 19.223,
"dataSeconds": 14.395,
"engineSeconds": 4.182
},
"processHotEngineSeconds": [3.438, 3.185, 3.346, 3.288, 3.294],
"processHotMedianEngineSeconds": 3.294,
"previousAcceptedMedianEngineSeconds": 3.896,
"observedMedianImprovementPercent": 15.451745,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [2.871, 2.897, 2.899, 2.964, 2.948, 2.922],
"medianEngineSeconds": 2.922,
"previousAcceptedMedianEngineSeconds": 3.582,
"observedMedianImprovementPercent": 18.425461,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"profile": {
"runId": "btr_1788555090941_297399_12",
"engineSeconds": 3.37,
"allThreadEventCycles": 77490041805,
"stockStateBySymbolIdPercent": 13.59,
"numericVmEvaluatePercent": 8.27,
"adjustedCloseMovingAveragePercent": 4.74,
"selectionSymbolIdScanPercent": 3.53,
"selectSymbolsPercent": 3.26,
"stringTrimPercent": 3.14,
"mallocPercent": 2.82,
"lostSamples": 0
},
"memory": {
"serviceCgroupCurrentBytes": 11481071616,
"serviceCgroupPeakBytes": 11488108544,
"cacheMemoryAddedBytes": 0
},
"testGate": {
"workspaceTotal": 544,
"passed": 536,
"ignoredManualBenchmarks": 8,
"failed": 0
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-profile-20260905/run.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-selection-profile-20260905/perf-report.txt"
],
"acceptance": {
"status": "accepted_generic_symbol_id_selection_stream",
"reason": "both independent five-year contracts preserve exact results and improve stable engine medians materially without adding cache memory or changing generic ranking, risk ordering, or PIT semantics"
}
}
@@ -0,0 +1,45 @@
{
"schemaVersion": "fidc-symbol-id-series-storage-rejection/v1",
"measuredAt": "2026-09-06T06:31:00+08:00",
"host": "192.168.31.177",
"candidateCommit": "5a7c49a4543b584503ff0d2c2ae513c68d25de8a",
"implementation": "remove duplicate string-keyed market and adjusted-close series maps and build symbol-id vectors directly",
"businessParity": {
"primaryFiveYearCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"secondaryFiveYearCanonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"genericRankFiveYearCanonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
"minuteCanonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"allTerminalAuditsClean": true,
"allResultsConsistent": true
},
"performance": {
"baselineDatasetConstructSeconds": [4.448, 4.546],
"candidateEarlyDatasetConstructSeconds": [3.899, 3.895, 3.663],
"candidateFinalRestartDatasetConstructSeconds": 4.472,
"candidateFinalRestartColdDataSeconds": 14.513,
"candidateFinalRestartColdTotalSeconds": 18.381,
"conclusion": "the apparent early improvement did not reproduce after a final restart under the current host phase; the final constructor time is equal to the adjacent baseline range"
},
"memory": {
"baselineSingleDatasetCurrentBytes": 11470495744,
"candidateSingleDatasetCurrentBytes": 11473674240,
"conclusion": "no measurable resident-memory reduction"
},
"testGate": {
"corePassed": 538,
"ignoredManualBenchmarks": 8,
"failed": 0
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-series-primary-five-year-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-series-secondary-five-year-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-series-generic-rank-five-year-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/symbol-id-series-minute-20260906.json",
"/srv/fidc/canonical/run/fidc-private/evidence/final-symbol-id-series-post-doc-sync-20260906.json"
],
"decision": {
"status": "rejected_and_removed",
"reason": "the candidate preserved correctness but did not provide a stable end-to-end or memory improvement across restart validation; duplicate maps are not a proven material bottleneck",
"nextTarget": "remove the SourceRowRecord to DailySnapshot to DataSet multi-stage materialization, or publish a content-addressed base panel that can be mapped across restarts"
}
}
@@ -0,0 +1,115 @@
{
"schemaVersion": "fidc-transient-selection-arena/v1",
"measuredAt": "2026-09-05T05:57:00+08:00",
"host": "192.168.31.177",
"engineCommit": "d89dd24f0a3993ed10b00957f947b65d91b00ffc",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "53680c33cd96487c52c47b96bb8c6f0bece69bb5617a6036d382f4262c181b2c",
"implementation": {
"description": "keep custom-rank and quote-plan stock states in a contiguous transient arena, sort lightweight state indexes, and isolate ordered and generic selection functions",
"candidateSnapshotCacheUsed": false,
"selectionResultCached": false,
"stateArenaSharedAcrossRuns": false,
"stateArenaLifetime": "single selection call",
"riskDecisionOrderChanged": false,
"pitSemanticsChanged": false
},
"rejectedIntermediate": {
"commit": "7f7fce1",
"problem": "stored StockExpressionState directly in the sortable tuple and repeatedly moved the large value during stable sorting",
"rollbackBaselineEngineSeconds": 5.934,
"observedBadCandidateEngineSeconds": [6.972, 8.868, 8.748, 9.317, 6.251],
"retainedInFinalCode": false
},
"genericRankFiveYearContract": {
"requestSha256": "06a5cdf6a96673d45cc54f4ff0f18a125dbcc0b80d0d52496f8a0c638bbc81d3",
"rankBy": "free_float_cap",
"totalReturn": 0.7140315244542004,
"tradeCount": 21876,
"canonicalSha256": "84367993911b42437939aad88f29379d007af6f5f171667ff7ad6677dd488fd2",
"resultStoreSha256": "e2c7401bb9950da6fdfc0b516428061651e3e5b874a5f80c4b03ab8a81e90191",
"rollbackMedianEngineSeconds": 5.934,
"finalEngineSeconds": [4.143, 4.125, 4.218, 4.226, 4.379],
"finalMedianEngineSeconds": 4.222,
"observedMedianImprovementPercent": 28.850691,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"primaryOrderedFiveYearContract": {
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"engineSeconds": [2.647, 2.692, 2.659, 2.813, 2.768],
"allRunMedianEngineSeconds": 2.692,
"previousAcceptedMedianEngineSeconds": 2.667,
"observedMedianDeltaPercent": 0.937383,
"classification": "within_host_load_variance",
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryOrderedFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"rollbackMedianEngineSeconds": 2.408,
"finalProcessHotMedianEngineSeconds": 2.372,
"observedMedianImprovementPercent": 1.495017,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"fullMinuteContract": {
"scheduleTime": "10:18",
"matchingType": "minute_last",
"slippageModel": "price_ratio",
"slippageValue": 0.001,
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
"totalSeconds": [0.6, 0.546],
"engineSeconds": [0.155, 0.164],
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"genericRankProfile": {
"runId": "btr_1788558913084_672102_18",
"engineSeconds": 4.46,
"allThreadEventCycles": 33149272847,
"transientStockStateBuilderPercent": 16.1,
"numericVmEvaluatePercent": 8.53,
"rankedSelectionPercent": 6.8,
"adjustedCloseMovingAveragePercent": 4.87,
"stableSortPercent": 3.68,
"mallocPercent": 3.34,
"lostSamples": 0
},
"memory": {
"serviceCgroupCurrentBytes": 11510001664,
"serviceCgroupPeakBytes": 11511304192,
"persistentCacheAddedBytes": 0
},
"testGate": {
"workspaceTotal": 544,
"passed": 536,
"ignoredManualBenchmarks": 8,
"failed": 0,
"quotePlanTransientCacheAssertions": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/free-float-rank-five-year-request-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-rollback-generic-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-generic-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-rollback-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-full-minute-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/transient-selection-final-profile-20260905/perf-report.txt"
],
"acceptance": {
"status": "accepted_generic_transient_selection_arena",
"reason": "the same frozen custom-rank contract is about 28.85 percent faster than an immediate rollback, both ordered five-year contracts and full-minute execution preserve exact outputs, the final design sorts indexes rather than large states, and no persistent cache is added"
}
}
@@ -0,0 +1,150 @@
{
"schemaVersion": "fidc-typed-adjustment-factor-snapshot/v1",
"generatedAt": "2026-09-07T18:12:00+08:00",
"scope": "FIDC engine, backtest runner and strategy runtime",
"changes": {
"engineCommit": "04b45adf98772f4ce3cd8b7a2c08d76480655fee",
"backtestServiceCommit": "76748bbc3d2cfc4d76ff8e35c5fcda1fcddd00ec",
"tradingPlatformCommit": "bec62d0f7f6126a36ec0a101f7b1a66b138537eb",
"datasetSchemaVersion": 58,
"sourceRowCacheSchemaVersion": 28,
"contract": "adjustment_factor_backward1 remains a nullable typed field from Arrow decode through DailyFactorSnapshot and AdjustedCloseSeries; dynamic NumericFactorMap values stay sparse",
"legacyInputPolicy": "extra_factors containing adjustment_factor_backward1 and non-positive or non-finite typed adjustment values are rejected"
},
"tests": {
"fidcCore": "445 passed, 6 ignored",
"backtestRunner": "351 passed, 3 ignored",
"backtestApi": "80 passed",
"strategyRuntime": "66 passed",
"runtimeRollingOrderRegression": "full 5/10/30/100-day framework rolling history produced a valid paper order"
},
"productionImplementation": {
"engineCommit": "04b45adf98772f4ce3cd8b7a2c08d76480655fee",
"serviceCommit": "76748bbc3d2cfc4d76ff8e35c5fcda1fcddd00ec",
"identitySha256": "2c0ab93663018f4fae8d61ba0c522c0f796837d952bbfb26a6bc229cc52bd722",
"runnerBinarySha256": "8591dfa763a52e06467dfe2ff2c8bef4d382ef875b356009fe5a7e8ea2013ea3",
"serviceBinarySha256": "4e467981f569e265c4ccc862c04be85eb99e9f4d107878785c5961a8b382d65f",
"status": "verified"
},
"fiveYearFreshProcess": {
"baseline": {
"runId": "btr_1788772852314_617886_0",
"dataSeconds": 14.121,
"sourceQuerySeconds": 4.304,
"datasetConstructSeconds": 3.672,
"riskFreeRateSeconds": 2.77,
"totalSeconds": 20.947,
"queryCompletedRssKb": 6654344,
"datasetConstructCompletedRssKb": 12775136
},
"candidate": {
"runId": "btr_1788775107699_632665_0",
"dataSeconds": 13.198,
"sourceQuerySeconds": 4.169,
"datasetConstructSeconds": 3.499,
"riskFreeRateSeconds": 0.01,
"totalSeconds": 17.381,
"queryCompletedRssKb": 6738520,
"datasetConstructCompletedRssKb": 12694000
},
"improvementPercent": {
"dataSeconds": 6.5364,
"sourceQuerySeconds": 3.1366,
"datasetConstructSeconds": 4.7113,
"datasetConstructCompletedRssKb": 0.6351,
"totalSecondsAfterRiskFreeNormalization": 4.4317
},
"comparisonNote": "total wall time is normalized only for the independently measured risk-free lookup difference; data and RSS values are compared directly"
},
"fiveYearMemoryCold": {
"baselineRunIds": [
"btr_1788772635840_613911_1",
"btr_1788772672944_613911_2",
"btr_1788772707181_613911_3"
],
"candidateRunIds": [
"btr_1788775166889_632665_1",
"btr_1788775202156_632665_2",
"btr_1788775353507_632665_5"
],
"baselineMedian": {
"totalSeconds": 16.809,
"dataSeconds": 12.8,
"sourceQuerySeconds": 4.411,
"dailyLoopSeconds": 1.077,
"datasetConstructSeconds": 3.701,
"engineSeconds": 3.278
},
"candidateMedian": {
"totalSeconds": 16.653,
"dataSeconds": 12.569,
"sourceQuerySeconds": 4.054,
"dailyLoopSeconds": 1.047,
"datasetConstructSeconds": 3.577,
"engineSeconds": 3.372
},
"improvementPercent": {
"totalSeconds": 0.9281,
"dataSeconds": 1.8047,
"sourceQuerySeconds": 8.0934,
"dailyLoopSeconds": 2.7855,
"datasetConstructSeconds": 3.3504,
"engineSeconds": -2.8676
},
"excludedQueueOutlier": {
"runId": "btr_1788775236942_632665_4",
"totalSeconds": 69.401,
"queueWaitSeconds": 48.625,
"blockingRunId": "btr_1788775223727_632665_3",
"reason": "another user backtest naturally occupied the single runner; it was not stopped or modified"
}
},
"controls": {
"currentClose": {
"runIds": [
"btr_1788775434926_632665_6",
"btr_1788775464828_632665_7",
"btr_1788775491657_632665_8"
],
"medianTotalSeconds": 10.044,
"totalReturn": 0.3201517861398,
"tradeCount": 5351,
"canonicalSha256": "7204c6f41b8e39fbf1af7fc55cd601b80f3427a7aa058394ccd8b0b14ca48eed",
"resultStoreSha256": "0b4d24ed5ec2b27cc4135707b4c51c78eb2c3e35a20da8108610778c30c72c73"
},
"staticDaily": {
"runId": "btr_1788775531308_632665_9",
"totalReturn": 0.13228843240310018,
"tradeCount": 4445,
"canonicalSha256": "fdfa855295c0b55bdbe6f39952ead1515e844bf033ced974d3c3ddc037a5d0b1",
"resultStoreSha256": "697566645116c76ff837cd36f7f9bbd7ad3eb30510a5b95012fb730d5072d511"
},
"fullMinute": {
"runId": "btr_1788775545217_632665_10",
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"canonicalSha256": "7dae3a618932b90d36e9968c027f08a94d69b8fc6a56b1ea0cc1a2cb771d85b8",
"resultStoreSha256": "bbbd7080b8fd7f6e8c3a8132499842bd0e4d644dbfdc3e8dbca7d2c0c1381e93"
},
"nextOpenFiveYear": {
"totalReturn": 0.9922618879291,
"tradeCount": 25827,
"canonicalSha256": "ac1d167cb1e1073e1d1ecb01e914f94d7560081c1d238e6b4418d86233250719",
"resultStoreSha256": "79570e0ae6b07badc1b693dc897dd1381647d259a4fe44ed3e50bc215e2fd088"
},
"terminalAuditsClean": true
},
"tradingRuntimeDeployment": {
"sourceCommit": "bec62d0f7f6126a36ec0a101f7b1a66b138537eb",
"strategyRuntimeBinarySha256": "b28aa9a20bff232d5d24ba6c8072c02ff921dea69a5ca3cb81712323e2d88d24",
"healthPorts": [9100, 9101, 9102, 9103, 9104, 9130],
"allHealthChecksPassed": true,
"orderRoutingConfigurationChanged": false,
"observedOrderRoutingMode": "disabled"
},
"decision": {
"status": "accepted",
"reason": "all business hashes and runtime rolling semantics remain exact while five-year DataSet construction, data time and steady memory-cold wall time improve",
"nextTarget": "construct the immutable Base Panel directly from Arrow column buffers so the remaining SourceRowRecord and DailyFactorSnapshot row materialization can be removed"
}
}
@@ -0,0 +1,95 @@
{
"schemaVersion": "fidc-typed-current-rolling-helper/v1",
"measuredAt": "2026-09-05T02:51:00+08:00",
"host": "192.168.31.177",
"engineCommit": "75ab0c06c66761d7b2b0edb3359359e13d1265e3",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "1071852a374620029398a967d485b827ed2defbb6ecbcc3f1ee6ffd3e2db76b6",
"implementation": {
"description": "compile static stock close/volume rolling_mean_current calls to typed DataSet kernels and bypass generic string normalization and helper dispatch",
"evaluationRemainsLazy": true,
"dynamicFieldsUseGenericFallback": true,
"stockStateSizeChanged": false,
"rollingFormulaChanged": false,
"pitSemanticsChanged": false
},
"primaryFiveYearContract": {
"startDate": "2021-08-23",
"endDate": "2026-08-28",
"frequency": "1d",
"matchingType": "next_bar_open",
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"processCold": {
"totalSeconds": 21.504,
"dataSeconds": 12.753,
"engineSeconds": 4.618,
"unattributedSeconds": 3.489
},
"processHotEngineSeconds": [5.292, 4.729, 4.778, 4.635, 4.685],
"processHotMedianEngineSeconds": 4.729,
"calendarMajorBaselineMedianEngineSeconds": 5.202,
"observedMedianImprovementPercent": 9.09265667051134,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [4.031, 4.08, 4.049, 3.976],
"medianEngineSeconds": 4.049,
"calendarMajorBaselineMedianEngineSeconds": 4.878,
"observedMedianImprovementPercent": 16.99466994669946,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"hardwareCounters": {
"candidateRunId": "btr_1788547776300_3162323_11",
"candidateEngineSeconds": 4.782,
"candidateCycles": 13368504015,
"candidateInstructions": 26049740736,
"candidateBranches": 4601262679,
"candidateBranchMisses": 19416727,
"fieldProjectionBaselineInstructions": 26663937176,
"fieldProjectionBaselineBranches": 4751846020,
"observedInstructionReductionPercent": 2.303472423993083,
"observedBranchReductionPercent": 3.1689440349331863
},
"profile": {
"runId": "btr_1788547704257_3162323_10",
"engineSeconds": 12.057,
"hostLoadAverageAfterRun": 35.31,
"performanceComparisonExcluded": true,
"genericResolveCurrentRollingMeanPresentInTopProfile": false,
"typedCurrentVolumeKernelPercent": 2.25,
"note": "profile percentages remain useful for call-path attribution, but this run overlapped heavy external and managed factor CPU load and is excluded from wall-time comparison"
},
"memory": {
"serviceCgroupCurrentBytes": 11495362560,
"serviceCgroupPeakBytes": 11497156608,
"processRssKiB": 11213936,
"processAnonymousKiB": 11197740
},
"testGate": {
"coreUnitTotal": 421,
"corePassed": 415,
"ignoredManualBenchmarks": 6,
"failed": 0,
"integrationSuitesPassed": true
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-profile-20260905/perf-report.txt",
"/srv/fidc/canonical/run/fidc-private/evidence/typed-current-rolling-perf-stat-20260905/perf-stat.csv"
],
"acceptance": {
"status": "accepted_generic_typed_lazy_helper",
"reason": "two different five-year contracts preserve exact outputs, both stable medians improve, hardware work falls, and the implementation keeps lazy short-circuit evaluation without enlarging per-stock state"
}
}
@@ -0,0 +1,101 @@
{
"schemaVersion": "fidc-uncached-selection-state/v1",
"measuredAt": "2026-09-05T05:07:00+08:00",
"host": "192.168.31.177",
"engineCommit": "29faf7932ed7838d0a2178a34b3fe6a259bd9052",
"serviceCommit": "9fd5a9e6d5668af57f6942fc3c4127953545d9c6",
"runnerBinarySha256": "c67dbb4a4432697853a9146790ae507f1bc774506104a9be814b3435665e6c36",
"implementation": {
"description": "construct transient market-cap ordered selection states by value and leave rejected candidates out of the per-day Arc HashMap cache",
"selectedStateBehavior": "later business use rebuilds and caches the selected or held symbol through the unchanged state API",
"genericRankingChanged": false,
"selectionResultCached": false,
"rollingValueCached": false,
"stateFieldsChanged": false,
"pitSemanticsChanged": false
},
"primaryFiveYearContract": {
"totalReturn": 0.9219861819172002,
"tradeCount": 26088,
"canonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234",
"resultStoreSha256": "92343fb369fea68b2544b654151c5c43a935940580b14afbe8eefb3844f72a54",
"restartOrCold": {
"totalSeconds": 16.142,
"dataSeconds": 12.836,
"engineSeconds": 2.665
},
"processHotEngineSeconds": [2.622, 2.683, 2.67, 2.619, 2.667],
"processHotMedianEngineSeconds": 2.667,
"previousAcceptedMedianEngineSeconds": 3.294,
"observedMedianImprovementPercent": 19.034608,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"secondaryFiveYearContract": {
"totalReturn": 1.1342962298106998,
"tradeCount": 19404,
"canonicalSha256": "0dbd3fad624097c673c4a5ec2f545f95e9c21d1d5337bb2b48ffeb22b16cb2b9",
"resultStoreSha256": "416d2f87241fb4c6b917f6aeecb588f82f6e7d51a103f4c53a74d11971f16839",
"engineSeconds": [2.318, 2.358, 2.35, 2.345, 2.367, 2.371],
"medianEngineSeconds": 2.358,
"previousAcceptedMedianEngineSeconds": 2.922,
"observedMedianImprovementPercent": 19.301848,
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"fullMinuteContract": {
"startDate": "2025-01-02",
"endDate": "2025-11-17",
"scheduleTime": "10:18",
"matchingType": "minute_last",
"slippageModel": "price_ratio",
"slippageValue": 0.001,
"totalReturn": 0.03473222656500008,
"tradeCount": 156,
"canonicalSha256": "457c086b1bca784fe83f447bb22451925e8f022456bb0fc7b54237a7a7886849",
"resultStoreSha256": "04d804ee02ad7bc9b3649b4a2b4ddd902a4242ef38e00edeed8058860e9a6997",
"totalSeconds": [0.592, 0.593],
"engineSeconds": [0.162, 0.165],
"resultConsistent": true,
"terminalAuditStatus": "clean"
},
"profile": {
"runId": "btr_1788555931028_369369_14",
"engineSeconds": 2.672,
"allThreadEventCycles": 62382573448,
"previousAllThreadEventCycles": 77490041805,
"observedCycleReductionPercent": 19.496013,
"stockStateBuilderPercent": 12.33,
"numericVmEvaluatePercent": 11.55,
"adjustedCloseMovingAveragePercent": 5.32,
"mallocPercent": 1.51,
"previousMallocPercent": 2.82,
"stockStateCacheClearInTopProfile": false,
"stockStateDropGlueInTopProfile": false,
"lostSamples": 0
},
"memory": {
"serviceCgroupCurrentBytes": 11482611712,
"serviceCgroupPeakBytes": 11484585984,
"cacheMemoryAddedBytes": 0
},
"testGate": {
"workspaceTotal": 544,
"passed": 536,
"ignoredManualBenchmarks": 8,
"failed": 0,
"strictClippyStatus": "baseline_blocked_by_137_preexisting_warnings"
},
"remoteArtifacts": [
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-primary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-secondary-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-full-minute-20260905.json",
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-profile-20260905/run.json",
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-profile-20260905/perf.data",
"/srv/fidc/canonical/run/fidc-private/evidence/uncached-selection-state-profile-20260905/perf-report.txt"
],
"acceptance": {
"status": "accepted_transient_selection_state",
"reason": "two independent five-year contracts and the full-minute contract preserve exact outputs, both daily strategies reduce stable engine medians by about nineteen percent, all-thread cycles and allocator share fall, and no cache memory is added"
}
}
@@ -0,0 +1,63 @@
# 表达式缺失值与执行参数验收
## 根因
原数值执行器把 NaN 比较结果直接变成 false,外层 NOT 因而可能变成 true。
`min(NaN, value)` 还会返回另一个有效值,使缺失因子参与筛选。下单标量继续经过
`max``clamp` 或整数转换时,也可能把无效输入变成零仓位或零数量。
## 执行合同
数值 VM 使用带类型的 Missing 值,数值缺失及非有限运算结果不再提前变为布尔 false。
| 表达式 | 结果 |
| --- | --- |
| NOT unknown | unknown |
| false AND unknown | false |
| true AND unknown | unknown |
| true OR unknown | true |
| false OR unknown | unknown |
最终布尔筛选只接受 true;显式 `if`/`iff` 与 CASE WHEN 一样,只在条件确认为 true 时取真分支。
显式 `nz` 保留策略自己声明的缺失值替代含义,框架不会自行填零。
短路仍不读取未使用分支。非法 clamp 范围返回错误,不允许使进程 panic。
Rhai 的逻辑运算不能承载可空布尔,因此动态脚本遇到未知数值比较时明确报错,不能
返回错误的命中;缺失 map 属性同样报错。已关闭会绕过自定义比较保护的 Fast Operators。
有限浮点比较仍使用现有 epsilon 口径,混合整数/浮点比较也受保护。
[Rhai 运算符文档](https://rhai.rs/book/rust/operators.html)说明了该分派边界。
下单数量、目标仓位、投入比例和筛选边界必须返回有限数值,否则记录
`missing_numeric_result`,包含表达式、证券、决策日和执行日。只有排名评估保留
独立的缺失值诊断路径;没有把数据源的缺行改写为价格或交易事实。
## 代码与测试
- `fda2e70`VM 三值逻辑及动态数值保护。
- `ea58ab2`:显式关闭 Rhai 快运算符,补齐缺失 map 保护。
- `e3f1028`:执行标量必须有限,排名与执行参数分离。
- 177 引擎:585 项通过、8 项跳过。
- Runner360 项通过、3 项跳过。
- 交易工作区链接 e3f1028:510 项通过、8 项跳过。
首次回归曾发现 Rhai 快路径仍绕过保护,修复后重新完整测试,未将失败候选部署。
## 真实回放
使用已保存的原始 strategy spec、初始资金、日期、基准、频率及全部执行配置,
通过独立 runner 真正重新执行。固定为服务实际使用的16个逻辑CPU、Rayon8线程、Tokio16线程。
- 五年日线:2021-08-23 至 2026-08-281,000万元,25,408笔成交。
- 分钟样本:2025-01-02 至 2025-11-17100万元,156笔成交。
- 10次回放的 canonical 与 result-store 均等于各自同 frozen bundle 基准。
- 包含 e3f1028 的最终回放为 `five-year-strict-1``minute-strict-1`
完整证据:`/Users/boris/WorkSpace/fidc-backtest-service/docs/evidence/numeric-condition-replay-20260909.json`
日线源行6,918,227;分钟样本仅636行,不能用其亚秒耗时宣传全部分钟策略的性能。
## 边界
该候选尚未部署到常驻回测或交易服务。此验证证明两种已有策略在有效冻结数据下结果不变,
不证明所有策略、所有原始财务公告/vintage、全部缺失数据原因或真实券商交易均已验收。
Rhai 未提供与数值 VM 完全相同的 nullable 表达式能力,目前选择明确拒绝,不能称为所有
动态语言表达式都已支持三值逻辑。完整 typed Base Panel 与对象分配优化仍待完成。
+36
View File
@@ -0,0 +1,36 @@
# Factor Decision Phases
Status: broker foundation implemented; factor compiler, evaluator and runtime-plan integration are not complete. Do not advertise this as a fully working stock-pool buy-condition feature.
## Separate Contracts
| Phase | Meaning | Must Not Do |
|---|---|---|
| Selection | Build and rank the candidate universe at the strategy decision clock | Pretend this also guards every later top-up |
| Buy permission | Decide whether this decision may create new buy exposure for a symbol | Convert a denied buy into a sell or silently drop a holding from a full target snapshot |
| Exit/reduction | Produce the explicitly configured exit or partial target | Normalize remaining targets upward without an explicit strategy rule |
| Execution risk | Apply actual execution-date price, ST, suspension, lifecycle, liquidity and cost constraints | Substitute decision-date risk facts for next-open execution facts |
| Existing orders | Continue the already submitted order under its execution risk and lifetime contract | Implicitly cancel or rewrite it merely because a later decision has a new buy denial |
## Broker Primitive
`StrategyDecision.buy_denials` is a symbol-to-reason map sampled by the strategy layer, not a factor evaluator. Merged decisions retain denials. The broker installs it only while processing that decision and restores the prior context afterward; it is never shared through DataSet caches.
New positive buy quantities and target-buy planning respect the map after standard market/risk checks. Sells remain permitted. The actual execution price determines whether a value/portfolio target requires buying: a target below the signal-day holding value can become a buy after a lower next open, so signal-day direction alone is insufficient.
Existing resting orders are not automatically canceled by this primitive. A buy amendment is denied if it increases total quantity or raises the limit price, even if the other dimension decreases. Reductions in both dimensions remain allowed after normal validation. A rejected amendment emits an update-rejection process event without replacing the original order state or queue priority. Full runtime-plan integration still requires testing.
## Required Integration
1. Split selection and buy-role output in the stock-pool compiler instead of folding both into `stock_filter`.
2. Evaluate buy expressions at the declared decision clock using typed field availability, units and frozen data identity. Missing data must retain its own diagnostic, not silently become a false trading signal.
3. Populate denials for every symbol a decision can buy, including portfolio targets, retained-target reentry and top-ups. Do not infer execution direction from signal-day value.
4. Preserve/consume constraints in Paper/Live strategy-plan conversion. No consumer may silently discard a nonempty denial map.
5. Carry the tested amendment policy through runtime-plan conversion; validate source-date and execution-date risk independently.
6. Verify same-bundle baseline parity when no buy constraint is configured, then test explicit buy failures across share, value, target and algorithmic orders.
## Current Evidence
On 177, broker tests verify blocked target top-ups, permitted sells, context restoration, existing pending-order preservation, a next-open target direction flip, and risk-increasing/reducing amendments with unchanged state on rejection. Full `fidc-core` tests passed: 453 unit tests and 122 integration tests, with 8 manual benchmarks ignored. The backtest runner previously compiled against the changed API.
The candidate is not deployed. The current OmniQuant compiler still needs the above integration, and no production readiness claim follows from these low-level tests.
@@ -0,0 +1,75 @@
# Fixed-Point and Minute-Stream Acceptance
Acceptance date: 2026-08-31
Engine commit: `cd116bc3ae77cac0989eb80185bb04d7440b8834`
## Scope
This acceptance separates execution precision from minute-data throughput. It
does not use a strategy-specific shortcut and does not change strategy,
matching, risk, slippage, commission, tax, or future-data semantics.
## Fixed-Point Boundary
- Execution money is signed micro-yuan `i128`.
- Stock gross amount, commission, stamp tax, transfer fee, cash, liabilities,
external cash flow, account units, position lot cost and PnL are fixed-point.
- Futures cash, margin, transaction cost, realized PnL and position PnL are
fixed-point.
- Market indicators and return statistics remain `f64`; conversion occurs only
at the execution-money boundary.
- One-micro-yuan budget overruns fail instead of being hidden by float epsilon.
Verification command:
```bash
cargo test -p fidc-core fixed -- --nocapture
```
Result: 8 passed, 0 failed.
## Minute Data Boundary
- `history_intraday_quotes_at` uses a sorted execution-date index and scans
backward until the requested bar count is satisfied.
- The active timestamp and `include_now` flag control visibility; later bars are
never visible.
- Daily minute execution consumes a borrowed k-way merge ordered by timestamp
and symbol. It does not clone the complete selected quote day before engine
dispatch.
- Execution quotes are released by trading date after the day finishes.
Release benchmark command:
```bash
cargo test -p fidc-core --test intraday_history_performance --release -- --ignored --nocapture
```
Observed on the local acceptance host:
| Case | Workload | Result |
| --- | --- | --- |
| Bounded history | 200 queries over 60,000 rows | 0.000227 seconds, checksum 351450348000 |
| Full-day materialization | 5,000 iterations x 240 rows | 0.049361 seconds |
| Borrowed quote stream | 5,000 iterations x 240 rows | 0.012232 seconds |
The materialized and streamed timestamp checksums were both
`2108693484000000`. The observed component speedup was about 4.04x. These
numbers are component evidence only and are not an end-to-end SLA.
## Regression
```bash
cargo test -p fidc-core --all-targets
```
Result: 528 passed, 0 failed, 1 ignored manual benchmark. This includes
execution-day risk, next-open open-price limits, minute timestamp visibility,
slippage, minimum commission, stamp tax, volume limits, corporate actions,
external cash-flow NAV treatment and futures account precision.
## Deployment Gate
This documentation-only correction does not require a service restart. Any
future Source Lake or engine deployment still requires the official managed
entrypoint and must fail closed while FIDC-managed factor work is active.
@@ -0,0 +1,51 @@
# Market Day View Component Benchmark
Date: 2026-08-31
## Scope
The platform-expression selection loop already iterates one factor slice for a
single trading date. The previous implementation still resolved the same date
in the market and candidate `BTreeMap` for every symbol. `DailySnapshotView`
borrows the existing immutable market/factor/candidate slices and dense row
position arrays once per date, then performs only `symbol_id -> row` lookups.
The view does not copy snapshots, cache strategy results, share account state,
or change missing-row behavior. The optimization is independent of strategy
text, thresholds, rolling windows, execution mode and portfolio size.
## Release Component A/B
Contract:
- 6,000 symbols;
- 200 complete lookup rounds;
- each lookup reads market close and candidate `allow_buy`;
- baseline and view checksums must be exactly equal;
- `cargo test --release`, system allocator, local macOS host.
| Round | Baseline seconds | Day view seconds |
| ---: | ---: | ---: |
| 1 | 0.009000 | 0.002939 |
| 2 | 0.004370 | 0.001555 |
| 3 | 0.004274 | 0.001578 |
Median component time changed from `0.004370s` to `0.001578s`, an observed
reduction of about `63.9%` (`2.77x`). This is a component result only and is
not a complete backtest SLA.
## Correctness Gates
- sparse market-only symbols remain absent from factor/candidate views;
- dense and binary-search fallback lookup semantics remain unchanged;
- full engine suite: 529 passed, 3 ignored manual benchmarks;
- next-open execution-day risk, minute matching, fees, slippage, volume limits,
corporate actions, delisting and futures tests all passed.
## Deployment Status
Not deployed. The 177 FIDC-managed Boris factor task is still active, so no
Source Lake, backtest service or engine restart is allowed. After the task
ends naturally, acceptance must use the same frozen bundle and compare daily
selection, orders, fills, holdings, NAV, risk facts and canonical digest for
multiple daily/minute and fixed/dynamic-universe strategies.
@@ -0,0 +1,133 @@
# FIDC 开源回测与交易引擎设计审查
## 审查范围
本次审查直接读取以下只读参考源码。源码位于
`/Volumes/T7-Data/WorkSpace/reference-trading`;原计划使用的
`/Volumes/SystemSSD` 在审查时未挂载,因此没有向本机系统盘写入参考仓库。
| 项目 | 审查提交 | 重点 |
|---|---|---|
| NautilusTrader | `ac22d5cf4a7e` | Rust 事件内核、统一回测/实盘组件、时间事件堆 |
| QuantConnect LEAN | `23b735d99a35` | 订阅同步、TimeSlice、Universe 生命周期、惰性集合 |
| Microsoft Qlib | `79633dd9506e` | 表达式缓存、日历切片、内存/磁盘分层 |
| vectorbt | `34b6d5935e3e` | NumPy/Numba 密集数组仿真、紧凑状态数组 |
| Zipline Reloaded | `943010b9da84` | Pipeline DAG、窗口预取、分段执行、复权读取 |
| Backtrader | `b853d7c90b67` | preload/runonce 与逐 bar/live 模式分离 |
## 可借鉴设计
### 1. 时间轴和执行状态必须统一,但热路径不必经过通用消息总线
NautilusTrader 的 `BacktestEngine` 复用数据、执行、风险和缓存组件,并用带稳定
序号的最小时间事件堆推进多个时钟。LEAN 用 `SubscriptionSynchronizer` 将不同订阅
合并到同一个 frontier,再生成唯一 `TimeSlice`。两者共同证明:回测和实盘应共享
订单、风控和时间语义,而不是共享一段策略特例代码。
FIDC 已经以 `decision_date``execution_date`、调度时间和撮合时间构成统一执行合同,
并由同一交易核心服务于回测、模拟盘和实盘。日线全市场选股不应改为逐字段消息
广播;这会给每个股票状态增加分配和动态分派。通用事件总线只保留在订单、成交、
配置审计及外部集成边界。
### 2. 批量研究计算和事件撮合必须使用不同执行形态
vectorbt 把纯数值组合压入连续数组和编译循环;Backtrader 在历史批量模式使用
`preload + runonce`,进入 live/replay 或受限内存模式后关闭该路径。Zipline 的
Pipeline 则先生成 DAG 执行计划,按依赖顺序计算,并允许按日期 chunk 控制内存。
FIDC 应继续保持:
- 日线基础特征、rolling、横截面 rank 和因子挖掘使用 Arrow/NumPy/DuckDB/紧凑数组;
- 订单、成交、T+1、涨跌停、停牌、公司行动和现金流使用确定性事件撮合;
- 不把向量化收益外推到存在订单状态和路径依赖的撮合过程;
- 不让逐 bar 实盘语义退化成预先知道整个未来数组的批量回测语义。
### 3. 不可变数据按内容身份共享,策略结果和可变账户状态严格隔离
Qlib 的缓存层、Zipline 的预取窗口和 Backtrader 的优化数据预载都说明:相同历史
数据不应由每个策略重复解码。FIDC 当前 Source Lake 的 Parquet/Arrow、冻结 query
scope、内容寻址 bundle、进程内 `DataSet` 和共享 result-store block 已符合这个方向。
共享键必须包含完整数据代际、字段投影、PIT 截止时间、复权口径和窗口。禁止共享:
- 选股结果、订单、仓位、账户、风控决策和策略局部变量;
- 缺少 manifest/SHA/PIT 身份的 DataFrame 或 dict
- 盘中 provisional 数据与正式收盘数据混用的缓存项。
### 4. 字段和因子要在计划阶段冻结,运行时只物化真正需要的数据
LEAN 的 `TimeSliceFactory` 复用空集合,并只在收到对应数据时创建集合;Zipline 的
Pipeline 使用执行计划和 refcount 释放中间值。FIDC 已有字段投影、
`DailySnapshotView`、rolling requirement、Factor DAG 和 numeric bytecode VM。
后续优化必须扩展这些类型化计划,而不是恢复宽 Python 行或每次构建完整 map。
## 当前性能事实
2026-09-06 在 177 使用同一冻结五年策略得到:
- restart/cold`17.985s`;数据准备 `14.659s`;引擎 `2.627s`
- process-hot:总耗时中位 `3.245s`;数据准备 `0.006s`;引擎 `2.629s`
- 26,088 笔成交、收益 `0.9219861819172002`、canonical SHA
`b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234`
在所有重复运行中一致;
- 当前剖面热点为 numeric VM `12.39%`、临时股票状态构建 `10.17%`、复权均线
`6.82%`
- Source Lake/进程共享缓存已经把同 bundle 数据准备从 `14.659s` 降到 `0.006s`
所以再把 DataFrame 改成 dict 或扩大 DuckDB 连接数不是当前热路径优化。
两项受开源类型化执行启发的候选已真实验证并删除:
1. 编译期枚举化全部 VM 标识符仅减少约 `1.44%` instructions,五年引擎中位只改善
`0.19%`,却净增约 562 行;
2. VM 槽位代际复用仅减少约 `0.48%` instructions,五年引擎中位回退约 `0.27%`
完整证据见 `docs/evidence/numeric-vm-binding-generation-rejection-20260906.json`
## FIDC 后续优化顺序
### P0:冷数据路径一次构建、分段发布、跨策略共享
以冻结 bundle 的完整身份构建 canonical base panel,并按年份或有界日期段发布
只读 mmap/Arrow 段;父任务只扫描一次 Source Lake,worker 只映射所需段。每日增量
只生成变化尾段,历史段保持同 SHA。必须以 process-cold `14.659s` 为基线,证明
冷启动下降且 RSS、PIT、复权和结果 SHA 不变。
### P1numeric VM 使用类型化 helper opcode 或经证据支持的 super-instruction
当前字符串标识符绑定不是主要成本。下一候选应在编译期把常见 rolling helper、
比较和短路组合编译为类型化 opcode,减少解释器指令数,同时保留 helper 懒求值和
错误信息。必须对主策略、第二种持仓数策略、通用自定义排序策略和分钟策略分别 A/B。
### P1:结果事件按消费者需求分层
撮合事实保持完整不可变;页面摘要、曲线、持仓和交易视图从 typed result store
按需投影。禁止每次回测向 PostgreSQL 写入大矩阵,也禁止为了列表或概览解码全部
事件。优化目标是 `resultSeconds + finalizationSeconds`,不能删减审计事实换速度。
### P2:分钟线有界流式窗口
参考 Zipline 的窗口预取和 Nautilus 的有序事件迭代器,按时间段加载分钟
RecordBatch,保持持仓、订单和指标 ring buffer 有界;不能一次展开全市场全区间
分钟 Python 对象,也不能在 chunk 边界丢失 corporate action、T+1 或订单队列状态。
## 明确不采用
- 不为单次策略把日循环拆成多线程;路径依赖会增加同步开销并破坏确定性。
- 不通过增加 worker、DuckDB 槽位或扩大 HTTP 窗口掩盖单任务热点。
- 不把全量历史数据复制成每个 worker 独占的 dict/DataFrame 缓存。
- 不直接引入另一个框架的事件总线、账户或撮合实现;只借鉴机制并用中国市场合同验收。
- 不缓存策略结果,也不以 request hash 返回旧结果代替真实回测。
## 验收矩阵
任何性能候选至少覆盖:
| 合同 | 必须保持 |
|---|---|
| 五年主策略 | 收益、成交数、canonical/result-store SHA、终态审计 |
| 五年第二策略 | 不同持仓数下的同一组证据 |
| 通用 rank 策略 | 非 market-cap 特例排序仍正确 |
| 分钟策略 | 调度点、分钟成交价、滑点、成交量限制和 SHA |
| 冷/热运行 | data/engine/result/finalization 分段、RSS、instructions/cycles |
任一合同漂移、只有微基准改善、或真实 wall/RSS 变差时,候选必须删除并保留拒绝证据。