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
79 changes: 55 additions & 24 deletions .github/workflows/build-hk-low-vol-snapshot-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ on:
type: choice
options:
- hk_low_vol_dividend_quality
data_source_mode:
description: "Source used to generate the factor snapshot."
required: true
default: public_yfinance_staging
type: choice
options:
- public_yfinance_staging
- longbridge_openapi_staging
universe_path:
description: "Local path in HkEquitySnapshotPipelines, gs://, or https:// universe CSV."
required: false
Expand All @@ -25,7 +33,7 @@ on:
required: false
type: string
history_start:
description: "Optional LongBridge history start date, for example 2025-01-01."
description: "Optional generated-source history start date, for example 2025-01-01."
required: false
type: string
benchmark_symbol:
Expand Down Expand Up @@ -93,6 +101,7 @@ env:
SNAPSHOT_REPOSITORY: QuantStrategyLab/HkEquitySnapshotPipelines
SNAPSHOT_REPO_DIR: external/HkEquitySnapshotPipelines
PROFILE: ${{ inputs.profile }}
DATA_SOURCE_MODE: ${{ inputs.data_source_mode }}
UNIVERSE_PATH: ${{ inputs.universe_path }}
AS_OF_DATE: ${{ inputs.as_of_date }}
HISTORY_START: ${{ inputs.history_start }}
Expand All @@ -110,7 +119,7 @@ env:

jobs:
build:
name: Build LongBridge-backed HK snapshot artifacts
name: Build HK snapshot artifacts
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -138,6 +147,13 @@ jobs:
exit 1
;;
esac
case "${DATA_SOURCE_MODE}" in
public_yfinance_staging|longbridge_openapi_staging) ;;
*)
echo "Unsupported data_source_mode: ${DATA_SOURCE_MODE}" >&2
exit 1
;;
esac

if [ "${ALLOW_RESEARCH_DEFAULTS}" = "true" ] && [ "${EXECUTE_PUBLISH}" = "true" ]; then
echo "allow_research_defaults=true is research smoke only and cannot be published to GCS." >&2
Expand Down Expand Up @@ -171,7 +187,14 @@ jobs:
run: |
set -euo pipefail
python -m pip install --upgrade pip
python -m pip install -e '.[longbridge]'
case "${DATA_SOURCE_MODE}" in
public_yfinance_staging)
python -m pip install -e '.[public-data]'

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 Install yfinance for the public data mode

When the workflow runs with the new default data_source_mode=public_yfinance_staging, this installs hk-equity-snapshot-pipelines[public-data], but the current HkEquitySnapshotPipelines pyproject.toml only defines test and longbridge extras, while the public snapshot provider raises if yfinance is unavailable. Pip will only warn about the unknown extra and install the base package, so the default public run reaches scripts/build_low_vol_dividend_public_factor_snapshot.py and fails before producing artifacts.

Useful? React with 👍 / 👎.

;;
longbridge_openapi_staging)
python -m pip install -e '.[longbridge]'
;;
esac

- name: Build, validate, and optionally publish artifacts
id: build
Expand Down Expand Up @@ -214,23 +237,30 @@ jobs:
;;
esac

LONG_BRIDGE_APP_KEY="$(gcloud secrets versions access latest --project="${LONGBRIDGE_SECRET_PROJECT_ID}" --secret="${LONGBRIDGE_APP_KEY_SECRET_NAME}")"
LONG_BRIDGE_APP_SECRET="$(gcloud secrets versions access latest --project="${LONGBRIDGE_SECRET_PROJECT_ID}" --secret="${LONGBRIDGE_APP_SECRET_SECRET_NAME}")"
LONG_BRIDGE_ACCESS_TOKEN="$(gcloud secrets versions access latest --project="${LONGBRIDGE_SECRET_PROJECT_ID}" --secret="${LONGBRIDGE_ACCESS_TOKEN_SECRET_NAME}")"
missing_credentials=()
[ -n "${LONG_BRIDGE_APP_KEY}" ] || missing_credentials+=(app_key)
[ -n "${LONG_BRIDGE_APP_SECRET}" ] || missing_credentials+=(app_secret)
[ -n "${LONG_BRIDGE_ACCESS_TOKEN}" ] || missing_credentials+=(access_token)
if [ "${#missing_credentials[@]}" -gt 0 ]; then
printf 'LongBridge Secret Manager values are empty: %s\n' "${missing_credentials[*]}" >&2
exit 1
generator_script="scripts/build_low_vol_dividend_public_factor_snapshot.py"
generated_path="${generated_dir}/factor_snapshot.public_yfinance_staging.csv"
generation_label="Public yfinance"
if [ "${DATA_SOURCE_MODE}" = "longbridge_openapi_staging" ]; then
LONG_BRIDGE_APP_KEY="$(gcloud secrets versions access latest --project="${LONGBRIDGE_SECRET_PROJECT_ID}" --secret="${LONGBRIDGE_APP_KEY_SECRET_NAME}")"
LONG_BRIDGE_APP_SECRET="$(gcloud secrets versions access latest --project="${LONGBRIDGE_SECRET_PROJECT_ID}" --secret="${LONGBRIDGE_APP_SECRET_SECRET_NAME}")"
LONG_BRIDGE_ACCESS_TOKEN="$(gcloud secrets versions access latest --project="${LONGBRIDGE_SECRET_PROJECT_ID}" --secret="${LONGBRIDGE_ACCESS_TOKEN_SECRET_NAME}")"
missing_credentials=()
[ -n "${LONG_BRIDGE_APP_KEY}" ] || missing_credentials+=(app_key)
[ -n "${LONG_BRIDGE_APP_SECRET}" ] || missing_credentials+=(app_secret)
[ -n "${LONG_BRIDGE_ACCESS_TOKEN}" ] || missing_credentials+=(access_token)
if [ "${#missing_credentials[@]}" -gt 0 ]; then
printf 'LongBridge Secret Manager values are empty: %s\n' "${missing_credentials[*]}" >&2
exit 1
fi
echo "::add-mask::${LONG_BRIDGE_APP_KEY}"
echo "::add-mask::${LONG_BRIDGE_APP_SECRET}"
echo "::add-mask::${LONG_BRIDGE_ACCESS_TOKEN}"
export LONG_BRIDGE_APP_KEY LONG_BRIDGE_APP_SECRET LONG_BRIDGE_ACCESS_TOKEN
generator_script="scripts/build_low_vol_dividend_longbridge_factor_snapshot.py"
generated_path="${generated_dir}/factor_snapshot.longbridge_staging.csv"
generation_label="LongBridge"
fi
echo "::add-mask::${LONG_BRIDGE_APP_KEY}"
echo "::add-mask::${LONG_BRIDGE_APP_SECRET}"
echo "::add-mask::${LONG_BRIDGE_ACCESS_TOKEN}"
export LONG_BRIDGE_APP_KEY LONG_BRIDGE_APP_SECRET LONG_BRIDGE_ACCESS_TOKEN

generated_path="${generated_dir}/factor_snapshot.longbridge_staging.csv"
generator_args=(
--universe "${universe_path}"
--output "${generated_path}"
Expand All @@ -247,18 +277,18 @@ jobs:
generator_args+=(--allow-research-defaults)
fi
set +e
python scripts/build_low_vol_dividend_longbridge_factor_snapshot.py "${generator_args[@]}" > "${generated_dir}/generation_summary.json"
python "${generator_script}" "${generator_args[@]}" > "${generated_dir}/generation_summary.json"
generator_status=$?
set -e
unset LONG_BRIDGE_APP_KEY LONG_BRIDGE_APP_SECRET LONG_BRIDGE_ACCESS_TOKEN
if [ -s "${generated_dir}/generation_summary.json" ]; then
python - <<'PY' "${generated_dir}/generation_summary.json"
python - <<'PY' "${generated_dir}/generation_summary.json" "${generation_label}"
import json
import sys
from pathlib import Path

payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
print("LongBridge generation summary:")
print(f"{sys.argv[2]} generation summary:")
for key in (
"profile",
"row_count",
Expand All @@ -281,7 +311,7 @@ jobs:
PY
fi
if [ "${generator_status}" -ne 0 ]; then
echo "LongBridge factor snapshot generation failed; see generation_summary.json in the generated input artifact." >&2
echo "${generation_label} factor snapshot generation failed; see generation_summary.json in the generated input artifact." >&2
exit "${generator_status}"
fi

Expand Down Expand Up @@ -326,7 +356,7 @@ jobs:
if-no-files-found: ignore
retention-days: 14

- name: Upload generated LongBridge input snapshot
- name: Upload generated input snapshot
if: always() && steps.build.outputs.generated_input_name != ''
uses: actions/upload-artifact@v7
with:
Expand All @@ -343,6 +373,7 @@ jobs:
echo
echo "- profile: \`${PROFILE}\`"
echo "- HkEquitySnapshotPipelines ref: \`${{ inputs.snapshot_ref }}\`"
echo "- data_source_mode: \`${DATA_SOURCE_MODE}\`"
echo "- universe_path: \`${UNIVERSE_PATH:-examples/low_vol_dividend_quality/longbridge_universe.seed.csv}\`"
echo "- allow_research_defaults: \`${ALLOW_RESEARCH_DEFAULTS}\`"
echo "- gcs_prefix: \`${GCS_PREFIX:-<not set>}\`"
Expand All @@ -366,5 +397,5 @@ jobs:
echo "\`\`\`"
echo
fi
echo "Evidence boundary: validated LongBridge-generated CSVs can be runtime artifact inputs when allow_research_defaults=false; final live order approval still requires dry-run execution, notification, and operator approval evidence."
echo "Evidence boundary: validated generated CSVs can be runtime artifact inputs when allow_research_defaults=false; final live order approval still requires dry-run execution, notification, and operator approval evidence."
} >> "${GITHUB_STEP_SUMMARY}"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The runtime now carries a structured `RuntimeTarget` / `RUNTIME_TARGET_JSON` alo
The LongBridge runtime can execute the current `runtime_enabled` `us_equity` profiles from `UsEquityStrategies`. It also carries HK profiles from `HkEquityStrategies`: eligible-but-disabled `hk_blue_chip_leader_rotation`, `hk_index_mean_reversion`, `hk_etf_regime_rotation`, plus runtime-enabled `hk_listed_global_etf_rotation`; `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.

Strategy documentation lives in [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies) and [`HkEquityStrategies`](https://github.com/QuantStrategyLab/HkEquityStrategies). Snapshot artifact contracts for the HK profile are produced by [`HkEquitySnapshotPipelines`](https://github.com/QuantStrategyLab/HkEquitySnapshotPipelines). The sections below focus on LongBridge runtime behavior, profile enablement, deployment, and credentials.
Strategy documentation lives in [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies) and [`HkEquityStrategies`](https://github.com/QuantStrategyLab/HkEquityStrategies). Snapshot artifact contracts for the HK profile are produced by [`HkEquitySnapshotPipelines`](https://github.com/QuantStrategyLab/HkEquitySnapshotPipelines). HK snapshot artifacts can be generated from a public yfinance source and then consumed by LongBridge for dry-run/execution, keeping broker permissions separate from data refresh. The sections below focus on LongBridge runtime behavior, profile enablement, deployment, and credentials.
This runtime matrix is the authoritative enablement source for LongBridge. Strategy packages carry strategy-layer logic, cadence, compatibility, and metadata.

### Execution boundary
Expand Down
6 changes: 4 additions & 2 deletions docs/hk_equity_runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ python scripts/print_strategy_switch_env_plan.py \

## 生成 `hk_low_vol_dividend_quality` snapshot artifacts

`hk_low_vol_dividend_quality` 是 snapshot-backed 策略,Cloud Run 切到这个 profile 前必须先有经过校验的 feature snapshot 和 manifest。`HkEquitySnapshotPipelines` 自身还需要单独开通 GCP WIF 或 GitHub Secrets;在这之前,`LongBridgePlatform` 提供一个手动桥接 workflow,使用本仓库已经允许的 `longbridge-hk` WIF 和 Secret Manager 权限生成真实 LongBridge OpenAPI artifact
`hk_low_vol_dividend_quality` 是 snapshot-backed 策略,Cloud Run 切到这个 profile 前必须先有经过校验的 feature snapshot 和 manifest。`LongBridgePlatform` 提供一个手动桥接 workflow,使用本仓库已经允许的 `longbridge-hk` WIF 发布 artifact。默认数据源是 `public_yfinance_staging`,不依赖 LongBridge 历史行情权限;如果账号已开通对应 HK market-data entitlement,也可以切到 `longbridge_openapi_staging`

手动生成并只打印 GCS 发布计划:

Expand All @@ -103,6 +103,7 @@ gh workflow run build-hk-low-vol-snapshot-artifacts.yml \
--repo QuantStrategyLab/LongBridgePlatform \
-f snapshot_ref=main \
-f profile=hk_low_vol_dividend_quality \
-f data_source_mode=public_yfinance_staging \
-f allow_research_defaults=false \
-f execute_publish=false
```
Expand All @@ -114,6 +115,7 @@ gh workflow run build-hk-low-vol-snapshot-artifacts.yml \
--repo QuantStrategyLab/LongBridgePlatform \
-f snapshot_ref=main \
-f profile=hk_low_vol_dividend_quality \
-f data_source_mode=public_yfinance_staging \
-f allow_research_defaults=false \
-f gcs_prefix=gs://qsl-runtime-logs-interactivebrokersquant/strategy-artifacts/hk_equity/hk_low_vol_dividend_quality \
-f execute_publish=true
Expand All @@ -128,7 +130,7 @@ LONGBRIDGE_FEATURE_SNAPSHOT_PATH=gs://qsl-runtime-logs-interactivebrokersquant/s
LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH=gs://qsl-runtime-logs-interactivebrokersquant/strategy-artifacts/hk_equity/hk_low_vol_dividend_quality/hk_low_vol_dividend_quality_factor_snapshot_latest.csv.manifest.json
```

注意:`allow_research_defaults=true` 只允许做研究 smoke,不允许发布到 GCS,也不能作为 live-enable 证据。
注意:`allow_research_defaults=true` 只允许做研究 smoke,不允许发布到 GCS,也不能作为 live-enable 证据。public yfinance 数据源用于让 snapshot artifact 生成和券商执行解耦;它仍需要按策略证据包记录数据源、生成时间和 broker dry-run 结果。

## 部署或同步 HK Cloud Run

Expand Down
12 changes: 9 additions & 3 deletions tests/test_build_hk_low_vol_snapshot_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def test_hk_low_vol_snapshot_artifact_workflow_uses_longbridge_wif_and_snapshot_
assert "GCP_WORKLOAD_IDENTITY_PROVIDER: projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main" in workflow
assert "GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: longbridge-platform-deploy@longbridgequant.iam.gserviceaccount.com" in workflow
assert "SNAPSHOT_REPOSITORY: QuantStrategyLab/HkEquitySnapshotPipelines" in workflow
assert "data_source_mode" in workflow
assert "public_yfinance_staging" in workflow
assert "scripts/build_low_vol_dividend_public_factor_snapshot.py" in workflow
assert "python -m pip install -e '.[public-data]'" in workflow
assert "python -m pip install -e '.[longbridge]'" in workflow
assert 'gcloud secrets versions access latest --project="${LONGBRIDGE_SECRET_PROJECT_ID}" --secret="${LONGBRIDGE_APP_KEY_SECRET_NAME}"' in workflow
assert "scripts/build_low_vol_dividend_longbridge_factor_snapshot.py" in workflow
Expand All @@ -25,14 +29,16 @@ def test_hk_low_vol_snapshot_artifact_workflow_blocks_research_defaults_publish(

assert "allow_research_defaults=true is research smoke only and cannot be published to GCS." in workflow
assert 'if [ "${ALLOW_RESEARCH_DEFAULTS}" = "true" ] && [ "${EXECUTE_PUBLISH}" = "true" ]; then' in workflow
assert "Evidence boundary: validated LongBridge-generated CSVs can be runtime artifact inputs when allow_research_defaults=false" in workflow
assert "Evidence boundary: validated generated CSVs can be runtime artifact inputs when allow_research_defaults=false" in workflow


def test_hk_low_vol_snapshot_artifact_workflow_keeps_generation_diagnostics():
workflow = WORKFLOW_PATH.read_text(encoding="utf-8")

assert "LongBridge generation summary:" in workflow
assert 'generation_label="Public yfinance"' in workflow
assert 'generation_label="LongBridge"' in workflow
assert 'print(f"{sys.argv[2]} generation summary:")' in workflow
assert "failed_symbols_preview" in workflow
assert "LongBridge factor snapshot generation failed; see generation_summary.json" in workflow
assert '${generation_label} factor snapshot generation failed; see generation_summary.json' in workflow
assert "if: always() && steps.build.outputs.generated_input_name != ''" in workflow
assert "if-no-files-found: ignore" in workflow