Skip to content

Commit 59e549c

Browse files
committed
Merge remote-tracking branch 'origin/main' into chore/require-runtime-target-json
2 parents 89bb49a + 824ee97 commit 59e549c

5 files changed

Lines changed: 26 additions & 31 deletions

File tree

docs/platform_runtime_inventory.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ For the platform / strategy-domain / configurable-profile matrix, see [`platform
8080
- runtime Telegram token secret
8181
- **Runtime notes**
8282
- Runtime-sensitive envs should use Secret Manager refs, not plain Cloud Run env values.
83-
- Strategy plugins are sidecars: `shadow` logs and notifies only; `paper`, `advisory`, and `live` semantics are governed by the plugin execution mode contract.
83+
- Strategy plugins are sidecars: only `shadow` notification mode is supported; plugin-driven paper, advisory, and live execution modes are not part of the shared contract.
8484
- The token refresher lives outside this repo:
8585
- `QuantStrategyLab/SchwabTokenAutoRefresher`
8686

docs/platform_runtime_inventory.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ _校验快照日期:2026-04-18_
7979
- runtime Telegram token secret
8080
- **运行说明**
8181
- 运行时敏感配置应使用 Secret Manager 引用,不应放在 Cloud Run 明文 env。
82-
- 策略插件是 sidecar:`shadow` 只写日志和通知;`paper``advisory``live` 的语义由插件执行模式契约统一约束
82+
- 策略插件是 sidecar:只支持 `shadow` 通知模式;插件驱动的 `paper``advisory``live` 执行模式不属于共享契约
8383
- token refresher 不在这个仓库里,而是在:
8484
- `QuantStrategyLab/SchwabTokenAutoRefresher`
8585

docs/strategy_plugin_runtime_contract.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ artifacts, such as the Crisis Response plugin produced by
1717

1818
Platform config should only decide which plugin artifacts are mounted for a
1919
strategy. It must not select the plugin mode. The mode lives inside the plugin
20-
artifact and is the single behavior contract.
20+
artifact and is fixed to notification-only `shadow`.
2121

2222
Suggested environment variable name: `STRATEGY_PLUGIN_MOUNTS_JSON`.
2323

@@ -53,9 +53,9 @@ or reinterpret the mode:
5353
}
5454
```
5555

56-
Do not put `mode` in the platform mount config. If the artifact says `paper`,
57-
the platform implements `paper`; if it says `live`, the platform implements
58-
`live` subject to risk checks and kill switches.
56+
Do not put `mode` in the platform mount config. `expected_mode` may be used only
57+
as a fail-closed guard and should be `shadow` when present. Artifacts declaring
58+
`paper`, `advisory`, or `live` are rejected.
5959

6060
## Runtime Loader
6161

@@ -80,8 +80,7 @@ The loader validates:
8080

8181
- the artifact is a JSON object
8282
- `strategy` and `plugin` match the configured mount
83-
- `mode`, `configured_mode`, and `effective_mode` are one of `shadow`, `paper`,
84-
`advisory`, or `live`
83+
- `mode`, `configured_mode`, and `effective_mode` are `shadow`
8584
- optional `expected_mode` matches `effective_mode`
8685
- duplicate platform mounts are rejected
8786
- platform mount config does not set `mode`
@@ -91,11 +90,6 @@ The loader validates:
9190
For `shadow`, platform runtimes should only add logs, runtime report fields, and
9291
notification context.
9392

94-
For `paper`, platform runtimes may maintain a simulated plugin ledger, but must
95-
not mutate real allocations.
96-
97-
For `advisory`, platform runtimes may surface a recommendation that requires
98-
human confirmation.
99-
100-
For `live`, platform runtimes may allow execution only through explicit platform
101-
risk budgets, kill switches, and data-freshness checks.
93+
`paper`, `advisory`, and `live` plugin modes are not supported by the shared
94+
contract. Platforms should not maintain plugin ledgers or execute plugin-driven
95+
allocation changes from this sidecar path.

src/quant_platform_kit/common/strategy_plugins.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@
1111
from typing import Any
1212

1313
PLUGIN_MODE_SHADOW = "shadow"
14-
PLUGIN_MODE_PAPER = "paper"
15-
PLUGIN_MODE_ADVISORY = "advisory"
16-
PLUGIN_MODE_LIVE = "live"
17-
SUPPORTED_STRATEGY_PLUGIN_MODES = frozenset(
18-
{PLUGIN_MODE_SHADOW, PLUGIN_MODE_PAPER, PLUGIN_MODE_ADVISORY, PLUGIN_MODE_LIVE}
19-
)
14+
SUPPORTED_STRATEGY_PLUGIN_MODES = frozenset({PLUGIN_MODE_SHADOW})
2015
DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR = Path(tempfile.gettempdir()) / "quant_strategy_plugin_artifacts"
2116

2217

tests/test_strategy_plugins.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from pathlib import Path
55

66
from quant_platform_kit.common.strategy_plugins import (
7-
PLUGIN_MODE_PAPER,
87
PLUGIN_MODE_SHADOW,
98
build_strategy_plugin_report_payload,
109
load_configured_strategy_plugin_signals,
@@ -27,8 +26,8 @@ def _signal_payload(*, strategy="tqqq_growth_income", plugin="crisis_response_sh
2726
"suggested_action": "watch_only",
2827
"would_trade_if_enabled": False,
2928
"execution_controls": {
30-
"broker_order_allowed": mode == "live",
31-
"live_allocation_mutation_allowed": mode == "live",
29+
"broker_order_allowed": False,
30+
"live_allocation_mutation_allowed": False,
3231
"repository_broker_write_allowed": False,
3332
"repository_allocation_mutation_allowed": False,
3433
},
@@ -79,18 +78,18 @@ def test_parse_strategy_plugin_mounts_rejects_platform_mode_selection(self):
7978
def test_load_strategy_plugin_signal_validates_identity_and_mode(self):
8079
with tempfile.TemporaryDirectory() as tmp_dir:
8180
signal_path = Path(tmp_dir) / "latest_signal.json"
82-
signal_path.write_text(json.dumps(_signal_payload(mode=PLUGIN_MODE_PAPER)), encoding="utf-8")
81+
signal_path.write_text(json.dumps(_signal_payload(mode=PLUGIN_MODE_SHADOW)), encoding="utf-8")
8382

8483
signal = load_strategy_plugin_signal(
8584
str(signal_path),
8685
expected_strategy="tqqq_growth_income",
8786
expected_plugin="crisis_response_shadow",
88-
expected_mode=PLUGIN_MODE_PAPER,
87+
expected_mode=PLUGIN_MODE_SHADOW,
8988
)
9089

9190
self.assertEqual(signal.strategy, "tqqq_growth_income")
9291
self.assertEqual(signal.plugin, "crisis_response_shadow")
93-
self.assertEqual(signal.effective_mode, PLUGIN_MODE_PAPER)
92+
self.assertEqual(signal.effective_mode, PLUGIN_MODE_SHADOW)
9493
self.assertFalse(signal.execution_controls["repository_broker_write_allowed"])
9594
self.assertEqual(signal.local_path, str(signal_path))
9695

@@ -139,11 +138,18 @@ def test_load_configured_strategy_plugin_signals_filters_strategy_and_disabled_m
139138

140139
self.assertEqual([signal.plugin for signal in signals], ["crisis_response_shadow"])
141140

142-
def test_validate_strategy_plugin_signal_payload_rejects_expected_mode_mismatch(self):
143-
with self.assertRaisesRegex(ValueError, "mode mismatch"):
141+
def test_validate_strategy_plugin_signal_payload_rejects_non_shadow_artifact_mode(self):
142+
with self.assertRaisesRegex(ValueError, "mode must be one of shadow"):
143+
validate_strategy_plugin_signal_payload(
144+
_signal_payload(mode="paper"),
145+
expected_mode=PLUGIN_MODE_SHADOW,
146+
)
147+
148+
def test_validate_strategy_plugin_signal_payload_rejects_non_shadow_expected_mode(self):
149+
with self.assertRaisesRegex(ValueError, "expected_mode must be one of shadow"):
144150
validate_strategy_plugin_signal_payload(
145151
_signal_payload(mode=PLUGIN_MODE_SHADOW),
146-
expected_mode=PLUGIN_MODE_PAPER,
152+
expected_mode="live",
147153
)
148154

149155
def test_build_strategy_plugin_report_payload_uses_compact_summary(self):

0 commit comments

Comments
 (0)