Skip to content

feat(paper): 框架级持仓保护止损 Position Guard(ADR-0052)#88

Closed
mirror29 wants to merge 13 commits into
mainfrom
feat/position-guard
Closed

feat(paper): 框架级持仓保护止损 Position Guard(ADR-0052)#88
mirror29 wants to merge 13 commits into
mainfrom
feat/position-guard

Conversation

@mirror29

Copy link
Copy Markdown
Owner

背景

回测只是历史,无法保证未来跑模拟盘的情况。把止盈止损「写进策略再回测」解决不了这个问题——为让回测曲线好看而调出来的止损位本身就是过拟合参数,未来一样不灵。

代码实测确认一个真实缺口:回测引擎与 live session 都是纯信号驱动,既有风控规则(MaxDrawdown / StoplossGuard / Cooldown)只「挡开仓」、从不强平已有持仓——单仓失控时没有兜底刀。

本 PR 加一道独立于策略 alpha、回测与 live 共用、行为一致的持仓级保护止损(灾难性兜底),专门封尾部风险。详见 ADR-0052(本地 docs/miro/,按约定不入 git)。

改动

新组件 engine/position_guard.py + 接入两套引擎 + 配置贯通 + 报表/工具透出,共 5 个 commit:

  1. PositionGuard 组件 + 单测 — 硬止损/止盈/移动止损三闸 + 峰值跟踪;出场打 tag 经 close_detectorexit_reason,直发 EXECUTION_ENGINE_ENDPOINT 绕过开仓闸
  2. 接入 backtest/live session — 两套引擎在 update_mark 之后同一逻辑点调 guard.evaluate,保证回测与模拟盘行为一致
  3. 配置贯通 runner/live_runnerprotective_* 三项默认(硬止损 0.20,止盈/移动止损默认关);live runner 对保护性 tag 跳过 enforce(回撤熔断锁期内仍能平仓)
  4. 报表汇总 + 一致性回归BacktestReport.protective_exits 计数;回测 vs live feed_bar 同口径回归
  5. 透出到 agentBacktestResponse 字段 + runner 映射 + orchestration TS client/tool,agent 能看到「框架兜底本次触发 N 次」

设计原则

  • 宽兜底,不是紧止损:默认 −20%,封尾部风险而非切正常波动;紧止损是策略层 alpha 的事
  • 默认只做亏损侧:硬止损默认开;止盈(封上行偏 alpha)、移动止损默认关,均可配
  • 回测与 live 一致:同组件、同阈值、同逻辑点——这是硬约束,否则又回到「回测不可信」
  • 不偷未来:bar close 判定、出场单下一根 bar 撮合,与 live 只能看收盘 bar 对齐
  • 与既有 RiskRule 组合:规则挡开仓 / guard 平持仓,正交互补;保护性出场 tag 自动喂 StoplossGuard/Cooldown 实现「止损后不立刻回场」

验证

  • uv run ruff check . ✅ / uv run pytest723 passed(含新增 test_position_guard.py 9 例 + e2e 一致性回归 3 例)
  • orchestration tsc --noEmit
  • 隔离验证(干净 main + 本 PR 5 commit):17 测试全过、schema/runner 自洽、无外部 WIP 依赖

注意

  • 默认 0.20 兜底现在对所有回测生效(为回测/live 一致),全套测试已确认无回归
  • protective_exits 已到 API 响应 + agent 工具描述;前端展示可作后续增强

🤖 Generated with Claude Code

mirror29 and others added 5 commits June 16, 2026 14:13
framework 级持仓保护止损:硬止损/止盈/移动止损三闸 + 峰值跟踪;出场打
tag 经 close_detector→exit_reason,直发 EXECUTION_ENGINE_ENDPOINT 绕过开仓闸。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
两套引擎在 update_mark 之后同一逻辑点调 guard.evaluate,保证回测与模拟盘
行为一致;阈值全 None 时退化为无 guard(向后兼容)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
config 加三项 protective_* 默认(硬止损 0.20,止盈/移动止损默认关);main 进程
runner 解析阈值传给子进程(picklable,子进程不读 Settings),live_runner 透传给
session 并对保护性 tag 跳过 enforce(回撤熔断锁期内仍能平仓,保留 notional 上限)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
report 加 protective_exits 计数把兜底触发可见化;test 扩单仓崩盘端到端兜底、
无 guard 对照、回测 vs live feed_bar 同口径(同组件/同阈值/同判定点)回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BacktestResponse 加 protective_exits 字段、runner 响应组装映射 report→response;
orchestration paper client 类型 + run_backtest tool 描述同步透出,让 agent 在回测
结果里看到"框架兜底本次触发 N 次",把回测对未来 live 的兜底可见化。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 16, 2026

Copy link
Copy Markdown

Deploying inalpha-web with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7ab462d
Status: ✅  Deploy successful!
Preview URL: https://ca58c8d9.inalpha-web.pages.dev
Branch Preview URL: https://feat-position-guard.inalpha-web.pages.dev

View logs

@claude

claude Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
本轮 commit 截至 7ab462d

PR 意图

引入框架级持仓保护止损(ADR-0052):PositionGuard 组件接入回测引擎与 live session,支持硬止损 / 止盈 / 移动止损;修复移动止损口径(自峰值价格回撤而非成本收益率降幅);三因子 is_protective_order 防止 tag 仿冒绕过风控豁免;末根 bar 收尾 flush 解决回测/live 不一致;同时清除 archetypes API 及 Bootstrap Sharpe CI 特性。


必修(critical / major)

[major] packages/orchestration/src/hooks/with-hooks.ts:100–109 — 复活 ask-path 审批死循环(ADR-0018 回归)

具体失败场景:Mastra dock 环境中 ctx.agent.threadId / resourceId 均为空(当前 AG-UI/Mastra 实测值恒空),只有每-turn 变化的 ctx.runId。新代码把 runId 当 sessionId 回退值返回:

  • Turn-1:用户触发 paper.promote_candidaterequiresApproval=true,ask-cache 以 "turn-1" 为 key 记录
  • Turn-2:用户明确同意,agent 重调 → runId="turn-2" → cache key 不同 → consume miss → 再次弹出确认 → 死循环

旧代码在此情形故意返回 undefined,让 ask-cache 回退到 "__global__" 稳定 key,跨 turn 能命中(ADR-0018 的修复核心)。新代码的注释 "runId changes per user-turn, askCache will miss across turns" 已自承问题,但仍返回了坏值。

被删除的回归测试 describe("ask-path e2e · dock 场景回归...") 验证了旧修复行为——用新代码会 fail,所以被一起删掉了,失去了回归保护。

修复方向:保留旧逻辑(只有 threadId / resourceId / sessionId 中有稳定值才返回,否则返回 undefined);或补充机制让 Mastra 真正填充稳定 id 后再改。


可选优化(medium)

[medium] services/paper/tests/test_backtest_e2e.py — guard + 策略同 bar 双 SELL 场景失去测试覆盖

原测试 test_backtest_guard_and_strategy_double_sell_same_bar_no_corruption 验证了「guard 止损 + 策略自主出场在同一 bar 双 SELL → 末态不穿仓 / 不负仓 / 不重复成交」。该测试被删除但无替代:新增的 test_backtest_protective_stop_on_last_bar_settles_at_close 测的是末根 flush,覆盖的是不同路径。

双 SELL 场景在当前代码仍可发生(guard 在第 N 根触发、策略同 N 根也 SELL),依赖 _try_fill 里的 can_afford_sell 守门;但守门逻辑无测试覆盖,can_afford_sell 的行为不一致时会静默穿仓。建议补一个中段触发(非末根)的 guard + 策略双 SELL E2E 回归用例。


LGTM 部分

  • PROTECTIVE_EXIT_TAGSposition_guard.py 移到 model/orders.py:干净解除 engine↔execution 循环依赖
  • _peak_pct → _peak_mark(峰值价格口径)+ _triggered_tag 签名更新:正确修复了「大盈利下 trailing 阈值被成本收益率口径放大」的语义错误,test_trailing_uses_price_drawdown_not_return_pct 钉住了新口径
  • 三因子 is_protective_order(SELL + tag + guard 前缀)+ 安全前提文档:防 tag 仿冒设计合理,局限(策略可控三因子)已在 docstring 和 ADR-0052 中明确,沙箱补完前可接受
  • flush_protective_at_close:末根 close 兜底平仓的语义正确(close 是决策时已知价,非 look-ahead;与 live 同口径);portfolio.snapshot 覆盖同 ts 末点逻辑清晰
  • add_strategy RuntimeError + bind_strategy RuntimeError 双层防呆:多策略误用会在两处被拒,test_guard_enabled_rejects_second_strategy / test_bind_strategy_rejects_second_strategy 均有覆盖
  • test_strategy_own_stop_loss_tag_not_counted_as_guard:核心回归测试,验证 protective_exits 不被策略自带 tag 污染
  • archetypes 删除:API / TS client / tools / orchestrator prompt 四处一并清理,无遗留引用
  • sharpe_ci 从主回测移除:bootstrap_sharpe_ci 仍保留在 holdout 验证路径(runner.py),行为一致,非全删

mirror29 and others added 8 commits June 16, 2026 14:56
- 移动止损改用「自峰值价格回撤」口径(非成本收益率降幅),并仅在仓位进入盈利区后
  生效——避免大盈利下过激触发 + 开仓即亏的假触发(CR medium 1)
- config 注释纠正:关闭硬止损=不设环境变量(None),0 不合法(gt=0)(CR medium 2)
- 末根 bar 触发的保护性出场单收尾按该根 close 兜底成交(决策价,非 look-ahead,
  与 live 同口径),修「末根触发漏计/持仓显示未平」的回测/live 不一致(CR medium 3)
- 补单测:trailing 价格回撤口径 / 从未盈利不触发 / 末根触发成交

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
flush_protective_at_close 按单 instrument 收尾、bind_strategy 单值——沿用引擎
「单 strategy 单 instrument per session」契约;多标的/多策略是引擎层整体未做项,
与引擎多模式化一并推进,非本闸单独修。CR 两条新 medium 仅在该未支持场景触发。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- PROTECTIVE_EXIT_TAGS 从 engine.position_guard 移到中立层 model.orders(Order.tag
  约定同源),position_guard/exchange/report/live_runner 均改引中立层;删掉
  exchange.flush_protective_at_close 里打补丁的局部 import → 双向环消除
- 多策略防呆:BacktestEngine.add_strategy 在 guard 启用下挂第二个策略即抛
  RuntimeError;PositionGuard.bind_strategy 绑第二个不同 strategy_id 也抛——把
  「单策略限制」从静默错误结果转成明确报错(未启 guard 的多策略回测不受影响)
- 补单测:bind 拒第二策略 / add_strategy 拒第二策略 / 无 guard 仍可多策略

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
显式传 rules + 启用 guard 时,guard 的 SELL 当根仅入 pending、下一根才成交,
同 bar 策略 BUY 经 RiskEngine 时查不到平仓记录 → 基于 closed_trades 的锁可能
放过重入单。生产 runner(rules=None)不触达、live 顺序路由不受影响;严格联动走 live。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
之前只豁免行为型锁 enforce,漏了 check_order_notional:配了 max_order_notional 时,
价涨/累积建仓后保护性 SELL 的 notional 必然超单笔上限 → 被 RISK_REJECTED → 持仓
不动 → 每根 bar 重试又被拒 → 止损静默失效(take_profit/trailing 尤甚)。改为两道闸
对保护性 tag 全豁免:notional 上限是防胖手指超大开仓单,平实际持仓量不属此列。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
order.tag 是策略可控字段,仅靠它判保护性出场 → 策略写 Order(tag="stop_loss") 即可
绕过 enforce + notional 上限(熔断锁期内继续开仓)。改为不可仿冒双因子:tag ∈ 保护集
**且** client_order_id 以 GUARD_ORDER_PREFIX("guard-")开头(由框架生成)。新增
model.orders.is_protective_order 集中判定,live_runner / exchange.flush 共用。

附带(CR medium):
- restore_position docstring 注明 resume 不还原 guard _peak_mark → trailing 首触延迟
  (硬止损/止盈不受影响,trailing 默认关)
- backtest 末根 snapshot 注明 Portfolio.snapshot 对同 ts 覆盖、不产生重复点(bot 漏看
  去重逻辑,实为非问题)
- 单测:tag 仿冒(无 guard 前缀)→ is_protective_order False、不豁免

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
is_protective_order 之前只校验 tag + guard 前缀,策略若同时伪造二者 + side=BUY 仍可
借豁免下超大开仓单(跳过 notional + enforce)。guard 永不生成 BUY,故加 side==SELL 作
第三因子,三者缺一即非保护单。补 BUY 仿冒回归测试。

附带(CR medium):
- live_runner 出单日志区分「策略信号」vs「框架 guard 保护性出场」,不再混算误导运维
- TS BacktestReport.protective_exits 改为非 optional(Python default=0 恒带该字段)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ajor)

之前 report 按 f.tag in PROTECTIVE_EXIT_TAGS 计数,但策略也能自打 tag="stop_loss" →
框架 guard 止损与策略自身止损混算,agent 误报"框架止损 N 次"。改为成交侧用三因子
is_protective_signature(side+tag+guard 前缀)算出 FillRecord.is_guard,report 按 is_guard
计数。新增 is_protective_signature 原语(供 Order 路径与成交侧共用),补策略自带 tag
不计入的回归测试。docstring 补三因子「非对抗性防伪、依赖 AST 审计+沙箱」安全前提。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mirror29

Copy link
Copy Markdown
Owner Author

关闭:本 PR 的特性(ADR-0052 PositionGuard)已经过 d12 线合入 main,#88 成了一条平行实现分支,base 被取代且与 main 多文件冲突(add/add),无法干净合并。

#88 七轮 CR 的全部加固(含两条真风控修复:三因子防 tag 仿冒、保护性出场豁免 notional 上限)已整理成针对当前 main 的纯加固 delta → #93。本分支保留作记录。

@mirror29 mirror29 closed this Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant