feat: Introduce Polymarket wallet activity watcher with configurable user aliases and link previews.
This commit is contained in:
@@ -62,6 +62,10 @@ POLYGON_WALLET_WATCH_POLYMARKET_CONTRACTS=
|
||||
# Polymarket Wallet Activity Watcher (all markets, not weather-only)
|
||||
POLYMARKET_WALLET_ACTIVITY_ENABLED=false
|
||||
POLYMARKET_WALLET_ACTIVITY_USERS=0x0000000000000000000000000000000000000000
|
||||
# Optional wallet nicknames:
|
||||
# - CSV: 0xabc...=Whale_A,0xdef...=Main_Account
|
||||
# - JSON: {"0xabc...":"Whale A","0xdef...":"Main Account"}
|
||||
POLYMARKET_WALLET_ACTIVITY_USER_ALIASES=
|
||||
POLYMARKET_WALLET_ACTIVITY_DATA_API_URL=https://data-api.polymarket.com
|
||||
POLYMARKET_WALLET_ACTIVITY_INTERVAL_SEC=20
|
||||
POLYMARKET_WALLET_ACTIVITY_TIMEOUT_SEC=10
|
||||
@@ -74,6 +78,7 @@ POLYMARKET_WALLET_ACTIVITY_IMMEDIATE_COOLDOWN_SEC=20
|
||||
POLYMARKET_WALLET_ACTIVITY_MAX_CHANGES_PER_MSG=5
|
||||
POLYMARKET_WALLET_ACTIVITY_NOTIFY_CLOSED=false
|
||||
POLYMARKET_WALLET_ACTIVITY_BOOTSTRAP_ALERT=false
|
||||
POLYMARKET_WALLET_ACTIVITY_LINK_PREVIEW=true
|
||||
POLYMARKET_WALLET_ACTIVITY_UPDATE_DEBOUNCE_SEC=30
|
||||
POLYMARKET_WALLET_ACTIVITY_UPDATE_MAX_HOLD_SEC=120
|
||||
POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MIN=0.01
|
||||
|
||||
@@ -76,6 +76,51 @@ def _parse_addresses(raw: Optional[str]) -> List[str]:
|
||||
return out
|
||||
|
||||
|
||||
def _parse_address_aliases(raw: Optional[str]) -> Dict[str, str]:
|
||||
"""
|
||||
Parse wallet aliases from either:
|
||||
- JSON object: {"0xabc...": "Whale A", "0xdef...": "Whale B"}
|
||||
- CSV pairs: 0xabc...=Whale A,0xdef...=Whale B
|
||||
"""
|
||||
out: Dict[str, str] = {}
|
||||
if not raw:
|
||||
return out
|
||||
|
||||
text = str(raw).strip()
|
||||
if not text:
|
||||
return out
|
||||
|
||||
if text.startswith("{"):
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
if isinstance(parsed, dict):
|
||||
for k, v in parsed.items():
|
||||
addr = _normalize_addr(k)
|
||||
alias = str(v or "").strip()
|
||||
if addr and alias:
|
||||
out[addr] = alias
|
||||
return out
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for part in text.split(","):
|
||||
row = part.strip()
|
||||
if not row:
|
||||
continue
|
||||
if "=" in row:
|
||||
left, right = row.split("=", 1)
|
||||
elif ":" in row:
|
||||
left, right = row.split(":", 1)
|
||||
else:
|
||||
continue
|
||||
addr = _normalize_addr(left)
|
||||
alias = str(right or "").strip()
|
||||
if addr and alias:
|
||||
out[addr] = alias
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _state_file() -> str:
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
return os.path.join(root, "data", "polymarket_wallet_activity_state.json")
|
||||
@@ -374,6 +419,7 @@ def _should_show_avg_price(avg_price: float) -> bool:
|
||||
def _format_change_block(
|
||||
change_type: str,
|
||||
wallet: str,
|
||||
wallet_alias: Optional[str],
|
||||
pos: Dict[str, Any],
|
||||
now_utc: str,
|
||||
) -> str:
|
||||
@@ -393,11 +439,14 @@ def _format_change_block(
|
||||
else:
|
||||
lines.append("🔄 仓位更新")
|
||||
|
||||
lines.append(f"钱包: {_short(wallet)}")
|
||||
wallet_label = _short(wallet)
|
||||
if wallet_alias:
|
||||
wallet_label = f"{wallet_alias} ({wallet_label})"
|
||||
lines.append(f"钱包: {wallet_label}")
|
||||
lines.append(f"市场: {title}")
|
||||
if market_url:
|
||||
lines.append(f"市场: {title} ({market_url})")
|
||||
else:
|
||||
lines.append(f"市场: {title}")
|
||||
# Keep raw URL on its own line so Telegram can generate link preview.
|
||||
lines.append(f"链接: {market_url}")
|
||||
|
||||
lines.append(f"买入方向: {outcome}")
|
||||
|
||||
@@ -434,6 +483,7 @@ def _build_message(
|
||||
wallet: str,
|
||||
changes: List[Tuple[str, Dict[str, Any]]],
|
||||
max_changes: int,
|
||||
wallet_alias: Optional[str] = None,
|
||||
) -> str:
|
||||
now_bj = (
|
||||
datetime.now(timezone.utc)
|
||||
@@ -444,7 +494,15 @@ def _build_message(
|
||||
lines = [f"🚨 钱包异动监控 ({len(changes)} 个异动):", ""]
|
||||
|
||||
for idx, (change_type, pos) in enumerate(shown):
|
||||
lines.append(_format_change_block(change_type, wallet, pos, f"{now_bj} 北京时间"))
|
||||
lines.append(
|
||||
_format_change_block(
|
||||
change_type,
|
||||
wallet,
|
||||
wallet_alias,
|
||||
pos,
|
||||
f"{now_bj} 北京时间",
|
||||
)
|
||||
)
|
||||
if idx != len(shown) - 1:
|
||||
lines.append("")
|
||||
|
||||
@@ -459,6 +517,9 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
|
||||
enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False)
|
||||
chat_id = os.getenv("TELEGRAM_CHAT_ID")
|
||||
users = _parse_addresses(os.getenv("POLYMARKET_WALLET_ACTIVITY_USERS"))
|
||||
user_aliases = _parse_address_aliases(
|
||||
os.getenv("POLYMARKET_WALLET_ACTIVITY_USER_ALIASES")
|
||||
)
|
||||
|
||||
if not enabled:
|
||||
logger.info("polymarket wallet activity watcher disabled")
|
||||
@@ -499,6 +560,7 @@ 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)
|
||||
link_preview = _env_bool("POLYMARKET_WALLET_ACTIVITY_LINK_PREVIEW", True)
|
||||
default_debounce_sec = max(poll_sec, 30)
|
||||
update_debounce_sec = max(
|
||||
poll_sec,
|
||||
@@ -653,8 +715,17 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
|
||||
)
|
||||
|
||||
if outgoing:
|
||||
msg = _build_message(user, outgoing, max_changes=max_changes)
|
||||
bot.send_message(chat_id, msg, disable_web_page_preview=True)
|
||||
msg = _build_message(
|
||||
user,
|
||||
outgoing,
|
||||
max_changes=max_changes,
|
||||
wallet_alias=user_aliases.get(user),
|
||||
)
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
msg,
|
||||
disable_web_page_preview=not link_preview,
|
||||
)
|
||||
logger.info(
|
||||
f"wallet activity pushed user={user} changes={len(outgoing)}"
|
||||
)
|
||||
@@ -687,4 +758,3 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
|
||||
)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
Reference in New Issue
Block a user