Publish Intelligent Advisory Site #40
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: Publish Intelligent Advisory Site | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| as_of: | |
| description: "Report date. Defaults to current UTC date when empty." | |
| required: false | |
| type: string | |
| site_url: | |
| description: "Public site URL used in RSS links." | |
| required: false | |
| default: "https://quantstrategylab.github.io/QuantAdvisorResearch" | |
| type: string | |
| political_events_path: | |
| description: "Path inside PoliticalEventTrackingResearch." | |
| required: false | |
| default: "data/live/political_events.csv" | |
| type: string | |
| political_watchlist_path: | |
| description: "Path inside PoliticalEventTrackingResearch." | |
| required: false | |
| default: "data/live/political_watchlist.csv" | |
| type: string | |
| ai_signal_path: | |
| description: "Path inside ResearchSignalContextPipelines." | |
| required: false | |
| default: "data/output/latest_signal.json" | |
| type: string | |
| theme_momentum_path: | |
| description: "Optional path inside ResearchSignalContextPipelines. Empty disables theme momentum context." | |
| required: false | |
| default: "data/output/theme_momentum_snapshot.json" | |
| type: string | |
| market_confirmation_path: | |
| description: "Optional market confirmation CSV path inside advisor repository. Empty disables this input." | |
| required: false | |
| default: "" | |
| type: string | |
| market_data_proxy_urls: | |
| description: "Optional comma/newline-separated public proxy URLs for free market data fetches." | |
| required: false | |
| default: "" | |
| type: string | |
| market_data_proxy_pool_url: | |
| description: "Optional public text URL returning one proxy per line for free market data fetches." | |
| required: false | |
| default: "" | |
| type: string | |
| schedule: | |
| - cron: "0 13 * * 6" | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| jobs: | |
| build-site: | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Checkout advisor repository | |
| uses: actions/checkout@v6 | |
| with: | |
| path: advisor | |
| - name: Checkout political event repository | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: QuantStrategyLab/PoliticalEventTrackingResearch | |
| path: political-events | |
| - name: Checkout signal context repository | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: QuantStrategyLab/ResearchSignalContextPipelines | |
| path: research-signal-context | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| - name: Install advisor package | |
| working-directory: advisor | |
| run: python -m pip install -e . | |
| - name: Build report and static site | |
| id: build_report | |
| working-directory: advisor | |
| env: | |
| INPUT_AS_OF: ${{ github.event.inputs.as_of || '' }} | |
| SITE_URL: ${{ github.event.inputs.site_url || 'https://quantstrategylab.github.io/QuantAdvisorResearch' }} | |
| POLITICAL_EVENTS_PATH: ${{ github.event.inputs.political_events_path || 'data/live/political_events.csv' }} | |
| POLITICAL_WATCHLIST_PATH: ${{ github.event.inputs.political_watchlist_path || 'data/live/political_watchlist.csv' }} | |
| AI_SIGNAL_PATH: ${{ github.event.inputs.ai_signal_path || 'data/output/latest_signal.json' }} | |
| THEME_MOMENTUM_PATH: ${{ github.event.inputs.theme_momentum_path || 'data/output/theme_momentum_snapshot.json' }} | |
| MARKET_CONFIRMATION_PATH: ${{ github.event.inputs.market_confirmation_path || '' }} | |
| MARKET_DATA_PROXY_URLS: ${{ github.event.inputs.market_data_proxy_urls || vars.MARKET_DATA_PROXY_URLS || '' }} | |
| MARKET_DATA_PROXY_POOL_URL: ${{ github.event.inputs.market_data_proxy_pool_url || vars.MARKET_DATA_PROXY_POOL_URL || '' }} | |
| run: | | |
| set -euo pipefail | |
| AS_OF="${INPUT_AS_OF:-$(date -u +%F)}" | |
| mkdir -p data/output/published site | |
| THEME_ARGS=() | |
| RESOLVED_THEME_MOMENTUM="" | |
| if [ -n "${THEME_MOMENTUM_PATH}" ] && [ -f "../research-signal-context/${THEME_MOMENTUM_PATH}" ]; then | |
| RESOLVED_THEME_MOMENTUM="../research-signal-context/${THEME_MOMENTUM_PATH}" | |
| THEME_ARGS=(--theme-momentum "${RESOLVED_THEME_MOMENTUM}") | |
| fi | |
| MARKET_ARGS=() | |
| if [ -n "${MARKET_CONFIRMATION_PATH}" ] && [ -f "${MARKET_CONFIRMATION_PATH}" ]; then | |
| MARKET_ARGS=(--market-confirmation "${MARKET_CONFIRMATION_PATH}") | |
| else | |
| GENERATED_MARKET_CONFIRMATION="data/output/published/market_confirmation_${AS_OF}.csv" | |
| MARKET_BUILD_ARGS=( | |
| --as-of "${AS_OF}" | |
| --political-watchlist "../political-events/${POLITICAL_WATCHLIST_PATH}" | |
| --ai-signal "../research-signal-context/${AI_SIGNAL_PATH}" | |
| --output "${GENERATED_MARKET_CONFIRMATION}" | |
| ) | |
| if [ -n "${RESOLVED_THEME_MOMENTUM}" ]; then | |
| MARKET_BUILD_ARGS+=(--theme-momentum "${RESOLVED_THEME_MOMENTUM}") | |
| fi | |
| if [ -n "${MARKET_DATA_PROXY_URLS}" ]; then | |
| MARKET_BUILD_ARGS+=(--proxy-urls "${MARKET_DATA_PROXY_URLS}") | |
| fi | |
| if [ -n "${MARKET_DATA_PROXY_POOL_URL}" ]; then | |
| MARKET_BUILD_ARGS+=(--proxy-pool-url "${MARKET_DATA_PROXY_POOL_URL}") | |
| fi | |
| python scripts/build_market_confirmation.py "${MARKET_BUILD_ARGS[@]}" | |
| MARKET_ARGS=(--market-confirmation "${GENERATED_MARKET_CONFIRMATION}") | |
| fi | |
| python scripts/build_advisory_report.py \ | |
| --as-of "${AS_OF}" \ | |
| --cadence weekly \ | |
| --political-events "../political-events/${POLITICAL_EVENTS_PATH}" \ | |
| --political-watchlist "../political-events/${POLITICAL_WATCHLIST_PATH}" \ | |
| --ai-signal "../research-signal-context/${AI_SIGNAL_PATH}" \ | |
| "${THEME_ARGS[@]}" \ | |
| "${MARKET_ARGS[@]}" \ | |
| --output-json "data/output/published/advisory_report_${AS_OF}.json" \ | |
| --output-md "data/output/published/advisory_report_${AS_OF}.md" | |
| REPORT_ARGS=("data/output/published/advisory_report_${AS_OF}.json") | |
| HISTORY_DIR="data/output/published/history" | |
| mkdir -p "${HISTORY_DIR}" | |
| HISTORY_INDEX="${HISTORY_DIR}/reports_index.json" | |
| if curl -fsL "${SITE_URL%/}/reports_index.json" -o "${HISTORY_INDEX}"; then | |
| python - "${SITE_URL}" "${HISTORY_INDEX}" "${HISTORY_DIR}" "${AS_OF}" <<'PY' | |
| import json | |
| import re | |
| import sys | |
| import urllib.parse | |
| import urllib.request | |
| from pathlib import Path | |
| site_url, index_path, history_dir, current_as_of = sys.argv[1:] | |
| history_dir_path = Path(history_dir) | |
| index = json.loads(Path(index_path).read_text(encoding="utf-8")) | |
| paths = [] | |
| seen_as_of = {current_as_of} | |
| for item in index.get("reports", []): | |
| as_of = str(item.get("as_of", "")).strip() | |
| json_name = Path(str(item.get("json", ""))).name | |
| if not as_of or as_of in seen_as_of: | |
| continue | |
| if not re.fullmatch(r"advisory_report_\d{4}-\d{2}-\d{2}\.json", json_name): | |
| continue | |
| seen_as_of.add(as_of) | |
| url = f"{site_url.rstrip('/')}/{urllib.parse.quote(json_name)}" | |
| destination = history_dir_path / json_name | |
| try: | |
| urllib.request.urlretrieve(url, destination) | |
| payload = json.loads(destination.read_text(encoding="utf-8")) | |
| except Exception as exc: # noqa: BLE001 - best-effort public archive recovery | |
| print(f"Skipped historical report {json_name}: {exc}") | |
| continue | |
| if str(payload.get("as_of", "")) != as_of: | |
| print(f"Skipped historical report {json_name}: as_of mismatch") | |
| continue | |
| paths.append(str(destination)) | |
| (history_dir_path / "report_paths.txt").write_text("\n".join(paths), encoding="utf-8") | |
| print(f"Recovered {len(paths)} historical reports from published site.") | |
| PY | |
| if [ -s "${HISTORY_DIR}/report_paths.txt" ]; then | |
| mapfile -t HISTORY_REPORTS < "${HISTORY_DIR}/report_paths.txt" | |
| REPORT_ARGS+=("${HISTORY_REPORTS[@]}") | |
| fi | |
| else | |
| echo "No existing reports_index.json found on published site; starting archive with current report." | |
| fi | |
| python scripts/publish_advisory_site.py \ | |
| --reports "${REPORT_ARGS[@]}" \ | |
| --output-dir site \ | |
| --site-url "${SITE_URL}" \ | |
| --feed-title "智慧投顾研究系统" | |
| for REPORT_JSON in "${REPORT_ARGS[@]}"; do | |
| cp "${REPORT_JSON}" site/ | |
| done | |
| cp "data/output/published/advisory_report_${AS_OF}.md" site/ | |
| cp "data/output/published/advisory_report_${AS_OF}.json.manifest.json" site/ | |
| echo "as_of=${AS_OF}" >> "$GITHUB_OUTPUT" | |
| echo "report_path=data/output/published/advisory_report_${AS_OF}.json" >> "$GITHUB_OUTPUT" | |
| - name: Upload site artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: model-recommendations-site | |
| path: advisor/site | |
| if-no-files-found: error | |
| - name: Configure Pages | |
| uses: actions/configure-pages@v6 | |
| - name: Upload Pages artifact | |
| uses: actions/upload-pages-artifact@v5 | |
| with: | |
| path: advisor/site | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v5 | |
| - name: Notify Telegram subscribers | |
| if: ${{ success() }} | |
| continue-on-error: true | |
| working-directory: advisor | |
| env: | |
| TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} | |
| TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} | |
| SITE_URL: ${{ github.event.inputs.site_url || 'https://quantstrategylab.github.io/QuantAdvisorResearch' }} | |
| REPORT_PATH: ${{ steps.build_report.outputs.report_path }} | |
| run: | | |
| set -euo pipefail | |
| python scripts/notify_advisory_telegram.py \ | |
| --report "${REPORT_PATH}" \ | |
| --site-url "${SITE_URL}" |