feat: Introduce core bot components, on-chain wallet watchers, Telegram notifications, and configuration management.

This commit is contained in:
2569718930@qq.com
2026-03-13 19:24:05 +08:00
parent edf2a37976
commit cd0a5d519f
11 changed files with 214 additions and 76 deletions
+7 -1
View File
@@ -1,6 +1,11 @@
import os
from dotenv import load_dotenv
from src.utils.telegram_chat_ids import (
get_primary_telegram_chat_id_from_env,
get_telegram_chat_ids_from_env,
)
def load_config():
"""
@@ -23,7 +28,8 @@ def load_config():
},
"telegram": {
"bot_token": os.getenv("TELEGRAM_BOT_TOKEN"),
"chat_id": os.getenv("TELEGRAM_CHAT_ID"),
"chat_id": get_primary_telegram_chat_id_from_env(),
"chat_ids": get_telegram_chat_ids_from_env(),
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
},
"config": {
+45
View File
@@ -0,0 +1,45 @@
from __future__ import annotations
import os
from typing import List
def _split_chat_ids(raw: str | None) -> List[str]:
if not raw:
return []
normalized = str(raw).replace("\r", ",").replace("\n", ",").replace(";", ",")
out: List[str] = []
for token in normalized.split(","):
value = token.strip()
if value:
out.append(value)
return out
def parse_telegram_chat_ids(*raw_values: str | None) -> List[str]:
"""Parse and de-duplicate chat ids while keeping original order."""
out: List[str] = []
seen: set[str] = set()
for raw in raw_values:
for chat_id in _split_chat_ids(raw):
if chat_id in seen:
continue
seen.add(chat_id)
out.append(chat_id)
return out
def get_telegram_chat_ids_from_env() -> List[str]:
"""
Preferred env is TELEGRAM_CHAT_IDS (comma-separated).
TELEGRAM_CHAT_ID is kept for backward compatibility.
"""
return parse_telegram_chat_ids(
os.getenv("TELEGRAM_CHAT_IDS"),
os.getenv("TELEGRAM_CHAT_ID"),
)
def get_primary_telegram_chat_id_from_env() -> str:
chat_ids = get_telegram_chat_ids_from_env()
return chat_ids[0] if chat_ids else ""
+26 -8
View File
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Tuple
from loguru import logger
from src.data_collection.city_registry import CITY_REGISTRY
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
SEVERITY_RANK = {
@@ -516,7 +517,7 @@ def build_trade_alert_for_city(
def _maybe_send_alert(
bot: Any,
chat_id: str,
chat_ids: List[str],
city: str,
alert_payload: Dict[str, Any],
state: Dict[str, Any],
@@ -552,6 +553,9 @@ def _maybe_send_alert(
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")
@@ -568,7 +572,21 @@ def _maybe_send_alert(
if last_sig_ts and now_ts - last_sig_ts < cooldown_sec:
return False
bot.send_message(chat_id, message)
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(
"trade alert 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,
@@ -581,19 +599,19 @@ def _maybe_send_alert(
logger.info(
f"trade alert 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)}"
f"evidence={_evidence_brief(alert_payload)} chat_targets={sent_count}"
)
return True
def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[threading.Thread]:
enabled = _env_bool("TELEGRAM_ALERT_PUSH_ENABLED", True)
chat_id = os.getenv("TELEGRAM_CHAT_ID")
chat_ids = get_telegram_chat_ids_from_env()
if not enabled:
logger.info("telegram alert push loop disabled")
return None
if not chat_id:
logger.warning("telegram alert push loop skipped: TELEGRAM_CHAT_ID is not set")
if not chat_ids:
logger.warning("telegram alert push loop skipped: TELEGRAM_CHAT_IDS is not set")
return None
mispricing_only = _env_bool("TELEGRAM_ALERT_MISPRICING_ONLY", True)
@@ -616,7 +634,7 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th
logger.exception(f"failed to initialize telegram push state path={state_path}")
logger.info(
f"telegram alert push loop started mode={'mispricing-only' if mispricing_only else 'full'} "
f"cities={len(cities)} interval={interval_sec}s "
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}"
)
@@ -630,7 +648,7 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th
alert_payload = build_trade_alert_for_city(city, config)
if _maybe_send_alert(
bot=bot,
chat_id=chat_id,
chat_ids=chat_ids,
city=city,
alert_payload=alert_payload,
state=state,