Strategy Optimization Watcher #59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Strategy Optimization Watcher | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| source_repo: | |
| description: "Repository that owns strategy metrics and receives optimization issues" | |
| required: true | |
| default: "QuantStrategyLab/CryptoLivePoolPipelines" | |
| source_ref: | |
| description: "Source repository ref to inspect" | |
| required: false | |
| default: "main" | |
| metrics_path: | |
| description: "JSON metrics payload path inside the source repository" | |
| required: false | |
| default: "data/output/strategy_metrics.json" | |
| workflow_file: | |
| description: "Trusted source workflow file that produced the metrics artifact" | |
| required: false | |
| default: "monthly_publish.yml" | |
| metrics_filename: | |
| description: "Sanitized metrics filename inside each source artifact" | |
| required: false | |
| default: "strategy_metrics.json" | |
| dry_run: | |
| description: "Do not create GitHub issues" | |
| required: false | |
| type: boolean | |
| default: true | |
| schedule: | |
| - cron: "17 6 * * *" | |
| - cron: "23 6 * * *" | |
| permissions: | |
| contents: read | |
| actions: read | |
| issues: write | |
| id-token: write | |
| concurrency: | |
| group: strategy-optimization-watcher-${{ github.event.inputs.source_repo || vars.STRATEGY_WATCH_SOURCE_REPO || 'QuantStrategyLab/CryptoLivePoolPipelines' }} | |
| cancel-in-progress: false | |
| jobs: | |
| strategy-optimization-watcher: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| SOURCE_REPO: ${{ github.event.inputs.source_repo || vars.STRATEGY_WATCH_SOURCE_REPO || 'QuantStrategyLab/CryptoLivePoolPipelines' }} | |
| SOURCE_REF: ${{ github.event.inputs.source_ref || vars.STRATEGY_WATCH_SOURCE_REF || 'main' }} | |
| METRICS_PATH: ${{ github.event.schedule == '23 6 * * *' && 'data/output/soxl-p3-watcher-comparison.json' || github.event.inputs.metrics_path || vars.STRATEGY_WATCH_METRICS_PATH || 'data/output/strategy_metrics.json' }} | |
| SOURCE_WORKFLOW_FILE: ${{ github.event.schedule == '23 6 * * *' && 'soxl-p1-p3-daily-research.yml' || github.event.inputs.workflow_file || vars.STRATEGY_WATCH_WORKFLOW_FILE || 'monthly_publish.yml' }} | |
| METRICS_FILENAME: ${{ github.event.inputs.metrics_filename || vars.STRATEGY_WATCH_METRICS_FILENAME || 'strategy_metrics.json' }} | |
| TERMINAL_PROFILE: ${{ github.event.schedule == '23 6 * * *' && 'soxl_soxx_trend_income' || vars.STRATEGY_WATCH_TERMINAL_PROFILE || '' }} | |
| STRATEGY_WATCH_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && format('{0}', inputs.dry_run) || vars.STRATEGY_WATCH_DRY_RUN || 'true' }} | |
| # Keep the watcher opt-in (dry-run remains the default), while allowing | |
| # the canonical US-equity metrics producer to be selected without a | |
| # repository-variable migration. | |
| ALLOWED_SOURCE_REPOS: ${{ vars.STRATEGY_WATCH_ALLOWED_SOURCE_REPOS || 'QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/UsEquitySnapshotPipelines' }} | |
| ALLOWED_SOURCE_REFS: ${{ vars.STRATEGY_WATCH_ALLOWED_SOURCE_REFS || 'main' }} | |
| steps: | |
| - name: Checkout Bridge | |
| uses: actions/checkout@v6.0.3 | |
| with: | |
| path: bridge | |
| persist-credentials: false | |
| - name: Detect GitHub App Credentials | |
| id: app_credentials | |
| env: | |
| APP_ID: ${{ vars.CROSS_REPO_GITHUB_APP_ID }} | |
| APP_PRIVATE_KEY: ${{ secrets.CROSS_REPO_GITHUB_APP_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${APP_ID:-}" ] && [ -n "${APP_PRIVATE_KEY:-}" ]; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Resolve Source Repository Name | |
| id: source_repo | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "${SOURCE_REPO}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then | |
| echo "Invalid SOURCE_REPO: ${SOURCE_REPO}. Expected owner/name." >&2 | |
| exit 1 | |
| fi | |
| allowed_match=false | |
| IFS=',' read -ra allowed_repos <<< "${ALLOWED_SOURCE_REPOS}" | |
| for allowed_repo in "${allowed_repos[@]}"; do | |
| allowed_repo="${allowed_repo//[[:space:]]/}" | |
| if [ "${allowed_repo}" = "${SOURCE_REPO}" ]; then | |
| allowed_match=true | |
| break | |
| fi | |
| done | |
| if [ "${allowed_match}" != "true" ]; then | |
| echo "SOURCE_REPO is not allowed for strategy watcher: ${SOURCE_REPO}" >&2 | |
| exit 1 | |
| fi | |
| ref_allowed=false | |
| IFS=',' read -ra allowed_refs <<< "${ALLOWED_SOURCE_REFS}" | |
| for allowed_ref in "${allowed_refs[@]}"; do | |
| allowed_ref="${allowed_ref//[[:space:]]/}" | |
| if [ "${allowed_ref}" = "${SOURCE_REF}" ]; then | |
| ref_allowed=true | |
| break | |
| fi | |
| done | |
| if [ "${ref_allowed}" != "true" ]; then | |
| echo "SOURCE_REF is not allowed for strategy watcher: ${SOURCE_REF}" >&2 | |
| exit 1 | |
| fi | |
| owner="${SOURCE_REPO%%/*}" | |
| repository="${SOURCE_REPO#*/}" | |
| echo "owner=${owner}" >> "$GITHUB_OUTPUT" | |
| echo "repository=${repository}" >> "$GITHUB_OUTPUT" | |
| - name: Create GitHub App Token For Source Repository | |
| id: source_app_token | |
| if: steps.app_credentials.outputs.available == 'true' | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@v3.2.0 | |
| with: | |
| app-id: ${{ vars.CROSS_REPO_GITHUB_APP_ID }} | |
| private-key: ${{ secrets.CROSS_REPO_GITHUB_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.source_repo.outputs.owner }} | |
| repositories: ${{ steps.source_repo.outputs.repository }} | |
| permission-actions: read | |
| permission-contents: read | |
| permission-issues: write | |
| - name: Verify Source Repository Token | |
| env: | |
| SOURCE_APP_TOKEN: ${{ steps.source_app_token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${SOURCE_REPO}" != "${GITHUB_REPOSITORY}" ] && [ -z "${SOURCE_APP_TOKEN:-}" ]; then | |
| echo "Cross-repository strategy watcher requires CROSS_REPO_GITHUB_APP_ID and CROSS_REPO_GITHUB_APP_PRIVATE_KEY." >&2 | |
| exit 1 | |
| fi | |
| - name: Checkout Source Metrics | |
| uses: actions/checkout@v6.0.3 | |
| with: | |
| repository: ${{ env.SOURCE_REPO }} | |
| ref: ${{ env.SOURCE_REF }} | |
| path: source | |
| token: ${{ steps.source_app_token.outputs.token || github.token }} | |
| persist-credentials: false | |
| - name: Fetch comparable strategy metrics from trusted source artifacts | |
| id: fetch-metrics | |
| env: | |
| GH_TOKEN: ${{ steps.source_app_token.outputs.token || github.token }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "${SOURCE_WORKFLOW_FILE}" =~ ^[A-Za-z0-9_.-]+\.ya?ml$ ]]; then | |
| echo "Invalid SOURCE_WORKFLOW_FILE" >&2 | |
| exit 1 | |
| fi | |
| if [[ ! "${METRICS_FILENAME}" =~ ^[A-Za-z0-9_.-]+\.json$ ]]; then | |
| echo "Invalid METRICS_FILENAME" >&2 | |
| exit 1 | |
| fi | |
| echo "Looking for completed ${SOURCE_WORKFLOW_FILE} metrics on ${SOURCE_REF} in ${SOURCE_REPO}..." | |
| # Restrict to the canonical branch so we never consume metrics from a | |
| # feature-branch or PR workflow run. | |
| mapfile -t RUN_IDS < <(gh run list --repo "${SOURCE_REPO}" --workflow "${SOURCE_WORKFLOW_FILE}" --branch "${SOURCE_REF}" --status success --limit 2 --json databaseId --jq '.[].databaseId') | |
| if [ "${#RUN_IDS[@]}" -eq 0 ]; then | |
| echo "No successful source run on ${SOURCE_REF} in ${SOURCE_REPO} — metrics not available yet" | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| mkdir -p source/data/output | |
| if [ "${METRICS_FILENAME}" = "strategy_metrics.json" ]; then | |
| current_run_id="${RUN_IDS[0]}" | |
| if ! gh run download "${current_run_id}" --repo "${SOURCE_REPO}" --dir "source/data/output/_artifacts/${current_run_id}" 2>/dev/null; then | |
| echo "Artifact download from run ${current_run_id} failed" | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| metrics_file=$(find "source/data/output/_artifacts/${current_run_id}" -name "strategy_metrics.json" -type f -print -quit) | |
| if [ -z "${metrics_file}" ]; then | |
| echo "strategy_metrics.json not found in trusted source artifacts" | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| cp "${metrics_file}" "source/${METRICS_PATH}" | |
| echo "Downloaded compatible strategy_metrics.json from run ${current_run_id}" | |
| echo "downloaded=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "${#RUN_IDS[@]}" -lt 2 ]; then | |
| echo "Two completed observations are required before comparing strategy_performance.v2 metrics" | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| current_run_id="${RUN_IDS[0]}" | |
| baseline_run_id="${RUN_IDS[1]}" | |
| for run_id in "${current_run_id}" "${baseline_run_id}"; do | |
| if ! gh run download "${run_id}" --repo "${SOURCE_REPO}" --dir "source/data/output/_artifacts/${run_id}" 2>/dev/null; then | |
| echo "Artifact download from run ${run_id} failed" | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| done | |
| current_file=$(find "source/data/output/_artifacts/${current_run_id}" -name "${METRICS_FILENAME}" -type f -print -quit) | |
| baseline_file=$(find "source/data/output/_artifacts/${baseline_run_id}" -name "${METRICS_FILENAME}" -type f -print -quit) | |
| if [ -z "${current_file}" ] || [ -z "${baseline_file}" ]; then | |
| echo "Comparable ${METRICS_FILENAME} artifacts are not available yet" | |
| terminal_file=$(find "source/data/output/_artifacts/${current_run_id}" -name "p1-status.json" -type f -print -quit) | |
| if [ -n "${terminal_file}" ]; then | |
| case "${SOURCE_WORKFLOW_FILE}" in | |
| soxl-p1-p3-daily-research.yml) | |
| echo "terminal_profile=${TERMINAL_PROFILE:-soxl_soxx_trend_income}" >> "$GITHUB_OUTPUT" | |
| ;; | |
| esac | |
| echo "terminal_status_path=${terminal_file#source/}" >> "$GITHUB_OUTPUT" | |
| echo "Found P1 terminal status for unavailable comparable metrics" | |
| fi | |
| echo "downloaded=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| python bridge/scripts/build_strategy_watcher_artifact_payload.py \ | |
| --current "${current_file}" \ | |
| --baseline "${baseline_file}" \ | |
| --source-repository "${SOURCE_REPO}" \ | |
| --workflow-file "${SOURCE_WORKFLOW_FILE}" \ | |
| --current-run-id "${current_run_id}" \ | |
| --baseline-run-id "${baseline_run_id}" \ | |
| --output "source/${METRICS_PATH}" | |
| echo "Built comparable watcher payload from ${baseline_run_id} and ${current_run_id}" | |
| echo "downloaded=true" >> "$GITHUB_OUTPUT" | |
| - name: Run Strategy Optimization Watcher | |
| env: | |
| GH_TOKEN: ${{ steps.source_app_token.outputs.token || github.token }} | |
| STRATEGY_WATCH_SOURCE_ROOT: ${{ github.workspace }}/source | |
| STRATEGY_WATCH_METRICS_PATH: ${{ env.METRICS_PATH }} | |
| STRATEGY_WATCH_SOURCE_REPO: ${{ env.SOURCE_REPO }} | |
| STRATEGY_WATCH_TERMINAL_STATUS_PATH: ${{ steps.fetch-metrics.outputs.terminal_status_path }} | |
| STRATEGY_WATCH_TERMINAL_PROFILE: ${{ steps.fetch-metrics.outputs.terminal_profile }} | |
| working-directory: bridge | |
| run: | | |
| set -euo pipefail | |
| mkdir -p data/output/strategy_optimization_watcher | |
| # Module invocation keeps the repository root on sys.path even when | |
| # the runner changes its working directory or wraps this step. | |
| python -m scripts.run_strategy_optimization_watcher | tee data/output/strategy_optimization_watcher/result.json | |
| - name: Run one bounded AI research diagnosis | |
| if: steps.fetch-metrics.outputs.downloaded == 'true' && github.event_name == 'schedule' | |
| env: | |
| CODEX_AUDIT_SERVICE_URL: ${{ secrets.CODEX_AUDIT_SERVICE_URL }} | |
| CODEX_AUDIT_SERVICE_AUDIENCE: ${{ vars.CODEX_AUDIT_SERVICE_AUDIENCE || 'quant-codex-audit' }} | |
| DEFAULT_ANALYZE_MODEL: ${{ vars.RESEARCH_DIAGNOSIS_MODEL || 'gpt-5.4-mini' }} | |
| RESEARCH_DIAGNOSIS_MAX_PER_RUN: "1" | |
| working-directory: bridge | |
| run: | | |
| set -euo pipefail | |
| python -m scripts.run_research_task_diagnosis \ | |
| --input data/output/strategy_optimization_watcher/result.json \ | |
| --max-per-run "${RESEARCH_DIAGNOSIS_MAX_PER_RUN}" \ | |
| | tee data/output/strategy_optimization_watcher/research-diagnosis.json | |
| - name: Extract bounded research task source snapshot | |
| if: success() | |
| working-directory: bridge | |
| run: | | |
| set -euo pipefail | |
| python - data/output/strategy_optimization_watcher/result.json data/output/strategy_optimization_watcher/research-task-source.json <<'PY' | |
| import json | |
| import sys | |
| source = json.load(open(sys.argv[1], encoding="utf-8")) | |
| snapshot = source.get("research_task_source_snapshot") | |
| if not isinstance(snapshot, dict) or snapshot.get("schema_version") != "qsl_research_task_source_snapshot.v1": | |
| raise SystemExit("watcher did not emit a bounded research task source snapshot") | |
| with open(sys.argv[2], "w", encoding="utf-8") as handle: | |
| json.dump(snapshot, handle, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False) | |
| PY | |
| - name: Publish research task index to the unified console | |
| if: success() && github.event_name == 'schedule' | |
| env: | |
| RESEARCH_TASK_SYNC_URL: ${{ vars.QSL_RESEARCH_TASK_SYNC_URL }} | |
| RESEARCH_TASK_SYNC_TOKEN: ${{ secrets.QSL_RESEARCH_TASK_SYNC_TOKEN }} | |
| working-directory: bridge | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${RESEARCH_TASK_SYNC_URL:-}" ] || [ -z "${RESEARCH_TASK_SYNC_TOKEN:-}" ]; then | |
| echo 'RESEARCH_TASK_SYNC_STATUS=NOT_CONFIGURED' >> "$GITHUB_STEP_SUMMARY" | |
| echo 'Research task index is not configured; issue-only watcher behavior remains unchanged.' >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| input_path="data/output/strategy_optimization_watcher/research-task-source.json" | |
| response_path="data/output/strategy_optimization_watcher/research-task-response.json" | |
| curl --fail --show-error --silent --retry 3 --retry-all-errors --connect-timeout 10 \ | |
| --request POST \ | |
| --header "Authorization: Bearer ${RESEARCH_TASK_SYNC_TOKEN}" \ | |
| --header 'Content-Type: application/json' \ | |
| --header 'User-Agent: QuantStrategyLab-AIAuditBridge-ResearchTask/1' \ | |
| --data-binary "@$input_path" \ | |
| --output "$response_path" \ | |
| "${RESEARCH_TASK_SYNC_URL%/}/api/internal/sync-research-task-source" | |
| python - "$response_path" <<'PY' | |
| import json | |
| import sys | |
| response = json.load(open(sys.argv[1], encoding="utf-8")) | |
| if response.get("ok") is not True or response.get("schema_version") != "qsl_research_task_source_snapshot.v1": | |
| raise SystemExit("unexpected research task source response") | |
| PY | |
| echo 'RESEARCH_TASK_SYNC_STATUS=PUBLISHED' >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload watcher diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: strategy-optimization-watcher-${{ github.run_id }} | |
| path: bridge/data/output/strategy_optimization_watcher/ | |
| if-no-files-found: warn |