Skip to content

Commit 6c44584

Browse files
Pigbibicodex
andauthored
fix: retire name-based legacy plugin mounts (#241)
* fix: retire name-based legacy plugin mounts Co-Authored-By: Codex <noreply@openai.com> * docs: define plugin signal mainline sequence Co-Authored-By: Codex <noreply@openai.com> * fix: reject legacy custom plugin mount bypass Co-Authored-By: Codex <noreply@openai.com> * docs: remove plugin contract whitespace violation Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
1 parent b2d405b commit 6c44584

12 files changed

Lines changed: 220 additions & 152 deletions

.github/workflows/manual-strategy-switch.yml

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,12 @@ on:
6060
required: false
6161
type: string
6262
plugin_mode:
63-
description: "auto mounts known strategy plugin artifacts; custom uses custom_plugin_mounts_json."
63+
description: "none is the safe default. auto is a compatibility alias for none; legacy custom mounts are disabled until P1/P2-bound plugin artifacts exist."
6464
required: true
6565
type: choice
66-
default: auto
66+
default: none
6767
options:
68-
- auto
6968
- none
70-
- custom
71-
custom_plugin_mounts_json:
72-
description: "JSON list or {strategy_plugins:[...]} when plugin_mode=custom."
73-
required: false
74-
type: string
7569
extra_variables_json:
7670
description: "Optional JSON object of non-secret extra variables. DCA profiles may include dca_mode and dca_base_investment_usd control fields. cash_only_execution_mode may be current, enabled, or disabled. Research-only option overlays are rejected."
7771
required: false
@@ -158,7 +152,6 @@ jobs:
158152
ACCOUNT_SCOPE: ${{ inputs.account_scope }}
159153
SERVICE_NAME: ${{ inputs.service_name }}
160154
PLUGIN_MODE: ${{ inputs.plugin_mode }}
161-
CUSTOM_PLUGIN_MOUNTS_JSON: ${{ inputs.custom_plugin_mounts_json }}
162155
EXTRA_VARIABLES_JSON: ${{ inputs.extra_variables_json }}
163156
RESERVED_CASH_RATIO: ${{ inputs.reserved_cash_ratio }}
164157
MIN_RESERVED_CASH_USD: ${{ inputs.min_reserved_cash_usd }}
@@ -351,9 +344,6 @@ jobs:
351344
if [ -n "${SERVICE_NAME:-}" ]; then
352345
args+=(--service-name "${SERVICE_NAME}")
353346
fi
354-
if [ -n "${CUSTOM_PLUGIN_MOUNTS_JSON:-}" ]; then
355-
args+=(--custom-plugin-mounts-json "${CUSTOM_PLUGIN_MOUNTS_JSON}")
356-
fi
357347
if [ -n "${EXTRA_VARIABLES_JSON:-}" ]; then
358348
args+=(--extra-variables-json "${EXTRA_VARIABLES_JSON}")
359349
fi

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ platform=longbridge
8383
target_name=sg
8484
strategy_profile=tqqq_growth_income
8585
execution_mode=live
86-
plugin_mode=auto
86+
plugin_mode=none
8787
apply=true
8888
trigger_platform_sync=true
8989
confirm_apply=APPLY_AND_SYNC
@@ -102,6 +102,7 @@ Notes:
102102
- LongBridge, IBKR, Schwab, and Firstrade `service_targets_mode=auto` checks the target repository's multi-service inventory, so even preview mode requires `RUNTIME_SETTINGS_GH_TOKEN`.
103103
- Binance runs through an Oracle Cloud VPS self-hosted runner. Repository variable writes are consumed by the next externally scheduled `main.yml` dispatch; the central switch does not dispatch that runtime workflow because it may execute live trading. A strategy cadence change also requires a separate review of the external VPS scheduler.
104104
- QMT remains dry-run only and has no live deployment configuration. Its generated target can stage repository variables, but `trigger_platform_sync=true` is rejected.
105+
- `plugin_mode=none` is the safe default. `auto` remains only as a compatibility alias for `none`; strategy names must not auto-mount a `latest_signal.json` artifact. Legacy `custom` mounts are disabled and cannot bypass P1/P2/P3 binding through the manual form. A future runtime plugin must be referenced by the frozen P2 candidate and recomputable in P3; see the [Strategy Plugin Contract V2](docs/qsl_strategy_plugin_contract_v2.zh-CN.md).
105106
- The workflow is bound to the `runtime-strategy-switch` GitHub Environment. For a personal system, required reviewers are optional; prefer storing `RUNTIME_SETTINGS_GH_TOKEN` as an Environment secret and rely on preview, confirmation text, and a least-privilege token for day-to-day safety.
106107
- Follow the simplified permission-control plan before enabling real switches: [docs/manual_strategy_switch_permission_control.zh-CN.md](docs/manual_strategy_switch_permission_control.zh-CN.md).
107108

README.zh-CN.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ platform=longbridge
7070
target_name=sg
7171
strategy_profile=tqqq_growth_income
7272
execution_mode=live
73-
plugin_mode=auto
73+
plugin_mode=none
7474
apply=true
7575
trigger_platform_sync=true
7676
confirm_apply=APPLY_AND_SYNC
@@ -88,6 +88,7 @@ confirm_apply=APPLY_AND_SYNC
8888
- LongBridge、IBKR、Schwab、Firstrade 的 `service_targets_mode=auto` 会检查目标仓库是否已有多服务清单,因此即使只做 preview 也需要 `RUNTIME_SETTINGS_GH_TOKEN`
8989
- Binance 运行在 Oracle Cloud VPS 的 self-hosted runner。仓库变量会在外部调度器下一次触发 `main.yml` 时被读取;中控不会自动触发该运行 workflow,因为它可能直接执行实盘。切换到不同运行频率的策略时,还必须单独复核 VPS 外部调度器。
9090
- QMT 当前仅支持 dry-run,尚无实盘部署配置;可以生成目标并暂存仓库变量,但会拒绝 `trigger_platform_sync=true`
91+
- 当前 `plugin_mode=none` 是安全默认值。`auto` 仅为兼容旧请求而保留,实际等同于 `none`;不得再按策略名称自动挂载 `latest_signal.json`。旧 `custom` mount 已禁用,不能借由手动表单绕过 P1/P2/P3 绑定。未来只有被冻结的 P2 候选明确引用、并可在 P3 复算的插件 artifact 才能接入运行时,详见[策略插件契约 V2](docs/qsl_strategy_plugin_contract_v2.zh-CN.md)
9192
- workflow 绑定 GitHub Environment `runtime-strategy-switch`。个人系统默认不需要 required reviewers;建议把 `RUNTIME_SETTINGS_GH_TOKEN` 配成这个 Environment 的 secret,真实写入靠 preview、确认词和 token 最小权限控制。
9293
- 启用真实切换前请按 [手动策略切换权限控制方案](docs/manual_strategy_switch_permission_control.zh-CN.md) 完成最简 secret、token 权限和回滚准备。
9394

docs/QSL_P0_P6_CURRENT_STATE_AND_DRIVER_POLICY.zh-CN.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ P0–P6 是每个研究候选从控制、输入、策略、证据到执行的**
3737

3838
- **单策略**:例如当前日更的 `tqqq_core_only_p2_v5`。它是此刻唯一接入日更 P1/P3 控制器的候选。
3939
- **组合策略**:各域已有独立的组合策略仓库和配置目录;运行配置也支持标记 `combo=true``combo_mode=dynamic`。但一个组合不是把若干单策略结果相加:它必须单独冻结成“组合候选”,明确成分策略版本、权重/再平衡规则、共同数据截止日、组合级风险和成本,然后从 P1/P2/P3 重新走证据链。`UsEquityStrategies` 的纯组合风险预算,以及 `MarketSignalSources` 的时点成分股和历史价格面板契约已经存在;组合 P1 的只读元数据绑定、P2 的冻结候选描述符和 P3 的脱敏证据索引契约也已经合入。它们仍只是防幸存者偏差、复权/成本假设和组合敞口的研究地基:尚没有已验证的组合 P1 原始输入、真实组合 P3 replay 或任何 P4–P6 资格。
40-
- **策略插件**:运行配置已有版本化 plugin mount(例如市场状态信号)的接口。插件只是候选的受约束输入或保护组件;它必须写进该候选的配置/证据,不能在运行中悄悄改参数、替换策略或绕过 P3。当前 TQQQ 日更链不挂载任何插件,也不执行任何组合策略。
40+
- **策略插件**:运行配置已有版本化 plugin mount(例如市场状态信号)的接口。插件只表示候选的受约束、可复算信号;策略在冻结配置中决定如何消费,插件不能在运行中悄悄改参数、替换策略、改写仓位或绕过 P3。AI 黑盒结论仅可走人工通知/研究建议路径,不能作为插件或策略输入。当前 TQQQ 日更链不挂载任何插件,也不执行任何组合策略。
41+
42+
旧运行设置曾按策略名称自动挂载 `latest_signal.json`;该行为现已退役为安全的空操作,不能再把旧插件 artifact 推断为当前候选输入。[策略插件契约 V2](qsl_strategy_plugin_contract_v2.zh-CN.md) 只定义后续实施边界:它不是已接线的 plugin runtime,也不改变当前 TQQQ v5、P4、P5 或 P6 状态。
4143

4244
因此,组合与插件在全局规划中是 P2 策略产品层的并行分支,而不是 P4/P5/P6 的捷径。下一条组合/插件研究线应先建立一个独立候选和 synthetic P1/P3 契约;在此之前,`NO_DRIVER_PARKED` 仍适用于它的 paper、shadow 和 live 阶段。
4345

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# 策略插件契约 V2(设计边界)
2+
3+
> 状态:`DESIGN_ONLY_NOT_RUNTIME`
4+
> 本文不创建 P4、P5、P6 权限,不改变任何策略参数、数据输入、券商设置或订单能力。
5+
6+
## 为什么需要这个契约
7+
8+
旧插件会按策略名称读取 `latest_signal.json` 并自动挂载到运行设置。它既不能证明输入来自哪个 P1 根,也没有被冻结到哪个 P2 候选,P3 也无法对同一插件结果复算。因此它只能作为历史研究材料,不能成为当前主线的运行输入。
9+
10+
互联网泡沫等长周期压力回测可以提出一个很有价值的研究假设:市场状态或风险约束可能降低杠杆 ETF 的尾部回撤。但它不是 TQQQ 已验证表现,更不是自动运行权限。TQQQ 于 2010 年才开始交易;更早时期只能称为明确假设和成本模型下的合成压力测试。
11+
12+
## 插件只表示信号
13+
14+
每个插件都是 `research_signal`:它从明确的输入产生有 schema 的市场状态、诊断或研究特征。它不决定仓位、不改写目标、不调参数、不调度,也不调用券商。
15+
16+
策略才是唯一的消费者。某个策略是否使用一份信号、使用哪些字段、阈值/组合逻辑、信号缺失时是否停车或采用固定中性处理,都必须写进该策略的冻结 P2 配置和代码 revision。风险预算、仓位和再平衡因此仍属于策略候选本身,而不是运行时可替换的插件。
17+
18+
AI 黑盒输出不是插件输入。它只能生成 `ai_advisory`:结构化的诊断摘要、紧急程度和通知建议,交由人决定是否发起新的研究候选。AI 不得自动把结论写进 P2 参数、选择插件、改变策略消费方式或影响 paper/shadow/live。
19+
20+
## 一个可接线插件必须冻结的身份
21+
22+
每份候选引用必须同时含有并哈希覆盖下列字段:
23+
24+
- `contract_schema=strategy_plugin_contract.v2``plugin_id``kind`、插件代码 revision 与配置 SHA-256;
25+
- 同一候选的 P1 input-root SHA-256、数据截止时点和候选 ID;
26+
- 明确的 P2 consumer entrypoint、输入/输出 schema,以及策略对该信号的冻结消费规则;
27+
- artifact 的内容 SHA-256 与有效期。`latest` 只能做控制台显示,绝不能做 P2/P3 证据指针。
28+
29+
缺少任一字段、哈希不匹配、数据截止不一致、未知 schema 或越权效果,必须 `PARKED`,不能回退到“按名称猜测”。
30+
31+
## 生命周期与复算
32+
33+
```text
34+
P1 同根输入
35+
-> research_signal 插件(可选,生成带身份的 artifact)
36+
-> P2 冻结候选明确引用 artifact SHA 和消费规则
37+
-> 策略依其冻结逻辑计算风险预算、目标和再平衡
38+
-> P3 从相同 P1/P2 重新计算信号与策略消费结果并逐项比对
39+
-> 后续独立 P4/P5/P6 门槛
40+
```
41+
42+
P3 必须复算插件输出、策略消费结果及其原因摘要,而不只是检查文件存在。任何 P4/P5/P6 证据都不能倒推为插件已获授权。
43+
44+
## 历史验证的证据边界
45+
46+
- `synthetic_stress_evidence``real_etf_evidence` 必须分桶记录、分别报告,不能合并为“已验证表现”。例如 2010 年前用 QQQ 日收益构造的每日 3 倍路径,只能说明合成压力情景,不代表真实 TQQQ 的费率、跟踪误差、流动性或可成交性。
47+
- 信号只能使用 `cutoff` 时点已知的数据,并在下一可交易时点计入成交;P1/P2/P3 要共同记录复权、分红/拆分、费用、滑点、融资、换手和无法成交假设。
48+
- 每个插件先登记 hypothesis card:经济机制、允许输入、预注册参数空间、基线策略、预期失效条件和最终未见验证区间。看过未见区间后再改动,必须成为新的候选。
49+
- 所有尝试过的参数和失败候选都要进入实验账本,不能只保留赢家;报告需同时覆盖滚动/扩展 walk-forward、未见样本、参数邻域稳定性,以及相对基线的净收益、回撤、回撤持续期、换手和错杀/漏检。
50+
51+
## 旧实现的迁移规则
52+
53+
-`market_regime_control` 和其他 `latest_signal.json` 工件标记为 `legacy_research_only`;它们可以保留作研究对照,不可自动挂载。
54+
- 运行设置的 `plugin_mode=none` 为默认;旧 `auto` 为兼容别名,实际同样不挂载任何插件。
55+
-`custom` mount 已禁用。它不能作为受控迁移或人工表单的旁路;在 signal.v2 适配器接线前,任何此类请求都必须失败。
56+
- 现有 TQQQ P2 v5 不引用插件;任何新的观察入口或草稿实现都不是本契约已接线的 runtime。
57+
- AI 诊断继续按独立的通知/Issue 路径运行;它不能伪装成历史信号或策略参数来源。
58+
59+
## 进入主线的顺序
60+
61+
1. **先完成公共骨架**:实现 `qsl.strategy-plugin-signal.v2` 的 schema、P1/P2 身份绑定、P3 verifier、golden vectors 和不可变 verified manifest;不接平台运行时。
62+
2. **第一条候选只观察**:新建独立的 TQQQ `P2 v6 plugin-observe` 候选。它的仓位目标必须与当前 core-only P2 v5 逐项一致,只把 `market_regime` 信号写入诊断、shadow 对照和 P3 证据。它不能读取 legacy resolver 或 `latest`
63+
3. **前瞻观察后才允许消费**:积累足够的真实、前瞻观察并通过分桶历史验证后,另起新的 `P2 v7 market-regime-consume` 候选。消费规则属于策略版本,且只能按冻结规则减少风险或转入已定义的防御资产;不得增加杠杆、重入或下单。
64+
4. **多个插件可并行研究,不能并行叠加**:TQQQ 市场状态、SOXL/SOXX 波动/反弹、IBIT 估值周期可各自开发 signal producer 和独立 P1/P2/P3 研究证据。把多个信号共同消费、定义优先级或冲突处理时,必须新建一个组合候选并串行验证。
65+
66+
目前只有第 0 步(退役旧自动挂载与写明契约)正在实施。现有 TQQQ P2 v5、任何旧 `latest_signal.json` 和草稿观察入口都不是第 1–3 步的已接线实现。
67+
68+
## 接线前验收清单
69+
70+
1. 固定的 P1/P2/P3 golden vector 可重复通过,且 P3 与 P2 的插件身份、artifact SHA 和输出完全一致。
71+
2. 负面测试覆盖过期 artifact、`latest` 指针、错根输入、错截止日、错代码 revision、未知 schema 和未声明的消费字段。
72+
3. 策略消费的 golden vector 覆盖信号状态变化、缺失信号和中性/停车分支;插件本身不得含有仓位、订单或执行字段。
73+
4. 回测报告同时披露合成标的、费用、滑点、融资/借券假设、参数搜索次数和训练/验证/未见样本切分。
74+
5. 接线仅能从一个明确冻结的 P2 候选开始;不得以批量自动迁移替代逐候选证据。

python/scripts/build_runtime_switch.py

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,7 @@
2727

2828
DEFAULT_ARTIFACT_BUCKET_URI = "gs://qsl-runtime-logs-shared"
2929
PLATFORM_CONFIG_PATH = ROOT / "platform-config.json"
30-
# Keep this list limited to strategy-scope artifacts that the publisher
31-
# currently produces. QPK may parse broader explicit mounts for forward
32-
# compatibility, but auto mode must not generate missing latest_signal paths.
33-
MARKET_REGIME_CONTROL_PROFILES = frozenset(
34-
{
35-
"tqqq_growth_income",
36-
"soxl_soxx_trend_income",
37-
}
38-
)
3930
IBIT_ZSCORE_EXIT_STRATEGY_PROFILE = "ibit_smart_dca"
40-
IBIT_ZSCORE_EXIT_PLUGIN = "ibit_zscore_exit"
4131
PLATFORM_DRY_RUN_VARIABLES = {
4232
"schwab": "SCHWAB_DRY_RUN_ONLY",
4333
"longbridge": "LONGBRIDGE_DRY_RUN_ONLY",
@@ -616,62 +606,21 @@ def _ibit_zscore_exit_extra_variables(
616606

617607

618608
def _auto_plugin_mounts(strategy_profile: str, artifact_bucket_uri: str, dca_mode: str = "") -> list[dict[str, Any]]:
619-
prefix = artifact_bucket_uri.rstrip("/")
620-
mounts: list[dict[str, Any]] = []
621-
if strategy_profile in MARKET_REGIME_CONTROL_PROFILES:
622-
mounts.append(
623-
{
624-
"strategy": strategy_profile,
625-
"plugin": "market_regime_control",
626-
"signal_path": (
627-
f"{prefix}/strategy-artifacts/us_equity/{strategy_profile}"
628-
"/plugins/market_regime_control/latest_signal.json"
629-
),
630-
"enabled": True,
631-
"expected_mode": "shadow",
632-
"expected_schema_version": "market_regime_control.v1",
633-
}
634-
)
635-
if strategy_profile == IBIT_ZSCORE_EXIT_STRATEGY_PROFILE and dca_mode == "smart":
636-
mounts.append(
637-
{
638-
"strategy": strategy_profile,
639-
"plugin": IBIT_ZSCORE_EXIT_PLUGIN,
640-
"signal_path": (
641-
f"{prefix}/strategy-artifacts/us_equity/{strategy_profile}"
642-
f"/plugins/{IBIT_ZSCORE_EXIT_PLUGIN}/latest_signal.json"
643-
),
644-
"enabled": True,
645-
"expected_mode": "shadow",
646-
"expected_schema_version": "ibit_zscore_exit.v1",
647-
}
648-
)
649-
return mounts
650-
651-
652-
def _custom_plugin_mounts(raw_json: str) -> list[dict[str, Any]]:
653-
text = str(raw_json or "").strip()
654-
if not text:
655-
return []
656-
try:
657-
payload = json.loads(text)
658-
except json.JSONDecodeError as exc:
659-
raise ValueError("custom_plugin_mounts_json must be valid JSON") from exc
660-
if isinstance(payload, dict):
661-
payload = payload.get("strategy_plugins", payload.get("plugins"))
662-
if not isinstance(payload, list):
663-
raise ValueError("custom_plugin_mounts_json must be a list or object with strategy_plugins")
664-
return [dict(item) for item in payload]
609+
"""Retire name-based plugin selection without expanding runtime authority."""
610+
del strategy_profile, artifact_bucket_uri, dca_mode
611+
return []
665612

666613

667614
def _plugin_mounts(args: argparse.Namespace, strategy_profile: str, dca_mode: str = "") -> list[dict[str, Any]]:
668-
mode = str(args.plugin_mode or "auto").strip().lower()
615+
mode = str(args.plugin_mode or "none").strip().lower()
669616
if mode == "none":
670617
return []
671618
if mode == "auto":
672619
return _auto_plugin_mounts(strategy_profile, args.artifact_bucket_uri, dca_mode)
673620
if mode == "custom":
674-
return _custom_plugin_mounts(args.custom_plugin_mounts_json)
621+
raise ValueError(
622+
"legacy custom plugin mounts are retired; a P1/P2/P3-bound strategy_plugin_signal.v2 adapter is required"
623+
)
675624
raise ValueError(f"unsupported plugin_mode {args.plugin_mode!r}")
676625

677626

@@ -987,11 +936,14 @@ def build_switch_target(args: argparse.Namespace) -> dict[str, Any]:
987936
extra_variables.update(_cash_only_extra_variables(args, platform))
988937
extra_variables.update(_option_overlay_extra_variables(args, runtime_target["strategy_profile"]))
989938
extra_variables.update(_dca_extra_variables(args, runtime_target["strategy_profile"], dca_controls))
939+
effective_plugin_mode = str(args.plugin_mode or "none").strip().lower()
940+
if effective_plugin_mode == "auto":
941+
effective_plugin_mode = "none"
990942
extra_variables.update(
991943
_ibit_zscore_exit_extra_variables(
992944
args,
993945
runtime_target["strategy_profile"],
994-
str(args.plugin_mode or "auto").strip().lower(),
946+
effective_plugin_mode,
995947
effective_dca_mode,
996948
)
997949
)
@@ -1059,7 +1011,7 @@ def build_parser() -> argparse.ArgumentParser:
10591011
parser.add_argument("--account-scope", default="")
10601012
parser.add_argument("--service-name", default="")
10611013
parser.add_argument("--execution-windows-json", default="")
1062-
parser.add_argument("--plugin-mode", choices=("auto", "none", "custom"), default="auto")
1014+
parser.add_argument("--plugin-mode", choices=("auto", "none", "custom"), default="none")
10631015
parser.add_argument("--custom-plugin-mounts-json", default="")
10641016
parser.add_argument("--artifact-bucket-uri", default=DEFAULT_ARTIFACT_BUCKET_URI)
10651017
parser.add_argument("--extra-variables-json", default="", help="JSON object of non-secret extra variables")

0 commit comments

Comments
 (0)