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
34 changes: 33 additions & 1 deletion .github/workflows/execution-report-heartbeat.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
name: Execution Report Heartbeat

# Schedule disabled; trade/error notifications own unattended alerting.
on:
workflow_dispatch:
inputs:
Expand All @@ -17,6 +16,8 @@ on:
options:
- "true"
- "false"
schedule:
- cron: "20 22 * * *"

env:
GCP_PROJECT_ID: firstradequant
Expand All @@ -41,8 +42,20 @@ jobs:
RUNTIME_HEARTBEAT_FAIL_WORKFLOW_ON_ALERT: ${{ inputs.fail_workflow_on_alert || vars.RUNTIME_HEARTBEAT_FAIL_WORKFLOW_ON_ALERT || 'true' }}
RUNTIME_HEARTBEAT_ACCEPT_STAGES: ${{ vars.RUNTIME_HEARTBEAT_ACCEPT_STAGES }}
RUNTIME_HEARTBEAT_REJECT_STAGES: ${{ vars.RUNTIME_HEARTBEAT_REJECT_STAGES }}
RUNTIME_HEARTBEAT_MARKET_AWARE: ${{ vars.RUNTIME_HEARTBEAT_MARKET_AWARE || 'true' }}
RUNTIME_HEARTBEAT_MARKET_CALENDAR: ${{ vars.FIRSTRADE_MARKET_CALENDAR }}
RUNTIME_HEARTBEAT_MARKET_TIMEZONE: ${{ vars.FIRSTRADE_MARKET_TIMEZONE }}
RUNTIME_HEARTBEAT_PUBLICATION_GRACE_MINUTES: ${{ vars.RUNTIME_HEARTBEAT_PUBLICATION_GRACE_MINUTES || '30' }}
RUNTIME_HEARTBEAT_SCHEDULER_AWARE: ${{ vars.RUNTIME_HEARTBEAT_SCHEDULER_AWARE || 'true' }}
RUNTIME_HEARTBEAT_SCHEDULER_LOCATION: ${{ vars.RUNTIME_HEARTBEAT_SCHEDULER_LOCATION || vars.CLOUD_RUN_REGION || 'us-central1' }}
RUNTIME_TARGET_ENABLED: ${{ vars.RUNTIME_TARGET_ENABLED }}
RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}
CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
CLOUD_RUN_SERVICES: ${{ vars.CLOUD_RUN_SERVICES }}
CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON }}
Comment thread
Pigbibi marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include per-target execution report URIs

Although the workflow now exports the repository-level EXECUTION_REPORT_GCS_URI, per-service configurations may validly set that value under each target's env (the deployment plan resolves target/default values before the global fallback). When targets write to different buckets or the global variable is unset, this heartbeat scans only the global URI—or falls back to strategy-runs—so valid reports for those targets are never found and the scheduled workflow emits false missing-report alerts. Extract all enabled targets' effective report URIs from CLOUD_RUN_SERVICE_TARGETS_JSON as well.

Useful? React with 👍 / 👎.

CLOUD_SCHEDULER_MAIN_TIME: ${{ vars.CLOUD_SCHEDULER_MAIN_TIME }}
EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }}
FIRSTRADE_GCS_STATE_BUCKET: ${{ vars.FIRSTRADE_GCS_STATE_BUCKET }}
FIRSTRADE_STATE_PREFIX: ${{ vars.FIRSTRADE_STATE_PREFIX }}
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
Expand All @@ -53,6 +66,19 @@ jobs:
uses: actions/checkout@v6

- name: Authenticate to Google Cloud
id: gcp_auth_primary
continue-on-error: true
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}

- name: Wait before Google Cloud authentication retry
if: ${{ steps.gcp_auth_primary.outcome == 'failure' }}
run: sleep 10

- name: Retry Google Cloud authentication
if: ${{ steps.gcp_auth_primary.outcome == 'failure' }}
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
Expand All @@ -61,5 +87,11 @@ jobs:
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v3

- name: Install market calendar
continue-on-error: true
run: >-
python -m pip install --disable-pip-version-check
--retries 3 --timeout 30 "pandas-market-calendars==5.4.0"

- name: Check recent execution report
run: python scripts/execution_report_heartbeat.py
13 changes: 13 additions & 0 deletions .github/workflows/runtime-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ jobs:
uses: actions/checkout@v6

- name: Authenticate to Google Cloud
id: gcp_auth_primary
continue-on-error: true
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}

- name: Wait before Google Cloud authentication retry
if: ${{ steps.gcp_auth_primary.outcome == 'failure' }}
run: sleep 10

- name: Retry Google Cloud authentication
if: ${{ steps.gcp_auth_primary.outcome == 'failure' }}
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
QSL_ENABLE_CLOUD_RUN_AUTOMATION: ${{ vars.QSL_ENABLE_CLOUD_RUN_AUTOMATION }}
CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON }}
CLOUD_SCHEDULER_LOCATION: ${{ vars.CLOUD_SCHEDULER_LOCATION }}
CLOUD_SCHEDULER_MAIN_TIME: ${{ vars.CLOUD_SCHEDULER_MAIN_TIME }}
CLOUD_SCHEDULER_PROBE_TIME: ${{ vars.CLOUD_SCHEDULER_PROBE_TIME }}
Expand Down
14 changes: 12 additions & 2 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,19 @@ def publish_log(text: str) -> None:
except TypeError:
log_message(text)

delivery_sent = True

def send_and_capture(text: str) -> bool | None:
nonlocal delivery_sent
outcome = sender(text)
delivery_sent = outcome is not False
return outcome

NotificationPublisher(
log_message=publish_log,
send_message=sender,
send_message=send_and_capture,
).publish(RenderedNotification(detailed_text=message, compact_text=message))
return True
return delivery_sent


def _should_publish_cycle_notification(result: Mapping[str, Any]) -> bool:
Expand Down Expand Up @@ -733,6 +741,8 @@ def log_message(message: str) -> None:
settings=settings,
notification_sender=notification_sender,
)
if not result["notification_sent"]:
result["notification_error"] = "delivery_not_acknowledged"
except Exception as exc:
result["notification_sent"] = False
result["notification_error"] = f"{type(exc).__name__}: {exc}"
Expand Down
17 changes: 14 additions & 3 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,18 +553,29 @@ def build_sender(token: str | None, chat_id: str | None, *, requests_module=None
if requests_module is None:
import requests as requests_module

def send_tg_message(message: str) -> None:
def send_tg_message(message: str) -> bool:
if not token or not chat_id:
return
return False
url = f"https://api.telegram.org/bot{token}/sendMessage"
try:
requests_module.post(
response = requests_module.post(
url,
json={"chat_id": chat_id, "text": _break_telegram_market_symbol_auto_links(message)},
timeout=15,
)
status_code = int(getattr(response, "status_code", 200) or 200)
if status_code < 200 or status_code >= 300:
print(f"Telegram send failed: HTTP {status_code}", flush=True)
return False
load_payload = getattr(response, "json", None)
payload = load_payload() if callable(load_payload) else None
if isinstance(payload, Mapping) and payload.get("ok") is False:
print("Telegram send failed: negative API acknowledgement", flush=True)
return False
except Exception as exc:
print(f"Telegram send failed: {type(exc).__name__}", flush=True)
return False
return True

return send_tg_message

Expand Down
Loading