From 9774324326ac43a63c902ffe3d9ba41e3a792772 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 10 May 2026 15:07:59 +0800 Subject: [PATCH 1/3] Require LongBridge runtime target JSON --- .github/workflows/sync-cloud-run-env.yml | 74 ++++------- README.md | 131 ++++++++++-------- requirements.txt | 2 +- runtime_config_support.py | 23 +--- scripts/print_strategy_switch_env_plan.py | 8 +- tests/test_notifications.py | 2 + tests/test_runtime_config_support.py | 153 +++++++++++++--------- tests/test_sync_cloud_run_env_workflow.sh | 15 +-- 8 files changed, 216 insertions(+), 192 deletions(-) diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index ccc9ce4..c45f454 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -10,28 +10,29 @@ env: GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: longbridge-platform-deploy@longbridgequant.iam.gserviceaccount.com jobs: - sync-hk: - name: Sync HK Cloud Run Env + sync-paper: + name: Sync PAPER Cloud Run Env runs-on: ubuntu-latest permissions: contents: read id-token: write - environment: longbridge-hk + environment: longbridge-paper env: ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }} - # Set CLOUD_RUN_REGION per Environment so HK/SG can target different regions. + # Set CLOUD_RUN_REGION per Environment so paper/SG can target different regions. CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }} CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }} ACCOUNT_PREFIX: ${{ vars.ACCOUNT_PREFIX }} TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }} LONGPORT_APP_KEY_SECRET_NAME: ${{ vars.LONGPORT_APP_KEY_SECRET_NAME }} LONGPORT_APP_SECRET_SECRET_NAME: ${{ vars.LONGPORT_APP_SECRET_SECRET_NAME }} - STRATEGY_PROFILE: ${{ vars.STRATEGY_PROFILE || 'soxl_soxx_trend_income' }} - ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || 'HK' }} + RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }} + ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || 'PAPER' }} LONGPORT_SECRET_NAME: ${{ vars.LONGPORT_SECRET_NAME }} LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }} LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }} LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }} + # Optional strategy overrides; leave unset to inherit the UsEquityStrategies profile defaults. INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }} QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }} NOTIFY_LANG: ${{ vars.NOTIFY_LANG }} @@ -48,7 +49,7 @@ jobs: if [ "${ENABLE_GITHUB_ENV_SYNC:-}" != "true" ]; then echo "enabled=false" >> "$GITHUB_OUTPUT" - echo "Skipping HK Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." >&2 + echo "Skipping PAPER Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." >&2 exit 0 fi @@ -83,9 +84,13 @@ jobs: import sys from us_equity_strategies import resolve_canonical_profile - profile = os.environ.get("STRATEGY_PROFILE", "").strip().lower() + raw_runtime_target = os.environ.get("RUNTIME_TARGET_JSON", "").strip() + if not raw_runtime_target: + raise SystemExit("RUNTIME_TARGET_JSON is required") + runtime_target = json.loads(raw_runtime_target) + profile = str(runtime_target.get("strategy_profile") or "").strip().lower() if not profile: - raise SystemExit("STRATEGY_PROFILE is required") + raise SystemExit("RUNTIME_TARGET_JSON.strategy_profile is required") canonical_profile = resolve_canonical_profile(profile) raw_status = subprocess.check_output( @@ -114,22 +119,10 @@ jobs: output.write( f"config_source_policy={str(selected.get('config_source_policy') or 'none')}\n" ) - normalized_region = os.environ.get("ACCOUNT_REGION", "").strip().upper() - dry_run_only = os.environ.get("LONGBRIDGE_DRY_RUN_ONLY", "").strip().lower() == "true" - runtime_target = { - "platform_id": "longbridge", - "strategy_profile": canonical_profile, - "dry_run_only": dry_run_only, - "deployment_selector": normalized_region or None, - "account_selector": [normalized_region] if normalized_region else [], - "account_scope": normalized_region or None, - "service_name": os.environ.get("CLOUD_RUN_SERVICE", "").strip() or None, - "execution_mode": "paper" if dry_run_only else "live", - } output.write(f"runtime_target_json={json.dumps(runtime_target, sort_keys=True)}\n") PY - - name: Validate HK env sync inputs + - name: Validate PAPER env sync inputs if: steps.config.outputs.enabled == 'true' env: REQUIRES_SNAPSHOT_ARTIFACTS: ${{ steps.strategy_requirements.outputs.requires_snapshot_artifacts }} @@ -160,10 +153,6 @@ jobs: missing_vars+=("TELEGRAM_TOKEN_SECRET_NAME or TELEGRAM_TOKEN") fi - if [ -z "${RUNTIME_TARGET_JSON:-}" ]; then - missing_vars+=("RUNTIME_TARGET_JSON") - fi - if [ -z "${LONGPORT_APP_KEY_SECRET_NAME:-}" ]; then missing_vars+=("LONGPORT_APP_KEY_SECRET_NAME") fi @@ -187,9 +176,9 @@ jobs: fi if [ "${#missing_vars[@]}" -gt 0 ]; then - echo "HK Cloud Run env sync is enabled, but these values are missing:" >&2 - echo " - If HK and SG run in different regions, set CLOUD_RUN_REGION on the longbridge-hk Environment." >&2 - echo " - Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-hk Environment so HK does not fall back to shared repository defaults." >&2 + echo "PAPER Cloud Run env sync is enabled, but these values are missing:" >&2 + echo " - If paper and SG run in different regions, set CLOUD_RUN_REGION on the longbridge-paper Environment." >&2 + echo " - Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-paper Environment so paper does not fall back to shared repository defaults." >&2 printf ' - %s\n' "${missing_vars[@]}" >&2 exit 1 fi @@ -260,7 +249,6 @@ jobs: "NOTIFY_LANG=${NOTIFY_LANG}" "LONGPORT_SECRET_NAME=${LONGPORT_SECRET_NAME}" "ACCOUNT_PREFIX=${ACCOUNT_PREFIX}" - "STRATEGY_PROFILE=${STRATEGY_PROFILE}" "ACCOUNT_REGION=${ACCOUNT_REGION}" "RUNTIME_TARGET_JSON=${RUNTIME_TARGET_JSON}" ) @@ -361,14 +349,14 @@ jobs: environment: longbridge-sg env: ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }} - # Set CLOUD_RUN_REGION per Environment so HK/SG can target different regions. + # Set CLOUD_RUN_REGION per Environment so paper/SG can target different regions. CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }} CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }} ACCOUNT_PREFIX: ${{ vars.ACCOUNT_PREFIX }} TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }} LONGPORT_APP_KEY_SECRET_NAME: ${{ vars.LONGPORT_APP_KEY_SECRET_NAME }} LONGPORT_APP_SECRET_SECRET_NAME: ${{ vars.LONGPORT_APP_SECRET_SECRET_NAME }} - STRATEGY_PROFILE: ${{ vars.STRATEGY_PROFILE || 'soxl_soxx_trend_income' }} + RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }} ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || 'SG' }} LONGPORT_SECRET_NAME: ${{ vars.LONGPORT_SECRET_NAME }} LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }} @@ -425,9 +413,13 @@ jobs: import sys from us_equity_strategies import resolve_canonical_profile - profile = os.environ.get("STRATEGY_PROFILE", "").strip().lower() + raw_runtime_target = os.environ.get("RUNTIME_TARGET_JSON", "").strip() + if not raw_runtime_target: + raise SystemExit("RUNTIME_TARGET_JSON is required") + runtime_target = json.loads(raw_runtime_target) + profile = str(runtime_target.get("strategy_profile") or "").strip().lower() if not profile: - raise SystemExit("STRATEGY_PROFILE is required") + raise SystemExit("RUNTIME_TARGET_JSON.strategy_profile is required") canonical_profile = resolve_canonical_profile(profile) raw_status = subprocess.check_output( @@ -457,17 +449,6 @@ jobs: f"config_source_policy={str(selected.get('config_source_policy') or 'none')}\n" ) normalized_region = os.environ.get("ACCOUNT_REGION", "").strip().upper() - dry_run_only = os.environ.get("LONGBRIDGE_DRY_RUN_ONLY", "").strip().lower() == "true" - runtime_target = { - "platform_id": "longbridge", - "strategy_profile": canonical_profile, - "dry_run_only": dry_run_only, - "deployment_selector": normalized_region or None, - "account_selector": [normalized_region] if normalized_region else [], - "account_scope": normalized_region or None, - "service_name": os.environ.get("CLOUD_RUN_SERVICE", "").strip() or None, - "execution_mode": "paper" if dry_run_only else "live", - } output.write(f"runtime_target_json={json.dumps(runtime_target, sort_keys=True)}\n") PY @@ -530,7 +511,7 @@ jobs: if [ "${#missing_vars[@]}" -gt 0 ]; then echo "SG Cloud Run env sync is enabled, but these values are missing:" >&2 - echo " - If HK and SG run in different regions, set CLOUD_RUN_REGION on the longbridge-sg Environment." >&2 + echo " - If paper and SG run in different regions, set CLOUD_RUN_REGION on the longbridge-sg Environment." >&2 echo " - Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-sg Environment so SG does not fall back to shared repository defaults." >&2 printf ' - %s\n' "${missing_vars[@]}" >&2 exit 1 @@ -602,7 +583,6 @@ jobs: "NOTIFY_LANG=${NOTIFY_LANG}" "LONGPORT_SECRET_NAME=${LONGPORT_SECRET_NAME}" "ACCOUNT_PREFIX=${ACCOUNT_PREFIX}" - "STRATEGY_PROFILE=${STRATEGY_PROFILE}" "ACCOUNT_REGION=${ACCOUNT_REGION}" "RUNTIME_TARGET_JSON=${RUNTIME_TARGET_JSON}" ) diff --git a/README.md b/README.md index 34ce0c5..f06f1d7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Quant system on LongPort OpenAPI and Google Cloud Run. This repository uses `QuantPlatformKit` for LongPort token handling, context bootstrap, account snapshot access, market data, and order submission. Cloud Run deploys this repository directly. -The runtime now carries a structured `RuntimeTarget` / `RUNTIME_TARGET_JSON` alongside the compatibility `STRATEGY_PROFILE` selector. +The runtime now carries a structured `RuntimeTarget` / `RUNTIME_TARGET_JSON` alongside the compatibility `STRATEGY_PROFILE` selector. Strategy-owned defaults come from `UsEquityStrategies`; platform variables are only explicit overrides. The LongBridge runtime can execute the seven current `runtime_enabled` `us_equity` profiles from `UsEquityStrategies`; `LongBridgePlatform` keeps the LongPort runtime, token refresh, execution, and notification flow. `STRATEGY_PROFILE` remains the compatibility selector for strategy routing, while `RuntimeTarget` describes the running service identity. @@ -39,7 +39,7 @@ Platform execution no longer depends on `strategy/allocation.py` or hard-coded s | `mega_cap_leader_rotation_top50_balanced` | Mega Cap Leader Rotation Top50 Balanced | Yes | Yes | `us_equity` | selectable balanced Top50 monthly leader rotation | | `soxl_soxx_trend_income` | SOXL/SOXX Semiconductor Trend Income | Yes | Yes | `us_equity` | current SG deployment | | `tqqq_growth_income` | TQQQ Growth Income | Yes | Yes | `us_equity` | selectable growth line | -| `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | Yes | Yes | `us_equity` | current HK feature-snapshot line | +| `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | Yes | Yes | `us_equity` | current PAPER deployment | Check the current matrix locally: @@ -61,47 +61,50 @@ Telegram notifications include structured execution and heartbeat messages, with |----------|----------|-------------| | `TELEGRAM_TOKEN` | Yes | Bot token for alerts; recommended to inject from Secret Manager secret `longbridge-telegram-token` | | `GLOBAL_TELEGRAM_CHAT_ID` | Yes | Telegram chat or user ID used by this service. | -| `LONGPORT_APP_KEY` | Yes | LongPort OpenAPI app key (for token refresh); recommended to inject from the region-specific Secret Manager secret for this deployment, such as `longport-app-key-hk` / `longport-app-key-sg` | -| `LONGPORT_APP_SECRET` | Yes | LongPort OpenAPI app secret (for token refresh); recommended to inject from the region-specific Secret Manager secret for this deployment, such as `longport-app-secret-hk` / `longport-app-secret-sg` | -| `LONGPORT_SECRET_NAME` | No | Secret Manager secret name for LongPort token (default: `longport_token_hk`) | +| `LONGPORT_APP_KEY` | Yes | LongPort OpenAPI app key (for token refresh); recommended to inject from the region-specific Secret Manager secret for this deployment, such as `longport-app-key-paper` / `longport-app-key-hk` / `longport-app-key-sg` | +| `LONGPORT_APP_SECRET` | Yes | LongPort OpenAPI app secret (for token refresh); recommended to inject from the region-specific Secret Manager secret for this deployment, such as `longport-app-secret-paper` / `longport-app-secret-hk` / `longport-app-secret-sg` | +| `LONGPORT_SECRET_NAME` | No | Secret Manager secret name for LongPort token (default: `longport_token_paper`) | | `ACCOUNT_PREFIX` | No | Alert/log prefix for account/environment (default: `DEFAULT`) | | `STRATEGY_PROFILE` | Yes | Strategy profile selector for compatibility and strategy routing. Set explicitly per deployment; enabled values include `global_etf_confidence_vol_gate`, `global_etf_rotation`, `mega_cap_leader_rotation_top50_balanced`, `russell_1000_multi_factor_defensive`, `soxl_soxx_trend_income`, `tech_communication_pullback_enhancement`, and `tqqq_growth_income`. The structured runtime target is carried separately as `RUNTIME_TARGET_JSON`. | -| `ACCOUNT_REGION` | No | Account region marker for platform-style deployment (e.g. `HK`, `SG`; defaults to `ACCOUNT_PREFIX` / `DEFAULT`) | +| `ACCOUNT_REGION` | No | Account region marker for platform-style deployment (e.g. `PAPER`, `HK`, `SG`; defaults to `ACCOUNT_PREFIX` / `DEFAULT`) | | `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. | | `LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET` | No | Set to `true` to convert fractional `limit buy` orders to `market buy` orders instead of skipping them. | | `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | No | Set to `true` to log raw LongBridge position quantity and available quantity for troubleshooting. | -| `INCOME_THRESHOLD_USD` | No | Optional override for the `tqqq_growth_income` income-layer threshold. Leave unset to use the strategy package default. | -| `QQQI_INCOME_RATIO` | No | Optional override for QQQI's share of the `tqqq_growth_income` income layer, 0–1. | +| `INCOME_THRESHOLD_USD` | No | Optional strategy override for the `tqqq_growth_income` income-layer threshold. Leave unset to use the strategy package default. | +| `QQQI_INCOME_RATIO` | No | Optional strategy override for QQQI's share of the `tqqq_growth_income` income layer, 0–1. | | `NOTIFY_LANG` | No | Notification language: `en` (English, default) or `zh` (Chinese) | | `GOOGLE_CLOUD_PROJECT` | No | GCP project ID (defaults to ADC project when unset) | Strategy allocation can still target fractional dollar values and fractional position weights. The LongBridge execution layer keeps the tested conservative rule: `limit buy` orders stay whole-share only by default, while `market buy` / `market sell` and `limit sell` can preserve fractional quantities when the target quantity is at least 1 share. If you set `LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET=true`, fractional `limit buy` orders are downgraded to market buys instead of being skipped. When a target value is zero, sell sizing uses the sellable position quantity instead of re-deriving shares from current price, so liquidation targets do not leave a residual share because of quote drift. -Secret Manager must contain the secret named by `LONGPORT_SECRET_NAME` (default: `longport_token_hk`), where the **latest version = active access token**. The app refreshes it when expiry is within 30 days. +Secret Manager must contain the secret named by `LONGPORT_SECRET_NAME` (default: `longport_token_paper`), where the **latest version = active access token**. The app refreshes it when expiry is within 30 days. Recommended runtime secrets in the `longbridgequant` project: - `longbridge-telegram-token` +- `longport-app-key-paper` - `longport-app-key-hk` - `longport-app-key-sg` +- `longport-app-secret-paper` - `longport-app-secret-hk` - `longport-app-secret-sg` +- `longport_token_paper` - `longport_token_hk` - `longport_token_sg` -### Multi-deployment isolation (HK/SG, etc.) +### Multi-deployment isolation (paper/HK/SG) -Deploy the same codebase as multiple Cloud Run services (e.g. `HK` and `SG`) by setting different values per service: +Deploy the same codebase as multiple Cloud Run services (e.g. `paper` and `SG` today, `HK` later) by setting different values per service: -- `LONGPORT_SECRET_NAME`: point to different secrets (e.g. `longport_token_hk`, `longport_token_sg`) -- `ACCOUNT_PREFIX`: e.g. `HK`, `SG` (all Telegram/log alerts will include `[ACCOUNT_PREFIX]`) -- `STRATEGY_PROFILE`: set per service; current live examples are `tech_communication_pullback_enhancement` on HK and `soxl_soxx_trend_income` on SG. The deployment control plane now also carries `RUNTIME_TARGET_JSON`; treat `STRATEGY_PROFILE` as a compatibility input that still selects the strategy implementation, not the only identity key. +- `LONGPORT_SECRET_NAME`: point to different secrets (e.g. `longport_token_paper`, `longport_token_hk`, `longport_token_sg`) +- `ACCOUNT_PREFIX`: e.g. `PAPER`, `HK`, `SG` (all Telegram/log alerts will include `[ACCOUNT_PREFIX]`) +- `STRATEGY_PROFILE`: set per service; current live examples are `mega_cap_leader_rotation_top50_balanced` on paper and `soxl_soxx_trend_income` on SG. `HK` will use the same pattern later. The deployment control plane now also carries `RUNTIME_TARGET_JSON`; treat `STRATEGY_PROFILE` as a compatibility input that still selects the strategy implementation, not the only identity key. - Current strategy domain is `us_equity`. `STRATEGY_PROFILE` still goes through a platform capability matrix plus a rollout allowlist derived from `runtime_enabled` strategy metadata: `eligible` means the platform can run it in theory, `enabled` means the current rollout really allows it. -- `ACCOUNT_REGION`: explicitly mark the deployed account region (`HK` / `SG`); if unset, the app falls back to `ACCOUNT_PREFIX` or `DEFAULT` +- `ACCOUNT_REGION`: explicitly mark the deployed account region (`PAPER` / `HK` / `SG`); if unset, the app falls back to `ACCOUNT_PREFIX` or `DEFAULT` - `LONGBRIDGE_DRY_RUN_ONLY`: set per service when that deployment should stay dry-run - `NOTIFY_LANG`: set `en` or `zh` per deployment -### GitHub-managed env sync for HK / SG +### GitHub-managed env sync for paper / HK / SG If code deployment still uses Google Cloud Trigger, but you want GitHub to be the single source of truth for runtime env vars, this repo includes `.github/workflows/sync-cloud-run-env.yml`. @@ -114,47 +117,52 @@ Recommended setup: - `GLOBAL_TELEGRAM_CHAT_ID` - **Repository Secrets (shared):** - Optional fallback only: `TELEGRAM_TOKEN` -- **GitHub Environment: `longbridge-hk`** +- **GitHub Environment: `longbridge-paper`** - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` - - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` - - Current live example: `STRATEGY_PROFILE=tech_communication_pullback_enhancement` - - Recommended secret-name values: `longport-app-key-hk`, `longport-app-secret-hk` + - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`) + - Current live example: `STRATEGY_PROFILE=mega_cap_leader_rotation_top50_balanced` + - Recommended secret-name values: `longport-app-key-paper`, `longport-app-secret-paper` - **GitHub Environment: `longbridge-sg`** - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` - - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` + - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`) - Current live example: `STRATEGY_PROFILE=soxl_soxx_trend_income` - Recommended secret-name values: `longport-app-key-sg`, `longport-app-secret-sg` +- **GitHub Environment: `longbridge-hk`** (reserved for later) + - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` + - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`) + - Current target example: `STRATEGY_PROFILE=` + - Recommended secret-name values: `longport-app-key-paper`, `longport-app-secret-paper` -On every push to `main`, the workflow updates both Cloud Run services with the shared and per-environment values above, and removes `TELEGRAM_CHAT_ID` from each Cloud Run service. +On every push to `main`, the workflow updates the configured Cloud Run services with the shared and per-environment values above, and removes `TELEGRAM_CHAT_ID` from each Cloud Run service. Important: -- `CLOUD_RUN_REGION` should be set on each GitHub Environment, not as one shared repository variable. This lets `HK` and `SG` live in different Cloud Run regions. -- `LONGPORT_APP_KEY_SECRET_NAME` and `LONGPORT_APP_SECRET_SECRET_NAME` should also be set on each GitHub Environment. Do not keep one repository-level default for them when `HK` and `SG` use different LongPort credentials. +- `CLOUD_RUN_REGION` should be set on each GitHub Environment, not as one shared repository variable. This lets `paper`, `HK`, and `SG` live in different Cloud Run regions. +- `LONGPORT_APP_KEY_SECRET_NAME` and `LONGPORT_APP_SECRET_SECRET_NAME` should also be set on each GitHub Environment. Do not keep one repository-level default for them when `paper`, `HK`, and `SG` use different LongPort credentials. - The workflow only becomes strict when `ENABLE_GITHUB_ENV_SYNC=true`. If this variable is unset, the sync job is skipped and the old Google Cloud Trigger-only setup keeps working. Once you set it to `true`, missing env-sync values become a hard failure so you do not get a false green deployment. The selected profile's snapshot/config requirements are resolved from `scripts/print_strategy_profile_status.py --json` instead of a hard-coded strategy-name list. - The workflow now also emits `RUNTIME_TARGET_JSON` so Cloud Run receives a structured runtime target alongside the legacy `STRATEGY_PROFILE` input. - GitHub now authenticates to Google Cloud with OIDC + Workload Identity Federation, so `GCP_SA_KEY` is no longer required for this workflow. - If you deploy with `gcloud run deploy --source` or a Cloud Run source trigger, also grant `roles/storage.objectViewer` on `gs://run-sources--` to the build service account, the deploy service account, and the default compute service account. Otherwise source deploy can fail before Cloud Build starts with `storage.objects.get` denied. -- Here "shared" only means **shared inside this repository** between the `HK` and `SG` Cloud Run services. The Telegram token can still be shared, but LongPort app credentials should live in Secret Manager and be referenced by per-environment secret-name variables; they are not meant to be a global secret set reused by unrelated quant repos. +- Here "shared" only means **shared inside this repository** between the `paper`, `HK`, and `SG` Cloud Run services. The Telegram token can still be shared, but LongPort app credentials should live in Secret Manager and be referenced by per-environment secret-name variables; they are not meant to be a global secret set reused by unrelated quant repos. - If you want one cross-project shared layer across multiple quant repos, keep it small: `GLOBAL_TELEGRAM_CHAT_ID` and `NOTIFY_LANG` are reasonable; account credentials and deployment keys are not. ### Deployment unit and naming - `QuantPlatformKit` is only a shared dependency; Cloud Run still deploys `LongBridgePlatform` itself. -- Recommended Cloud Run service names: `longbridge-quant-hk-service` and `longbridge-quant-sg-service`. -- Keep using two triggers and two GitHub Environments. The split key is still `CLOUD_RUN_SERVICE + CLOUD_RUN_REGION`, and the runtime identity is now explicit through `RUNTIME_TARGET_JSON` with `STRATEGY_PROFILE + ACCOUNT_REGION` kept for compatibility. +- Recommended Cloud Run service names: `longbridge-quant-paper-service`, `longbridge-quant-hk-service`, and `longbridge-quant-sg-service`. +- Keep using two triggers and two GitHub Environments today; add `HK` with the same pattern later. The split key is still `CLOUD_RUN_SERVICE + CLOUD_RUN_REGION`, and the runtime identity is now explicit through `RUNTIME_TARGET_JSON` with `STRATEGY_PROFILE + ACCOUNT_REGION` kept for compatibility. - If you later rename or move this repository, rebuild the GitHub source binding in Google Cloud for both triggers instead of assuming the existing source binding will follow the rename. - For the shared deployment model and trigger migration checklist, see [`QuantPlatformKit/docs/deployment_model.md`](../QuantPlatformKit/docs/deployment_model.md). ### Quick deploy 1. Enable **Cloud Run** and **Secret Manager API** in GCP. -2. Create secret `longport_token_hk` for HK / `longport_token_sg` for SG (or your custom `LONGPORT_SECRET_NAME`) in Secret Manager and add your LongPort access token as the first version. +2. Create secret `longport_token_paper` for paper / `longport_token_hk` for HK / `longport_token_sg` for SG (or your custom `LONGPORT_SECRET_NAME`) in Secret Manager and add your LongPort access token as the first version. 3. Set the required env vars above on the Cloud Run service. 4. Deploy the app to Cloud Run (e.g. `gcloud run deploy` from repo root with Dockerfile or buildpack). 5. Create a Cloud Scheduler job that POSTs to the Cloud Run URL. Choose the cron from the strategy-layer cadence in `UsEquityStrategies`; this platform repo only owns the runtime trigger wiring. -IAM: the Cloud Run service account needs **Secret Manager Admin** (or Secret Accessor for the configured `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, and `LONGPORT_APP_SECRET_SECRET_NAME`, such as `longport_token_hk`, `longport-app-key-hk`, `longport-app-secret-hk`) and **Logs Writer**. Build/deploy typically uses a separate account with Artifact Registry Writer, Cloud Run Admin, Service Account User. +IAM: the Cloud Run service account needs **Secret Manager Admin** (or Secret Accessor for the configured `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, and `LONGPORT_APP_SECRET_SECRET_NAME`, such as `longport_token_paper`, `longport-app-key-paper`, `longport-app-secret-paper`) and **Logs Writer**. Build/deploy typically uses a separate account with Artifact Registry Writer, Cloud Run Admin, Service Account User. --- @@ -165,6 +173,7 @@ IAM: the Cloud Run service account needs **Secret Manager Admin** (or Secret Acc 基于 LongPort OpenAPI 和 Google Cloud Run 的量化交易系统。 这个仓库通过 `QuantPlatformKit` 复用 LongPort token 处理、上下文初始化、账户快照、行情读取和下单逻辑。Cloud Run 直接部署这个仓库。 +LongBridge 的账户身份按 `paper`、`HK`、`SG` 三个维度建模;当前线上运行的是 `paper` 和 `SG`,`HK` 以后按同样模式补齐。 `LongBridgePlatform` 现在可直接执行 `UsEquityStrategies` 里的 7 条 `runtime_enabled` `us_equity` 策略:`global_etf_confidence_vol_gate`、`global_etf_rotation`、`mega_cap_leader_rotation_top50_balanced`、`russell_1000_multi_factor_defensive`、`soxl_soxx_trend_income`、`tqqq_growth_income` 和 `tech_communication_pullback_enhancement`。较弱或重复的研究 profile 已从 LongBridge 可配置入口移除。仓库本身继续保留 LongPort 运行时、token 刷新、执行和通知流程。 完整策略说明现在放在 [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies)。下面这些章节只保留 LongBridge 运行时、profile 启用状态、部署和凭据说明。 @@ -190,7 +199,7 @@ IAM: the Cloud Run service account needs **Secret Manager Admin** (or Secret Acc | `mega_cap_leader_rotation_top50_balanced` | Mega Cap Leader Rotation Top50 Balanced | Yes | Yes | `us_equity` | 可选的 Top50 平衡月度龙头轮动线 | | `soxl_soxx_trend_income` | SOXL/SOXX 半导体趋势收益 | Yes | Yes | `us_equity` | 当前 SG 部署线路 | | `tqqq_growth_income` | TQQQ 增长收益 | Yes | Yes | `us_equity` | 可选增长线路 | -| `tech_communication_pullback_enhancement` | 科技通信回调增强 | Yes | Yes | `us_equity` | 当前 HK feature-snapshot 线路 | +| `tech_communication_pullback_enhancement` | 科技通信回调增强 | Yes | Yes | `us_equity` | 当前 paper feature-snapshot 线路 | 本地可直接查看当前矩阵: @@ -212,46 +221,49 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换 |------|------|------| | `TELEGRAM_TOKEN` | 是 | Telegram 机器人 Token;建议通过 Secret Manager 的 `longbridge-telegram-token` 注入 | | `GLOBAL_TELEGRAM_CHAT_ID` | 是 | 这个服务使用的 Telegram Chat ID。 | -| `LONGPORT_APP_KEY` | 是 | LongPort OpenAPI 应用密钥(用于刷新 Token);建议从当前部署对应区域的 Secret Manager 密钥注入,例如 `longport-app-key-hk` / `longport-app-key-sg` | -| `LONGPORT_APP_SECRET` | 是 | LongPort OpenAPI 应用密钥(用于刷新 Token);建议从当前部署对应区域的 Secret Manager 密钥注入,例如 `longport-app-secret-hk` / `longport-app-secret-sg` | -| `LONGPORT_SECRET_NAME` | 否 | Secret Manager 中的密钥名称(默认: `longport_token_hk`) | +| `LONGPORT_APP_KEY` | 是 | LongPort OpenAPI 应用密钥(用于刷新 Token);建议从当前部署对应区域的 Secret Manager 密钥注入,例如 `longport-app-key-paper` / `longport-app-key-hk` / `longport-app-key-sg` | +| `LONGPORT_APP_SECRET` | 是 | LongPort OpenAPI 应用密钥(用于刷新 Token);建议从当前部署对应区域的 Secret Manager 密钥注入,例如 `longport-app-secret-paper` / `longport-app-secret-hk` / `longport-app-secret-sg` | +| `LONGPORT_SECRET_NAME` | 否 | Secret Manager 中的密钥名称(默认: `longport_token_paper`) | | `ACCOUNT_PREFIX` | 否 | 通知/日志前缀,区分账户环境(默认: `DEFAULT`) | | `STRATEGY_PROFILE` | 是 | 策略档位选择。每个部署都要显式设置;已启用值包括 `global_etf_confidence_vol_gate`、`global_etf_rotation`、`mega_cap_leader_rotation_top50_balanced`、`russell_1000_multi_factor_defensive`、`soxl_soxx_trend_income`、`tech_communication_pullback_enhancement` 和 `tqqq_growth_income` | -| `ACCOUNT_REGION` | 否 | 平台化部署时的账户区域标记(如 `HK`、`SG`;默认按 `ACCOUNT_PREFIX` / `DEFAULT` 推断) | +| `ACCOUNT_REGION` | 否 | 平台化部署时的账户区域标记(如 `PAPER`、`HK`、`SG`;默认按 `ACCOUNT_PREFIX` / `DEFAULT` 推断) | | `LONGBRIDGE_DRY_RUN_ONLY` | 否 | 设为 `true` 时,该部署保持 dry-run。 | | `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | 否 | 设为 `true` 时输出 LongBridge 原始持仓数量和可卖数量,便于排查。 | -| `INCOME_THRESHOLD_USD` | 否 | 可选的 `tqqq_growth_income` 收入层启动阈值覆盖。不填时使用策略包默认值。 | -| `QQQI_INCOME_RATIO` | 否 | 可选的 QQQI 收入层占比覆盖,0–1。 | +| `INCOME_THRESHOLD_USD` | 否 | 可选的 `tqqq_growth_income` 收入层启动阈值覆盖(策略 override)。不填时使用策略包默认值。 | +| `QQQI_INCOME_RATIO` | 否 | 可选的 QQQI 收入层占比覆盖,0–1(策略 override)。 | | `NOTIFY_LANG` | 否 | 通知语言: `en`(英文,默认)或 `zh`(中文) | | `GOOGLE_CLOUD_PROJECT` | 否 | GCP 项目 ID(未设置时使用 ADC 默认项目) | 策略分配层仍然可以按目标金额和目标比例计算出小数仓位;LongBridge 执行层只提交整数股订单,因为实测账户的 OpenAPI `submit_order` 会拒绝碎股委托数量。目标市值为 0 时,卖出数量直接按可卖整数股持仓计算,不再用当前报价反推股数,避免因报价漂移留下 1 股残仓。 -Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `longport_token_hk`),**最新版本 = 当前有效的 access token**。Token 到期前 30 天会自动刷新。 +Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `longport_token_paper`),**最新版本 = 当前有效的 access token**。Token 到期前 30 天会自动刷新。 建议在 `longbridgequant` 项目里维护这些运行时 secret: - `longbridge-telegram-token` +- `longport-app-key-paper` - `longport-app-key-hk` - `longport-app-key-sg` +- `longport-app-secret-paper` - `longport-app-secret-hk` - `longport-app-secret-sg` +- `longport_token_paper` - `longport_token_hk` - `longport_token_sg` -### 多部署隔离(港区/新加坡等) +### 多部署隔离(paper/HK/SG) -同一代码库可部署为多个 Cloud Run 服务(如 `HK` 和 `SG`),通过以下变量区分: +同一代码库可部署为多个 Cloud Run 服务(如 `paper` 和 `SG`,`HK` 以后按同样模式补齐),通过以下变量区分: -- `LONGPORT_SECRET_NAME`: 指向不同密钥(如 `longport_token_hk`、`longport_token_sg`) -- `ACCOUNT_PREFIX`: 如 `HK`、`SG`(所有通知/日志将包含 `[ACCOUNT_PREFIX]`) -- `STRATEGY_PROFILE`: 按服务分别设置;当前线上 HK 用 `tech_communication_pullback_enhancement`,SG 用 `soxl_soxx_trend_income`。控制面会另外携带 `RUNTIME_TARGET_JSON`,`STRATEGY_PROFILE` 继续只作为兼容选择器。 +- `LONGPORT_SECRET_NAME`: 指向不同密钥(如 `longport_token_paper`、`longport_token_hk`、`longport_token_sg`) +- `ACCOUNT_PREFIX`: 如 `PAPER`、`HK`、`SG`(所有通知/日志将包含 `[ACCOUNT_PREFIX]`) +- `STRATEGY_PROFILE`: 按服务分别设置;当前线上 paper 用 `mega_cap_leader_rotation_top50_balanced`,SG 用 `soxl_soxx_trend_income`,HK 以后按同样模式补齐。控制面会另外携带 `RUNTIME_TARGET_JSON`,`STRATEGY_PROFILE` 继续只作为兼容选择器。 - 当前策略域是 `us_equity`。`STRATEGY_PROFILE` 现在会先经过平台能力矩阵,再经过从 `runtime_enabled` 策略元数据派生的 rollout allowlist:`eligible` 表示平台理论可跑,`enabled` 表示当前 rollout 真正放开。 -- `ACCOUNT_REGION`: 显式标记部署账户区域(`HK` / `SG`);未设置时会回退到 `ACCOUNT_PREFIX` 或 `DEFAULT` +- `ACCOUNT_REGION`: 显式标记部署账户区域(`PAPER` / `HK` / `SG`);未设置时会回退到 `ACCOUNT_PREFIX` 或 `DEFAULT` - `LONGBRIDGE_DRY_RUN_ONLY`: 需要保持模拟运行时按服务单独设置 - `NOTIFY_LANG`: 每个部署可独立设置 `en` 或 `zh` -### GitHub 统一管理 HK / SG 环境变量 +### GitHub 统一管理 paper / HK / SG 环境变量 如果代码部署继续走 Google Cloud Trigger,但你想把运行时环境变量统一放在 GitHub 管理,这个仓库已经提供 `.github/workflows/sync-cloud-run-env.yml`。 @@ -264,43 +276,48 @@ Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `lo - `GLOBAL_TELEGRAM_CHAT_ID` - **仓库级 Secrets(共享):** - 仅保留为 fallback:`TELEGRAM_TOKEN` -- **GitHub Environment: `longbridge-hk`** +- **GitHub Environment: `longbridge-paper`** - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` - - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO` - - 当前线上示例:`STRATEGY_PROFILE=tech_communication_pullback_enhancement` - - 建议的 secret-name 值:`longport-app-key-hk`、`longport-app-secret-hk` + - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`) + - 当前线上示例:`STRATEGY_PROFILE=mega_cap_leader_rotation_top50_balanced` + - 建议的 secret-name 值:`longport-app-key-paper`、`longport-app-secret-paper` - **GitHub Environment: `longbridge-sg`** - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` - - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO` + - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`) - 当前线上示例:`STRATEGY_PROFILE=soxl_soxx_trend_income` - 建议的 secret-name 值:`longport-app-key-sg`、`longport-app-secret-sg` +- **GitHub Environment: `longbridge-hk`**(保留给后续补齐) + - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` + - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`) + - 当前目标示例:`STRATEGY_PROFILE=` + - 建议的 secret-name 值:`longport-app-key-hk`、`longport-app-secret-hk` -每次 push 到 `main` 时,这个 workflow 会分别更新两个 Cloud Run 服务,把共享和各自隔离的变量同步进去,并删除旧的 `TELEGRAM_CHAT_ID`。 +每次 push 到 `main` 时,这个 workflow 会更新配置的 Cloud Run 服务,把共享和各自隔离的变量同步进去,并删除旧的 `TELEGRAM_CHAT_ID`。 注意: -- `CLOUD_RUN_REGION` 应该分别放在 `longbridge-hk` 和 `longbridge-sg` 这两个 Environment 里,不要再当成一个仓库级共享变量。这样 HK 和 SG 才能各自更新到自己的 region。 -- `LONGPORT_APP_KEY_SECRET_NAME` 和 `LONGPORT_APP_SECRET_SECRET_NAME` 也应该分别放在各自的 GitHub Environment 里。既然 HK 和 SG 用的是不同 LongPort 凭据,就不要再给它们保留一个仓库级默认值。 +- `CLOUD_RUN_REGION` 应该分别放在 `longbridge-paper`、`longbridge-hk` 和 `longbridge-sg` 这些 Environment 里,不要再当成一个仓库级共享变量。这样 paper、HK 和 SG 才能各自更新到自己的 region。 +- `LONGPORT_APP_KEY_SECRET_NAME` 和 `LONGPORT_APP_SECRET_SECRET_NAME` 也应该分别放在各自的 GitHub Environment 里。既然 paper、HK 和 SG 用的是不同 LongPort 凭据,就不要再给它们保留一个仓库级默认值。 - 现在 workflow 只有在 `ENABLE_GITHUB_ENV_SYNC=true` 时才会严格检查配置。没打开这个开关时,它会直接跳过,不影响原来只靠 Google Cloud Trigger 的老流程;一旦打开,缺任何配置都会直接失败,避免你以为已经同步成功。目标策略需要的 snapshot/config 输入会通过 `scripts/print_strategy_profile_status.py --json` 动态解析,不再维护硬编码策略名列表。 - GitHub 现在通过 OIDC + Workload Identity Federation 登录 Google Cloud,这个 workflow 不再需要 `GCP_SA_KEY`。 - 如果你用 `gcloud run deploy --source` 或 Cloud Run source trigger 部署,还要给 `gs://run-sources--` 这个 staging bucket 补 `roles/storage.objectViewer`,对象是 build service account、deploy service account、默认 compute service account。少了这层权限,部署会在 Cloud Build 启动前直接报 `storage.objects.get denied`。 -- 这里的“共享”只是指 **同一个仓库里的 HK / SG 两个服务共享**。Telegram token 可以继续共用,但 LongPort app 凭据建议放到 Secret Manager,并通过各自 Environment 里的 secret-name 变量引用,不建议把它们当成所有 quant 共用的全局 secrets。 +- 这里的“共享”只是指 **同一个仓库里的 paper / HK / SG 服务共享**。Telegram token 可以继续共用,但 LongPort app 凭据建议放到 Secret Manager,并通过各自 Environment 里的 secret-name 变量引用,不建议把它们当成所有 quant 共用的全局 secrets。 - 如果你真的要在多个 quant 仓库之间保留一层全局共享,建议只保留 `GLOBAL_TELEGRAM_CHAT_ID` 和 `NOTIFY_LANG` 这种低耦合配置。 ### 部署单元和命名建议 - `QuantPlatformKit` 只是共享依赖,不单独部署;Cloud Run 继续只部署 `LongBridgePlatform`。 -- 推荐 Cloud Run 服务名:`longbridge-quant-hk-service` 和 `longbridge-quant-sg-service`。 -- 继续保留两个 trigger 和两个 GitHub Environment,区分键始终是 `CLOUD_RUN_SERVICE + CLOUD_RUN_REGION`,运行身份再通过 `STRATEGY_PROFILE + ACCOUNT_REGION` 明确下来。 +- 推荐 Cloud Run 服务名:`longbridge-quant-paper-service`、`longbridge-quant-hk-service` 和 `longbridge-quant-sg-service`。 +- 继续保留两个 trigger 和两个 GitHub Environment today;HK 补齐后再按同样模式扩展第三个环境。区分键始终是 `CLOUD_RUN_SERVICE + CLOUD_RUN_REGION`,运行身份再通过 `STRATEGY_PROFILE + ACCOUNT_REGION` 明确下来。 - 如果后面改 GitHub 仓库名或再次迁组织,Google Cloud 里的两个 trigger 都要重新选择 GitHub 来源,不要假设旧绑定会自动跟过去。 - 统一部署模型和触发器迁移清单见 [`QuantPlatformKit/docs/deployment_model.md`](../QuantPlatformKit/docs/deployment_model.md)。 ### 快速部署 1. 在 GCP 中启用 **Cloud Run** 和 **Secret Manager API**。 -2. 在 Secret Manager 中为 HK 创建 `longport_token_hk`、为 SG 创建 `longport_token_sg`(或使用你自定义的 `LONGPORT_SECRET_NAME`),并将 LongPort access token 作为第一个版本写入。 +2. 在 Secret Manager 中为 paper 创建 `longport_token_paper`、为 HK 创建 `longport_token_hk`、为 SG 创建 `longport_token_sg`(或使用你自定义的 `LONGPORT_SECRET_NAME`),并将 LongPort access token 作为第一个版本写入。 3. 在 Cloud Run 服务上配置上述环境变量。 4. 部署至 Cloud Run(如从仓库根目录执行 `gcloud run deploy`)。 5. 创建 Cloud Scheduler 定时任务,POST 到 Cloud Run URL。cron 频率以 `UsEquityStrategies` 里的策略层 cadence 为准;这个平台仓只维护运行时触发 wiring。 -IAM: Cloud Run 服务账号需要 **Secret Manager Admin**(或当前 `LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` 对应 secret 的 Secret Accessor,例如 `longport_token_hk`、`longport-app-key-hk`、`longport-app-secret-hk`)和 **Logs Writer** 权限。 +IAM: Cloud Run 服务账号需要 **Secret Manager Admin**(或当前 `LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` 对应 secret 的 Secret Accessor,例如 `longport_token_paper`、`longport-app-key-paper`、`longport-app-secret-paper`)和 **Logs Writer** 权限。 diff --git a/requirements.txt b/requirements.txt index 57e0d18..a8ad1cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ flask gunicorn -./vendor_wheels/quant_platform_kit-0.7.21-py3-none-any.whl +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@573fc9e9917cf1f2c1acda9232c5a23a8a05d797 ./vendor_wheels/us_equity_strategies-0.7.35-py3-none-any.whl pandas requests diff --git a/runtime_config_support.py b/runtime_config_support.py index 6af1ced..7a5b7c1 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -12,7 +12,7 @@ ) from quant_platform_kit.common.runtime_target import ( RuntimeTarget, - resolve_runtime_identity_from_env, + resolve_runtime_target_from_env, ) from strategy_registry import ( LONGBRIDGE_PLATFORM, @@ -77,23 +77,12 @@ def load_platform_runtime_settings( project_id_resolver: Callable[[], str | None], ) -> PlatformRuntimeSettings: account_prefix = os.getenv("ACCOUNT_PREFIX", "DEFAULT") - resolved_identity = resolve_runtime_identity_from_env( - os.environ, - platform_id=LONGBRIDGE_PLATFORM, - default_strategy_profile=os.getenv("STRATEGY_PROFILE"), - dry_run_only=resolve_bool_value(os.getenv("LONGBRIDGE_DRY_RUN_ONLY")), - deployment_selector=infer_account_region( - os.getenv("ACCOUNT_REGION"), - account_prefix=account_prefix, - ), - account_scope=infer_account_region( - os.getenv("ACCOUNT_REGION"), - account_prefix=account_prefix, - ), - service_name=os.getenv("K_SERVICE"), + runtime_target = resolve_runtime_target_from_env( + env=os.environ, + expected_platform_id=LONGBRIDGE_PLATFORM, ) strategy_definition = resolve_strategy_definition( - resolved_identity.strategy_profile, + runtime_target.strategy_profile, platform_id=LONGBRIDGE_PLATFORM, ) strategy_metadata = resolve_strategy_metadata( @@ -136,7 +125,7 @@ def load_platform_runtime_settings( feature_snapshot_manifest_path=runtime_paths.feature_snapshot_manifest_path, strategy_config_path=runtime_paths.strategy_config_path, strategy_config_source=runtime_paths.strategy_config_source, - runtime_target=resolved_identity.runtime_target, + runtime_target=runtime_target, ) diff --git a/scripts/print_strategy_switch_env_plan.py b/scripts/print_strategy_switch_env_plan.py index 22cdda3..61fd952 100644 --- a/scripts/print_strategy_switch_env_plan.py +++ b/scripts/print_strategy_switch_env_plan.py @@ -58,7 +58,9 @@ def build_switch_plan(profile: str, *, account_region: str | None = None) -> dic service_name=f"longbridge-quant-{normalized_region.lower()}-service" if normalized_region else None, ) - set_env: dict[str, str] = {"STRATEGY_PROFILE": definition.profile} + set_env: dict[str, str] = { + "RUNTIME_TARGET_JSON": json.dumps(runtime_target.to_dict(), separators=(",", ":")) + } if normalized_region: set_env["ACCOUNT_REGION"] = normalized_region set_env["ACCOUNT_PREFIX"] = normalized_region @@ -71,11 +73,11 @@ def build_switch_plan(profile: str, *, account_region: str | None = None) -> dic optional_env = ["LONGBRIDGE_DRY_RUN_ONLY"] remove_if_present: list[str] = [] notes = [ - "Keep ACCOUNT_PREFIX and ACCOUNT_REGION aligned to the current HK or SG service identity.", + "Keep ACCOUNT_PREFIX and ACCOUNT_REGION aligned to the current paper or SG service identity.", ] if not normalized_region: - notes.append("Pass --account-region HK or --account-region SG if you want ACCOUNT_PREFIX/ACCOUNT_REGION placeholders filled in.") + notes.append("Pass --account-region PAPER or --account-region SG if you want ACCOUNT_PREFIX/ACCOUNT_REGION placeholders filled in.") if requires_feature_snapshot: set_env["LONGBRIDGE_FEATURE_SNAPSHOT_PATH"] = "" diff --git a/tests/test_notifications.py b/tests/test_notifications.py index cd80fca..dc7a08f 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -84,6 +84,8 @@ def test_build_strategy_display_name_supports_i18n(self): def test_supported_strategy_profiles_have_translated_names(self): zh_name = build_strategy_display_name(build_translator("zh")) en_name = build_strategy_display_name(build_translator("en")) + self.assertEqual(zh_name("global_etf_confidence_vol_gate"), "全球 ETF 置信波动门控") + self.assertEqual(en_name("global_etf_confidence_vol_gate"), "Global ETF Confidence Vol Gate") for profile in SUPPORTED_STRATEGY_PROFILES: self.assertNotEqual(zh_name(profile), profile) diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index e37d6dc..0cc867d 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -34,11 +34,58 @@ SAMPLE_STRATEGY_PROFILE = "soxl_soxx_trend_income" +BASE_LONGBRIDGE_PROFILES = frozenset( + { + "global_etf_rotation", + "mega_cap_leader_rotation_top50_balanced", + "russell_1000_multi_factor_defensive", + "tqqq_growth_income", + "soxl_soxx_trend_income", + "tech_communication_pullback_enhancement", + } +) +OPTIONAL_LONGBRIDGE_PROFILES = frozenset({"global_etf_confidence_vol_gate"}) + + +def expected_longbridge_profiles(actual_profiles) -> frozenset[str]: + actual = frozenset(actual_profiles) + return BASE_LONGBRIDGE_PROFILES | (OPTIONAL_LONGBRIDGE_PROFILES & actual) + + +def runtime_target_json( + strategy_profile: str, + *, + dry_run_only: bool = False, + platform_id: str = "longbridge", + deployment_selector: str | None = None, + account_selector: list[str] | tuple[str, ...] | None = None, + account_scope: str | None = None, + service_name: str | None = None, +) -> str: + payload: dict[str, object] = { + "platform_id": platform_id, + "strategy_profile": strategy_profile, + "dry_run_only": dry_run_only, + } + if deployment_selector is not None: + payload["deployment_selector"] = deployment_selector + if account_selector is not None: + payload["account_selector"] = list(account_selector) + if account_scope is not None: + payload["account_scope"] = account_scope + if service_name is not None: + payload["service_name"] = service_name + payload["execution_mode"] = "paper" if dry_run_only else "live" + return json.dumps(payload, separators=(",", ":")) class RuntimeConfigSupportTests(unittest.TestCase): def test_load_platform_runtime_settings_uses_defaults_with_explicit_strategy_profile(self): - with patch.dict(os.environ, {"STRATEGY_PROFILE": SAMPLE_STRATEGY_PROFILE}, clear=True): + with patch.dict( + os.environ, + {"RUNTIME_TARGET_JSON": runtime_target_json(SAMPLE_STRATEGY_PROFILE)}, + clear=True, + ): settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") self.assertEqual(settings.project_id, "project-1") @@ -66,7 +113,6 @@ def test_load_platform_runtime_settings_prefers_runtime_target_json(self): with patch.dict( os.environ, { - "STRATEGY_PROFILE": SAMPLE_STRATEGY_PROFILE, "ACCOUNT_REGION": "hk", "RUNTIME_TARGET_JSON": ( '{"platform_id":"longbridge","strategy_profile":"global_etf_rotation",' @@ -90,45 +136,27 @@ def test_load_platform_runtime_settings_prefers_runtime_target_json(self): def test_load_platform_runtime_settings_requires_strategy_profile(self): with patch.dict(os.environ, {}, clear=True): - with self.assertRaisesRegex(EnvironmentError, "STRATEGY_PROFILE is required"): + with self.assertRaisesRegex(EnvironmentError, "RUNTIME_TARGET_JSON is required"): load_platform_runtime_settings(project_id_resolver=lambda: "project-1") def test_platform_supported_profiles_are_filtered_by_registry(self): - self.assertEqual( - get_supported_profiles_for_platform(LONGBRIDGE_PLATFORM), - frozenset( - { - "global_etf_confidence_vol_gate", - "global_etf_rotation", - "mega_cap_leader_rotation_top50_balanced", - "russell_1000_multi_factor_defensive", - "tqqq_growth_income", - "soxl_soxx_trend_income", - "tech_communication_pullback_enhancement", - } - ), - ) + profiles = get_supported_profiles_for_platform(LONGBRIDGE_PLATFORM) + self.assertEqual(profiles, expected_longbridge_profiles(profiles)) def test_platform_eligible_profiles_are_exposed_by_capability_matrix(self): - self.assertEqual( - get_eligible_profiles_for_platform(LONGBRIDGE_PLATFORM), - frozenset( - { - "global_etf_confidence_vol_gate", - "global_etf_rotation", - "mega_cap_leader_rotation_top50_balanced", - "russell_1000_multi_factor_defensive", - "tqqq_growth_income", - "soxl_soxx_trend_income", - "tech_communication_pullback_enhancement", - } - ), - ) + profiles = get_eligible_profiles_for_platform(LONGBRIDGE_PLATFORM) + self.assertEqual(profiles, expected_longbridge_profiles(profiles)) def test_dry_run_only_is_loaded_from_env(self): with patch.dict( os.environ, - {"STRATEGY_PROFILE": SAMPLE_STRATEGY_PROFILE, "LONGBRIDGE_DRY_RUN_ONLY": "true"}, + { + "RUNTIME_TARGET_JSON": runtime_target_json( + SAMPLE_STRATEGY_PROFILE, + dry_run_only=True, + ), + "LONGBRIDGE_DRY_RUN_ONLY": "true", + }, clear=True, ): settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") @@ -139,7 +167,7 @@ def test_fractional_limit_buy_fallback_is_loaded_from_env(self): with patch.dict( os.environ, { - "STRATEGY_PROFILE": SAMPLE_STRATEGY_PROFILE, + "RUNTIME_TARGET_JSON": runtime_target_json(SAMPLE_STRATEGY_PROFILE), "LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET": "true", }, clear=True, @@ -152,7 +180,7 @@ def test_debug_position_snapshot_is_loaded_from_env(self): with patch.dict( os.environ, { - "STRATEGY_PROFILE": SAMPLE_STRATEGY_PROFILE, + "RUNTIME_TARGET_JSON": runtime_target_json(SAMPLE_STRATEGY_PROFILE), "LONGBRIDGE_DEBUG_POSITION_SNAPSHOT": "true", }, clear=True, @@ -165,7 +193,7 @@ def test_income_layer_overrides_are_loaded_from_env(self): with patch.dict( os.environ, { - "STRATEGY_PROFILE": "tqqq_growth_income", + "RUNTIME_TARGET_JSON": runtime_target_json("tqqq_growth_income"), "INCOME_THRESHOLD_USD": "100000", "QQQI_INCOME_RATIO": "0.5", }, @@ -181,7 +209,9 @@ def test_tech_runtime_execution_window_override_is_loaded_from_env(self): with patch.dict( os.environ, { - "STRATEGY_PROFILE": "tech_communication_pullback_enhancement", + "RUNTIME_TARGET_JSON": runtime_target_json( + "tech_communication_pullback_enhancement" + ), "LONGBRIDGE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS": "31", }, clear=True, @@ -194,14 +224,21 @@ def test_tech_runtime_execution_window_override_is_loaded_from_env(self): def test_rejects_invalid_qqqi_income_ratio(self): with patch.dict( os.environ, - {"STRATEGY_PROFILE": "tqqq_growth_income", "QQQI_INCOME_RATIO": "1.5"}, + { + "RUNTIME_TARGET_JSON": runtime_target_json("tqqq_growth_income"), + "QQQI_INCOME_RATIO": "1.5", + }, clear=True, ): with self.assertRaisesRegex(ValueError, "QQQI_INCOME_RATIO"): load_platform_runtime_settings(project_id_resolver=lambda: "project-1") def test_rejects_human_readable_alias(self): - with patch.dict(os.environ, {"STRATEGY_PROFILE": "semiconductor_trend_income"}, clear=True): + with patch.dict( + os.environ, + {"RUNTIME_TARGET_JSON": runtime_target_json("semiconductor_trend_income")}, + clear=True, + ): with self.assertRaises(ValueError): load_platform_runtime_settings(project_id_resolver=lambda: "project-1") @@ -227,7 +264,11 @@ def test_account_region_defaults_when_prefix_missing(self): self.assertEqual(region, DEFAULT_ACCOUNT_REGION) def test_unsupported_strategy_profile_fails_fast(self): - with patch.dict(os.environ, {"STRATEGY_PROFILE": "balanced_income"}, clear=True): + with patch.dict( + os.environ, + {"RUNTIME_TARGET_JSON": runtime_target_json("balanced_income")}, + clear=True, + ): with self.assertRaisesRegex(ValueError, "Unsupported STRATEGY_PROFILE"): load_platform_runtime_settings(project_id_resolver=lambda: "project-1") @@ -242,18 +283,7 @@ def test_platform_profile_status_matrix_matches_current_longbridge_rollout(self) rows = get_platform_profile_status_matrix() by_profile = {row["canonical_profile"]: row for row in rows} - self.assertEqual( - set(by_profile), - { - "global_etf_rotation", - "global_etf_confidence_vol_gate", - "mega_cap_leader_rotation_top50_balanced", - "russell_1000_multi_factor_defensive", - "tqqq_growth_income", - "soxl_soxx_trend_income", - "tech_communication_pullback_enhancement", - }, - ) + self.assertEqual(set(by_profile), expected_longbridge_profiles(by_profile)) self.assertEqual( by_profile["soxl_soxx_trend_income"], { @@ -275,12 +305,13 @@ def test_platform_profile_status_matrix_matches_current_longbridge_rollout(self) self.assertEqual(by_profile["global_etf_rotation"]["display_name"], "Global ETF Rotation") self.assertTrue(by_profile["global_etf_rotation"]["eligible"]) self.assertTrue(by_profile["global_etf_rotation"]["enabled"]) - self.assertEqual( - by_profile["global_etf_confidence_vol_gate"]["display_name"], - "Global ETF Confidence Vol Gate", - ) - self.assertTrue(by_profile["global_etf_confidence_vol_gate"]["eligible"]) - self.assertTrue(by_profile["global_etf_confidence_vol_gate"]["enabled"]) + if "global_etf_confidence_vol_gate" in by_profile: + self.assertEqual( + by_profile["global_etf_confidence_vol_gate"]["display_name"], + "Global ETF Confidence Vol Gate", + ) + self.assertTrue(by_profile["global_etf_confidence_vol_gate"]["eligible"]) + self.assertTrue(by_profile["global_etf_confidence_vol_gate"]["enabled"]) self.assertTrue(by_profile["tech_communication_pullback_enhancement"]["eligible"]) self.assertTrue(by_profile["tech_communication_pullback_enhancement"]["enabled"]) self.assertEqual(by_profile["tech_communication_pullback_enhancement"]["display_name"], "Tech/Communication Pullback Enhancement") @@ -295,7 +326,9 @@ def test_loads_feature_snapshot_env_for_tech_profile(self): with patch.dict( os.environ, { - "STRATEGY_PROFILE": "tech_communication_pullback_enhancement", + "RUNTIME_TARGET_JSON": runtime_target_json( + "tech_communication_pullback_enhancement" + ), "LONGBRIDGE_FEATURE_SNAPSHOT_PATH": "gs://bucket/tech.csv", "LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH": "gs://bucket/tech.csv.manifest.json", "LONGBRIDGE_STRATEGY_CONFIG_PATH": "/workspace/configs/tech.json", @@ -315,7 +348,9 @@ def test_derives_feature_snapshot_paths_from_artifact_root(self): with patch.dict( os.environ, { - "STRATEGY_PROFILE": "mega_cap_leader_rotation_top50_balanced", + "RUNTIME_TARGET_JSON": runtime_target_json( + "mega_cap_leader_rotation_top50_balanced" + ), "STRATEGY_ARTIFACT_ROOT": tmp_dir, }, clear=True, diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index e4d1021..9e602fb 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -26,7 +26,7 @@ grep -Fq 'Wait for Cloud Run deployment of current commit' "$workflow_file" grep -Fq 'target_sha="${GITHUB_SHA}"' "$workflow_file" grep -Fq "gcloud run services describe \"\${CLOUD_RUN_SERVICE}\" --region \"\${CLOUD_RUN_REGION}\" --format='value(spec.template.metadata.labels.commit-sha)'" "$workflow_file" grep -Fq 'Timed out waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-}' "$workflow_file" -grep -Fq "environment: longbridge-hk" "$workflow_file" +grep -Fq "environment: longbridge-paper" "$workflow_file" grep -Fq "environment: longbridge-sg" "$workflow_file" grep -Fq 'ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }}' "$workflow_file" grep -Fq 'GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}' "$workflow_file" @@ -42,17 +42,17 @@ grep -Fq 'INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}' "$workflow_fil grep -Fq 'QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}' "$workflow_file" grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }}' "$workflow_file" grep -Fq 'LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET: ${{ vars.LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET }}' "$workflow_file" -grep -Fq "STRATEGY_PROFILE: \${{ vars.STRATEGY_PROFILE || 'soxl_soxx_trend_income' }}" "$workflow_file" -grep -Fq "ACCOUNT_REGION: \${{ vars.ACCOUNT_REGION || 'HK' }}" "$workflow_file" +grep -Fq 'RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}' "$workflow_file" +grep -Fq "ACCOUNT_REGION: \${{ vars.ACCOUNT_REGION || 'PAPER' }}" "$workflow_file" grep -Fq "ACCOUNT_REGION: \${{ vars.ACCOUNT_REGION || 'SG' }}" "$workflow_file" grep -Fq 'echo "enabled=false" >> "$GITHUB_OUTPUT"' "$workflow_file" -grep -Fq "Skipping HK Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." "$workflow_file" +grep -Fq "Skipping PAPER Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." "$workflow_file" grep -Fq "Skipping SG Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." "$workflow_file" -grep -Fq "HK Cloud Run env sync is enabled, but these values are missing:" "$workflow_file" +grep -Fq "PAPER Cloud Run env sync is enabled, but these values are missing:" "$workflow_file" grep -Fq "SG Cloud Run env sync is enabled, but these values are missing:" "$workflow_file" -grep -Fq "set CLOUD_RUN_REGION on the longbridge-hk Environment" "$workflow_file" +grep -Fq "set CLOUD_RUN_REGION on the longbridge-paper Environment" "$workflow_file" grep -Fq "set CLOUD_RUN_REGION on the longbridge-sg Environment" "$workflow_file" -grep -Fq "Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-hk Environment" "$workflow_file" +grep -Fq "Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-paper Environment" "$workflow_file" grep -Fq "Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-sg Environment" "$workflow_file" grep -Fq "if: steps.config.outputs.enabled == 'true'" "$workflow_file" grep -Fq 'missing_vars+=("TELEGRAM_TOKEN_SECRET_NAME or TELEGRAM_TOKEN")' "$workflow_file" @@ -81,7 +81,6 @@ grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}' "$workflow_file" grep -Fq 'LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET=${LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET}' "$workflow_file" grep -Fq 'INCOME_THRESHOLD_USD=${INCOME_THRESHOLD_USD}' "$workflow_file" grep -Fq 'QQQI_INCOME_RATIO=${QQQI_INCOME_RATIO}' "$workflow_file" -grep -Fq 'STRATEGY_PROFILE=${STRATEGY_PROFILE}' "$workflow_file" grep -Fq 'ACCOUNT_REGION=${ACCOUNT_REGION}' "$workflow_file" grep -Fq 'RUNTIME_TARGET_JSON=${RUNTIME_TARGET_JSON}' "$workflow_file" grep -Fq '"LONGBRIDGE_FRACTIONAL_SHARES_ENABLED"' "$workflow_file" From a1bf46d95de749c1e558bd42b0bb5ecc4c0d0e9c Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 10 May 2026 15:11:51 +0800 Subject: [PATCH 2/3] Pin UsEquityStrategies runtime target dependency --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a8ad1cc..1fd3f71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@573fc9e9917cf1f2c1acda9232c5a23a8a05d797 -./vendor_wheels/us_equity_strategies-0.7.35-py3-none-any.whl +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@53911cbe32f6932e759522e54aa38ca5350aa44e pandas requests pytz From 053b858e5b202130324635b102219619e5054bca Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 10 May 2026 15:14:09 +0800 Subject: [PATCH 3/3] Update LongBridge shared chat test runtime target --- tests/test_shared_chat_id_fallback.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_shared_chat_id_fallback.py b/tests/test_shared_chat_id_fallback.py index da6f361..91e985f 100644 --- a/tests/test_shared_chat_id_fallback.py +++ b/tests/test_shared_chat_id_fallback.py @@ -137,7 +137,10 @@ def test_global_telegram_chat_id_is_used(self): os.environ, { "GLOBAL_TELEGRAM_CHAT_ID": "shared-chat-id", - "STRATEGY_PROFILE": "soxl_soxx_trend_income", + "RUNTIME_TARGET_JSON": ( + '{"platform_id":"longbridge","strategy_profile":"soxl_soxx_trend_income",' + '"dry_run_only":false,"execution_mode":"live"}' + ), }, clear=False, ):