shy3130
8e4258cf20
feat: support historical price limits and share capital
2026-07-18 23:37:54 +08:00
shy3130
44ab51e65d
feat: improve strategy scoring and backtest execution
2026-07-18 16:59:53 +08:00
lytem28
b6cf0495e1
feat: complete matrix-native backtest engine
...
Unify strategy execution across backtest, screener, and monitoring; isolate backtest workloads in spawn workers; and add shared matrix caching plus valid-bar indicator acceleration.
2026-07-16 12:17:27 +08:00
wshy
b869f3a552
Merge pull request #104 from im47cn/feat/wf-optimization
...
feat(walkforward): 滚动窗口 walk-forward 优化 — IS 优化 + OOS 验证
2026-07-13 18:48:47 +08:00
shy3130
97e1cdcc78
feat(backtest): 交易记录显示具体触发信号 (不再只显示「信号」类别)
...
原问题: 多个信号在 _build_signal_mask 里 OR 成一个 mask 后, 只记录
exit_reason="信号", 具体哪个信号触发的信息丢失。回测里买入/卖出原因
只能看到笼统的「信号」, 不知道是 MA金叉还是 MACD死叉。
实现:
- 后端 TradeRecord 加 entry_signal_id/exit_signal_id 字段
- 新增 _resolve_signal_id() 在触发行回查 panel 列, 找出为 True 的具体信号
- 三个 simulate 方法加 entry_signal_ids/exit_signal_ids 参数
- strategy.py 把信号列表传给 engine, _trade_to_dict 透传新字段
- 前端 useSignalNames() hook 合并内置 (BUILTIN_SIGNAL_DEFINITIONS) + 自定义
信号名称; TradeLegCell/ExitReasonBadge 显示具体信号名
- 信号名称前端解析 (复用 cnSignal), 后端只返回 signal_id, 不读磁盘
效果: 买入腿显示「MA5上穿MA20」, 卖出腿显示「MACD死叉」, 原因列从
通用「信号」变成具体信号名。止损/止盈/到期等非信号退出不受影响。
验证: 191 passed, 前端类型检查无错误
2026-07-12 18:31:36 +08:00
shy3130
ff059a0cda
fix(backtest): 分钟K回测爆内存 — 按触发日精确读取 + 紧凑数组缓存
...
上一提交 (365f1cc ) 修 index bug 时把 cache 从紧凑 numpy 数组改成臃肿的
polars DataFrame, 分钟K回测内存翻几倍导致 OOM 死机。且 _load_minute_for_fills
用 get_minute_range 扫描整个触发区间 (start~end), 触发日稀疏时读了大量无关
日期, 全市场长周期回测直接爆内存。
修复:
1. repository 新增 get_minute_by_dates — 按日期列表精确读 date=YYYY-MM-DD
分区文件, 不扫描区间。内存与回测区间长度解耦, 只随触发日数量增长。
2. _load_minute_for_fills 改用 get_minute_by_dates, 每批 50 天分批读取。
3. cache 改回 float64 紧凑 2D 数组 (6 列: open/high/low/close/volume/amount),
.cast(Float64) 保证 to_numpy 不退化回 object, 原来的 index bug 不复发。
4. _resolve_minute_fill 改用整数列索引 (arr[:,0]) 替代列名索引。
内存对比 (全市场 1 年):
之前: get_minute_range 扫全年 ~365 文件 + DataFrame 缓存 → 5-10GB
现在: 只读触发日文件 + float64 数组 → 几十 MB
验证: 191 passed (含 7 个分钟K回归测试)
2026-07-12 18:01:19 +08:00
shy3130
365f1ccbfb
feat(minute-k): 分钟K同步流式落盘 + 段大小可配 + 回测成交修复 + 卡死超时调整
...
1. 分钟K流式落盘 (修复1年全市场卡死)
- sync_minute_batch 加 on_segment 回调,每段拉完立即写盘
- 抽出 _write_minute_partition 公共函数,消除重复
- 内存峰值从全量(~25GB)降到单段,避免 OOM
2. 段大小可配 (默认20交易日,范围[5,30])
- 新增 minute_sync_segment_days 偏好项
- 「单次获取」与「获取1年」共用此分段设置
- 前端设置弹窗新增分段大小步进器
3. 回测分钟K精确成交修复 (从未成功跑通过)
- _resolve_minute_fill 改接受 DataFrame,避免 to_numpy() 退化为
object 数组后字段名索引抛 IndexError
- _load_minute_for_fills 用 partition_by 向量化分组替换逐行循环
- 新增 test_minute_fill.py 回归测试 (6 用例)
4. 卡死 watchdog 超时阈值按任务类型区分
- 普通任务 600s → 1200s;分钟K长任务 → 1800s
- create(timeout_s=) 支持按 job 存阈值,reap_stale 读 job 自身值
- 修复分钟K正常拉取被误杀导致僵尸线程继续写盘的问题
5. UI 优化
- 设置弹窗按 自动同步/手动获取/清空 三区块分组
- 「单次获取」改为按分段大小拉一段 (天数=分段设置)
- 状态文案: 盘后自动同步 → 自动同步已开启/已关闭
2026-07-12 14:23:22 +08:00
shy3130
20ef070099
feat(backtest): 分钟K精确回测 — 穿越价/VWAP 成交 + Pro+ 门控
...
- engine.py: MatcherConfig 加 minute_fill; _resolve_minute_fill (穿越价/VWAP/降级)
+ _load_minute_for_fills; simulate_portfolio/independent_candidates 接入
- strategy.py: StrategyBacktestConfig 加 minute_fill
- backtest.py: strategy_stream 加 minute_fill 参数 + Pro+ 门控 + 数据范围检查
- repository.py: 新增 get_minute_range (多symbol x 日期范围)
- backtestTask.ts/StrategyBacktest.tsx: 激活 highGranularity 开关 + Pro+ 门控
2026-07-12 11:04:43 +08:00
im47cn
db535c8004
feat(panelcache): IO 遥测量化跨折重复扫盘占比
...
walk-forward 各折 train 窗口日历重叠 ~75%, 但 PanelCache key 精确到区间,
跨折永不命中 -> 每折重复扫盘。为判断是否值得做"整区间加载一次+切片"优化,
先埋轻量遥测再决策 (避免盲优化)。
- PanelCache 累加真实扫盘耗时/次数 + 命中/single-flight 复用次数, 暴露 stats();
BacktestEngine.cache_stats() 透传 (不让上层伸手进私有 _cache)。
- WalkForwardService.run 首尾快照, 返回 cache_telemetry.load_panel_pct
(扫盘耗时/WF总耗时) + scans/hits/reuses, 并 logger.info 一行。
- 测试: PanelCache 计数 (miss扫盘/命中/并发复用); WF 测试补最小 engine 桩
(生产路径不加 getattr 兜底, 避免静默吞掉真缺 engine 的 bug)。
2026-07-12 00:52:32 +08:00
im47cn
7b27c81dd0
perf(panelcache): single-flight 消除并发同 key 缓存踩踏
...
PanelCache 把 compute_fn 放锁外避免不同 key 串行化, 但代价是同 key
冷启动踩踏: 优化器/批量回测的 max_workers 个线程几乎同时 miss 空缓存,
全部在锁外并行调 compute_fn, 同一面板被并行加载 N 份 (N=并发线程数),
而非承诺的"只加载一次"。全市场 parquet 冷扫 ×N 的 IO/内存峰值。
改为 single-flight: 同 key 只推举一个 leader 执行 compute, 其余线程
登记为跟随者等 leader 结果复用。要点:
- compute 仍在锁外 → 不同 key 依旧并发, 保留原设计优点, 只消除同 key 重复。
- 失败不缓存: leader 抛错时摘除 inflight 让后续重试, 异常透传所有跟随者。
新增 2 个并发回归测试: 8 线程同 key 踩踏断言 compute 仅 1 次;
失败路径断言异常透传全部等待者且不缓存失败可重试。
2026-07-11 07:55:08 +08:00
wshy
6bb6eadb12
fix(storage): 兼容 parquet 分区 schema 演进
...
兼容 quote_ts 等新增存储列导致的新旧 parquet 分区 schema 差异,恢复老用户 enriched 缓存刷新和看板/选股页面显示。
2026-07-09 17:38:44 +08:00
im47cn
61d1f28fd8
polish(backtest): 采纳 #67 审查建议 — MC clip 防御 + Sortino 口径注释 + 标签澄清 ( #81 )
...
作者 review PR #67 时提的 3 条非阻断建议, 本 PR 采纳:
1. 蒙特卡罗回撤加 np.clip(pnls, -0.9999, None): 单笔 pnl <= -100% 时 (1+pnl)<=0
会让 cumprod 符号翻转、净值非正、回撤失真。回测有止损实际不发生, 纯防御兜底。
加测试 test_mc_drawdown_clips_sub_minus_100pct_pnl 用 -150% 输入验证分位仍有界。
2. _calc_stats 逐笔 Sortino 沿用 sharpe 的 sqrt(252) 基准, docstring 注释点明这是
'内部一致 > 局部绝对' 的刻意选择, 免未来审查者重复质疑。
3. 前端标签 '蒙卡回撤(95%最坏)' → '(95%置信不差于此)', 避免误解为 95 分位值。
全量 130 测试通过 (含 upstream 已修的 trailing 测试, issue #66 已解决)。
2026-07-09 16:48:13 +08:00
Jinfeng Sun and Claude Opus 4.8
9aa96edbd7
改进: 并发韧性 + 数据性能 + 死代码清理 + 前端 UX + ST/Sharpe 修复 ( #78 )
...
* fix(concurrency): 共享缓存/任务表加锁, 全局限速, depth 原子写, 认证热路径缓存
修复多线程下的竞态与阻塞:
- overview/strategy_cache/PanelCache/StrategyMonitor._watching 四处共享状态加锁,
消除 "dict/OrderedDict mutated" 与丢更新/半写读取
- strategy_cache/depth parquet 改临时文件 + os.replace 原子写
- rate_limits 改进程级共享时间轴限速, 并发同步不再聚合超过单能力 rpm;
scheduler 令牌账目与 sleep 分离, sleep 不再独占锁串行化其他请求
- auth.is_configured() 内存缓存, 认证中间件不再每请求读盘阻塞事件循环
- api/backtest 任务清理/取消全程持 _jobs_lock, 并用 Semaphore(2) 限并发重回测
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
* perf(data): limit_ladder 去 N+1 全市场重算, 指标裁剪, factor 向量化
- limit_ladder 前一日 consecutive 改窄读单日 parquet 存储列 (谓词/投影下推),
替代 range(1,10) 逐日 _load_enriched_for_date 全市场指标重算 (最坏 9x)
- compute_indicators 新增可选 needed 裁剪 (默认 None 行为逐位不变, 已对照验证),
factor 只算所需因子列
- factor._calc_period_return 用 Polars join 替代 Python 逐行 price_map 循环,
_add_groups 去 map_elements 改纯表达式 (输出逐位一致)
- screener ext value_map 按 parquet mtime 记忆化, 免每请求磁盘重读
(DuckDB 过滤仍用隔离 :memory: 连接, 不扩大注入面)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
* refactor(backend): 报表存储去重, 删死代码, DuckDB 视图重建收敛, 管道失败如实标记
- 三份近乎逐字复制的 *_reports.py 收敛到共享 JsonReportStore (原子写 + 锁),
各模块公有 API/id 格式/上限/落盘 schema 完全保持不变
- 删除 ext_pull.py 中字节相同的死 _run_loop (Python 只绑第二个) 及无用 import
- 13 张 DuckDB 视图重建收敛为唯一权威 repository.rebuild_views(),
daily_pipeline 与 /api/data/clear 改为调用 (修好 clear 路径漏挂视图的漂移)
- daily_pipeline 累积 stage_errors 并在末尾抛出, 部分失败不再误报成功;
free/None 模式的能力门控跳过不计入失败
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
* feat(frontend): SSE 连接态, 路由代码分割, 查询失效修复, 三态与无障碍
- 实时行情 SSE: 连接态 store + 指数退避 + 断线徽标/toast (避免静默丢告警);
回测 SSE 断线有界重连 + 可重试, 不再永久卡住进度条
- router 全部 React.lazy + Suspense, vite manualChunks 拆图表库
(echarts 变独立 1MB 懒加载 chunk, 首屏包显著减小)
- 修 Data 清库后其它页显示旧数据 (改回广域失效); 修 Watchlist kline 失效键
永不匹配; query key 收敛到 QK 工厂 (新增 strategyDetail)
- Monitor/Analysis/StockAnalysis/ExtPages/CustomSignals 补 loading 门控与
error/empty 三态区分
- 新增共享 Modal 原语 (焦点陷阱/ESC/焦点还原/aria), 改造 3 个高频弹窗;
Toast/AlertToast 加 aria-live 与键盘可达; Watchlist/LimitUpLadder 卡片 memo
修复本轮 review 发现的缺陷:
- Modal 焦点 effect 依赖 onClose 致每次输入抢焦点 → 改 ref 只装一次
- StrategySettingsDialog 删除确认框被 Modal 面板裁剪 → 移出作兄弟节点
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
* fix(quant): 修正 ST 板块限价套错 与 因子 Sharpe 年化频率
两个不报错但会算错数的领域 bug:
1. ST 5% 涨跌停限幅被无条件套到创业板/科创板 ST 股:
注册制改革后 创业板(300/301)、科创板(688/689) 的风险警示股仍执行 20%,
北交所 30%, 只有主板 ST 才是 5%。原代码 _is_st 先判且覆盖板块限幅, 导致
创业板/科创板 ST 的涨停价按 5% 计算 → +5% 被误报涨停、真 +20% 涨停被漏报,
污染 signal_limit_up / consecutive_limit_ups / 连板梯队 / near_limit_up。
修正: ST 5% 仅在 ~(创业板|科创板|北交所) 时生效 (EOD + 盘中两条路径 + near_limit_up)。
2. 因子回测 Sharpe 一律乘 √252, 但 group_nav 每点是一个调仓周期收益:
月频调仓下是月收益, 乘 √252 会把 Sharpe 高估 √(252/12) ≈ 4.6x (周频 ≈2.2x),
使无效因子显示成明星因子, 废掉"先筛无效指标"的用途。
修正: 年化系数按 config.rebalance 取 √252/√52/√12。
新增 tests/test_st_limit_and_sharpe.py (5 例) 覆盖两处修正。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-07-08 18:10:19 +08:00
Jinfeng Sun and Claude Opus 4.8
e5a94c42d5
feat: ETF 支持(选股 / 回测 / 监控) ( #61 )
...
* feat(screener): 选股引擎支持 ETF
- 12 个内置策略打 asset_types 白名单 + strategy_supports_asset;涨停类
(连板/断板反包)仅股票,其余 10 个技术类对 ETF 开放
- ScreenerService(repo, asset_type) 分流取数,ETF 复用 kline_etf_enriched,
跳过股票专用历史缓存与涨停信号;进程级 _history_cache key 含 asset_type
- API /run、/run_preset 透传 asset_type;/strategies 按资产过滤;
股票专有策略在 ETF 下返回空
- 新增 enriched_dirname(asset_type) 共享 helper;get_enriched_latest_asset
增 refresh 参数(供轮询线程避免冷缓存同步重算)
- 前端「策略」页加 股票/ETF 切换,ETF 走实时单跑(空日期→用 ETF 自身最新日);
QK.screenerStrategies 按 asset_type keyed
- 测试:test_screener_etf.py
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
* feat(backtest): 回测支持 ETF(个股/因子/策略组合)
- 三条回测路径 + 共用 BacktestEngine 面板加载按 asset_type 路由到
kline_etf_enriched(复用 enriched_dirname);PanelCache key 隔离资产;
ETF 跳过股票专用 get_enriched_range 缓存
- 面板 compute_all/名称 JOIN 按 asset_type 取维表(get_instruments_asset),
修复 ETF 策略回测用错股票维表致名称为空/涨停信号算错
- BacktestConfig/FactorConfig/StrategyBacktestConfig 增 asset_type
- 三个回测 API + SSE stream 透传 asset_type;_make_job_key 纳入 asset_type
(修复 stream 与 cancel job_key 不对齐致取消失效的回归)
- 前端策略组合页/因子页加 股票/ETF 切换,标的搜索与策略列表跟随资产;
assetType 持久化
- 测试:test_backtest_etf.py(含 job_key 一致性回归);既有回测测试替身同步
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
* feat(monitor): 监控规则支持 ETF
- engine.evaluate(df, asset_type) 按规则 asset_type 分轮评估;quote_service
增开 ETF 评估轮(用 ETF enriched 快照),股票轮不受影响、不重置其策略结果
- ETF 评估轮独立 try(异常不丢弃已算出的股票告警)+ refresh=False(不在轮询
线程触发 ETF 冷缓存同步重算)
- ETF 版历史加载器(main.py 注入)+ 按规则 asset_type 选加载器
- _strategy_pools 按 (sid, asset_type) 键,避免同策略股票/ETF 规则互相覆盖
- name_map 仅在有 ETF 规则时补 ETF 维表, setdefault 保股票名优先
- RuleModel/normalize 增 asset_type(默认 stock,持久化往返)
- 前端 RuleEditor 加 股票/ETF 选择,策略列表与标的搜索跟随资产
- 测试:test_monitor_etf.py
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
* feat(etf): 前端 API 绑定透传 asset_type + 文档
- api.ts: screener/backtest 绑定加 assetType 参数,MonitorRule 类型加 asset_type
- docs/features.md: 标注选股/回测/监控的 ETF 支持范围与前提
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
* fix(reliability): 管道并发/原子写/能力探测/监控告警多处加固
后端可靠性专项修复(均带回归测试, backend 全套 64 passed):
并发与数据完整性:
- 盘后管道单飞: JobStore.create() 去重纳入 pending∨running, 关闭"两次快速点击"
并发双跑窗口; 新增 _heavy_run_lock 执行槽挡住 reap 后僵尸线程并发写 parquet
- adj_factor/minute 全部改走原子写(tmp+replace), 消除 kill/断电致 all.parquet 损坏
- 分块拉取失败聚合 WARNING 可见化(不再静默当成功); 复权失败标的会保持旧价已提示
能力探测:
- 周期重探(60min)热更新 app.state.capabilities, 付费 Key 过期/续费无需重启即可见
- 瞬时探测失败(超时/连接/5xx, 按 _is_transient 判定)不降级、保留旧付费档;
真 401/无权限仍正常降级回落 free-api
监控告警:
- 评估仅在连续竞价(9:30-11:30/13:00-15:00)+ 快照当日新鲜度下进行, 避开集合竞价/
收盘后陈旧价与节假日误告警
- scope=sector fail-closed(validate 拒绝新建 + _apply_scope 返回空), 修复板块规则
对全市场刷屏
- 飞书 webhook 加退避重试并移到独立线程池 fire-and-forget, 不再阻塞行情轮询线程
单标的新鲜度: 新增 repo.symbols_lagging() 检测掉队标的并 WARNING + 计入 job 结果
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-07-08 12:12:29 +08:00
im47cn
3a5f3e05b6
feat(backtest): 稳健性指标 Sortino + 蒙特卡罗回撤分位 + per-trade 明细 ( #67 )
...
* feat(backtest): 新增稳健性指标 Sortino + 蒙特卡罗回撤分位 + per-trade 明细
回测原本只有 Sharpe/Calmar/最大回撤, 缺防过拟合视角。本 PR 补三类:
1. Sortino 比率: 用下行偏差 (MAR=0 目标半方差) 替代总标准差, 只惩罚负
收益波动。无亏损时约定返回 None (不虚报 0/inf)。各 stats 函数用与自身
Sharpe 相同的收益基准 (逐笔 or 日频)。
2. 蒙特卡罗最大回撤分位: 对逐笔收益有放回重抽样 1000 次, 估计回撤分布,
报 P50(中位) 与 P95(95% 置信最坏)。回答"仅因成交顺序运气回撤能有多坏",
单次样本内回撤看不到这个。固定种子 (42) 保证可复现/可测。
3. per-trade 明细: best/worst/median_pnl/avg_holding_days, 补 _calc_stats
与 _calc_portfolio_stats 原本缺失的逐笔视角。
三个 stats 函数 (_calc_stats / _calc_independent_candidate_result /
_calc_portfolio_stats) 全部接入, 用共享静态 helper (_sortino_ratio /
_mc_drawdown_percentiles / _per_trade_block) 避免重复。纯 additive, 既有
字段不变, 空交易安全。
前端 StrategyBacktest 结果区新增 索提诺 / 蒙卡回撤(中位) / 蒙卡回撤(95%最坏)
三个指标卡 (stats 为 Record<string,any>, 无需改类型)。
新增 tests/backtest/test_robustness_metrics.py 11 用例: Sortino 手算校验/
无下行 None/优于 Sharpe/样本不足; 蒙卡确定性/P95≤P50≤0/全正零回撤/样本不足;
_calc_stats 与 portfolio 字段集成 + 空交易安全。
注: upstream main 既有 test_trailing_take_profit_exits_after_activation
失败与本 PR 无关 (改动前后一致复现)。
* test+fix(backtest): 子代理审查修复 — 补 full 主路径覆盖 + finite 护栏 + 内存上限
子代理审查发现的缺口, 本次补全:
覆盖 (测试):
- [关键] full 模式主路径 _calc_independent_candidate_result 原无集成测试,
漏拼 sortino/mc 字典展开会导致前端指标卡空值却测试全绿。新增该分支断言。
- MC 确定性测试从"两次相等"升级为钉死快照值 (p50=-0.0976/p95=-0.2108),
一旦有人把种子改成系统熵立即红。
- p95 断言从恒真的 p95<=p50 (percentile 单调性数学恒成立, 无信息) 改为
p50<0 且 p95<p95 严格更差, 真正验证数值。
- 补全盈利交易 → sortino 集成层为 None 的用例 (原仅 helper 层覆盖)。
健壮性 (实现):
- 三个 helper 入口 finite 过滤: inf/nan 收益会让 cumprod 传播 nan → 分位 nan
→ asdict 直出非法 JSON (项目 _safe 只洗 rows 不洗 stats)。剔除后与纯净输入
结果一致 (加测试守护)。
- MC 内存护栏: full 模式可数千笔, n_sims×n×8B×4 数组瞬时可达数百 MB。
按 2M 单元上限压降模拟次数 (小样本 n_sims=1000 不变, 快照稳定)。
注: 逐笔年化口径 (_calc_stats 对 pnls ×√252) 落在死路径 simulate() (无 app
调用方) 且复刻既有 Sharpe, 不在本 PR 范围。两条活路径 (portfolio/independent)
均用日频基准, ×√252 正确。
2026-07-08 12:10:05 +08:00
shy3130
f8914c9140
feat: 新增企业微信推送、个股分析页重构、价位算法优化及多处交互改进 (v0.1.82)
...
- 推送: 新增企业微信群机器人通道(与飞书并列), 监控告警/复盘报告多渠道分发
- 个股分析: 历史报告改为右侧常驻栏(展示全部股票报告), 进页面自动恢复上次选股
- 价位算法: 成交密集区改用换手率衰减模型(国内主流筹码分布); 缺口回补判定修正(必须完全穿越缺口)
- 回测: 回撤止盈改纯峰值口径(与 trailing_stop 一致); 佣金/印花税/滑点合并一行; 建仓口径加问号气泡说明
- 分时图: 自选列表迷你分时图加渐变填充(对齐个股对话框风格)
- 图表: 个股分析日K图右侧标签适配浅色主题; 价位标签与下方文字行双向 hover 高亮联动
2026-07-07 17:56:25 +08:00
im47cn
1fd7e84785
feat(backtest): 成本模型拆分为佣金+印花税(仅卖出)+滑点 ( #46 )
...
* feat(backtest): 成本模型拆分为佣金+印花税(仅卖出)+滑点
MatcherConfig 新增 commission_pct/stamp_tax_pct 及 buy_cost_pct()/
sell_cost_pct() 方法, 撮合三处现场统一改用方法计算。
印花税单边(仅卖出)是 A 股与美股的本质差异: 旧的 fees_pct 双边模型
把印花税摊到买卖两腿, 会高估买入成本、低估卖出成本。拆分后买入腿=
佣金+滑点, 卖出腿=佣金+印花税+滑点。
commission_pct 未设时回退 fees_pct, 印花税未设时为 0, 完全向后兼容,
既有 12 个 portfolio 测试不变。strategy/api(含 SSE + 缓存键)全链透传。
新增 tests/backtest/test_cost_model.py 覆盖向后兼容/拆分/印花税仅卖出/
优先级/撮合传导 5 个用例。
* fix(backtest): 打通成本模型前端链路并修复 cancel 缓存键
子代理审查发现 PR3 半成品缺陷, 本次补全:
C1 (正确性): /strategy/cancel 的 _make_job_key 漏传 commission_pct/
stamp_tax_pct, 一旦用户设成本参数, cancel 算出的 job_key 与 stream 失配
导致取消静默失败。补 None-aware 解析对齐两侧口径。
C2 (完整性): 前端 SSE/sync 链未透传新字段, 新成本模型从 UI 够不到。
- backtestTask.ts / api.ts: 类型 + query 透传 commission_pct/stamp_tax_pct
- StrategyBacktest.tsx: 佣金映射到 commission_pct, 新增印花税(千分之)输入,
默认 1 (A股千1), 映射 stamp_tax_pct; 状态持久化
- storage.ts: stampTax 类型声明
测试加固:
- test_stamp_tax_only_deducts_on_sell_leg: 去掉硬编码 shares=9900,
改为断言两次运行 shares 相等 (守护买入腿 sizing 不受卖出成本污染)
+ 从结果反推卖出市值校验差额
- 新增 commission_pct=0.0 非 None 边界 (防 falsy 回退)
- 新增 job_key 区分成本参数的回归测试 (守护 C1)
后端 26 测试全绿; 前端 tsc 仅剩既有 baseUrl deprecation (非本次引入)。
2026-07-03 23:15:32 +08:00
shy3130
9dbd32246c
feat(backtest): 风控新增固定止盈 + 优化回测范围
...
风控: MatcherConfig 增加 take_profit_pct, 单标的与组合引擎风险检查新增
涨破止盈线卖出逻辑 (退出原因 take_profit);strategy.py 透传 overrides,
api/strategy.py 与前端类型暴露 take_profit;风控 tab 增加止盈输入框,
摘要/结果头/退出标签同步展示;修复未设止损时止盈检查被跳过的 bug。
回测范围: StockPoolPicker 输入框右侧增加「导入自选」「清空」按钮与
「当前范围」指示器, 移除冗余的「留空=全市场」提示。
2026-06-29 17:17:20 +08:00
shy3130
3e79f94da8
feat(backtest): 拆分建仓/清仓成交口径并修复退出优先级
...
成交口径: 单个 matching 拆成 entry_fill/exit_fill, 各自可选 close_t/open_t+1,
默认建仓次日开盘、清仓当日收盘 (最贴近真实交易)。老 matching 参数经
__post_init__ 自动映射为 entry=exit=matching, 完全向后兼容。
退出优先级修复: max_hold 不再抢占卖点信号 (signal 提到 max_hold 之前);
统一为 风控(止损/移动止损/止盈) → signal → max_hold → end, 删除 close/open
模式下风控调用顺序的差异, open_t+1 下到期仓位也能正确触发止损。
新增 3 个测试: signal 优先于 max_hold / open 模式到期触发止损 / 默认口径
买次日开盘卖收盘。现有 13 个测试经兼容映射后行为不变, 全部通过。
2026-06-23 14:00:09 +08:00
shy3130
7369358d08
Initial release v0.1.19
2026-06-18 17:18:23 +08:00