Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/deployment_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- `QuantPlatformKit` remains the shared platform package and is **not deployed as a runtime service**.
- The current runtime repositories (`InteractiveBrokersPlatform`, `CharlesSchwabPlatform`, `LongBridgePlatform`, `BinancePlatform`) are the **transitional deployment units**.
- The **target state** is one deployment repository per broker platform, with strategy behavior selected through `RuntimeTarget` / `RUNTIME_TARGET_JSON` and compatibility selectors such as `STRATEGY_PROFILE`.
- The **target state** is one deployment repository per broker platform, with structured runtime identity carried through `RuntimeTarget` / `RUNTIME_TARGET_JSON` and `STRATEGY_PROFILE` retained only as a compatibility routing selector.
- Strategy or platform repositories should always depend on a fixed `QuantPlatformKit` Git tag instead of `main`.

For the live runtime inventory across repositories, projects, services, schedulers, runtime identities, and current secret names, see [`platform_runtime_inventory.md`](./platform_runtime_inventory.md).
Expand Down Expand Up @@ -152,7 +152,7 @@ Within one broker platform repository, selecting a strategy by configuration is
Recommended selector:

- `RUNTIME_TARGET_JSON` for structured runtime identity
- `STRATEGY_PROFILE` for compatibility with existing strategy routing
- `STRATEGY_PROFILE` only for compatibility with existing strategy routing

Good examples:

Expand Down
3 changes: 3 additions & 0 deletions docs/deployment_model.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- `QuantPlatformKit` 是共享平台代码仓库,**不单独部署**。
- `InteractiveBrokersPlatform`、`CharlesSchwabPlatform`、`LongBridgePlatform`、`BinancePlatform` 这些仓库才是实际运行单元。
- 目标状态是每个平台一个部署仓库,结构化运行身份通过 `RuntimeTarget` / `RUNTIME_TARGET_JSON` 传递,`STRATEGY_PROFILE` 只作为兼容路由选择器保留。
- 策略仓库应该固定依赖某个 Git tag,不要直接依赖 `main`。

如果要看公开 runtime 接线清单,包括仓库、项目、服务、scheduler、runtime identity、selector 和 secret 入口,见 [`platform_runtime_inventory.zh-CN.md`](./platform_runtime_inventory.zh-CN.md)。
Expand Down Expand Up @@ -122,6 +123,7 @@ Cloud Run 继续只部署运行仓库,不部署 `QuantPlatformKit`。

可以参数化的:

- `RUNTIME_TARGET_JSON`
- `IB_GATEWAY_MODE`
- `ACCOUNT_PREFIX`
- `SERVICE_NAME`
Expand Down Expand Up @@ -195,4 +197,5 @@ Cloud Run 继续只部署运行仓库,不部署 `QuantPlatformKit`。
- 平台共享代码进 `QuantPlatformKit`
- 平台运行仓库继续作为部署单元
- GCP / VPS 只部署平台运行仓库
- 运行身份优先看 `RUNTIME_TARGET_JSON`,`STRATEGY_PROFILE` 只保留兼容路由
- 版本靠固定 tag 管理
18 changes: 10 additions & 8 deletions docs/platform_runtime_inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,25 +91,27 @@ For the platform / strategy-domain / configurable-profile matrix, see [`platform
- **Cloud Run project**
- `longbridgequant`
- **Services**
- HK: `longbridge-quant-hk-service`
- PAPER: `longbridge-quant-paper-service`
- HK: reserved / not yet wired
- SG: `longbridge-quant-sg-service`
- **Runtime service account**
- `longbridge-platform-runtime@longbridgequant.iam.gserviceaccount.com`
- **Schedulers**
- `longbridge-quant-hk-service-scheduler` in `asia-east2`
- `longbridge-quant-paper-service-scheduler` in `asia-east2`
- HK: reserved / not yet wired
- `longbridge-quant-sg-service-scheduler` in `asia-southeast1`
- **Core runtime selectors**
- `STRATEGY_PROFILE=<runtime_enabled us_equity profile>` per regional service
- `ACCOUNT_REGION=HK|SG`
- `LONGPORT_SECRET_NAME=<region token secret>`
- `STRATEGY_PROFILE=<runtime_enabled us_equity profile>` per account service
- `ACCOUNT_REGION=PAPER|HK|SG`
- `LONGPORT_SECRET_NAME=<account token secret>`
- **Runtime secrets**
- Secret Manager refs for LongPort app key / app secret
- region token secrets selected by `LONGPORT_SECRET_NAME`
- account token secrets selected by `LONGPORT_SECRET_NAME`
- runtime Telegram token secret
- **Runtime notes**
- HK and SG keep two independent Cloud Run services, two triggers, and two GitHub Environments.
- PAPER and SG are live today; HK keeps the same deployment pattern when it is added. Each account identity gets its own Cloud Run service, trigger, and GitHub Environment.
- Snapshot-backed profiles require feature snapshot path / manifest envs; direct-runtime profiles do not.
- App key / secret are region-specific Secret Manager refs; Telegram token is shared inside the LongBridge project.
- App key / secret are account-specific Secret Manager refs; Telegram token is shared inside the LongBridge project.
- `SERVICE_NAME` should use the full runtime-facing service names above, not older short prefixes.

### Binance
Expand Down
6 changes: 4 additions & 2 deletions docs/platform_strategy_matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ For strategy behavior, research status, and archived backtest evidence, see
- There are currently two strategy domains:
- `us_equity`
- `crypto`
- Runtime repositories now expose `RUNTIME_TARGET_JSON` plus `STRATEGY_PROFILE`; this is **not** a full multi-strategy marketplace yet.
- Runtime repositories now expose `RUNTIME_TARGET_JSON` as the primary structured runtime identity, with `STRATEGY_PROFILE` kept as a compatibility routing selector; this is **not** a full multi-strategy marketplace yet.
- Today, each US equity platform can switch among the `runtime_enabled` `us_equity` profiles published by `UsEquityStrategies`, subject to each platform's rollout configuration.
- Platform runtime adapters are generated from strategy input/target-mode declarations plus platform capabilities, so new in-contract profiles should not need per-platform allowlist edits.
- The shared contract is in `QuantPlatformKit`; real `us_equity` strategy implementations now live in `UsEquityStrategies`, while platform repositories own runtime adapters and broker execution.
Expand All @@ -29,7 +29,7 @@ For strategy behavior, research status, and archived backtest evidence, see
|---|---|---|---|---|---|---|
| IBKR | `QuantStrategyLab/InteractiveBrokersPlatform` | `us_equity` | `RUNTIME_TARGET_JSON` + `ACCOUNT_GROUP` | `STRATEGY_PROFILE=<runtime_enabled us_equity profile>` | Cloud Run | Yes - controlled by platform rollout config |
| Charles Schwab | `QuantStrategyLab/CharlesSchwabPlatform` | `us_equity` | `RUNTIME_TARGET_JSON` | `STRATEGY_PROFILE=<runtime_enabled us_equity profile>` | Cloud Run | Yes - controlled by platform rollout config |
| LongBridge | `QuantStrategyLab/LongBridgePlatform` | `us_equity` | `RUNTIME_TARGET_JSON` + `ACCOUNT_REGION` | `STRATEGY_PROFILE=<runtime_enabled us_equity profile>` per regional service | Cloud Run | Yes - controlled by platform rollout config |
| LongBridge | `QuantStrategyLab/LongBridgePlatform` | `us_equity` | `RUNTIME_TARGET_JSON` + `ACCOUNT_REGION` | `STRATEGY_PROFILE=<runtime_enabled us_equity profile>` per account service | Cloud Run | Yes - paper and SG today; HK later |
| Binance | `QuantStrategyLab/BinancePlatform` | `crypto` | `RUNTIME_TARGET_JSON` (workflow-local) | `crypto_leader_rotation` | Oracle Cloud + self-hosted runner | No - only this profile is supported today |

## What this means right now
Expand All @@ -42,6 +42,8 @@ Platforms currently in this domain:
- `CharlesSchwabPlatform`
- `LongBridgePlatform`

LongBridge account identities are modeled as `paper`, `HK`, and `SG`; today the live services use `paper` and `SG`, and `HK` follows the same contract when it is introduced.

Important limitation:

- This does **not** mean any arbitrary future `us_equity` strategy can run by name alone.
Expand Down
2 changes: 1 addition & 1 deletion docs/platform_strategy_matrix.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ _核对时间:2026-04-18_
- 当前只有两个策略大类:
- `us_equity`
- `crypto`
- 各个平台仓库现在都已经保留了 `STRATEGY_PROFILE` 入口,但这**还不是**真正的多策略平台。
- 各个平台仓库现在都已经保留了 `RUNTIME_TARGET_JSON` 结构化运行身份,`STRATEGY_PROFILE` 只作为兼容路由入口,但这**还不是**真正的多策略平台。
- 现在每个美股平台仓库都可以在 `UsEquityStrategies` 发布的 `runtime_enabled` `us_equity` profile 之间切换,前提是对应平台 rollout 配置已经放开。
- 平台 runtime adapter 会根据策略输入、target mode 和平台 capability 自动生成;规范内的新 profile 不应该再需要三个平台分别手写 allowlist。
- 共享契约在 `QuantPlatformKit`;真实的 `us_equity` 策略实现现在放在 `UsEquityStrategies`,平台仓库负责运行时适配和券商执行。
Expand Down
11 changes: 6 additions & 5 deletions docs/us_equity_live_switch_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ Do not change service names as part of a strategy switch.
| --- | --- | --- |
| IBKR | `interactive-brokers-quant-service` | `ACCOUNT_GROUP` |
| Schwab | `charles-schwab-quant-service` | single service |
| LongBridge HK | `longbridge-quant-hk-service` | `ACCOUNT_REGION=HK` |
| LongBridge PAPER | `longbridge-quant-paper-service` | `ACCOUNT_REGION=PAPER` |
| LongBridge HK | reserved / not yet wired | `ACCOUNT_REGION=HK` |
| LongBridge SG | `longbridge-quant-sg-service` | `ACCOUNT_REGION=SG` |

## Step 1: verify the target profile before touching env
Expand Down Expand Up @@ -265,7 +266,7 @@ Do not stop at Cloud Run env.
Verify the first heartbeat or execution notification shows:

- the expected display name
- the expected account prefix (`[HK]` / `[SG]` for LongBridge)
- the expected account prefix (`[PAPER]` / `[HK]` / `[SG]` for LongBridge)
- no stale strategy-specific service-name suffix in the LongBridge notification prefix

If the profile uses feature snapshots, also verify:
Expand Down Expand Up @@ -319,12 +320,12 @@ Why:
- `tech_communication_pullback_enhancement` is a feature-snapshot profile
- the strategy has a packaged canonical config; set the env path only when overriding it

### Example C: switch LongBridge HK to `russell_1000_multi_factor_defensive`
### Example C: switch LongBridge PAPER to `russell_1000_multi_factor_defensive`

Keep:

- `ACCOUNT_PREFIX=HK`
- `ACCOUNT_REGION=HK`
- `ACCOUNT_PREFIX=PAPER`
- `ACCOUNT_REGION=PAPER`
- `LONGPORT_SECRET_NAME`
- `LONGPORT_APP_KEY_SECRET_NAME`
- `LONGPORT_APP_SECRET_SECRET_NAME`
Expand Down
6 changes: 0 additions & 6 deletions src/quant_platform_kit/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@
from .runtime_target import (
build_runtime_context_fields,
RuntimeTarget,
ResolvedRuntimeIdentity,
build_runtime_target,
resolve_runtime_identity_from_env,
resolve_runtime_target_from_env,
resolve_runtime_target_strategy_profile_from_env,
)
from .strategy_plugins import (
PLUGIN_MODE_SHADOW,
Expand All @@ -40,7 +37,6 @@
"SUPPORTED_STRATEGY_PLUGIN_MODES",
"localize_notification_text",
"RuntimeTarget",
"ResolvedRuntimeIdentity",
"build_runtime_context_fields",
"build_run_id",
"emit_runtime_log",
Expand All @@ -56,9 +52,7 @@
"load_strategy_plugin_signal",
"normalize_strategy_plugin_mode",
"parse_strategy_plugin_mounts",
"resolve_runtime_identity_from_env",
"resolve_runtime_target_from_env",
"resolve_runtime_target_strategy_profile_from_env",
"translator_uses_zh",
"validate_strategy_plugin_signal_payload",
]
101 changes: 15 additions & 86 deletions src/quant_platform_kit/common/runtime_target.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

import json
from dataclasses import dataclass, asdict
from collections.abc import Iterable
from dataclasses import dataclass, asdict
from typing import Any, Mapping


Expand All @@ -26,12 +26,6 @@ def to_dict(self) -> dict[str, object]:
return payload


@dataclass(frozen=True)
class ResolvedRuntimeIdentity:
strategy_profile: str
runtime_target: RuntimeTarget


def build_runtime_context_fields(
extra_context_fields: Mapping[str, Any] | None = None,
*,
Expand Down Expand Up @@ -105,25 +99,11 @@ def _coerce_optional_bool(value: object) -> bool | None:
def resolve_runtime_target_from_env(
*,
env: Mapping[str, str | None],
platform_id: str,
strategy_profile: str,
dry_run_only: bool,
deployment_selector: str | None = None,
account_selector: Iterable[str] | str | None = None,
account_scope: str | None = None,
service_name: str | None = None,
expected_platform_id: str | None = None,
) -> RuntimeTarget:
raw_payload = _normalize_optional_string(env.get("RUNTIME_TARGET_JSON"))
if raw_payload is None:
return build_runtime_target(
platform_id=platform_id,
strategy_profile=strategy_profile,
dry_run_only=dry_run_only,
deployment_selector=deployment_selector,
account_selector=account_selector,
account_scope=account_scope,
service_name=service_name,
)
raise EnvironmentError("RUNTIME_TARGET_JSON is required")

try:
payload = json.loads(raw_payload)
Expand All @@ -135,15 +115,19 @@ def resolve_runtime_target_from_env(

resolved_platform_id = _normalize_optional_string(payload.get("platform_id"))
if resolved_platform_id is None:
resolved_platform_id = platform_id
elif resolved_platform_id != platform_id:
raise ValueError("RUNTIME_TARGET_JSON.platform_id is required")
if expected_platform_id is not None and resolved_platform_id != expected_platform_id:
raise ValueError(
"RUNTIME_TARGET_JSON.platform_id does not match the runtime platform"
)
resolved_strategy_profile = payload.get("strategy_profile", strategy_profile)
resolved_strategy_profile = _normalize_optional_string(
payload.get("strategy_profile")
)
if resolved_strategy_profile is None:
raise ValueError("RUNTIME_TARGET_JSON.strategy_profile is required")
resolved_dry_run_only = _coerce_optional_bool(payload.get("dry_run_only"))
if resolved_dry_run_only is None:
resolved_dry_run_only = dry_run_only
raise ValueError("RUNTIME_TARGET_JSON.dry_run_only is required")

execution_mode = payload.get("execution_mode")
if execution_mode is not None and str(execution_mode).strip():
Expand All @@ -157,63 +141,8 @@ def resolve_runtime_target_from_env(
platform_id=resolved_platform_id,
strategy_profile=resolved_strategy_profile,
dry_run_only=resolved_dry_run_only,
deployment_selector=payload.get("deployment_selector", deployment_selector),
account_selector=payload.get("account_selector", account_selector),
account_scope=payload.get("account_scope", account_scope),
service_name=payload.get("service_name", service_name),
)


def resolve_runtime_target_strategy_profile_from_env(
env: Mapping[str, str | None],
*,
default_strategy_profile: str | None,
) -> str | None:
raw_payload = _normalize_optional_string(env.get("RUNTIME_TARGET_JSON"))
if raw_payload is None:
return _normalize_optional_string(default_strategy_profile)

try:
payload = json.loads(raw_payload)
except json.JSONDecodeError as exc:
raise ValueError("RUNTIME_TARGET_JSON must contain valid JSON") from exc

if not isinstance(payload, dict):
raise ValueError("RUNTIME_TARGET_JSON must decode to an object")

resolved_strategy_profile = _normalize_optional_string(
payload.get("strategy_profile")
)
return resolved_strategy_profile or _normalize_optional_string(default_strategy_profile)


def resolve_runtime_identity_from_env(
env: Mapping[str, str | None],
*,
platform_id: str,
default_strategy_profile: str | None,
dry_run_only: bool,
deployment_selector: str | None = None,
account_selector: Iterable[str] | str | None = None,
account_scope: str | None = None,
service_name: str | None = None,
) -> ResolvedRuntimeIdentity:
strategy_profile = resolve_runtime_target_strategy_profile_from_env(
env,
default_strategy_profile=default_strategy_profile,
)
if strategy_profile is None:
raise EnvironmentError("STRATEGY_PROFILE is required")
return ResolvedRuntimeIdentity(
strategy_profile=strategy_profile,
runtime_target=resolve_runtime_target_from_env(
env=env,
platform_id=platform_id,
strategy_profile=strategy_profile,
dry_run_only=dry_run_only,
deployment_selector=deployment_selector,
account_selector=account_selector,
account_scope=account_scope,
service_name=service_name,
),
deployment_selector=payload.get("deployment_selector"),
account_selector=payload.get("account_selector"),
account_scope=payload.get("account_scope"),
service_name=payload.get("service_name"),
)
29 changes: 11 additions & 18 deletions tests/test_runtime_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,6 @@ def test_resolve_runtime_target_from_env_prefers_structured_json(self) -> None:
'"execution_mode":"paper"}'
)
},
platform_id="longbridge",
strategy_profile="fallback_profile",
dry_run_only=False,
deployment_selector="SG",
account_selector=("SG",),
account_scope="SG",
service_name="fallback-service",
)

self.assertEqual(target.platform_id, "longbridge")
Expand All @@ -78,12 +71,10 @@ def test_resolve_runtime_target_from_env_rejects_mismatched_execution_mode(self)
"RUNTIME_TARGET_JSON": (
'{"platform_id":"schwab","strategy_profile":"tqqq_growth_income",'
'"dry_run_only":false,"execution_mode":"paper"}'
)
},
platform_id="schwab",
strategy_profile="tqqq_growth_income",
dry_run_only=False,
)
)
},
expected_platform_id="schwab",
)

def test_resolve_runtime_target_from_env_rejects_mismatched_platform(self) -> None:
with self.assertRaisesRegex(
Expand All @@ -95,13 +86,15 @@ def test_resolve_runtime_target_from_env_rejects_mismatched_platform(self) -> No
"RUNTIME_TARGET_JSON": (
'{"platform_id":"ibkr","strategy_profile":"global_etf_rotation",'
'"dry_run_only":false}'
)
},
platform_id="longbridge",
strategy_profile="global_etf_rotation",
dry_run_only=False,
)
},
expected_platform_id="longbridge",
)

def test_resolve_runtime_target_from_env_requires_structured_json(self) -> None:
with self.assertRaisesRegex(EnvironmentError, "RUNTIME_TARGET_JSON is required"):
resolve_runtime_target_from_env(env={})

def test_build_runtime_context_fields_merges_runtime_target_without_overwriting_fields(self) -> None:
target = build_runtime_target(
platform_id="longbridge",
Expand Down