Document market monitor bot settings and retire wallet activity

This commit is contained in:
2569718930@qq.com
2026-04-01 17:53:31 +08:00
parent 400916b023
commit 342ed77283
5 changed files with 389 additions and 56 deletions
+8 -1
View File
@@ -76,7 +76,14 @@ TELEGRAM_ALERT_PUSH_INTERVAL_SEC=300
TELEGRAM_ALERT_PUSH_COOLDOWN_SEC=1800
TELEGRAM_ALERT_MIN_TRIGGER_COUNT=2
TELEGRAM_ALERT_MIN_SEVERITY=medium
TELEGRAM_ALERT_MISPRICING_ONLY=true
TELEGRAM_ALERT_MISPRICING_INTERVAL_SEC=7200
TELEGRAM_ALERT_MISPRICING_MAX_YES_BUY=0.10
TELEGRAM_MARKET_FOCUS_DIGEST_ENABLED=true
TELEGRAM_MARKET_FOCUS_DIGEST_HOURS=11,18
TELEGRAM_MARKET_FOCUS_DIGEST_TOP_N=5
TELEGRAM_MARKET_FOCUS_DIGEST_GRACE_MINUTES=180
TELEGRAM_MARKET_TIMEZONE=Asia/Shanghai
TELEGRAM_ALERT_CITIES=ankara,london,paris,seoul,hong kong,shanghai,singapore,tokyo,tel aviv,toronto,buenos aires,wellington,new york,chicago,dallas,miami,atlanta,seattle,lucknow,sao paulo,munich
POLYWEATHER_MONITORING_ALERT_CHAT_IDS=
@@ -158,7 +165,7 @@ POLYGON_WALLET_WATCH_POLYMARKET_ONLY=true
POLYGON_WALLET_WATCH_INCLUDE_DEFAULT_PM_CONTRACTS=true
POLYGON_WALLET_WATCH_POLYMARKET_CONTRACTS=
# Polymarket wallet activity
# Polymarket wallet activity (retired; replaced by market monitor digests + critical alerts)
POLYMARKET_WALLET_ACTIVITY_ENABLED=false
POLYMARKET_WALLET_ACTIVITY_USERS=
POLYMARKET_WALLET_ACTIVITY_CHAT_ID=
+65 -3
View File
@@ -107,7 +107,9 @@ PolyWeather 的环境变量很多,但不是所有变量都属于同一层级
- `POLYWEATHER_PAYMENT_ENABLED`
- `POLYMARKET_MARKET_SCAN_ENABLED`
- `POLYGON_WALLET_WATCH_ENABLED`
- `POLYMARKET_WALLET_ACTIVITY_ENABLED`
- `TELEGRAM_ALERT_PUSH_ENABLED`
- `TELEGRAM_MARKET_FOCUS_DIGEST_ENABLED`
- `POLYMARKET_WALLET_ACTIVITY_ENABLED`(已退役,建议保持 `false`
### 4.3 L3:运行调优项
@@ -119,6 +121,15 @@ PolyWeather 的环境变量很多,但不是所有变量都属于同一层级
- 各类 `*_TIMEOUT_SEC`
- 各类 `*_COOLDOWN_SEC`
- 各类 `*_INTERVAL_SEC`
- `TELEGRAM_ALERT_MIN_TRIGGER_COUNT`
- `TELEGRAM_ALERT_MIN_SEVERITY`
- `TELEGRAM_ALERT_MISPRICING_ONLY`
- `TELEGRAM_ALERT_MISPRICING_INTERVAL_SEC`
- `TELEGRAM_ALERT_MISPRICING_MAX_YES_BUY`
- `TELEGRAM_MARKET_FOCUS_DIGEST_HOURS`
- `TELEGRAM_MARKET_FOCUS_DIGEST_TOP_N`
- `TELEGRAM_MARKET_FOCUS_DIGEST_GRACE_MINUTES`
- `TELEGRAM_MARKET_TIMEZONE`
- `POLYWEATHER_PAYMENT_RPC_URLS`
- `TAF_CACHE_TTL_SEC`
@@ -204,7 +215,7 @@ TELEGRAM_BOT_TOKEN=...
TELEGRAM_CHAT_ID=...
POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather
POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db
POLYWEATHER_STATE_STORAGE_MODE=dual
POLYWEATHER_STATE_STORAGE_MODE=sqlite
UID=1000
GID=1000
POLYWEATHER_AUTH_ENABLED=true
@@ -215,6 +226,20 @@ SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=...
TELEGRAM_ALERT_PUSH_ENABLED=true
TELEGRAM_ALERT_PUSH_INTERVAL_SEC=300
TELEGRAM_ALERT_PUSH_COOLDOWN_SEC=1800
TELEGRAM_ALERT_MIN_TRIGGER_COUNT=2
TELEGRAM_ALERT_MIN_SEVERITY=medium
TELEGRAM_ALERT_MISPRICING_ONLY=true
TELEGRAM_ALERT_MISPRICING_INTERVAL_SEC=7200
TELEGRAM_ALERT_MISPRICING_MAX_YES_BUY=0.10
TELEGRAM_MARKET_FOCUS_DIGEST_ENABLED=true
TELEGRAM_MARKET_FOCUS_DIGEST_HOURS=11,18
TELEGRAM_MARKET_FOCUS_DIGEST_TOP_N=5
TELEGRAM_MARKET_FOCUS_DIGEST_GRACE_MINUTES=180
TELEGRAM_MARKET_TIMEZONE=Asia/Shanghai
POLYMARKET_WALLET_ACTIVITY_ENABLED=false
```
说明:
@@ -223,8 +248,45 @@ POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=...
- Windows / macOS 一般可以直接保留默认值。
- `POLYWEATHER_RUNTIME_DATA_DIR` 建议放在仓库外,例如 `/var/lib/polyweather`
- `docker-compose.yml` 会把这个目录同时挂载到容器内的 `/var/lib/polyweather``/app/data`,兼容现有缓存与 SQLite 路径。
- `POLYWEATHER_STATE_STORAGE_MODE` 当前推荐先用 `dual`,验证后再切 `sqlite`
- `POLYWEATHER_STATE_STORAGE_MODE` 当前线上推荐直接使用 `sqlite`
- `POLYWEATHER_PAYMENT_RPC_URLS` 支持逗号分隔多个 RPC;如果暂时只用单 RPC,也可以继续只配 `POLYWEATHER_PAYMENT_RPC_URL`
- 机器人市场监控当前分成两类消息:`关键提醒``关注清单`
- `TELEGRAM_MARKET_TIMEZONE` 建议保持 `Asia/Shanghai`,更适合亚洲时区盯盘节奏。
- `POLYMARKET_WALLET_ACTIVITY_ENABLED` 已退役,保留为 `false` 即可,不建议再启用钱包异动监听。
### 6.3 机器人市场监控建议配置
这套配置用于替代旧的钱包异动监听,围绕市场本身做两类推送:
- `关键提醒`:实时错价/触发条件满足时发送
- `关注清单`:按亚洲时区定时推送当日重点市场摘要
推荐值:
```env
TELEGRAM_ALERT_PUSH_ENABLED=true
TELEGRAM_ALERT_PUSH_INTERVAL_SEC=300
TELEGRAM_ALERT_PUSH_COOLDOWN_SEC=1800
TELEGRAM_ALERT_MIN_TRIGGER_COUNT=2
TELEGRAM_ALERT_MIN_SEVERITY=medium
TELEGRAM_ALERT_MISPRICING_ONLY=true
TELEGRAM_ALERT_MISPRICING_INTERVAL_SEC=7200
TELEGRAM_ALERT_MISPRICING_MAX_YES_BUY=0.10
TELEGRAM_MARKET_FOCUS_DIGEST_ENABLED=true
TELEGRAM_MARKET_FOCUS_DIGEST_HOURS=11,18
TELEGRAM_MARKET_FOCUS_DIGEST_TOP_N=5
TELEGRAM_MARKET_FOCUS_DIGEST_GRACE_MINUTES=180
TELEGRAM_MARKET_TIMEZONE=Asia/Shanghai
POLYMARKET_WALLET_ACTIVITY_ENABLED=false
```
说明:
- `TELEGRAM_ALERT_MISPRICING_ONLY=true` 表示关键提醒优先围绕错价/市场触发,不把机器人做成泛通知器。
- `TELEGRAM_MARKET_FOCUS_DIGEST_HOURS=11,18` 对应亚洲白天与傍晚两个摘要窗口,适合提前筛出今晚值得关注的市场。
- `TELEGRAM_MARKET_FOCUS_DIGEST_TOP_N=5` 建议先保持较小,避免机器人一次推太多城市。
- `TELEGRAM_MARKET_FOCUS_DIGEST_GRACE_MINUTES=180` 用于容忍进程重启/部署后的补发窗口。
- `POLYMARKET_WALLET_ACTIVITY_ENABLED=false` 表示停用旧的钱包异动监听,统一收敛到市场监控。
## 7. 当前建议的运维规则
+4 -4
View File
@@ -1135,9 +1135,9 @@ def _build_telegram_messages_mispricing(
center_line += f" | 领先 {lead_gap:+.1f}{temp_symbol}"
if snapshot.get("available") and signal_state.get("should_push"):
title_zh = "🚨 PolyWeather 错价雷达"
title_zh = "🚨 PolyWeather 市场监控"
elif snapshot.get("available"):
title_zh = "📍 PolyWeather 错价观察"
title_zh = "📍 PolyWeather 市场观察"
else:
title_zh = "🚨 PolyWeather 异动预警" if (has_active_trigger or suppressed) else "📍 PolyWeather 状态快照"
@@ -1175,9 +1175,9 @@ def _build_telegram_messages_mispricing(
lines_zh.append(f"地图:{final_map}")
title_en = (
"🚨 PolyWeather Mispricing Radar"
"🚨 PolyWeather Market Monitor"
if snapshot.get("available") and signal_state.get("should_push")
else "📍 PolyWeather Mispricing Watch"
else "📍 PolyWeather Market Watch"
if snapshot.get("available")
else "🚨 PolyWeather Alert"
)
+16 -30
View File
@@ -6,10 +6,7 @@ from datetime import datetime
from importlib import import_module
from typing import Any, Callable, Dict, List, Optional
from src.utils.telegram_chat_ids import (
get_polymarket_wallet_activity_chat_ids_from_env,
get_telegram_chat_ids_from_env,
)
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
def _env_bool(name: str, default: bool) -> bool:
@@ -168,11 +165,16 @@ class StartupCoordinator:
"interval_sec": interval,
"cities_count": cities_count,
"chat_targets": len(chat_ids),
"focus_digest_enabled": _env_bool("TELEGRAM_MARKET_FOCUS_DIGEST_ENABLED", True),
"focus_digest_hours": str(
os.getenv("TELEGRAM_MARKET_FOCUS_DIGEST_HOURS") or "11,18"
).strip(),
"timezone": str(os.getenv("TELEGRAM_MARKET_TIMEZONE") or "Asia/Shanghai").strip(),
}
validation_error = None if chat_ids else "missing_TELEGRAM_CHAT_IDS"
return self._start_with_validation(
key="trade_alert_push",
label="错价雷达推送",
label="市场监控推送",
configured_enabled=enabled,
details=details,
validation_error=validation_error,
@@ -213,32 +215,16 @@ class StartupCoordinator:
)
def _start_polymarket_wallet_activity_loop(self) -> LoopStatus:
enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False)
chat_ids = get_polymarket_wallet_activity_chat_ids_from_env()
users_count = _parse_csv_count(os.getenv("POLYMARKET_WALLET_ACTIVITY_USERS"))
poll = max(5, _env_int("POLYMARKET_WALLET_ACTIVITY_INTERVAL_SEC", 20))
details = {
"poll_sec": poll,
"users_count": users_count,
"link_preview": _env_bool("POLYMARKET_WALLET_ACTIVITY_LINK_PREVIEW", True),
"chat_targets": len(chat_ids),
}
validation_error = None
if not chat_ids:
validation_error = (
"missing_POLYMARKET_WALLET_ACTIVITY_CHAT_IDS_or_TELEGRAM_CHAT_IDS"
)
elif users_count == 0:
validation_error = "missing_POLYMARKET_WALLET_ACTIVITY_USERS"
return self._start_with_validation(
return LoopStatus(
key="polymarket_wallet_activity",
label="Polymarket 钱包异动监听",
configured_enabled=enabled,
details=details,
validation_error=validation_error,
starter=lambda: import_module(
"src.onchain.polymarket_wallet_activity_watcher"
).start_polymarket_wallet_activity_loop(self.bot),
label="Polymarket 钱包异动监听(已停用)",
configured_enabled=False,
started=False,
reason="retired_replaced_by_market_monitor",
details={
"replacement": "trade_alert_push",
"note": "wallet activity watcher retired in favor of market monitor digests and critical alerts",
},
)
def _start_weekly_reward_loop(self) -> LoopStatus:
+296 -18
View File
@@ -4,7 +4,7 @@ import os
import re
import threading
import time
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
from loguru import logger
@@ -67,6 +67,16 @@ def _norm_prob(v: Any) -> Optional[float]:
return max(0.0, min(1.0, n))
def _fmt_cents(value: Any) -> Optional[str]:
numeric = _norm_prob(value)
if numeric is None:
return None
cents = numeric * 100.0
rounded = round(cents, 1)
text = f"{rounded:.1f}".rstrip("0").rstrip(".")
return f"{text}c"
def _safe_float(v: Any) -> Optional[float]:
if v is None:
return None
@@ -230,7 +240,7 @@ def _save_state(path: str, state: Dict[str, Any]) -> None:
def _cleanup_state(state: Dict[str, Any], now_ts: int, keep_sec: int = 7 * 86400) -> None:
for bucket_name in ("by_signature",):
for bucket_name in ("by_signature", "focus_digest_slots"):
bucket = state.get(bucket_name, {})
if not isinstance(bucket, dict):
state[bucket_name] = {}
@@ -252,6 +262,249 @@ def _cleanup_state(state: Dict[str, Any], now_ts: int, keep_sec: int = 7 * 86400
last_by_city.pop(city, None)
def _parse_hour_list(raw: Optional[str], default: List[int]) -> List[int]:
if not raw:
return default
out: List[int] = []
seen: set[int] = set()
for part in str(raw).replace(";", ",").split(","):
token = str(part).strip()
if not token:
continue
try:
hour = int(token)
except Exception:
continue
if not (0 <= hour <= 23) or hour in seen:
continue
seen.add(hour)
out.append(hour)
return sorted(out) or default
def _resolve_market_timezone(timezone_name: str):
normalized = str(timezone_name or "").strip() or "Asia/Shanghai"
if normalized == "Asia/Shanghai":
return timezone(timedelta(hours=8), name="Asia/Shanghai")
try:
from zoneinfo import ZoneInfo
return ZoneInfo(normalized)
except Exception:
return timezone.utc
def _market_monitor_score(alert_payload: Dict[str, Any]) -> float:
severity = str(alert_payload.get("severity") or "none").lower()
severity_score = {"high": 36.0, "medium": 24.0, "none": 0.0}.get(severity, 0.0)
trigger_count = int(alert_payload.get("trigger_count") or 0)
trigger_score = min(18.0, float(trigger_count) * 9.0)
snapshot = alert_payload.get("market_snapshot") or {}
if not isinstance(snapshot, dict):
snapshot = {}
if not snapshot.get("available"):
return 0.0
edge_percent = abs(_safe_float(snapshot.get("edge_percent")) or 0.0)
edge_score = min(22.0, edge_percent * 2.5)
yes_buy = _norm_prob(snapshot.get("yes_buy"))
if yes_buy is None:
forecast_bucket = snapshot.get("forecast_bucket") or {}
if isinstance(forecast_bucket, dict):
yes_buy = _norm_prob(forecast_bucket.get("yes_buy"))
pricing_score = 0.0
if yes_buy is not None:
if yes_buy < 0.10:
pricing_score = 14.0
elif yes_buy < 0.20:
pricing_score = 9.0
elif yes_buy < 0.35:
pricing_score = 5.0
confidence = str(snapshot.get("confidence") or "").strip().lower()
confidence_score = {"high": 10.0, "medium": 6.0, "low": 2.0}.get(confidence, 0.0)
suppression = alert_payload.get("suppression") or {}
suppressed_penalty = -20.0 if bool(suppression.get("suppressed")) else 0.0
return max(0.0, severity_score + trigger_score + edge_score + pricing_score + confidence_score + suppressed_penalty)
def _priority_label(score: float) -> str:
if score >= 72:
return "高优先级"
if score >= 48:
return "重点观察"
return "继续观察"
def _join_trigger_types_cn_local(rules: Dict[str, Dict[str, Any]]) -> str:
label_map = {
"ankara_center_deb_hit": "中心站触及 DEB",
"momentum_spike": "短时动量异动",
"forecast_breakthrough": "实测击穿模型",
"advection": "暖平流信号",
}
parts: List[str] = []
for key, label in label_map.items():
row = rules.get(key) or {}
if row.get("triggered"):
parts.append(label)
return " + ".join(parts)
def _focus_trigger_summary(alert_payload: Dict[str, Any]) -> str:
rules = alert_payload.get("rules") or {}
if not isinstance(rules, dict):
return "市场与天气分歧待观察"
return _join_trigger_types_cn_local(rules) or "市场与天气分歧待观察"
def _build_focus_digest_message(
payloads: List[Dict[str, Any]],
*,
slot_label: str,
top_n: int,
timezone_name: str,
) -> str:
ranked = sorted(
payloads,
key=lambda item: _market_monitor_score(item),
reverse=True,
)
shortlisted = [
item for item in ranked
if _market_monitor_score(item) > 0 and bool((item.get("market_snapshot") or {}).get("available"))
][:top_n]
if not shortlisted:
return ""
lines = [
f"🌐 PolyWeather 市场监控 · {slot_label}",
f"时区:{timezone_name}",
"",
]
for idx, payload in enumerate(shortlisted, start=1):
city = str(payload.get("city") or "").strip().lower()
city_name = (CITY_REGISTRY.get(city) or {}).get("display_name") or city.title() or "--"
snapshot = payload.get("market_snapshot") or {}
evidence = payload.get("evidence") or {}
inputs = evidence.get("inputs") or {}
bucket = str(
(snapshot.get("forecast_bucket") or {}).get("label")
or snapshot.get("top_bucket")
or "--"
).strip()
yes_buy = _norm_prob(snapshot.get("yes_buy"))
if yes_buy is None:
forecast_bucket = snapshot.get("forecast_bucket") or {}
if isinstance(forecast_bucket, dict):
yes_buy = _norm_prob(forecast_bucket.get("yes_buy"))
yes_buy_text = _fmt_cents(yes_buy) or "--"
edge_percent = _safe_float(snapshot.get("edge_percent"))
current_temp = _safe_float(inputs.get("current_temp"))
deb_prediction = _safe_float(inputs.get("deb_prediction"))
market_url = str(snapshot.get("market_url") or snapshot.get("primary_market_url") or "").strip()
score = _market_monitor_score(payload)
lines.append(f"{idx}. {city_name} | {_priority_label(score)}")
lines.append(
" "
+ f"{bucket} | Yes {yes_buy_text} | 偏差 "
+ (f"{edge_percent:+.1f}%" if edge_percent is not None else "--")
)
if current_temp is not None or deb_prediction is not None:
lines.append(
" "
+ (f"实测 {current_temp:.1f}°C" if current_temp is not None else "实测 --")
+ " | "
+ (f"DEB {deb_prediction:.1f}°C" if deb_prediction is not None else "DEB --")
)
lines.append(f" 触发:{_focus_trigger_summary(payload)}")
if market_url:
lines.append(f" 链接:{market_url}")
lines.append("")
lines.append("用途:先筛今晚值得盯的市场,真正进入关键窗口时仍会继续推送。")
return "\n".join(lines).strip()
def _maybe_send_focus_digest(
bot: Any,
chat_ids: List[str],
payloads: List[Dict[str, Any]],
state: Dict[str, Any],
*,
timezone_name: str,
digest_hours: List[int],
top_n: int,
grace_minutes: int,
) -> bool:
if not chat_ids or not payloads or not digest_hours:
return False
try:
local_now = datetime.now(_resolve_market_timezone(timezone_name))
except Exception:
local_now = datetime.now()
timezone_name = "local"
eligible_slot: Optional[datetime] = None
for hour in sorted(digest_hours, reverse=True):
slot_dt = local_now.replace(hour=hour, minute=0, second=0, microsecond=0)
delta_minutes = int((local_now - slot_dt).total_seconds() // 60)
if 0 <= delta_minutes <= grace_minutes:
eligible_slot = slot_dt
break
if eligible_slot is None:
return False
slot_key = eligible_slot.strftime("%Y-%m-%d@%H")
digest_slots = state.setdefault("focus_digest_slots", {})
if digest_slots.get(slot_key):
return False
slot_label = "白天关注" if eligible_slot.hour < 15 else "今晚关注"
message = _build_focus_digest_message(
payloads,
slot_label=slot_label,
top_n=top_n,
timezone_name=timezone_name,
)
if not message:
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 focus digest push failed slot={} chat_id={} error={}",
slot_key,
chat_id,
exc,
)
if sent_count <= 0:
return False
digest_slots[slot_key] = int(time.time())
logger.info(
"market focus digest pushed slot={} timezone={} items={} chat_targets={}",
slot_key,
timezone_name,
min(top_n, len(payloads)),
sent_count,
)
return True
def _severity_ok(alert_payload: Dict[str, Any], min_severity: str, min_trigger_count: int) -> bool:
triggered_alerts = alert_payload.get("triggered_alerts") or []
if any(alert.get("force_push") for alert in triggered_alerts):
@@ -572,7 +825,7 @@ def _maybe_send_alert(
"active": False,
"cleared_ts": now_ts,
}
logger.info(f"trade alert disarmed city={city}")
logger.info(f"market monitor disarmed city={city}")
return True
return False
@@ -601,12 +854,7 @@ def _maybe_send_alert(
bot.send_message(chat_id, message)
sent_count += 1
except Exception as exc:
logger.warning(
"trade alert push failed city={} chat_id={} error={}",
city,
chat_id,
exc,
)
logger.warning("market monitor push failed city={} chat_id={} error={}", city, chat_id, exc)
if sent_count <= 0:
return False
@@ -620,7 +868,7 @@ def _maybe_send_alert(
}
state.setdefault("by_signature", {})[signature] = now_ts
logger.info(
f"trade alert pushed city={city} severity={alert_payload.get('severity')} "
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}"
)
@@ -631,10 +879,10 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th
enabled = _env_bool("TELEGRAM_ALERT_PUSH_ENABLED", True)
chat_ids = get_telegram_chat_ids_from_env()
if not enabled:
logger.info("telegram alert push loop disabled")
logger.info("telegram market monitor loop disabled")
return None
if not chat_ids:
logger.warning("telegram alert push loop skipped: TELEGRAM_CHAT_IDS is not set")
logger.warning("telegram market monitor loop skipped: TELEGRAM_CHAT_IDS is not set")
return None
mispricing_only = _env_bool("TELEGRAM_ALERT_MISPRICING_ONLY", True)
@@ -649,26 +897,40 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th
min_severity = os.getenv("TELEGRAM_ALERT_MIN_SEVERITY", "medium").strip().lower()
cities = _parse_city_list(os.getenv("TELEGRAM_ALERT_CITIES"))
state_path = _state_file()
focus_digest_enabled = _env_bool("TELEGRAM_MARKET_FOCUS_DIGEST_ENABLED", True)
focus_digest_hours = _parse_hour_list(
os.getenv("TELEGRAM_MARKET_FOCUS_DIGEST_HOURS"),
[11, 18],
)
focus_digest_top_n = max(3, min(8, _env_int("TELEGRAM_MARKET_FOCUS_DIGEST_TOP_N", 5)))
focus_digest_grace_minutes = max(
30,
min(240, _env_int("TELEGRAM_MARKET_FOCUS_DIGEST_GRACE_MINUTES", 180)),
)
market_timezone = str(os.getenv("TELEGRAM_MARKET_TIMEZONE") or "Asia/Shanghai").strip() or "Asia/Shanghai"
def _runner() -> None:
try:
_save_state(state_path, _load_state(state_path))
except Exception:
logger.exception(f"failed to initialize telegram push state path={state_path}")
logger.exception(f"failed to initialize market monitor state path={state_path}")
logger.info(
f"telegram alert push loop started mode={'mispricing-only' if mispricing_only else 'full'} "
f"telegram market monitor loop started mode={'mispricing-only' if mispricing_only else 'full'} "
f"cities={len(cities)} interval={interval_sec}s chat_targets={len(chat_ids)} "
f"cooldown={cooldown_sec}s min_triggers={min_trigger_count} min_severity={min_severity} "
f"state_path={state_path}"
f"focus_digest_enabled={focus_digest_enabled} focus_hours={focus_digest_hours} "
f"timezone={market_timezone} state_path={state_path}"
)
while True:
cycle_started = time.time()
state = _load_state(state_path)
_cleanup_state(state, int(cycle_started))
cycle_payloads: List[Dict[str, Any]] = []
for city in cities:
try:
alert_payload = build_trade_alert_for_city(city, config)
cycle_payloads.append(alert_payload)
if _maybe_send_alert(
bot=bot,
chat_ids=chat_ids,
@@ -683,18 +945,34 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th
try:
_save_state(state_path, state)
except Exception:
logger.exception(f"failed to save telegram push state city={city}")
logger.exception(f"failed to save market monitor state city={city}")
except Exception:
logger.exception(f"telegram alert push loop failed for city={city}")
logger.exception(f"telegram market monitor loop failed for city={city}")
time.sleep(1)
if focus_digest_enabled:
try:
if _maybe_send_focus_digest(
bot=bot,
chat_ids=chat_ids,
payloads=cycle_payloads,
state=state,
timezone_name=market_timezone,
digest_hours=focus_digest_hours,
top_n=focus_digest_top_n,
grace_minutes=focus_digest_grace_minutes,
):
_save_state(state_path, state)
except Exception:
logger.exception("failed to push market focus digest")
elapsed = time.time() - cycle_started
sleep_sec = max(5, interval_sec - int(elapsed))
time.sleep(sleep_sec)
thread = threading.Thread(
target=_runner,
name="telegram-trade-alert-pusher",
name="telegram-market-monitor-pusher",
daemon=True,
)
thread.start()