feat: Introduce comprehensive weather data querying, analysis, and display services, integrate Polymarket data collection, and add Telegram notification utilities.

This commit is contained in:
2569718930@qq.com
2026-03-11 10:23:33 +08:00
parent 878e3280d1
commit b3f46430ad
9 changed files with 505 additions and 63 deletions
+205 -23
View File
@@ -237,6 +237,109 @@ def _diff_positions(
return changes
def _merge_pending_update(
pending_updates: Dict[str, Dict[str, Any]],
pos_key: str,
pos: Dict[str, Any],
now_ts: int,
) -> None:
entry = pending_updates.get(pos_key)
size_delta = _safe_float(pos.get("size_delta"))
old_size = _safe_float(pos.get("old_size"))
new_size = _safe_float(pos.get("size"))
old_avg = _safe_float(pos.get("old_avg_price"))
new_avg = _safe_float(pos.get("avg_price"))
if entry is None:
pending_updates[pos_key] = {
"count": 1,
"first_ts": now_ts,
"last_ts": now_ts,
"title": pos.get("title"),
"slug": pos.get("slug"),
"event_slug": pos.get("event_slug"),
"outcome": pos.get("outcome"),
"asset": pos.get("asset"),
"condition_id": pos.get("condition_id"),
"old_size": old_size,
"size": new_size,
"size_delta": size_delta,
"old_avg_price": old_avg,
"avg_price": new_avg,
"position_value": _safe_float(pos.get("position_value")),
"cash_pnl": _safe_float(pos.get("cash_pnl")),
"percent_pnl": _safe_float(pos.get("percent_pnl")),
}
return
entry["count"] = int(entry.get("count", 1)) + 1
entry["last_ts"] = now_ts
entry["size_delta"] = _safe_float(entry.get("size_delta")) + size_delta
entry["size"] = new_size
entry["avg_price"] = new_avg
entry["position_value"] = _safe_float(pos.get("position_value"))
entry["cash_pnl"] = _safe_float(pos.get("cash_pnl"))
entry["percent_pnl"] = _safe_float(pos.get("percent_pnl"))
pending_updates[pos_key] = entry
def _finalize_pending_update(
pending_entry: Dict[str, Any],
now_ts: int,
) -> Dict[str, Any]:
first_ts = int(pending_entry.get("first_ts") or now_ts)
last_ts = int(pending_entry.get("last_ts") or now_ts)
return {
"title": pending_entry.get("title") or "",
"slug": pending_entry.get("slug") or "",
"event_slug": pending_entry.get("event_slug") or "",
"outcome": pending_entry.get("outcome") or "",
"asset": pending_entry.get("asset") or "",
"condition_id": pending_entry.get("condition_id") or "",
"old_size": _safe_float(pending_entry.get("old_size")),
"size": _safe_float(pending_entry.get("size")),
"size_delta": _safe_float(pending_entry.get("size_delta")),
"old_avg_price": _safe_float(pending_entry.get("old_avg_price")),
"avg_price": _safe_float(pending_entry.get("avg_price")),
"position_value": _safe_float(pending_entry.get("position_value")),
"cash_pnl": _safe_float(pending_entry.get("cash_pnl")),
"percent_pnl": _safe_float(pending_entry.get("percent_pnl")),
"agg_count": int(pending_entry.get("count") or 1),
"agg_span_sec": max(0, last_ts - first_ts),
}
def _flush_ready_pending_updates(
pending_updates: Dict[str, Dict[str, Any]],
now_ts: int,
debounce_sec: int,
max_hold_sec: int,
force_keys: Optional[set] = None,
) -> List[Tuple[str, Dict[str, Any]]]:
out: List[Tuple[str, Dict[str, Any]]] = []
keys = list(pending_updates.keys())
for key in keys:
entry = pending_updates.get(key)
if not isinstance(entry, dict):
pending_updates.pop(key, None)
continue
if force_keys and key in force_keys:
out.append(("update", _finalize_pending_update(entry, now_ts)))
pending_updates.pop(key, None)
continue
first_ts = int(entry.get("first_ts") or now_ts)
last_ts = int(entry.get("last_ts") or now_ts)
quiet_enough = (now_ts - last_ts) >= debounce_sec
held_too_long = (now_ts - first_ts) >= max_hold_sec
if quiet_enough or held_too_long:
out.append(("update", _finalize_pending_update(entry, now_ts)))
pending_updates.pop(key, None)
return out
def _fmt_pct(value: float) -> str:
# Data API may return either ratio (0.12) or percent (12.0).
display = value * 100.0 if abs(value) <= 1.5 else value
@@ -279,8 +382,14 @@ def _format_change_block(
lines: List[str] = []
if change_type == "new":
lines.append("🆕 新开仓位")
elif change_type == "closed":
lines.append("❌ 仓位关闭")
else:
lines.append("🔄 仓位更新")
agg_count = int(pos.get("agg_count") or 1)
if agg_count > 1:
lines.append("🔁 连续仓位变动汇总")
else:
lines.append("🔄 仓位更新")
lines.append(f"钱包: {_short(wallet)}")
if market_url:
@@ -295,11 +404,20 @@ def _format_change_block(
now_size = _safe_float(pos.get("size"))
delta = _safe_float(pos.get("size_delta"))
lines.append(f"持有数量: {old_size:.3f} -> {now_size:.3f}{delta:+.3f})")
agg_count = int(pos.get("agg_count") or 1)
if agg_count > 1:
span_sec = int(_safe_float(pos.get("agg_span_sec")))
lines.append(f"变动次数: {agg_count} 次 | 聚合窗口: {span_sec}s")
else:
lines.append(f"持有数量: {_safe_float(pos.get('size')):.3f}")
avg_price = _safe_float(pos.get("avg_price"))
if _should_show_avg_price(avg_price):
old_avg_price = _safe_float(pos.get("old_avg_price"))
agg_count = int(pos.get("agg_count") or 1)
if change_type == "update" and agg_count > 1:
if _should_show_avg_price(old_avg_price) or _should_show_avg_price(avg_price):
lines.append(f"建仓均价: {_fmt_price(old_avg_price)} -> {_fmt_price(avg_price)}")
elif _should_show_avg_price(avg_price):
lines.append(f"建仓均价: {_fmt_price(avg_price)}")
lines.append(f"当前价值: {_fmt_usd(_safe_float(pos.get('position_value')))}")
@@ -360,6 +478,14 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
max_changes = max(1, _env_int("POLYMARKET_WALLET_ACTIVITY_MAX_CHANGES_PER_MSG", 5))
notify_closed = _env_bool("POLYMARKET_WALLET_ACTIVITY_NOTIFY_CLOSED", False)
bootstrap_alert = _env_bool("POLYMARKET_WALLET_ACTIVITY_BOOTSTRAP_ALERT", False)
update_debounce_sec = max(
poll_sec,
_env_int("POLYMARKET_WALLET_ACTIVITY_UPDATE_DEBOUNCE_SEC", 90),
)
update_max_hold_sec = max(
update_debounce_sec,
_env_int("POLYMARKET_WALLET_ACTIVITY_UPDATE_MAX_HOLD_SEC", 240),
)
# 价格过滤范围配置
min_price = _env_float("POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MIN", 0.0)
@@ -374,13 +500,15 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
logger.info(
f"polymarket wallet activity watcher started users={len(users)} "
f"poll={poll_sec}s data_api={data_api_url} price_filter={min_price}-{max_price}"
f"poll={poll_sec}s data_api={data_api_url} price_filter={min_price}-{max_price} "
f"update_debounce={update_debounce_sec}s update_max_hold={update_max_hold_sec}s"
)
while True:
touched = False
for user in users:
try:
now_ts = int(time.time())
rows = _fetch_positions(
session=session,
base_url=data_api_url,
@@ -388,19 +516,30 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
timeout_sec=timeout_sec,
)
current = _build_snapshot(rows, min_size_abs=min_size_abs)
prev = (
(users_state.get(user) or {}).get("positions")
if isinstance(users_state.get(user), dict)
else {}
) or {}
if not prev and not bootstrap_alert:
users_state[user] = {
"positions": current,
"updated_at": int(time.time()),
}
touched = True
continue
user_state = users_state.get(user) if isinstance(users_state.get(user), dict) else {}
prev = (user_state.get("positions") if isinstance(user_state, dict) else {}) or {}
if not isinstance(prev, dict):
prev = {}
pending_updates = (
user_state.get("pending_updates") if isinstance(user_state, dict) else {}
) or {}
if not isinstance(pending_updates, dict):
pending_updates = {}
initialized = bool(user_state.get("initialized")) if isinstance(user_state, dict) else False
# First cycle for each wallet only initializes baseline unless bootstrap alert is enabled.
if not initialized:
if not bootstrap_alert:
users_state[user] = {
"positions": current,
"pending_updates": {},
"initialized": True,
"updated_at": now_ts,
}
touched = True
continue
prev = {}
changes = _diff_positions(
previous=prev,
@@ -411,16 +550,63 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
max_price=max_price,
)
if changes:
msg = _build_message(user, changes, max_changes=max_changes)
outgoing: List[Tuple[str, Dict[str, Any]]] = []
for change_type, pos in changes:
pos_key = _position_key(pos)
if change_type == "update":
_merge_pending_update(
pending_updates=pending_updates,
pos_key=pos_key,
pos=pos,
now_ts=now_ts,
)
continue
outgoing.extend(
_flush_ready_pending_updates(
pending_updates=pending_updates,
now_ts=now_ts,
debounce_sec=update_debounce_sec,
max_hold_sec=update_max_hold_sec,
force_keys={pos_key},
)
)
outgoing.append((change_type, pos))
# If a key disappeared from snapshot, flush pending summary now.
missing_keys = {k for k in pending_updates.keys() if k not in current}
if missing_keys:
outgoing.extend(
_flush_ready_pending_updates(
pending_updates=pending_updates,
now_ts=now_ts,
debounce_sec=update_debounce_sec,
max_hold_sec=update_max_hold_sec,
force_keys=missing_keys,
)
)
outgoing.extend(
_flush_ready_pending_updates(
pending_updates=pending_updates,
now_ts=now_ts,
debounce_sec=update_debounce_sec,
max_hold_sec=update_max_hold_sec,
)
)
if outgoing:
msg = _build_message(user, outgoing, max_changes=max_changes)
bot.send_message(chat_id, msg, disable_web_page_preview=True)
logger.info(
f"wallet activity pushed user={user} changes={len(changes)}"
f"wallet activity pushed user={user} changes={len(outgoing)}"
)
users_state[user] = {
"positions": current,
"updated_at": int(time.time()),
"pending_updates": pending_updates,
"initialized": True,
"updated_at": now_ts,
}
touched = True
except Exception:
@@ -444,7 +630,3 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread