Skip to content

Strategy Optimization Watcher #61

Strategy Optimization Watcher

Strategy Optimization Watcher #61

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