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
25 changes: 25 additions & 0 deletions .github/workflows/manual-strategy-switch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,31 @@ jobs:
print(f"- `{assignment['name']}` = `{assignment['value']}`")
PY

- name: Preflight IBKR deployment plan
if: env.PLATFORM == 'ibkr' && env.SERVICE_TARGETS_MODE != 'off'
Comment on lines +410 to +411

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preflight IBKR switches in service-targets-off mode

When service_targets_mode=off is used against an IBKR repository that has no existing CLOUD_RUN_SERVICE_TARGETS_JSON, the earlier bypass guard permits the operation, but this condition skips the deployment planner entirely and apply=true proceeds directly to GitHub variable writes. This leaves the supported single-service/no-inventory path outside the new fail-closed contract and can publish live settings that the IBKR planner would reject; either synthesize a candidate inventory for this path or reject IBKR writes when no planner can run.

Useful? React with 👍 / 👎.

env:
TARGET_REPOSITORY: ${{ steps.platform.outputs.repository }}
run: |
set -euo pipefail
platform_root="${RUNNER_TEMP}/interactive-brokers-platform"
repository_variables_file="${RUNNER_TEMP}/ibkr-repository-variables.json"
trap 'rm -f "${repository_variables_file}"' EXIT

gh auth setup-git
git clone --quiet --depth 1 --branch main \
"https://github.com/${TARGET_REPOSITORY}.git" \
"${platform_root}"
python3 -m pip install --quiet uv
uv sync --frozen --no-dev --quiet --project "${platform_root}"
gh variable list \
--repo "${TARGET_REPOSITORY}" \
--json name,value \
> "${repository_variables_file}"
Comment on lines +426 to +429

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Merge environment-scoped variables into the IBKR preflight

When an IBKR switch uses the supported variable_scope=environment path, the earlier inventory step reads CLOUD_RUN_SERVICE_TARGETS_JSON from that GitHub Environment, but this step fetches only repository variables. _candidate_environment therefore runs the planner without any unchanged environment-scoped settings, so it can reject a valid deployment or approve a state different from the one the sync workflow will receive. Fetch the selected environment's variables as well and merge them over repository variables before applying the candidate assignments.

Useful? React with 👍 / 👎.

python3 python/scripts/preflight_ibkr_switch.py \
--target-file "${TARGET_FILE}" \
--platform-root "${platform_root}" \
--repository-variables-file "${repository_variables_file}"

- name: Apply GitHub variable updates
if: env.APPLY_SWITCH == 'true'
run: python3 python/scripts/runtime_settings.py apply "${TARGET_FILE}" --yes
Expand Down
38 changes: 26 additions & 12 deletions internal_dependency_matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,28 +62,42 @@
"path": "pyproject.toml",
"package": "cn-equity-strategies",
"source_repo": "CnEquityStrategies",
"ref": "73844e92a8570a61e5a9dc6c245809d0b27b89bc"
"ref": "00fa762466617d0961bb8b03821f56b1b9c2b866"
},
{
"consumer_repo": "CnEquitySnapshotPipelines",
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "8ba8276948ff71a8cc0a810f98b7437a1311c671"
},
{
"consumer_repo": "CnEquitySnapshotPipelines",
"path": "uv.lock",
"package": "cn-equity-strategies",
"source_repo": "CnEquityStrategies",
"ref": "73844e92a8570a61e5a9dc6c245809d0b27b89bc"
"ref": "00fa762466617d0961bb8b03821f56b1b9c2b866"
},
{
"consumer_repo": "CnEquitySnapshotPipelines",
"path": "uv.lock",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "8ba8276948ff71a8cc0a810f98b7437a1311c671"
},
{
"consumer_repo": "CnEquityStrategies",
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "92458590a463e7219f0369a3505031ee74414135"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "CnEquityStrategies",
"path": "uv.lock",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "92458590a463e7219f0369a3505031ee74414135"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "CryptoLivePoolPipelines",
Expand Down Expand Up @@ -118,14 +132,14 @@
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "92458590a463e7219f0369a3505031ee74414135"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "CryptoStrategies",
"path": "uv.lock",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "92458590a463e7219f0369a3505031ee74414135"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "FirstradePlatform",
Expand Down Expand Up @@ -174,14 +188,14 @@
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "92458590a463e7219f0369a3505031ee74414135"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "HkEquityStrategies",
"path": "uv.lock",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "92458590a463e7219f0369a3505031ee74414135"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "InteractiveBrokersPlatform",
Expand All @@ -195,7 +209,7 @@
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "92458590a463e7219f0369a3505031ee74414135"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "InteractiveBrokersPlatform",
Expand All @@ -216,7 +230,7 @@
"path": "uv.lock",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "92458590a463e7219f0369a3505031ee74414135"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "InteractiveBrokersPlatform",
Expand Down Expand Up @@ -342,14 +356,14 @@
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "8ba8276948ff71a8cc0a810f98b7437a1311c671"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
},
{
"consumer_repo": "UsEquityStrategies",
"path": "uv.lock",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "8ba8276948ff71a8cc0a810f98b7437a1311c671"
"ref": "776fe71e57e2924fcd1c73126f41d244242240bb"
}
]
}
48 changes: 33 additions & 15 deletions platform-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,27 @@
"profiles": {
"us_daily": {
"timezone": "America/New_York",
"main_time": "45 15 * * *",
"probe_time": "35 9,15 * * *",
"precheck_time": "45 9 * * *"
"main_time": "45 15 * * 1-5",
"probe_time": "35 9,15 * * 1-5",
"precheck_time": "45 9 * * 1-5"
},
"us_dca_month_end": {
"timezone": "America/New_York",
"main_time": "45 15 25-29 * *",
"probe_time": "35 9,15 25-29 * *",
"precheck_time": "45 9 25-29 * *"
"main_time": "45 15 * * 1-5",
"probe_time": "35 9,15 * * 1-5",
"precheck_time": "45 9 * * 1-5"
},
"us_snapshot_month_start": {
"timezone": "America/New_York",
"main_time": "45 15 1-7 * *",
"probe_time": "35 9,15 1-7 * *",
"precheck_time": "45 9 1-7 * *"
"main_time": "45 15 * * 1-5",
"probe_time": "35 9,15 * * 1-5",
"precheck_time": "45 9 * * 1-5"
},
"hk_daily": {
"timezone": "Asia/Hong_Kong",
"main_time": "45 15 * * *",
"probe_time": "35 9,15 * * *",
"precheck_time": "45 9 * * *"
"main_time": "45 15 * * 1-5",
"probe_time": "35 9,15 * * 1-5",
"precheck_time": "45 9 * * 1-5"
},
"hk_snapshot_month_start": {
"timezone": "Asia/Hong_Kong",
Expand Down Expand Up @@ -501,6 +501,13 @@
"dry_run"
],
"blocked_live_reason": "",
"runtime_artifacts": {
"feature_snapshot": {
"required": true,
"path": "gs://qsl-runtime-logs-shared/strategy-artifacts/us_equity/global_etf_rotation/global_etf_rotation_feature_snapshot_latest.csv",
"manifest_path": "gs://qsl-runtime-logs-shared/strategy-artifacts/us_equity/global_etf_rotation/global_etf_rotation_feature_snapshot_latest.csv.manifest.json"
}
},
"features": {
"income_layer": true,
"option_overlay": true,
Expand Down Expand Up @@ -541,6 +548,13 @@
"dry_run"
],
"blocked_live_reason": "",
"runtime_artifacts": {
"feature_snapshot": {
"required": true,
"path": "gs://qsl-runtime-logs-shared/strategy-artifacts/us_equity/russell_top50_leader_rotation_staging/russell_top50_leader_rotation_feature_snapshot_latest.csv",
"manifest_path": "gs://qsl-runtime-logs-shared/strategy-artifacts/us_equity/russell_top50_leader_rotation_staging/russell_top50_leader_rotation_feature_snapshot_latest.csv.manifest.json"
}
},
"features": {
"income_layer": true,
"option_overlay": true,
Expand Down Expand Up @@ -691,13 +705,17 @@
"scheduler_profile": "hk_snapshot_month_start",
"runtime_enabled": true,
"lifecycle_stage": "runtime_enabled",
"can_switch_live": true,
"can_switch_live": false,
"allowed_execution_modes": [
"live",
"paper",
"dry_run"
Comment on lines 709 to 711

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the paper snapshot profile switchable from the console

The catalog exposes hk_low_vol_dividend_quality_snapshot as paper-capable, so the console permits that selection, but its generated switch inputs have no snapshot-path controls and therefore omit both artifact variables. Because the same profile is marked feature_snapshot.required=true without catalog paths, build_runtime_switch.py rejects every such console dispatch with requires feature snapshot path and manifest path. Supply a configured paper artifact, add console inputs for the pair, or stop advertising paper mode until the route exists.

Useful? React with 👍 / 👎.

],
"blocked_live_reason": "",
"blocked_live_reason": "production_snapshot_artifact_route_not_configured",
"runtime_artifacts": {
"feature_snapshot": {
"required": true
}
},
"features": {
"income_layer": false,
"option_overlay": false,
Expand Down
58 changes: 58 additions & 0 deletions python/scripts/build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
}
SCHEDULER_FIELDS = {"timezone", "main_time", "probe_time", "precheck_time"}
MARKET_FIELDS = {"market", "market_calendar", "market_timezone"}
FEATURE_SNAPSHOT_FIELDS = {"required", "path", "manifest_path"}
RUNTIME_MODELS = {"cloud_run", "oracle_vps_self_hosted", "not_configured"}
SETTINGS_ACTIVATION_MODES = {
"cloud_run_sync_workflow",
Expand Down Expand Up @@ -79,6 +80,13 @@ def validate(config: dict) -> list[str]:
errors.append(
f"scheduler profile {profile}: {field} must have 2 time fields or 5 cron fields"
)
continue
if profile.startswith("us_"):
cron = value.split()
if len(cron) != 5 or cron[2] != "*" or cron[4] != "1-5":
errors.append(
f"scheduler profile {profile}: {field} must be Mon-Fri cron with day-of-month '*'"
)
for pid, pdata in config.get("platforms", {}).items():
if "capabilities" not in pdata:
errors.append(f"platform {pid}: missing capabilities")
Expand Down Expand Up @@ -220,6 +228,56 @@ def validate(config: dict) -> list[str]:
f"{scheduler_timezone!r} must match market_timezone "
f"{market_timezone!r}"
)
runtime_artifacts = sdata.get("runtime_artifacts")
if runtime_artifacts is None:
continue
if not isinstance(runtime_artifacts, dict):
errors.append(f"strategy {sid}: runtime_artifacts must be an object")
continue
unsupported_artifacts = sorted(set(runtime_artifacts) - {"feature_snapshot"})
if unsupported_artifacts:
errors.append(
f"strategy {sid}: unsupported runtime_artifacts {unsupported_artifacts}"
)
feature_snapshot = runtime_artifacts.get("feature_snapshot")
if feature_snapshot is None:
continue
if not isinstance(feature_snapshot, dict):
errors.append(
f"strategy {sid}: runtime_artifacts.feature_snapshot must be an object"
)
continue
unsupported_fields = sorted(set(feature_snapshot) - FEATURE_SNAPSHOT_FIELDS)
if unsupported_fields:
errors.append(
f"strategy {sid}: unsupported feature snapshot fields {unsupported_fields}"
)
required = feature_snapshot.get("required")
if not isinstance(required, bool):
errors.append(
f"strategy {sid}: runtime_artifacts.feature_snapshot.required must be boolean"
)
snapshot_path = feature_snapshot.get("path")
manifest_path = feature_snapshot.get("manifest_path")
for field, value in (("path", snapshot_path), ("manifest_path", manifest_path)):
if value is not None and (
not isinstance(value, str) or not value.startswith("gs://")
):
errors.append(
f"strategy {sid}: runtime_artifacts.feature_snapshot.{field} must be a gs:// URI"
)
has_snapshot_path = isinstance(snapshot_path, str) and bool(snapshot_path.strip())
has_manifest_path = isinstance(manifest_path, str) and bool(manifest_path.strip())
if has_snapshot_path != has_manifest_path:
errors.append(
f"strategy {sid}: feature snapshot path and manifest_path must be configured together"
)
if required is True and sdata.get("can_switch_live") is True and not (
has_snapshot_path and has_manifest_path
):
errors.append(
f"strategy {sid}: live feature snapshot requires path and manifest_path"
)
return errors


Expand Down
Loading