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
93 changes: 72 additions & 21 deletions .github/workflows/manual-strategy-switch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ on:
required: false
type: string
service_name:
description: "Cloud Run service name. blank = platform default."
description: "Runtime service name. blank = platform default."
required: false
type: string
plugin_mode:
Expand Down Expand Up @@ -102,7 +102,7 @@ on:
- enabled
- disabled
service_targets_mode:
description: "auto updates an existing IBKR target; allow_create explicitly permits a new target."
description: "auto updates an existing multi-service target when configured; allow_create explicitly permits a new target."
required: true
type: choice
default: auto
Expand All @@ -116,7 +116,7 @@ on:
type: boolean
default: false
trigger_platform_sync:
description: "After apply, dispatch the target platform sync-cloud-run-env workflow."
description: "After apply, dispatch a supported Cloud Run environment sync workflow."
required: true
type: boolean
default: false
Expand All @@ -125,7 +125,7 @@ on:
required: false
type: string
platform_sync_workflow:
description: "Target platform workflow filename."
description: "Cloud Run target platform workflow filename."
required: false
type: string
default: sync-cloud-run-env.yml
Expand Down Expand Up @@ -192,6 +192,28 @@ jobs:
exit 2
fi

settings_activation="$(
python3 python/scripts/runtime_settings.py settings-activation "${PLATFORM}"
)"
if [ "${TRIGGER_PLATFORM_SYNC}" = "true" ]; then
case "${settings_activation}" in
cloud_run_sync_workflow)
;;
next_runtime_workflow_dispatch)
echo "Oracle/VPS runtime activates settings on its next externally scheduled GitHub Actions dispatch; trigger_platform_sync is unsupported for ${PLATFORM}." >&2
exit 2
;;
not_wired)
echo "QMT has no live runtime configuration; trigger_platform_sync is unsupported." >&2
exit 2
;;
*)
echo "Unsupported settings activation mode for ${PLATFORM}: ${settings_activation}" >&2
exit 2
;;
esac
fi

if [ "${APPLY_SWITCH}" = "true" ]; then
if [ -z "${GH_TOKEN:-}" ]; then
echo "RUNTIME_SETTINGS_GH_TOKEN is required for apply=true." >&2
Expand All @@ -208,12 +230,20 @@ jobs:
fi
fi

if [ "${PLATFORM}" = "ibkr" ] \
&& [ "${SERVICE_TARGETS_MODE}" != "off" ] \
&& [ -z "${GH_TOKEN:-}" ]; then
echo "RUNTIME_SETTINGS_GH_TOKEN is required for IBKR service-target preview because the workflow must read and patch CLOUD_RUN_SERVICE_TARGETS_JSON." >&2
exit 2
fi
case "${PLATFORM}" in
longbridge|ibkr|schwab|firstrade)
if [ "${SERVICE_TARGETS_MODE}" != "off" ] && [ -z "${GH_TOKEN:-}" ]; then
echo "RUNTIME_SETTINGS_GH_TOKEN is required for multi-service preview because the workflow must read and patch CLOUD_RUN_SERVICE_TARGETS_JSON." >&2
exit 2
fi
;;
*)
if [ "${SERVICE_TARGETS_MODE}" = "allow_create" ]; then
echo "service_targets_mode=allow_create is not supported for ${PLATFORM}." >&2
exit 2
fi
;;
esac

- name: Resolve platform repository
id: platform
Expand All @@ -223,22 +253,30 @@ jobs:
echo "repository=${repo}" >> "$GITHUB_OUTPUT"

- name: Fetch existing service targets
if: env.SERVICE_TARGETS_MODE != 'off' && env.PLATFORM == 'ibkr'
if: >-
(env.APPLY_SWITCH == 'true' || env.SERVICE_TARGETS_MODE != 'off') &&
contains(fromJSON('["longbridge","ibkr","schwab","firstrade"]'), env.PLATFORM)
env:
TARGET_REPOSITORY: ${{ steps.platform.outputs.repository }}
run: |
set -euo pipefail
output_file="${RUNNER_TEMP}/existing-service-targets.json"
target_environment=""
if [ "${VARIABLE_SCOPE}" = "environment" ]; then
if [ "${PLATFORM}" = "ibkr" ] && [ "${VARIABLE_SCOPE}" = "environment" ]; then
target_environment="${GITHUB_ENVIRONMENT_NAME:-${TARGET_NAME}}"
fi
python - <<'PY' "${TARGET_REPOSITORY}" "${output_file}" "${target_environment}"
python - <<'PY' \
"${TARGET_REPOSITORY}" \
"${output_file}" \
"${target_environment}" \
"${PLATFORM}" \
"${SERVICE_TARGETS_MODE}"
import json
import os
import subprocess
import sys

repo, output_path, environment = sys.argv[1:4]
repo, output_path, environment, platform, mode = sys.argv[1:6]
command = ["gh", "variable", "list", "--repo", repo, "--json", "name,value"]
if environment:
command.extend(["--env", environment])
Expand All @@ -253,8 +291,14 @@ jobs:
value = str(item.get("value") or "").strip()
break
if not value:
with open(output_path, "w", encoding="utf-8") as handle:
handle.write("{}")
if (platform == "ibkr" and mode != "off") or mode == "allow_create":
with open(output_path, "w", encoding="utf-8") as handle:
handle.write("{}")
else:
try:
os.unlink(output_path)
except FileNotFoundError:
pass
raise SystemExit(0)
try:
payload = json.loads(value)
Expand All @@ -263,7 +307,18 @@ jobs:
with open(output_path, "w", encoding="utf-8") as handle:
json.dump(payload, handle, ensure_ascii=False, separators=(",", ":"))
PY
echo "EXISTING_SERVICE_TARGETS_JSON_FILE=${output_file}" >> "$GITHUB_ENV"
if [ -f "${output_file}" ]; then
echo "EXISTING_SERVICE_TARGETS_JSON_FILE=${output_file}" >> "$GITHUB_ENV"
fi

- name: Reject unsafe service inventory bypass
if: env.APPLY_SWITCH == 'true' && env.SERVICE_TARGETS_MODE == 'off'
run: |
set -euo pipefail
if [ -f "${EXISTING_SERVICE_TARGETS_JSON_FILE:-}" ]; then
echo "existing CLOUD_RUN_SERVICE_TARGETS_JSON requires service_targets_mode=patch or allow_create" >&2
exit 2
fi

- name: Build switch target
run: |
Expand Down Expand Up @@ -370,10 +425,6 @@ jobs:
schwab|firstrade)
gh workflow run "${workflow}" --repo "${TARGET_REPOSITORY}" --ref main
;;
qmt|binance)
echo "${PLATFORM} platform Cloud Run sync workflow is not configured yet; skipped."
exit 0
;;
*)
echo "No platform sync dispatch rule for ${PLATFORM}" >&2
exit 2
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ python3 -m unittest discover -s python/tests -v

## Manual Strategy Switch

`.github/workflows/manual-strategy-switch.yml` provides a central manual switch entrypoint. It builds a transient runtime target from workflow inputs, validates it with `python/scripts/runtime_settings.py`, and writes GitHub variables into the target platform repository. It currently supports `longbridge`, `ibkr`, `schwab`, and `firstrade`.
`.github/workflows/manual-strategy-switch.yml` provides a central manual switch entrypoint. It builds a transient runtime target from workflow inputs, validates it with `python/scripts/runtime_settings.py`, and writes GitHub variables into the target platform repository. It supports `longbridge`, `ibkr`, `schwab`, `firstrade`, `qmt`, and `binance`.

Recommended flow:

1. Run once with `apply=false` to preview the assignments.
2. Check `repository`, `environment`, `strategy_profile`, `service_name`, `execution_mode`, and plugin mounts.
3. Re-run with `apply=true` and `confirm_apply=APPLY` to write variables.
4. Set `trigger_platform_sync=true` and `confirm_apply=APPLY_AND_SYNC` when the target platform should dispatch its Cloud Run env sync workflow.
4. For a Cloud Run platform, set `trigger_platform_sync=true` and `confirm_apply=APPLY_AND_SYNC` to dispatch its environment sync workflow.

Example:

Expand All @@ -82,12 +82,16 @@ confirm_apply=APPLY_AND_SYNC
Notes:

- This is a GitHub Actions `workflow_dispatch` form, not a public web app. The default `apply=false` mode only previews assignments and writes nothing remotely.
- LongBridge defaults to environment-scoped variables; `target_name=sg` resolves to `longbridge-sg`.
- LongBridge defaults to environment-scoped variables; `target_name=sg` resolves to `longbridge-sg`. When a repository-level multi-service target list exists, the same switch also updates its exact service entry.
- Multi-service targets use `service_name` as their primary identity. Multiple services may share one `account_scope`; a switch never selects a sibling service by account scope.
- `CLOUD_RUN_SERVICE_TARGETS_JSON` supports both a bare array and an object with `targets`. New services require the explicit `allow_create` mode.
- Schwab defaults to repository-scoped variables.
- Firstrade defaults to repository-scoped variables; `target_name=live` uses `firstrade-quant-service` and `account_scope=US`.
- IBKR `service_targets_mode=auto` only patches an existing service/account-scope entry inside `CLOUD_RUN_SERVICE_TARGETS_JSON`, so other services are preserved and an unknown target fails closed. Use `allow_create` only when intentionally provisioning a new target.
- IBKR `service_targets_mode=auto` patches the exact existing service entry inside `CLOUD_RUN_SERVICE_TARGETS_JSON`; account scope is only a fallback for a legacy entry with no service identity. Other services are preserved and an unknown target fails closed. Use `allow_create` only when intentionally provisioning a new target.
- Cross-repository variable writes and workflow dispatches require a `RUNTIME_SETTINGS_GH_TOKEN` secret in this repository with sufficient target-repository variable/workflow permissions. The workflow does not fall back to the default `github.token` for remote writes.
- IBKR `service_targets_mode=auto` must read and patch the target repository's `CLOUD_RUN_SERVICE_TARGETS_JSON`, so even preview mode requires `RUNTIME_SETTINGS_GH_TOKEN` for IBKR.
- 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`.
- 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.
- 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.
- 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.
- 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).

Expand Down
13 changes: 8 additions & 5 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ python3 -m unittest discover -s python/tests -v

## 一键切换策略

`.github/workflows/manual-strategy-switch.yml` 提供手动触发的中控切换入口。它会根据表单参数生成运行目标,复用 `python/scripts/runtime_settings.py` 校验并写入目标平台仓库的 GitHub variables。当前支持 `longbridge`、`ibkr`、`schwab`、`firstrade`。
`.github/workflows/manual-strategy-switch.yml` 提供手动触发的中控切换入口。它会根据表单参数生成运行目标,复用 `python/scripts/runtime_settings.py` 校验并写入目标平台仓库的 GitHub variables。当前支持 `longbridge`、`ibkr`、`schwab`、`firstrade`、`qmt`、`binance`

推荐流程:

1. 第一次运行保持 `apply=false`,只看 preview。
2. 确认 `repository`、`environment`、`strategy_profile`、`service_name`、`execution_mode`,以及由 `strategy_profile` 派生的 `scheduler` / 插件挂载正确。
3. 再运行 `apply=true`,并填写 `confirm_apply=APPLY`,写入目标仓库变量。
4. 如果要让平台仓同步 Cloud Run 环境,额外设置 `trigger_platform_sync=true`,并填写 `confirm_apply=APPLY_AND_SYNC`。
4. Cloud Run 平台,如需同步运行环境,额外设置 `trigger_platform_sync=true`,并填写 `confirm_apply=APPLY_AND_SYNC`。

常用例子:

Expand All @@ -69,12 +69,15 @@ confirm_apply=APPLY_AND_SYNC
注意:

- 这是 GitHub Actions 的 `workflow_dispatch` 手动表单,不是公开网页。默认 `apply=false` 只生成预览,不写任何远端变量。
- LongBridge 默认写入 environment variables,例如 `target_name=sg` 会落到 `longbridge-sg`。
- LongBridge 默认写入 environment variables,例如 `target_name=sg` 会落到 `longbridge-sg`;如果仓库已有多服务目标清单,同一次切换也会更新其 repository-level 精确服务条目
- Schwab 默认写入 repository variables。
- Firstrade 默认写入 repository variables,`target_name=live` 会使用 `firstrade-quant-service` 和 `account_scope=US`。
- IBKR 如果目标仓库已有 `CLOUD_RUN_SERVICE_TARGETS_JSON`,workflow 会 patch 指定 service/account_scope 的 target entry,避免覆盖其他 IBKR 服务。
- 多服务目标以 `service_name` 为唯一主标识;同一 `account_scope` 可以有多个策略服务,切换只更新精确服务,不会覆盖兄弟目标。
- `CLOUD_RUN_SERVICE_TARGETS_JSON` 同时支持数组和 `{targets:[...]}`;新增服务必须显式选择 `service_targets_mode=allow_create`。
- 跨仓写 variables 和触发 workflow 必须在本仓配置 `RUNTIME_SETTINGS_GH_TOKEN` secret,token 至少需要目标仓库的 variables/workflow 写权限;不会回退到默认 `github.token` 写远端变量。
- IBKR 的 `service_targets_mode=auto` 需要读取并 patch 目标仓库的 `CLOUD_RUN_SERVICE_TARGETS_JSON`,因此即使只做 preview 也需要 `RUNTIME_SETTINGS_GH_TOKEN`。
- LongBridge、IBKR、Schwab、Firstrade 的 `service_targets_mode=auto` 会检查目标仓库是否已有多服务清单,因此即使只做 preview 也需要 `RUNTIME_SETTINGS_GH_TOKEN`。
- Binance 运行在 Oracle Cloud VPS 的 self-hosted runner。仓库变量会在外部调度器下一次触发 `main.yml` 时被读取;中控不会自动触发该运行 workflow,因为它可能直接执行实盘。切换到不同运行频率的策略时,还必须单独复核 VPS 外部调度器。
- QMT 当前仅支持 dry-run,尚无实盘部署配置;可以生成目标并暂存仓库变量,但会拒绝 `trigger_platform_sync=true`。
- workflow 绑定 GitHub Environment `runtime-strategy-switch`。个人系统默认不需要 required reviewers;建议把 `RUNTIME_SETTINGS_GH_TOKEN` 配成这个 Environment 的 secret,真实写入靠 preview、确认词和 token 最小权限控制。
- 启用真实切换前请按 [手动策略切换权限控制方案](docs/manual_strategy_switch_permission_control.zh-CN.md) 完成最简 secret、token 权限和回滚准备。

Expand Down
20 changes: 18 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,23 @@

QuantRuntimeSettings is a **config-driven** runtime settings package that serves as the central control plane for QuantStrategyLab deployments. It defines versioned strategy-to-platform assignments and hosts a Cloudflare Workers-based strategy switch console.

The generated `RUNTIME_TARGET_JSON` payload is the canonical desired-state contract for a switch. `scheduler` and plugin mount outputs are derived from `strategy_profile`, while `execution_mode` is part of the target and validated against strategy-profile policy.
The generated `RUNTIME_TARGET_JSON` payload is the canonical desired-state contract for one deployment target. `scheduler`, `market`, `market_calendar`, `market_timezone`, and plugin mount outputs are derived from `strategy_profile`, while `execution_mode` is validated against strategy-profile policy.

### Multi-strategy identity and storage

- A platform may run multiple strategies through multiple deployment targets, but one `service_name` has one active `strategy_profile`.
- `service_name` is the primary target identity. `account_scope` may be shared by multiple services and is only a legacy fallback when an existing entry has no service identity.
- `CLOUD_RUN_SERVICE_TARGETS_JSON` accepts either a bare target array or an object with `targets`.
- Repository-scoped multi-service platforms update only the exact service entry. Creating a new service requires the explicit `allow_create` path.
- LongBridge keeps its selected `RUNTIME_TARGET_JSON` and account variables in the GitHub Environment, while the repository-scoped service-target aggregate is updated in the same switch operation for deployment reconciliation.
- Legacy targets may omit market metadata. Newly generated targets always carry the complete market metadata trio; partial metadata is rejected.

### Deployment topology

- LongBridge, IBKR, Schwab, and Firstrade use Cloud Run and may activate settings through their environment-sync workflows.
- Binance uses an Oracle Cloud VPS self-hosted GitHub Actions runner. Settings become active on the next externally scheduled runtime workflow dispatch; the settings control plane never dispatches this live-capable workflow as a sync substitute and does not reconfigure the external VPS scheduler.
- QMT has no live deployment configuration and remains dry-run only.
- `platform-config.json` records `runtime_model`, `settings_activation`, and `live_configured` for every platform. Unsupported activation combinations fail validation.

The repository has a **three-tier architecture** built around a single source of truth:

Expand Down Expand Up @@ -85,7 +101,7 @@ platform-config.json (single source of truth)

Defines the entire runtime configuration universe:

- **4 domains**: `us_equity`, `hk_equity`, `cn_equity`, `crypto`
- **4 domains**: `us_equity`, `hk_equity`, `cn_equity`, `crypto`, including market calendar and timezone metadata
- **6 platforms**: `longbridge`, `ibkr`, `schwab`, `firstrade`, `qmt`, `binance`
- **18 strategy profiles** with features: income layer, option overlay, DCA, combo
- **Platform capabilities, CSS theming, default accounts, repositories, variable scopes**
Expand Down
Loading