移除 PolyWeather 市场提醒功能及监控基础设施
- 删除 market_alert_engine.py:交易预警引擎 - 删除 alertmanager_telegram_relay.py:Alertmanager 到 Telegram 转发 - 移除 telegram_push.py 中市场监控推送循环 - 移除 runtime_coordinator.py 中 trade_alert_push 协程 - 移除 docker-compose.yml 中 Prometheus/Alertmanager/Grafana 服务 - 移除 monitoring/ 目录:prometheus/alertmanager/grafana 配置 Tested: ruff check . ✓
This commit is contained in:
@@ -45,68 +45,3 @@ services:
|
|||||||
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
|
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
|
||||||
user: "${UID:-1000}:${GID:-1000}"
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
|
|
||||||
polyweather_prometheus:
|
|
||||||
image: prom/prometheus:v3.4.1
|
|
||||||
container_name: polyweather_prometheus
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles: ["monitoring"]
|
|
||||||
depends_on:
|
|
||||||
- polyweather_web
|
|
||||||
command:
|
|
||||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
|
||||||
- "--storage.tsdb.path=/prometheus"
|
|
||||||
- "--storage.tsdb.retention.time=15d"
|
|
||||||
- "--web.enable-lifecycle"
|
|
||||||
volumes:
|
|
||||||
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
||||||
- ./monitoring/prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
|
|
||||||
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}/monitoring/prometheus:/prometheus
|
|
||||||
ports:
|
|
||||||
- "${POLYWEATHER_PROMETHEUS_PORT:-9090}:9090"
|
|
||||||
|
|
||||||
polyweather_alertmanager:
|
|
||||||
image: prom/alertmanager:v0.28.1
|
|
||||||
container_name: polyweather_alertmanager
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles: ["monitoring"]
|
|
||||||
depends_on:
|
|
||||||
- polyweather_alert_relay
|
|
||||||
command:
|
|
||||||
- "--config.file=/etc/alertmanager/alertmanager.yml"
|
|
||||||
- "--storage.path=/alertmanager"
|
|
||||||
volumes:
|
|
||||||
- ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
|
||||||
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}/monitoring/alertmanager:/alertmanager
|
|
||||||
ports:
|
|
||||||
- "${POLYWEATHER_ALERTMANAGER_PORT:-9093}:9093"
|
|
||||||
|
|
||||||
polyweather_alert_relay:
|
|
||||||
<<: *polyweather-base
|
|
||||||
container_name: polyweather_alert_relay
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles: ["monitoring"]
|
|
||||||
command: python scripts/alertmanager_telegram_relay.py
|
|
||||||
volumes:
|
|
||||||
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather
|
|
||||||
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
|
|
||||||
ports:
|
|
||||||
- "${POLYWEATHER_ALERT_RELAY_PORT:-9099}:9099"
|
|
||||||
user: "${UID:-1000}:${GID:-1000}"
|
|
||||||
|
|
||||||
polyweather_grafana:
|
|
||||||
image: grafana/grafana-oss:12.0.2
|
|
||||||
container_name: polyweather_grafana
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles: ["monitoring"]
|
|
||||||
depends_on:
|
|
||||||
- polyweather_prometheus
|
|
||||||
environment:
|
|
||||||
GF_SECURITY_ADMIN_USER: ${POLYWEATHER_GRAFANA_ADMIN_USER:-admin}
|
|
||||||
GF_SECURITY_ADMIN_PASSWORD: ${POLYWEATHER_GRAFANA_ADMIN_PASSWORD:-polyweather}
|
|
||||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
|
||||||
volumes:
|
|
||||||
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
||||||
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
|
||||||
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}/monitoring/grafana:/var/lib/grafana
|
|
||||||
ports:
|
|
||||||
- "${POLYWEATHER_GRAFANA_PORT:-3001}:3000"
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
global:
|
|
||||||
resolve_timeout: 5m
|
|
||||||
|
|
||||||
route:
|
|
||||||
receiver: telegram-relay
|
|
||||||
group_by: ["alertname"]
|
|
||||||
group_wait: 30s
|
|
||||||
group_interval: 5m
|
|
||||||
repeat_interval: 3h
|
|
||||||
|
|
||||||
receivers:
|
|
||||||
- name: telegram-relay
|
|
||||||
webhook_configs:
|
|
||||||
- url: http://polyweather_alert_relay:9099/alerts
|
|
||||||
send_resolved: true
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": {
|
|
||||||
"type": "grafana",
|
|
||||||
"uid": "-- Grafana --"
|
|
||||||
},
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"fiscalYearStartMonth": 0,
|
|
||||||
"graphTooltip": 0,
|
|
||||||
"id": null,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"unit": "reqps"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 1,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "sum by (status) (rate(polyweather_http_requests_total[5m]))",
|
|
||||||
"legendFormat": "{{status}}",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "HTTP Requests by Status",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"unit": "ms"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 2,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "sum(rate(polyweather_http_request_duration_ms_sum[5m])) / clamp_min(sum(rate(polyweather_http_request_duration_ms_count[5m])), 1)",
|
|
||||||
"legendFormat": "avg",
|
|
||||||
"refId": "A"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expr": "max(polyweather_http_request_duration_ms_max)",
|
|
||||||
"legendFormat": "max",
|
|
||||||
"refId": "B"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "HTTP Latency",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"unit": "reqps"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 3,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "sum by (source, outcome) (rate(polyweather_source_requests_total[5m]))",
|
|
||||||
"legendFormat": "{{source}} / {{outcome}}",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Source Requests by Outcome",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "orange",
|
|
||||||
"value": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 25
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "percentunit"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 4,
|
|
||||||
"options": {
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "(sum(increase(polyweather_source_requests_total{outcome!~\"success|cache_hit\"}[15m])) / clamp_min(sum(increase(polyweather_source_requests_total[15m])), 1))",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Source Error Rate (15m)",
|
|
||||||
"type": "stat"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"refresh": "30s",
|
|
||||||
"schemaVersion": 41,
|
|
||||||
"style": "dark",
|
|
||||||
"tags": [
|
|
||||||
"polyweather",
|
|
||||||
"ops"
|
|
||||||
],
|
|
||||||
"templating": {
|
|
||||||
"list": []
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-6h",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {},
|
|
||||||
"timezone": "browser",
|
|
||||||
"title": "PolyWeather Overview",
|
|
||||||
"uid": "polyweather-overview",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
|
|
||||||
providers:
|
|
||||||
- name: PolyWeather Dashboards
|
|
||||||
orgId: 1
|
|
||||||
folder: PolyWeather
|
|
||||||
type: file
|
|
||||||
disableDeletion: false
|
|
||||||
editable: true
|
|
||||||
options:
|
|
||||||
path: /var/lib/grafana/dashboards
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
|
|
||||||
datasources:
|
|
||||||
- name: PolyWeather Prometheus
|
|
||||||
type: prometheus
|
|
||||||
access: proxy
|
|
||||||
url: http://polyweather_prometheus:9090
|
|
||||||
isDefault: true
|
|
||||||
editable: false
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
groups:
|
|
||||||
- name: polyweather-runtime
|
|
||||||
rules:
|
|
||||||
- alert: PolyWeatherWebDown
|
|
||||||
expr: up{job="polyweather-web"} == 0
|
|
||||||
for: 2m
|
|
||||||
labels:
|
|
||||||
severity: critical
|
|
||||||
annotations:
|
|
||||||
summary: "PolyWeather web metrics endpoint is down"
|
|
||||||
description: "/metrics on polyweather_web has been unreachable for more than 2 minutes."
|
|
||||||
|
|
||||||
- alert: PolyWeatherHttp5xxBurst
|
|
||||||
expr: sum(increase(polyweather_http_requests_total{status=~"5.."}[10m])) > 10
|
|
||||||
for: 5m
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
annotations:
|
|
||||||
summary: "PolyWeather HTTP 5xx burst"
|
|
||||||
description: "More than 10 server errors were observed in the last 10 minutes."
|
|
||||||
|
|
||||||
- alert: PolyWeatherHighSourceErrorRate
|
|
||||||
expr: |
|
|
||||||
(
|
|
||||||
sum(increase(polyweather_source_requests_total{outcome!~"success|cache_hit"}[15m]))
|
|
||||||
/
|
|
||||||
clamp_min(sum(increase(polyweather_source_requests_total[15m])), 1)
|
|
||||||
) > 0.25
|
|
||||||
and sum(increase(polyweather_source_requests_total[15m])) > 20
|
|
||||||
for: 10m
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
annotations:
|
|
||||||
summary: "PolyWeather source error rate is high"
|
|
||||||
description: "External weather source errors exceeded 25% over the last 15 minutes."
|
|
||||||
|
|
||||||
- alert: PolyWeatherOpenMeteoCooldownLoop
|
|
||||||
expr: sum(increase(polyweather_source_requests_total{source="open_meteo",outcome=~"cooldown_skip|error"}[15m])) > 20
|
|
||||||
for: 10m
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
annotations:
|
|
||||||
summary: "Open-Meteo is rate-limited or erroring"
|
|
||||||
description: "Open-Meteo has produced repeated cooldown skips or errors in the last 15 minutes."
|
|
||||||
|
|
||||||
- alert: PolyWeatherSlowHttpAverage
|
|
||||||
expr: |
|
|
||||||
(
|
|
||||||
sum(rate(polyweather_http_request_duration_ms_sum[10m]))
|
|
||||||
/
|
|
||||||
clamp_min(sum(rate(polyweather_http_request_duration_ms_count[10m])), 1)
|
|
||||||
) > 2000
|
|
||||||
for: 10m
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
annotations:
|
|
||||||
summary: "PolyWeather average HTTP latency is high"
|
|
||||||
description: "Average HTTP request duration exceeded 2s over the last 10 minutes."
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
global:
|
|
||||||
scrape_interval: 30s
|
|
||||||
evaluation_interval: 30s
|
|
||||||
|
|
||||||
rule_files:
|
|
||||||
- /etc/prometheus/alerts.yml
|
|
||||||
|
|
||||||
alerting:
|
|
||||||
alertmanagers:
|
|
||||||
- static_configs:
|
|
||||||
- targets:
|
|
||||||
- polyweather_alertmanager:9093
|
|
||||||
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: polyweather-web
|
|
||||||
metrics_path: /metrics
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- polyweather_web:8000
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
def _chat_ids() -> List[str]:
|
|
||||||
raw = (
|
|
||||||
os.getenv("POLYWEATHER_MONITORING_ALERT_CHAT_IDS")
|
|
||||||
or os.getenv("TELEGRAM_CHAT_IDS")
|
|
||||||
or os.getenv("TELEGRAM_CHAT_ID")
|
|
||||||
or ""
|
|
||||||
)
|
|
||||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
||||||
|
|
||||||
|
|
||||||
def _format_alerts(payload: Dict[str, Any]) -> str:
|
|
||||||
alerts = payload.get("alerts") or []
|
|
||||||
if not isinstance(alerts, list) or not alerts:
|
|
||||||
return "PolyWeather monitoring received an empty alert payload."
|
|
||||||
lines = ["PolyWeather monitoring alert"]
|
|
||||||
for alert in alerts[:10]:
|
|
||||||
if not isinstance(alert, dict):
|
|
||||||
continue
|
|
||||||
status = str(alert.get("status") or "unknown").upper()
|
|
||||||
labels = alert.get("labels") or {}
|
|
||||||
annotations = alert.get("annotations") or {}
|
|
||||||
alert_name = labels.get("alertname") or "unknown_alert"
|
|
||||||
severity = labels.get("severity") or "info"
|
|
||||||
summary = annotations.get("summary") or annotations.get("description") or ""
|
|
||||||
lines.append(f"- [{status}] {alert_name} ({severity})")
|
|
||||||
if summary:
|
|
||||||
lines.append(f" {summary}")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def _send_telegram_message(text: str) -> None:
|
|
||||||
token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
|
|
||||||
chat_ids = _chat_ids()
|
|
||||||
if not token or not chat_ids:
|
|
||||||
return
|
|
||||||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
||||||
for chat_id in chat_ids:
|
|
||||||
try:
|
|
||||||
requests.post(
|
|
||||||
url,
|
|
||||||
json={
|
|
||||||
"chat_id": chat_id,
|
|
||||||
"text": text,
|
|
||||||
"disable_web_page_preview": True,
|
|
||||||
},
|
|
||||||
timeout=10,
|
|
||||||
).raise_for_status()
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
def _send_alert_notifications(text: str) -> None:
|
|
||||||
_send_telegram_message(text)
|
|
||||||
|
|
||||||
|
|
||||||
class _Handler(BaseHTTPRequestHandler):
|
|
||||||
def do_GET(self) -> None: # noqa: N802
|
|
||||||
if self.path.rstrip("/") == "/healthz":
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(b'{"ok":true}')
|
|
||||||
return
|
|
||||||
self.send_error(404)
|
|
||||||
|
|
||||||
def do_POST(self) -> None: # noqa: N802
|
|
||||||
if self.path.rstrip("/") != "/alerts":
|
|
||||||
self.send_error(404)
|
|
||||||
return
|
|
||||||
length = int(self.headers.get("Content-Length", "0") or "0")
|
|
||||||
raw = self.rfile.read(length) if length > 0 else b"{}"
|
|
||||||
try:
|
|
||||||
payload = json.loads(raw.decode("utf-8"))
|
|
||||||
except Exception:
|
|
||||||
self.send_error(400, "invalid json")
|
|
||||||
return
|
|
||||||
if not isinstance(payload, dict):
|
|
||||||
self.send_error(400, "invalid payload")
|
|
||||||
return
|
|
||||||
_send_alert_notifications(_format_alerts(payload))
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(b'{"ok":true}')
|
|
||||||
|
|
||||||
def log_message(self, format: str, *args: object) -> None: # noqa: A003
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
port = 9099
|
|
||||||
server = HTTPServer(("0.0.0.0", port), _Handler)
|
|
||||||
server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -83,7 +83,6 @@ class StartupCoordinator:
|
|||||||
|
|
||||||
def start_all(self) -> RuntimeStatus:
|
def start_all(self) -> RuntimeStatus:
|
||||||
loops = [
|
loops = [
|
||||||
self._start_trade_alert_loop(),
|
|
||||||
self._start_airport_high_freq_loop(),
|
self._start_airport_high_freq_loop(),
|
||||||
self._start_dashboard_prewarm_loop(),
|
self._start_dashboard_prewarm_loop(),
|
||||||
self._start_polygon_wallet_loop(),
|
self._start_polygon_wallet_loop(),
|
||||||
@@ -152,37 +151,6 @@ class StartupCoordinator:
|
|||||||
details=details,
|
details=details,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _start_trade_alert_loop(self) -> LoopStatus:
|
|
||||||
enabled = _env_bool("TELEGRAM_ALERT_PUSH_ENABLED", True)
|
|
||||||
chat_ids = get_telegram_chat_ids_from_env()
|
|
||||||
interval = max(60, _env_int("TELEGRAM_ALERT_PUSH_INTERVAL_SEC", 1800))
|
|
||||||
cities_count = _parse_csv_count(os.getenv("TELEGRAM_ALERT_CITIES"))
|
|
||||||
details = {
|
|
||||||
"mode": "critical-alerts",
|
|
||||||
"interval_sec": interval,
|
|
||||||
"cities_count": cities_count,
|
|
||||||
"chat_targets": len(chat_ids),
|
|
||||||
"alert_cooldown_sec": max(
|
|
||||||
60,
|
|
||||||
_env_int("TELEGRAM_ALERT_PUSH_COOLDOWN_SEC", 21600),
|
|
||||||
),
|
|
||||||
"min_trigger_count": max(1, _env_int("TELEGRAM_ALERT_MIN_TRIGGER_COUNT", 3)),
|
|
||||||
"min_severity": str(os.getenv("TELEGRAM_ALERT_MIN_SEVERITY") or "high").strip().lower(),
|
|
||||||
"daily_signal_cap": "none",
|
|
||||||
}
|
|
||||||
validation_error = None if chat_ids else "missing_TELEGRAM_CHAT_IDS"
|
|
||||||
return self._start_with_validation(
|
|
||||||
key="trade_alert_push",
|
|
||||||
label="市场监控推送",
|
|
||||||
configured_enabled=enabled,
|
|
||||||
details=details,
|
|
||||||
validation_error=validation_error,
|
|
||||||
starter=lambda: import_module("src.utils.telegram_push").start_trade_alert_push_loop(
|
|
||||||
self.bot,
|
|
||||||
self.config,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _start_airport_high_freq_loop(self) -> LoopStatus:
|
def _start_airport_high_freq_loop(self) -> LoopStatus:
|
||||||
enabled = _env_bool("TELEGRAM_AIRPORT_PUSH_ENABLED", True)
|
enabled = _env_bool("TELEGRAM_AIRPORT_PUSH_ENABLED", True)
|
||||||
chat_ids = get_telegram_chat_ids_from_env()
|
chat_ids = get_telegram_chat_ids_from_env()
|
||||||
@@ -269,10 +237,9 @@ class StartupCoordinator:
|
|||||||
label="Polymarket 钱包异动监听(已停用)",
|
label="Polymarket 钱包异动监听(已停用)",
|
||||||
configured_enabled=False,
|
configured_enabled=False,
|
||||||
started=False,
|
started=False,
|
||||||
reason="retired_replaced_by_market_monitor",
|
reason="retired",
|
||||||
details={
|
details={
|
||||||
"replacement": "trade_alert_push",
|
"note": "wallet activity watcher retired",
|
||||||
"note": "wallet activity watcher retired in favor of market monitor digests and critical alerts",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -483,211 +483,6 @@ def _alert_signature(alert_payload: Dict[str, Any]) -> str:
|
|||||||
raw = json.dumps(signature_payload, sort_keys=True, ensure_ascii=True)
|
raw = json.dumps(signature_payload, sort_keys=True, ensure_ascii=True)
|
||||||
return hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
return hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def build_trade_alert_for_city(
|
|
||||||
city: str,
|
|
||||||
config: Dict[str, Any],
|
|
||||||
force_refresh: bool = False,
|
|
||||||
target_date: Optional[str] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
from web.app import _analyze, _build_city_detail_payload
|
|
||||||
from src.analysis.market_alert_engine import build_trading_alerts
|
|
||||||
|
|
||||||
city_weather = _analyze(city, force_refresh=force_refresh)
|
|
||||||
try:
|
|
||||||
aggregate_detail = _build_city_detail_payload(
|
|
||||||
city_weather,
|
|
||||||
target_date=target_date,
|
|
||||||
)
|
|
||||||
market_scan = aggregate_detail.get("market_scan")
|
|
||||||
if isinstance(market_scan, dict):
|
|
||||||
city_weather = {**city_weather, "market_scan": market_scan}
|
|
||||||
except Exception as exc:
|
|
||||||
logger.debug(f"market scan attach skipped city={city}: {exc}")
|
|
||||||
|
|
||||||
resolved_target_date = target_date or city_weather.get("local_date")
|
|
||||||
if resolved_target_date:
|
|
||||||
datetime.strptime(resolved_target_date, "%Y-%m-%d")
|
|
||||||
|
|
||||||
map_url = os.getenv("POLYWEATHER_MAP_URL") or "https://polyweather-pro.vercel.app/"
|
|
||||||
alert_payload = build_trading_alerts(
|
|
||||||
city_weather=city_weather,
|
|
||||||
map_url=map_url,
|
|
||||||
)
|
|
||||||
alert_payload["target_date"] = resolved_target_date
|
|
||||||
return alert_payload
|
|
||||||
|
|
||||||
|
|
||||||
def _maybe_send_alert(
|
|
||||||
bot: Any,
|
|
||||||
chat_ids: List[str],
|
|
||||||
city: str,
|
|
||||||
alert_payload: Dict[str, Any],
|
|
||||||
state: Dict[str, Any],
|
|
||||||
cooldown_sec: int,
|
|
||||||
min_severity: str,
|
|
||||||
min_trigger_count: int,
|
|
||||||
) -> bool:
|
|
||||||
now_ts = int(time.time())
|
|
||||||
last_by_city = state.setdefault("last_by_city", {})
|
|
||||||
last_city = last_by_city.get(city) or {}
|
|
||||||
is_active = _severity_ok(alert_payload, min_severity, min_trigger_count)
|
|
||||||
if not _market_price_cap_ok(alert_payload):
|
|
||||||
is_active = False
|
|
||||||
message = ((alert_payload.get("telegram") or {}).get("zh") or "").strip()
|
|
||||||
|
|
||||||
if not is_active or not message:
|
|
||||||
if last_city.get("active"):
|
|
||||||
last_by_city[city] = {
|
|
||||||
**last_city,
|
|
||||||
"active": False,
|
|
||||||
"cleared_ts": now_ts,
|
|
||||||
}
|
|
||||||
logger.info(f"market monitor disarmed city={city}")
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not chat_ids:
|
|
||||||
return False
|
|
||||||
|
|
||||||
signature = _alert_signature(alert_payload)
|
|
||||||
trigger_key = _trigger_type_key(alert_payload)
|
|
||||||
last_city_sig = last_city.get("signature")
|
|
||||||
last_city_key = str(last_city.get("trigger_key") or "")
|
|
||||||
last_city_ts = int(last_city.get("ts") or 0)
|
|
||||||
last_sig_ts = int((state.get("by_signature") or {}).get(signature) or 0)
|
|
||||||
last_city_active = bool(last_city.get("active"))
|
|
||||||
|
|
||||||
if last_city_active and last_city_key == trigger_key and last_city_sig == signature:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if last_city_ts and now_ts - last_city_ts < cooldown_sec:
|
|
||||||
return False
|
|
||||||
if last_sig_ts and now_ts - last_sig_ts < cooldown_sec:
|
|
||||||
return False
|
|
||||||
|
|
||||||
sent_count = 0
|
|
||||||
for chat_id in chat_ids:
|
|
||||||
try:
|
|
||||||
bot.send_message(chat_id, message)
|
|
||||||
sent_count += 1
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning("market monitor push failed city={} chat_id={} error={}", city, chat_id, exc)
|
|
||||||
if sent_count <= 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
last_by_city[city] = {
|
|
||||||
"signature": signature,
|
|
||||||
"trigger_key": trigger_key,
|
|
||||||
"severity": alert_payload.get("severity"),
|
|
||||||
"ts": now_ts,
|
|
||||||
"active": True,
|
|
||||||
"evidence": alert_payload.get("evidence"),
|
|
||||||
}
|
|
||||||
state.setdefault("by_signature", {})[signature] = now_ts
|
|
||||||
logger.info(
|
|
||||||
f"market monitor pushed city={city} severity={alert_payload.get('severity')} "
|
|
||||||
f"trigger_count={alert_payload.get('trigger_count')} trigger_key={trigger_key} "
|
|
||||||
f"evidence={_evidence_brief(alert_payload)} chat_targets={sent_count}"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _run_market_monitor_cycle(
|
|
||||||
bot: Any,
|
|
||||||
config: Dict[str, Any],
|
|
||||||
*,
|
|
||||||
chat_ids: List[str],
|
|
||||||
cities: List[str],
|
|
||||||
state: Dict[str, Any],
|
|
||||||
alert_cooldown_sec: int,
|
|
||||||
min_severity: str,
|
|
||||||
min_trigger_count: int,
|
|
||||||
sleep_between_cities_sec: float = 1.0,
|
|
||||||
) -> bool:
|
|
||||||
state_dirty = False
|
|
||||||
|
|
||||||
for city in cities:
|
|
||||||
try:
|
|
||||||
alert_payload = build_trade_alert_for_city(city, config)
|
|
||||||
if _maybe_send_alert(
|
|
||||||
bot=bot,
|
|
||||||
chat_ids=chat_ids,
|
|
||||||
city=city,
|
|
||||||
alert_payload=alert_payload,
|
|
||||||
state=state,
|
|
||||||
cooldown_sec=alert_cooldown_sec,
|
|
||||||
min_severity=min_severity,
|
|
||||||
min_trigger_count=min_trigger_count,
|
|
||||||
):
|
|
||||||
state_dirty = True
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"telegram market monitor loop failed for city={city}")
|
|
||||||
if sleep_between_cities_sec > 0:
|
|
||||||
time.sleep(sleep_between_cities_sec)
|
|
||||||
|
|
||||||
return state_dirty
|
|
||||||
|
|
||||||
|
|
||||||
def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[threading.Thread]:
|
|
||||||
enabled = _env_bool("TELEGRAM_ALERT_PUSH_ENABLED", True)
|
|
||||||
chat_ids = get_telegram_chat_ids_from_env()
|
|
||||||
if not enabled:
|
|
||||||
logger.info("telegram market monitor loop disabled")
|
|
||||||
return None
|
|
||||||
if not chat_ids:
|
|
||||||
logger.warning("telegram market monitor loop skipped: TELEGRAM_CHAT_IDS is not set")
|
|
||||||
return None
|
|
||||||
|
|
||||||
interval_sec = max(60, _env_int("TELEGRAM_ALERT_PUSH_INTERVAL_SEC", 1800))
|
|
||||||
cities = _parse_city_list(os.getenv("TELEGRAM_ALERT_CITIES"))
|
|
||||||
state_path = _state_file()
|
|
||||||
alert_cooldown_sec = max(60, _env_int("TELEGRAM_ALERT_PUSH_COOLDOWN_SEC", 21600))
|
|
||||||
min_trigger_count = max(1, _env_int("TELEGRAM_ALERT_MIN_TRIGGER_COUNT", 3))
|
|
||||||
min_severity = str(os.getenv("TELEGRAM_ALERT_MIN_SEVERITY") or "high").strip().lower()
|
|
||||||
if min_severity not in SEVERITY_RANK:
|
|
||||||
min_severity = "high"
|
|
||||||
def _runner() -> None:
|
|
||||||
try:
|
|
||||||
_save_state(state_path, _load_state(state_path))
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"failed to initialize market monitor state path={state_path}")
|
|
||||||
logger.info(
|
|
||||||
f"telegram market monitor loop started mode=critical-alerts "
|
|
||||||
f"cities={len(cities)} interval={interval_sec}s chat_targets={len(chat_ids)} "
|
|
||||||
f"alert_cooldown={alert_cooldown_sec}s "
|
|
||||||
f"min_severity={min_severity} min_trigger_count={min_trigger_count} "
|
|
||||||
f"state_path={state_path}"
|
|
||||||
)
|
|
||||||
while True:
|
|
||||||
cycle_started = time.time()
|
|
||||||
state = _load_state(state_path)
|
|
||||||
_cleanup_state(state, int(cycle_started))
|
|
||||||
if _run_market_monitor_cycle(
|
|
||||||
bot=bot,
|
|
||||||
config=config,
|
|
||||||
chat_ids=chat_ids,
|
|
||||||
cities=cities,
|
|
||||||
state=state,
|
|
||||||
alert_cooldown_sec=alert_cooldown_sec,
|
|
||||||
min_severity=min_severity,
|
|
||||||
min_trigger_count=min_trigger_count,
|
|
||||||
):
|
|
||||||
_save_state(state_path, state)
|
|
||||||
|
|
||||||
elapsed = time.time() - cycle_started
|
|
||||||
sleep_sec = max(5, interval_sec - int(elapsed))
|
|
||||||
time.sleep(sleep_sec)
|
|
||||||
|
|
||||||
thread = threading.Thread(
|
|
||||||
target=_runner,
|
|
||||||
name="telegram-market-monitor-pusher",
|
|
||||||
daemon=True,
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
return thread
|
|
||||||
|
|
||||||
|
|
||||||
# ── high-freq airport push loop ──
|
# ── high-freq airport push loop ──
|
||||||
|
|
||||||
HIGH_FREQ_AIRPORT_CITIES = {"seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul", "paris", "hong kong", "lau fau shan", "taipei"}
|
HIGH_FREQ_AIRPORT_CITIES = {"seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul", "paris", "hong kong", "lau fau shan", "taipei"}
|
||||||
|
|||||||
Reference in New Issue
Block a user