Skip to content

Build HK Low-Vol Snapshot Artifacts #9

Build HK Low-Vol Snapshot Artifacts

Build HK Low-Vol Snapshot Artifacts #9

name: Build HK Low-Vol Snapshot Artifacts
on:
workflow_dispatch:
inputs:
snapshot_ref:
description: "HkEquitySnapshotPipelines ref to use."
required: true
default: main
type: string
profile:
description: "HK snapshot profile to build."
required: true
default: hk_low_vol_dividend_quality_snapshot
type: choice
options:
- hk_low_vol_dividend_quality_snapshot
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
default: examples/low_vol_dividend_quality/longbridge_universe.seed.csv
type: string
as_of_date:
description: "Optional snapshot date, for example 2026-06-03."
required: false
type: string
history_start:
description: "Optional generated-source history start date, for example 2025-01-01."
required: false
type: string
benchmark_symbol:
description: "HK benchmark symbol for beta/suspension metrics."
required: false
default: 2800.HK
type: string
allow_research_defaults:
description: "Fill missing non-price fields for research smoke only; cannot be published to GCS."
required: true
default: false
type: boolean
artifact_dir:
description: "Artifact output directory inside HkEquitySnapshotPipelines."
required: false
default: data/output/hk_low_vol_dividend_quality_snapshot
type: string
gcs_prefix:
description: "Optional GCS prefix for validated runtime artifacts."
required: false
default: gs://qsl-runtime-logs-interactivebrokersquant/strategy-artifacts/hk_equity/hk_low_vol_dividend_quality_snapshot
type: string
execute_publish:
description: "Actually upload validated artifacts to GCS. false only prints the publish plan."
required: true
default: false
type: boolean
min_adv20_hkd:
description: "Optional minimum 20-day ADV filter in HKD."
required: false
type: string
min_market_cap_hkd:
description: "Optional minimum market-cap filter in HKD."
required: false
type: string
longbridge_secret_project_id:
description: "GCP project that stores LongBridge HK Secret Manager secrets."
required: false
default: longbridgequant
type: string
longbridge_app_key_secret_name:
description: "Secret Manager secret name for LongBridge app key."
required: false
default: longport-app-key-hk
type: string
longbridge_app_secret_secret_name:
description: "Secret Manager secret name for LongBridge app secret."
required: false
default: longport-app-secret-hk
type: string
longbridge_access_token_secret_name:
description: "Secret Manager secret name for LongBridge access token."
required: false
default: longport_token_hk
type: string
concurrency:
group: hk-low-vol-snapshot-${{ github.ref }}
cancel-in-progress: false
env:
GCP_PROJECT_ID: longbridgequant
GCP_WORKLOAD_IDENTITY_PROVIDER: projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main
GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: longbridge-platform-deploy@longbridgequant.iam.gserviceaccount.com
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 }}
BENCHMARK_SYMBOL: ${{ inputs.benchmark_symbol }}
ALLOW_RESEARCH_DEFAULTS: ${{ inputs.allow_research_defaults }}
ARTIFACT_DIR_INPUT: ${{ inputs.artifact_dir }}
GCS_PREFIX: ${{ inputs.gcs_prefix }}
EXECUTE_PUBLISH: ${{ inputs.execute_publish }}
MIN_ADV20_HKD: ${{ inputs.min_adv20_hkd }}
MIN_MARKET_CAP_HKD: ${{ inputs.min_market_cap_hkd }}
LONGBRIDGE_SECRET_PROJECT_ID: ${{ inputs.longbridge_secret_project_id }}
LONGBRIDGE_APP_KEY_SECRET_NAME: ${{ inputs.longbridge_app_key_secret_name }}
LONGBRIDGE_APP_SECRET_SECRET_NAME: ${{ inputs.longbridge_app_secret_secret_name }}
LONGBRIDGE_ACCESS_TOKEN_SECRET_NAME: ${{ inputs.longbridge_access_token_secret_name }}
jobs:
build:
name: Build HK snapshot artifacts
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
environment: longbridge-hk
steps:
- name: Checkout LongBridgePlatform
uses: actions/checkout@v6
- name: Checkout HkEquitySnapshotPipelines
uses: actions/checkout@v6
with:
repository: ${{ env.SNAPSHOT_REPOSITORY }}
ref: ${{ inputs.snapshot_ref }}
path: ${{ env.SNAPSHOT_REPO_DIR }}
- name: Validate request
run: |
set -euo pipefail
case "${PROFILE}" in
hk_low_vol_dividend_quality_snapshot) ;;
*)
echo "Unsupported HK snapshot profile: ${PROFILE}" >&2
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
exit 1
fi
if [ "${EXECUTE_PUBLISH}" = "true" ] && [ -z "${GCS_PREFIX:-}" ]; then
echo "gcs_prefix is required when execute_publish=true." >&2
exit 1
fi
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v3
with:
project_id: ${{ env.GCP_PROJECT_ID }}
version: '>= 416.0.0'
- name: Install HK snapshot package
working-directory: ${{ env.SNAPSHOT_REPO_DIR }}
run: |
set -euo pipefail
python -m pip install --upgrade pip
case "${DATA_SOURCE_MODE}" in
public_yfinance_staging)
python -m pip install -e '.[public-data]'
;;
longbridge_openapi_staging)
python -m pip install -e '.[longbridge]'
;;
esac
- name: Build, validate, and optionally publish artifacts
id: build
working-directory: ${{ env.SNAPSHOT_REPO_DIR }}
run: |
set -euo pipefail
generated_dir="data/input/generated/${PROFILE}"
artifact_dir="${ARTIFACT_DIR_INPUT:-data/output/${PROFILE}}"
mkdir -p "${generated_dir}" "${artifact_dir}"
snapshot_uri="${GCS_PREFIX%/}/hk_low_vol_dividend_quality_snapshot_factor_snapshot_latest.csv"
manifest_uri="${GCS_PREFIX%/}/hk_low_vol_dividend_quality_snapshot_factor_snapshot_latest.csv.manifest.json"
{
echo "artifact_dir=${SNAPSHOT_REPO_DIR}/${artifact_dir}"
echo "generated_input_dir=${SNAPSHOT_REPO_DIR}/${generated_dir}"
echo "artifact_name=hk-low-vol-dividend-quality-artifacts-${GITHUB_RUN_ID}"
echo "generated_input_name=hk-low-vol-dividend-quality-input-${GITHUB_RUN_ID}"
echo "snapshot_gcs_uri=${snapshot_uri}"
echo "manifest_gcs_uri=${manifest_uri}"
} >> "${GITHUB_OUTPUT}"
universe_path="${UNIVERSE_PATH:-examples/low_vol_dividend_quality/longbridge_universe.seed.csv}"
case "${universe_path}" in
gs://*|https://*)
resolved_universe_dir="data/input/resolved/${PROFILE}/universe"
resolved_env="${RUNNER_TEMP:-/tmp}/resolved_hk_universe.env"
python scripts/resolve_hk_snapshot_inputs.py \
--factor-snapshot "${universe_path}" \
--output-dir "${resolved_universe_dir}" \
--env-output "${resolved_env}"
# shellcheck disable=SC1090
source "${resolved_env}"
universe_path="${FACTOR_SNAPSHOT_PATH}"
;;
*)
if [ ! -f "${universe_path}" ]; then
echo "universe CSV not found: ${universe_path}" >&2
exit 1
fi
;;
esac
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
generator_args=(
--universe "${universe_path}"
--output "${generated_path}"
--benchmark-symbol "${BENCHMARK_SYMBOL:-2800.HK}"
--json
)
if [ -n "${AS_OF_DATE:-}" ]; then
generator_args+=(--as-of "${AS_OF_DATE}")
fi
if [ -n "${HISTORY_START:-}" ]; then
generator_args+=(--history-start "${HISTORY_START}")
fi
if [ "${ALLOW_RESEARCH_DEFAULTS}" = "true" ]; then
generator_args+=(--allow-research-defaults)
fi
set +e
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" "${generation_label}"
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
print(f"{sys.argv[2]} generation summary:")
for key in (
"profile",
"row_count",
"symbols_requested",
"symbols_failed",
"source_quality",
"artifact_evidence_role",
"live_enablement_evidence_status",
"live_order_approval_status",
):
if key in payload:
print(f"- {key}: {payload[key]}")
failed = payload.get("failed_symbols") or []
if failed:
print("- failed_symbols_preview:")
for item in failed[:10]:
print(f" - {item}")
if len(failed) > 10:
print(f" - ... {len(failed) - 10} more")
PY
fi
if [ "${generator_status}" -ne 0 ]; then
echo "${generation_label} factor snapshot generation failed; see generation_summary.json in the generated input artifact." >&2
exit "${generator_status}"
fi
build_args=(
--factor-snapshot "${generated_path}"
--output-dir "${artifact_dir}"
)
if [ -n "${MIN_ADV20_HKD:-}" ]; then
build_args+=(--min-adv20-hkd "${MIN_ADV20_HKD}")
fi
if [ -n "${MIN_MARKET_CAP_HKD:-}" ]; then
build_args+=(--min-market-cap-hkd "${MIN_MARKET_CAP_HKD}")
fi
hkeq-build-low-vol-dividend-quality-snapshot "${build_args[@]}"
hkeq-validate-snapshot-artifact-pack \
--profile "${PROFILE}" \
--artifact-dir "${artifact_dir}" \
--json > "${artifact_dir}/artifact_pack_validation.json"
cp "${generated_dir}/generation_summary.json" "${artifact_dir}/source_input_summary.json"
if [ -n "${GCS_PREFIX:-}" ]; then
publish_args=(
--profile "${PROFILE}"
--artifact-dir "${artifact_dir}"
--gcs-prefix "${GCS_PREFIX}"
)
if [ "${EXECUTE_PUBLISH}" = "true" ]; then
publish_args+=(--execute)
fi
python scripts/publish_hk_snapshot_artifacts.py "${publish_args[@]}" | tee "${artifact_dir}/publish_plan.txt"
else
echo "gcs_prefix is not set; skipping GCS publish." | tee "${artifact_dir}/publish_plan.txt"
fi
- name: Upload validated artifact pack
if: always() && steps.build.outputs.artifact_name != ''
uses: actions/upload-artifact@v7
with:
name: ${{ steps.build.outputs.artifact_name }}
path: ${{ steps.build.outputs.artifact_dir }}
if-no-files-found: ignore
retention-days: 14
- name: Upload generated input snapshot
if: always() && steps.build.outputs.generated_input_name != ''
uses: actions/upload-artifact@v7
with:
name: ${{ steps.build.outputs.generated_input_name }}
path: ${{ steps.build.outputs.generated_input_dir }}
if-no-files-found: ignore
retention-days: 14
- name: Append job summary
run: |
set -euo pipefail
{
echo "## HK low-vol dividend snapshot artifacts"
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>}\`"
echo "- execute_publish: \`${EXECUTE_PUBLISH}\`"
echo "- artifact: \`${{ steps.build.outputs.artifact_name }}\`"
echo "- generated input: \`${{ steps.build.outputs.generated_input_name }}\`"
echo
if [ "${EXECUTE_PUBLISH}" = "true" ] && [ -n "${GCS_PREFIX:-}" ]; then
echo "Published runtime artifact URIs:"
echo
echo "- snapshot: \`${{ steps.build.outputs.snapshot_gcs_uri }}\`"
echo "- manifest: \`${{ steps.build.outputs.manifest_gcs_uri }}\`"
echo
echo "Suggested longbridge-hk environment values for dry-run runtime:"
echo
echo "\`\`\`bash"
echo "STRATEGY_PROFILE=hk_low_vol_dividend_quality_snapshot"
echo "LONGBRIDGE_DRY_RUN_ONLY=true"
echo "LONGBRIDGE_FEATURE_SNAPSHOT_PATH=${{ steps.build.outputs.snapshot_gcs_uri }}"
echo "LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH=${{ steps.build.outputs.manifest_gcs_uri }}"
echo "\`\`\`"
echo
fi
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}"