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 Watcher (all markets, not weather-only)
|
||||||
POLYMARKET_WALLET_ACTIVITY_ENABLED=false
|
POLYMARKET_WALLET_ACTIVITY_ENABLED=false
|
||||||
POLYMARKET_WALLET_ACTIVITY_USERS=0x0000000000000000000000000000000000000000
|
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_DATA_API_URL=https://data-api.polymarket.com
|
||||||
POLYMARKET_WALLET_ACTIVITY_INTERVAL_SEC=20
|
POLYMARKET_WALLET_ACTIVITY_INTERVAL_SEC=20
|
||||||
POLYMARKET_WALLET_ACTIVITY_TIMEOUT_SEC=10
|
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_MAX_CHANGES_PER_MSG=5
|
||||||
POLYMARKET_WALLET_ACTIVITY_NOTIFY_CLOSED=false
|
POLYMARKET_WALLET_ACTIVITY_NOTIFY_CLOSED=false
|
||||||
POLYMARKET_WALLET_ACTIVITY_BOOTSTRAP_ALERT=false
|
POLYMARKET_WALLET_ACTIVITY_BOOTSTRAP_ALERT=false
|
||||||
|
POLYMARKET_WALLET_ACTIVITY_LINK_PREVIEW=true
|
||||||
POLYMARKET_WALLET_ACTIVITY_UPDATE_DEBOUNCE_SEC=30
|
POLYMARKET_WALLET_ACTIVITY_UPDATE_DEBOUNCE_SEC=30
|
||||||
POLYMARKET_WALLET_ACTIVITY_UPDATE_MAX_HOLD_SEC=120
|
POLYMARKET_WALLET_ACTIVITY_UPDATE_MAX_HOLD_SEC=120
|
||||||
POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MIN=0.01
|
POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MIN=0.01
|
||||||
|
|||||||
@@ -76,6 +76,51 @@ def _parse_addresses(raw: Optional[str]) -> List[str]:
|
|||||||
return out
|
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:
|
def _state_file() -> str:
|
||||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
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")
|
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(
|
def _format_change_block(
|
||||||
change_type: str,
|
change_type: str,
|
||||||
wallet: str,
|
wallet: str,
|
||||||
|
wallet_alias: Optional[str],
|
||||||
pos: Dict[str, Any],
|
pos: Dict[str, Any],
|
||||||
now_utc: str,
|
now_utc: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -393,11 +439,14 @@ def _format_change_block(
|
|||||||
else:
|
else:
|
||||||
lines.append("🔄 仓位更新")
|
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:
|
if market_url:
|
||||||
lines.append(f"市场: {title} ({market_url})")
|
# Keep raw URL on its own line so Telegram can generate link preview.
|
||||||
else:
|
lines.append(f"链接: {market_url}")
|
||||||
lines.append(f"市场: {title}")
|
|
||||||
|
|
||||||
lines.append(f"买入方向: {outcome}")
|
lines.append(f"买入方向: {outcome}")
|
||||||
|
|
||||||
@@ -434,6 +483,7 @@ def _build_message(
|
|||||||
wallet: str,
|
wallet: str,
|
||||||
changes: List[Tuple[str, Dict[str, Any]]],
|
changes: List[Tuple[str, Dict[str, Any]]],
|
||||||
max_changes: int,
|
max_changes: int,
|
||||||
|
wallet_alias: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
now_bj = (
|
now_bj = (
|
||||||
datetime.now(timezone.utc)
|
datetime.now(timezone.utc)
|
||||||
@@ -444,7 +494,15 @@ def _build_message(
|
|||||||
lines = [f"🚨 钱包异动监控 ({len(changes)} 个异动):", ""]
|
lines = [f"🚨 钱包异动监控 ({len(changes)} 个异动):", ""]
|
||||||
|
|
||||||
for idx, (change_type, pos) in enumerate(shown):
|
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:
|
if idx != len(shown) - 1:
|
||||||
lines.append("")
|
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)
|
enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False)
|
||||||
chat_id = os.getenv("TELEGRAM_CHAT_ID")
|
chat_id = os.getenv("TELEGRAM_CHAT_ID")
|
||||||
users = _parse_addresses(os.getenv("POLYMARKET_WALLET_ACTIVITY_USERS"))
|
users = _parse_addresses(os.getenv("POLYMARKET_WALLET_ACTIVITY_USERS"))
|
||||||
|
user_aliases = _parse_address_aliases(
|
||||||
|
os.getenv("POLYMARKET_WALLET_ACTIVITY_USER_ALIASES")
|
||||||
|
)
|
||||||
|
|
||||||
if not enabled:
|
if not enabled:
|
||||||
logger.info("polymarket wallet activity watcher disabled")
|
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))
|
max_changes = max(1, _env_int("POLYMARKET_WALLET_ACTIVITY_MAX_CHANGES_PER_MSG", 5))
|
||||||
notify_closed = _env_bool("POLYMARKET_WALLET_ACTIVITY_NOTIFY_CLOSED", False)
|
notify_closed = _env_bool("POLYMARKET_WALLET_ACTIVITY_NOTIFY_CLOSED", False)
|
||||||
bootstrap_alert = _env_bool("POLYMARKET_WALLET_ACTIVITY_BOOTSTRAP_ALERT", 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)
|
default_debounce_sec = max(poll_sec, 30)
|
||||||
update_debounce_sec = max(
|
update_debounce_sec = max(
|
||||||
poll_sec,
|
poll_sec,
|
||||||
@@ -653,8 +715,17 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
|
|||||||
)
|
)
|
||||||
|
|
||||||
if outgoing:
|
if outgoing:
|
||||||
msg = _build_message(user, outgoing, max_changes=max_changes)
|
msg = _build_message(
|
||||||
bot.send_message(chat_id, msg, disable_web_page_preview=True)
|
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(
|
logger.info(
|
||||||
f"wallet activity pushed user={user} changes={len(outgoing)}"
|
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()
|
thread.start()
|
||||||
return thread
|
return thread
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user