[{"content":"This is a complete walkthrough of the seven snippets currently published in Code Vault — a personal repository of crypto trading radars, autonomous trading systems, and security utilities, all in pure Python with zero or near-zero API costs. Every snippet below includes the full source code so you can read, fork, and run them locally. Use the table of contents on the right to jump to a specific tool.\n⚠️ Risk warning — These tools touch live markets and on-chain data. Several of them push real-time alerts to Telegram and one of them (the AI autonomous trader) opens virtual positions on Binance Futures. Read the per-tool notes carefully. Use at your own risk; the author makes no warranty for trading outcomes.\nTrading Radar Vitalik Sell Radar Date: 2026.05.02　Tags: Python · WebSocket · Ethereum · Telegram · Event-Driven\nGitHub: vitalik-sell-radar\nWebSocket event-driven Vitalik wallet sell detection with Telegram alerts\nMonitors Vitalik Buterin\u0026rsquo;s wallet (vitalik.eth) for ERC-20 token sells via WebSocket event subscription — zero polling, sub-second latency. Automatically classifies recipients as DEX Router (Uniswap, 1inch, SushiSwap), CEX hot wallet (Binance, Coinbase, Kraken), or LP Pool. Fetches real-time token prices from DexScreener. Multi-RPC failover with auto-reconnect. Pure Python, zero cost — uses free public RPC nodes.\nFull source code #!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; Vitalik Sell Radar — Event-Driven Edition WebSocket subscription to ERC-20 Transfer events, real-time detection of Vitalik\u0026#39;s sell activity, instant push to Telegram. Architecture: 1. WebSocket subscribes to Transfer(from=vitalik) events → sub-second detection 2. Classifies sell behavior (transfers to DEX Router / CEX / LP Pool) 3. Queries token info + price via DexScreener 4. Pushes alert to Telegram 5. Auto-reconnect + multi-RPC failover \u0026#34;\u0026#34;\u0026#34; import asyncio import json import logging import os import signal import sys import time from datetime import datetime, timezone from pathlib import Path import aiohttp import websockets # Load .env file def load_env(): env_path = Path(__file__).parent / \u0026#34;.env\u0026#34; if env_path.exists(): for line in env_path.read_text().splitlines(): line = line.strip() if line and not line.startswith(\u0026#34;#\u0026#34;) and \u0026#34;=\u0026#34; in line: k, v = line.split(\u0026#34;=\u0026#34;, 1) os.environ.setdefault(k.strip(), v.strip()) load_env() # ============================================================ # Configuration # ============================================================ # Vitalik\u0026#39;s main wallet VITALIK_ADDRESS = \u0026#34;0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\u0026#34; VITALIK_PADDED = \u0026#34;0x\u0026#34; + VITALIK_ADDRESS[2:].lower().zfill(64) # ERC-20 Transfer event topic TRANSFER_TOPIC = \u0026#34;0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\u0026#34; # WebSocket RPC endpoints (free, support eth_subscribe) WS_ENDPOINTS = [ \u0026#34;wss://ethereum-rpc.publicnode.com\u0026#34;, \u0026#34;wss://eth.drpc.org\u0026#34;, \u0026#34;wss://ethereum.publicnode.com\u0026#34;, ] # HTTP RPC for querying token info HTTP_RPC = os.environ.get(\u0026#34;HTTP_RPC\u0026#34;, \u0026#34;https://eth.drpc.org\u0026#34;) # Telegram TG_BOT_TOKEN = os.environ.get(\u0026#34;TG_BOT_TOKEN\u0026#34;, \u0026#34;\u0026#34;) TG_CHAT_ID = os.environ.get(\u0026#34;TG_CHAT_ID\u0026#34;, \u0026#34;\u0026#34;) # Known DEX Router addresses (sell destinations) KNOWN_DEX_ROUTERS = { # Uniswap \u0026#34;0x7a250d5630b4cf539739df2c5dacb4c659f2488d\u0026#34;: \u0026#34;Uniswap V2 Router\u0026#34;, \u0026#34;0xe592427a0aece92de3edee1f18e0157c05861564\u0026#34;: \u0026#34;Uniswap V3 Router\u0026#34;, \u0026#34;0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45\u0026#34;: \u0026#34;Uniswap V3 Router2\u0026#34;, \u0026#34;0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad\u0026#34;: \u0026#34;Uniswap Universal Router\u0026#34;, \u0026#34;0xef1c6e67703c7bd7107eed8303fbe6ec2554bf6b\u0026#34;: \u0026#34;Uniswap Universal Router (old)\u0026#34;, # 1inch \u0026#34;0x1111111254eeb25477b68fb85ed929f73a960582\u0026#34;: \u0026#34;1inch V5\u0026#34;, \u0026#34;0x111111125421ca6dc452d289314280a0f8842a65\u0026#34;: \u0026#34;1inch V6\u0026#34;, # SushiSwap \u0026#34;0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f\u0026#34;: \u0026#34;SushiSwap Router\u0026#34;, # CoW Protocol \u0026#34;0x9008d19f58aabd9ed0d60971565aa8510560ab41\u0026#34;: \u0026#34;CoW Settlement\u0026#34;, # 0x \u0026#34;0xdef1c0ded9bec7f1a1670819833240f027b25eff\u0026#34;: \u0026#34;0x Exchange Proxy\u0026#34;, # Curve \u0026#34;0x99a58482bd75cbab83b27ec03ca68ff489b5788f\u0026#34;: \u0026#34;Curve Router\u0026#34;, } # Known CEX hot wallets (partial list) KNOWN_CEX = { \u0026#34;0x28c6c06298d514db089934071355e5743bf21d60\u0026#34;: \u0026#34;Binance Hot Wallet\u0026#34;, \u0026#34;0x21a31ee1afc51d94c2efccaa2092ad1028285549\u0026#34;: \u0026#34;Binance Hot Wallet 2\u0026#34;, \u0026#34;0xdfd5293d8e347dfe59e90efd55b2956a1343963d\u0026#34;: \u0026#34;Binance Hot Wallet 3\u0026#34;, \u0026#34;0x56eddb7aa87536c09ccc2793473599fd21a8b17f\u0026#34;: \u0026#34;Binance Hot Wallet 4\u0026#34;, \u0026#34;0x71660c4005ba85c37ccec55d0c4493e66fe775d3\u0026#34;: \u0026#34;Coinbase\u0026#34;, \u0026#34;0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43\u0026#34;: \u0026#34;Coinbase 10\u0026#34;, \u0026#34;0x503828976d22510aad0201ac7ec88293211d23da\u0026#34;: \u0026#34;Coinbase 2\u0026#34;, \u0026#34;0x2faf487a4414fe77e2327f0bf4ae2a264a776ad2\u0026#34;: \u0026#34;FTX (defunct)\u0026#34;, \u0026#34;0x267be1c1d684f78cb4f6a176c4911b741e4ffdc0\u0026#34;: \u0026#34;Kraken\u0026#34;, \u0026#34;0xae2d4617c862309a3d75a0ffb358c7a5009c673f\u0026#34;: \u0026#34;Kraken 10\u0026#34;, } # Minimum notification amount (USD), 0 = notify all MIN_NOTIFY_USD = int(os.environ.get(\u0026#34;MIN_NOTIFY_USD\u0026#34;, \u0026#34;0\u0026#34;)) # Reconnect settings RECONNECT_DELAY = 5 MAX_RECONNECT_DELAY = 60 # ============================================================ # Logging # ============================================================ logging.basicConfig( level=logging.INFO, format=\u0026#34;%(asctime)s [%(levelname)s] %(message)s\u0026#34;, datefmt=\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;, ) log = logging.getLogger(\u0026#34;vitalik-radar\u0026#34;) # ============================================================ # Global state # ============================================================ # Dedup set for processed tx hashes (last 1000) seen_txs: set = set() seen_txs_list: list = [] # HTTP session http_session: aiohttp.ClientSession | None = None # Token info cache: {address: {symbol, name, decimals}} token_cache: dict = {} # Running flag running = True # ============================================================ # Utility functions # ============================================================ def shorten_addr(addr: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Shorten address for display\u0026#34;\u0026#34;\u0026#34; return f\u0026#34;{addr[:6]}...{addr[-4:]}\u0026#34; def decode_transfer_value(data_hex: str, decimals: int) -\u0026gt; float: \u0026#34;\u0026#34;\u0026#34;Decode Transfer event value\u0026#34;\u0026#34;\u0026#34; try: raw = int(data_hex, 16) return raw / (10 ** decimals) except: return 0.0 def topic_to_address(topic: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Extract 20-byte address from 32-byte topic\u0026#34;\u0026#34;\u0026#34; return \u0026#34;0x\u0026#34; + topic[-40:] def classify_recipient(to_addr: str) -\u0026gt; tuple[str, str]: \u0026#34;\u0026#34;\u0026#34; Classify recipient address. Returns (type, name) Type: \u0026#34;dex\u0026#34; / \u0026#34;cex\u0026#34; / \u0026#34;pool\u0026#34; / \u0026#34;unknown\u0026#34; \u0026#34;\u0026#34;\u0026#34; to_lower = to_addr.lower() if to_lower in KNOWN_DEX_ROUTERS: return \u0026#34;dex\u0026#34;, KNOWN_DEX_ROUTERS[to_lower] if to_lower in KNOWN_CEX: return \u0026#34;cex\u0026#34;, KNOWN_CEX[to_lower] return \u0026#34;unknown\u0026#34;, \u0026#34;\u0026#34; async def get_http_session() -\u0026gt; aiohttp.ClientSession: global http_session if http_session is None or http_session.closed: http_session = aiohttp.ClientSession() return http_session async def rpc_call(method: str, params: list) -\u0026gt; dict | None: \u0026#34;\u0026#34;\u0026#34;HTTP JSON-RPC call\u0026#34;\u0026#34;\u0026#34; session = await get_http_session() try: async with session.post( HTTP_RPC, json={\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;method\u0026#34;: method, \u0026#34;params\u0026#34;: params}, timeout=aiohttp.ClientTimeout(total=10), ) as resp: data = await resp.json() return data.get(\u0026#34;result\u0026#34;) except Exception as e: log.error(f\u0026#34;RPC call {method} failed: {e}\u0026#34;) return None async def get_token_info(token_addr: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;Query ERC-20 token info (symbol, name, decimals)\u0026#34;\u0026#34;\u0026#34; addr_lower = token_addr.lower() if addr_lower in token_cache: return token_cache[addr_lower] info = {\u0026#34;symbol\u0026#34;: \u0026#34;???\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Unknown\u0026#34;, \u0026#34;decimals\u0026#34;: 18, \u0026#34;address\u0026#34;: token_addr} # symbol() result = await rpc_call(\u0026#34;eth_call\u0026#34;, [ {\u0026#34;to\u0026#34;: token_addr, \u0026#34;data\u0026#34;: \u0026#34;0x95d89b41\u0026#34;}, \u0026#34;latest\u0026#34; ]) if result and len(result) \u0026gt; 2: try: hex_str = result[2:] if len(hex_str) \u0026gt;= 128: offset = int(hex_str[:64], 16) * 2 length = int(hex_str[offset:offset+64], 16) symbol_hex = hex_str[offset+64:offset+64+length*2] info[\u0026#34;symbol\u0026#34;] = bytes.fromhex(symbol_hex).decode(\u0026#34;utf-8\u0026#34;, errors=\u0026#34;replace\u0026#34;).strip(\u0026#39;\\x00\u0026#39;) elif len(hex_str) == 64: info[\u0026#34;symbol\u0026#34;] = bytes.fromhex(hex_str).decode(\u0026#34;utf-8\u0026#34;, errors=\u0026#34;replace\u0026#34;).strip(\u0026#39;\\x00\u0026#39;) except: pass # decimals() result = await rpc_call(\u0026#34;eth_call\u0026#34;, [ {\u0026#34;to\u0026#34;: token_addr, \u0026#34;data\u0026#34;: \u0026#34;0x313ce567\u0026#34;}, \u0026#34;latest\u0026#34; ]) if result and len(result) \u0026gt; 2: try: info[\u0026#34;decimals\u0026#34;] = int(result, 16) except: pass # name() result = await rpc_call(\u0026#34;eth_call\u0026#34;, [ {\u0026#34;to\u0026#34;: token_addr, \u0026#34;data\u0026#34;: \u0026#34;0x06fdde03\u0026#34;}, \u0026#34;latest\u0026#34; ]) if result and len(result) \u0026gt; 2: try: hex_str = result[2:] if len(hex_str) \u0026gt;= 128: offset = int(hex_str[:64], 16) * 2 length = int(hex_str[offset:offset+64], 16) name_hex = hex_str[offset+64:offset+64+length*2] info[\u0026#34;name\u0026#34;] = bytes.fromhex(name_hex).decode(\u0026#34;utf-8\u0026#34;, errors=\u0026#34;replace\u0026#34;).strip(\u0026#39;\\x00\u0026#39;) elif len(hex_str) == 64: info[\u0026#34;name\u0026#34;] = bytes.fromhex(hex_str).decode(\u0026#34;utf-8\u0026#34;, errors=\u0026#34;replace\u0026#34;).strip(\u0026#39;\\x00\u0026#39;) except: pass token_cache[addr_lower] = info return info async def check_if_pool(addr: str) -\u0026gt; bool: \u0026#34;\u0026#34;\u0026#34;Check if address is a Uniswap V2/V3 pool (has token0 method)\u0026#34;\u0026#34;\u0026#34; result = await rpc_call(\u0026#34;eth_call\u0026#34;, [ {\u0026#34;to\u0026#34;: addr, \u0026#34;data\u0026#34;: \u0026#34;0x0dfe1681\u0026#34;}, \u0026#34;latest\u0026#34; # token0() ]) if result and len(result) == 66: # 0x + 64 hex chars return True return False async def get_eth_price() -\u0026gt; float: \u0026#34;\u0026#34;\u0026#34;Get ETH price from CoinGecko\u0026#34;\u0026#34;\u0026#34; session = await get_http_session() try: async with session.get( \u0026#34;https://api.coingecko.com/api/v3/simple/price?ids=ethereum\u0026amp;vs_currencies=usd\u0026#34;, timeout=aiohttp.ClientTimeout(total=5), ) as resp: data = await resp.json() return data.get(\u0026#34;ethereum\u0026#34;, {}).get(\u0026#34;usd\u0026#34;, 0) except: return 2300 # fallback async def get_token_price_usd(token_addr: str) -\u0026gt; float | None: \u0026#34;\u0026#34;\u0026#34;Get token USD price from DexScreener (free, no API key needed)\u0026#34;\u0026#34;\u0026#34; session = await get_http_session() try: async with session.get( f\u0026#34;https://api.dexscreener.com/latest/dex/tokens/{token_addr}\u0026#34;, timeout=aiohttp.ClientTimeout(total=5), ) as resp: data = await resp.json() pairs = data.get(\u0026#34;pairs\u0026#34;, []) if pairs: # Pick pair with highest liquidity pairs.sort(key=lambda p: float(p.get(\u0026#34;liquidity\u0026#34;, {}).get(\u0026#34;usd\u0026#34;, 0) or 0), reverse=True) price = float(pairs[0].get(\u0026#34;priceUsd\u0026#34;, 0) or 0) return price if price \u0026gt; 0 else None except: pass return None # ============================================================ # Telegram notifications # ============================================================ async def send_telegram(text: str): \u0026#34;\u0026#34;\u0026#34;Send Telegram message\u0026#34;\u0026#34;\u0026#34; if not TG_BOT_TOKEN: log.warning(\u0026#34;[TG] No bot token configured, skipping notification\u0026#34;) return session = await get_http_session() url = f\u0026#34;https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage\u0026#34; payload = { \u0026#34;chat_id\u0026#34;: TG_CHAT_ID, \u0026#34;text\u0026#34;: text, \u0026#34;parse_mode\u0026#34;: \u0026#34;HTML\u0026#34;, \u0026#34;disable_web_page_preview\u0026#34;: True, } try: async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=10)) as resp: result = await resp.json() if not result.get(\u0026#34;ok\u0026#34;): log.error(f\u0026#34;[TG] Send failed: {result.get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;)}\u0026#34;) # Retry with plain text if HTML parse fails if \u0026#34;parse\u0026#34; in result.get(\u0026#34;description\u0026#34;, \u0026#34;\u0026#34;).lower(): payload[\u0026#34;parse_mode\u0026#34;] = None async with session.post(url, json=payload) as resp2: pass else: log.info(\u0026#34;[TG] Message sent\u0026#34;) except Exception as e: log.error(f\u0026#34;[TG] Error: {e}\u0026#34;) # ============================================================ # Event handling # ============================================================ async def handle_transfer_event(log_entry: dict): \u0026#34;\u0026#34;\u0026#34;Process a single Transfer event\u0026#34;\u0026#34;\u0026#34; tx_hash = log_entry.get(\u0026#34;transactionHash\u0026#34;, \u0026#34;\u0026#34;) token_addr = log_entry.get(\u0026#34;address\u0026#34;, \u0026#34;\u0026#34;) topics = log_entry.get(\u0026#34;topics\u0026#34;, []) data = log_entry.get(\u0026#34;data\u0026#34;, \u0026#34;0x0\u0026#34;) # Dedup dedup_key = f\u0026#34;{tx_hash}:{token_addr}\u0026#34; if dedup_key in seen_txs: return seen_txs.add(dedup_key) seen_txs_list.append(dedup_key) # Cleanup (keep last 1000) while len(seen_txs_list) \u0026gt; 1000: old = seen_txs_list.pop(0) seen_txs.discard(old) # Parse topics: [Transfer, from, to] if len(topics) \u0026lt; 3: return from_addr = topic_to_address(topics[1]) to_addr = topic_to_address(topics[2]) # Confirm it\u0026#39;s from Vitalik if from_addr.lower() != VITALIK_ADDRESS.lower(): return # Get token info token_info = await get_token_info(token_addr) symbol = token_info[\u0026#34;symbol\u0026#34;] decimals = token_info[\u0026#34;decimals\u0026#34;] amount = decode_transfer_value(data, decimals) if amount == 0: return # Classify recipient recipient_type, recipient_name = classify_recipient(to_addr) # If unknown, check if it\u0026#39;s an LP pool is_pool = False if recipient_type == \u0026#34;unknown\u0026#34;: is_pool = await check_if_pool(to_addr) if is_pool: recipient_type = \u0026#34;pool\u0026#34; recipient_name = \u0026#34;LP Pool\u0026#34; # Determine if this is a \u0026#34;sell\u0026#34; action is_sell = recipient_type in (\u0026#34;dex\u0026#34;, \u0026#34;cex\u0026#34;, \u0026#34;pool\u0026#34;) # If not a sell, just a regular transfer — log silently if not is_sell: log.info(f\u0026#34;[Transfer] {symbol} {amount:,.2f} → {shorten_addr(to_addr)} (regular transfer, not notifying)\u0026#34;) return # Get price token_price = await get_token_price_usd(token_addr) usd_value = amount * token_price if token_price else None # Below minimum notification amount — skip if usd_value is not None and usd_value \u0026lt; MIN_NOTIFY_USD: log.info(f\u0026#34;[Sell] {symbol} ${usd_value:.0f} \u0026lt; ${MIN_NOTIFY_USD} minimum, skipping\u0026#34;) return # Build notification message timestamp = datetime.now(timezone.utc).strftime(\u0026#34;%H:%M:%S UTC\u0026#34;) sell_type_label = { \u0026#34;dex\u0026#34;: \u0026#34;🔄 DEX Sell\u0026#34;, \u0026#34;cex\u0026#34;: \u0026#34;🏦 CEX Transfer\u0026#34;, \u0026#34;pool\u0026#34;: \u0026#34;🌊 Pool Sell\u0026#34;, } msg_lines = [ f\u0026#34;\u0026lt;b\u0026gt;🚨 Vitalik Sell Signal\u0026lt;/b\u0026gt;\u0026#34;, f\u0026#34;\u0026#34;, f\u0026#34;Token: \u0026lt;b\u0026gt;{symbol}\u0026lt;/b\u0026gt;\u0026#34;, f\u0026#34;Amount: {amount:,.4f}\u0026#34;, ] if usd_value is not None: msg_lines.append(f\u0026#34;Value: \u0026lt;b\u0026gt;${usd_value:,.0f}\u0026lt;/b\u0026gt;\u0026#34;) if token_price is not None: msg_lines.append(f\u0026#34;Price: ${token_price:,.8f}\u0026#34;) msg_lines.extend([ f\u0026#34;\u0026#34;, f\u0026#34;Type: {sell_type_label.get(recipient_type, \u0026#39;Unknown\u0026#39;)}\u0026#34;, f\u0026#34;Destination: {recipient_name or shorten_addr(to_addr)}\u0026#34;, f\u0026#34;Time: {timestamp}\u0026#34;, f\u0026#34;\u0026#34;, f\u0026#34;TX: https://etherscan.io/tx/{tx_hash}\u0026#34;, f\u0026#34;Token: https://dexscreener.com/ethereum/{token_addr}\u0026#34;, ]) msg = \u0026#34;\\n\u0026#34;.join(msg_lines) log.info(f\u0026#34;[SELL DETECTED] {symbol} {amount:,.4f} → {recipient_name or to_addr} | ${usd_value or \u0026#39;?\u0026#39;}\u0026#34;) await send_telegram(msg) # ============================================================ # WebSocket listener main loop # ============================================================ async def subscribe_and_listen(ws_url: str): \u0026#34;\u0026#34;\u0026#34;Connect to WebSocket and subscribe to Vitalik Transfer events\u0026#34;\u0026#34;\u0026#34; log.info(f\u0026#34;Connecting to {ws_url}...\u0026#34;) async with websockets.connect( ws_url, ping_interval=20, ping_timeout=30, close_timeout=10, max_size=2**20, # 1MB ) as ws: # Subscribe to Transfer FROM Vitalik sub_from = { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;method\u0026#34;: \u0026#34;eth_subscribe\u0026#34;, \u0026#34;params\u0026#34;: [\u0026#34;logs\u0026#34;, { \u0026#34;topics\u0026#34;: [TRANSFER_TOPIC, VITALIK_PADDED] }] } await ws.send(json.dumps(sub_from)) resp = await asyncio.wait_for(ws.recv(), timeout=10) data = json.loads(resp) if \u0026#34;error\u0026#34; in data: raise Exception(f\u0026#34;Subscribe failed: {data[\u0026#39;error\u0026#39;]}\u0026#34;) sub_id_from = data.get(\u0026#34;result\u0026#34;, \u0026#34;\u0026#34;) log.info(f\u0026#34;✅ Subscribed to Transfer FROM Vitalik (id={sub_id_from[:10]}...)\u0026#34;) # Also subscribe to Transfer TO Vitalik (monitor buys/receives) sub_to = { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 2, \u0026#34;method\u0026#34;: \u0026#34;eth_subscribe\u0026#34;, \u0026#34;params\u0026#34;: [\u0026#34;logs\u0026#34;, { \u0026#34;topics\u0026#34;: [TRANSFER_TOPIC, None, VITALIK_PADDED] }] } await ws.send(json.dumps(sub_to)) resp2 = await asyncio.wait_for(ws.recv(), timeout=10) data2 = json.loads(resp2) sub_id_to = data2.get(\u0026#34;result\u0026#34;, \u0026#34;\u0026#34;) if sub_id_to: log.info(f\u0026#34;✅ Subscribed to Transfer TO Vitalik (id={sub_id_to[:10]}...)\u0026#34;) log.info(\u0026#34;🔍 Monitoring Vitalik wallet... waiting for events\u0026#34;) # Listen for events async for message in ws: if not running: break try: evt = json.loads(message) if \u0026#34;params\u0026#34; not in evt: continue sub_id = evt[\u0026#34;params\u0026#34;].get(\u0026#34;subscription\u0026#34;, \u0026#34;\u0026#34;) log_entry = evt[\u0026#34;params\u0026#34;].get(\u0026#34;result\u0026#34;, {}) if sub_id == sub_id_from: # Vitalik sent tokens → check if it\u0026#39;s a sell await handle_transfer_event(log_entry) elif sub_id == sub_id_to: # Vitalik received tokens — log silently token_addr = log_entry.get(\u0026#34;address\u0026#34;, \u0026#34;\u0026#34;) token_info = await get_token_info(token_addr) data_hex = log_entry.get(\u0026#34;data\u0026#34;, \u0026#34;0x0\u0026#34;) amount = decode_transfer_value(data_hex, token_info[\u0026#34;decimals\u0026#34;]) log.debug(f\u0026#34;[Receive] {token_info[\u0026#39;symbol\u0026#39;]} +{amount:,.4f}\u0026#34;) except json.JSONDecodeError: continue except Exception as e: log.error(f\u0026#34;Event handling error: {e}\u0026#34;, exc_info=True) async def main(): \u0026#34;\u0026#34;\u0026#34;Main loop with auto-reconnect\u0026#34;\u0026#34;\u0026#34; global running # Validate required config if not TG_BOT_TOKEN: log.warning(\u0026#34;TG_BOT_TOKEN not set — Telegram notifications disabled\u0026#34;) if not TG_CHAT_ID: log.warning(\u0026#34;TG_CHAT_ID not set — Telegram notifications disabled\u0026#34;) # Signal handling def shutdown(sig, frame): global running log.info(f\u0026#34;Received signal {sig}, shutting down...\u0026#34;) running = False signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGTERM, shutdown) # Startup notice log.info(\u0026#34;=\u0026#34; * 50) log.info(\u0026#34;Vitalik Sell Radar — Started\u0026#34;) log.info(f\u0026#34;Monitoring: {VITALIK_ADDRESS}\u0026#34;) log.info(f\u0026#34;Min notify: ${MIN_NOTIFY_USD}\u0026#34;) log.info(f\u0026#34;Telegram: {\u0026#39;✅ Configured\u0026#39; if TG_BOT_TOKEN else \u0026#39;❌ Not configured\u0026#39;}\u0026#34;) log.info(\u0026#34;=\u0026#34; * 50) if TG_BOT_TOKEN and TG_CHAT_ID: await send_telegram( \u0026#34;🟢 Vitalik Sell Radar — Started\\n\\n\u0026#34; f\u0026#34;Monitoring: {shorten_addr(VITALIK_ADDRESS)}\\n\u0026#34; f\u0026#34;Min notify: ${MIN_NOTIFY_USD}\\n\u0026#34; \u0026#34;Mode: WebSocket event-driven (sub-second latency)\u0026#34; ) endpoint_idx = 0 reconnect_delay = RECONNECT_DELAY while running: ws_url = WS_ENDPOINTS[endpoint_idx % len(WS_ENDPOINTS)] try: await subscribe_and_listen(ws_url) reconnect_delay = RECONNECT_DELAY # reset on success except websockets.exceptions.ConnectionClosed as e: log.warning(f\u0026#34;WebSocket closed: {e}\u0026#34;) except asyncio.TimeoutError: log.warning(\u0026#34;WebSocket timeout\u0026#34;) except Exception as e: log.error(f\u0026#34;WebSocket error: {e}\u0026#34;) if not running: break # Switch endpoint and retry endpoint_idx += 1 log.info(f\u0026#34;Reconnecting in {reconnect_delay}s... (next: {WS_ENDPOINTS[endpoint_idx % len(WS_ENDPOINTS)]})\u0026#34;) await asyncio.sleep(reconnect_delay) reconnect_delay = min(reconnect_delay * 1.5, MAX_RECONNECT_DELAY) # Cleanup if http_session and not http_session.closed: await http_session.close() log.info(\u0026#34;Vitalik Sell Radar — Stopped\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: asyncio.run(main()) On-Chain Narrative Radar Date: 2026.04.28　Tags: Python · GMGN · DEXScreener · Telegram\nMomentum-driven on-chain token discovery across ETH/SOL/BSC/Base\nMomentum is the only push engine — narrative is just a classification label. Scans every 30 seconds across 4 chains. Tokens must show 3 consecutive rounds of market cap increase with ≥5% total gain to trigger an alert. Narratives (Musk/Trump, Binance/CZ, celebrity) are classified as ★★★/★★/★ labels but never trigger pushes on their own. Safety checks via RugCheck (SOL) and GoPlus (EVM). Pure Python, zero AI cost.\nFull source code #!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; 叙事雷达 → 链上雷达 v1 纯Python，零AI成本（关键词匹配 + 叙事去重） 三条推送通道： 1. 全新叙事 — 从未见过的概念/故事，全链推 2. 马斯克/川普相关 — 重点ETH+SOL，BSC也推 3. 币安/CZ相关 — 只推BSC 数据源：GMGN新币 + DEXScreener搜索 叙事历史：SQLite去重 \u0026#34;\u0026#34;\u0026#34; import requests import json import time import os import re import sqlite3 import hashlib from datetime import datetime, timedelta from pathlib import Path from difflib import SequenceMatcher # === 配置 === DATA_DIR = os.path.expanduser(\u0026#34;~/crypto-trading\u0026#34;) DB_FILE = os.path.join(DATA_DIR, \u0026#34;narrative_history.db\u0026#34;) LOG_FILE = os.path.join(DATA_DIR, \u0026#34;narrative_radar.log\u0026#34;) SEEN_FILE = os.path.join(DATA_DIR, \u0026#34;narrative_seen.json\u0026#34;) FLAP_SEEN_FILE = os.path.join(DATA_DIR, \u0026#34;flap_seen.json\u0026#34;) # 扫描间隔 SCAN_INTERVAL = 30 # 30秒（GMGN数据约1-5分钟刷新一次，10秒太频繁且数据不变） # 动量追踪器 — 内存中记录每个币的价格/市值快照 # {address: [{\u0026#39;ts\u0026#39;: timestamp, \u0026#39;mc\u0026#39;: market_cap, \u0026#39;vol\u0026#39;: volume, \u0026#39;price\u0026#39;: price}, ...]} MOMENTUM_TRACKER = {} MOMENTUM_PUSHED = {} # {address: {\u0026#39;count\u0026#39;: N, \u0026#39;last_ts\u0026#39;: ts, \u0026#39;last_mc\u0026#39;: mc}} 推送计数 MOMENTUM_CONSECUTIVE_UP = 3 # 连续涨3轮（数据实际变化时才算一轮） # 从.env读取TG配置 def load_env(): env = {} env_file = os.path.expanduser(\u0026#34;~/.env\u0026#34;) if os.path.exists(env_file): with open(env_file) as f: for line in f: line = line.strip() if \u0026#39;=\u0026#39; in line and not line.startswith(\u0026#39;#\u0026#39;): k, v = line.split(\u0026#39;=\u0026#39;, 1) env[k] = v return env ENV = load_env() TG_TOKEN = ENV.get(\u0026#39;TELEGRAM_BOT_TOKEN\u0026#39;, \u0026#39;\u0026#39;) TG_CHAT_ID = int(os.environ.get(\u0026#39;TG_CHAT_ID\u0026#39;, \u0026#39;0\u0026#39;)) GMGN_HEADERS = { \u0026#39;User-Agent\u0026#39;: \u0026#39;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36\u0026#39;, \u0026#39;Accept\u0026#39;: \u0026#39;application/json\u0026#39;, \u0026#39;Referer\u0026#39;: \u0026#39;https://gmgn.ai/\u0026#39;, } # ============================================================ # 马斯克/川普关键词库（大小写不敏感） # ============================================================ MUSK_TRUMP_KEYWORDS = { # 马斯克核心 \u0026#39;musk\u0026#39;, \u0026#39;elon\u0026#39;, \u0026#39;elonmusk\u0026#39;, # SpaceX/Tesla/X \u0026#39;spacex\u0026#39;, \u0026#39;starship\u0026#39;, \u0026#39;tesla\u0026#39;, \u0026#39;cybertruck\u0026#39;, \u0026#39;roadster\u0026#39;, \u0026#39;neuralink\u0026#39;, \u0026#39;boring\u0026#39;, \u0026#39;hyperloop\u0026#39;, \u0026#39;xai\u0026#39;, \u0026#39;grok\u0026#39;, # 马斯克相关人物/宠物/梗 \u0026#39;floki\u0026#39;, \u0026#39;shiba\u0026#39;, # 只在新币上下文中用 \u0026#39;doge father\u0026#39;, \u0026#39;dogefather\u0026#39;, \u0026#39;technoking\u0026#39;, \u0026#39;mars colony\u0026#39;, \u0026#39;mars\u0026#39;, # 川普核心 \u0026#39;trump\u0026#39;, \u0026#39;donald\u0026#39;, \u0026#39;maga\u0026#39;, \u0026#39;potus\u0026#39;, \u0026#39;trump47\u0026#39;, \u0026#39;melania\u0026#39;, \u0026#39;barron\u0026#39;, \u0026#39;ivanka\u0026#39;, # 川普相关 \u0026#39;dark maga\u0026#39;, \u0026#39;darkmaga\u0026#39;, \u0026#39;ultra maga\u0026#39;, \u0026#39;save america\u0026#39;, \u0026#39;truth social\u0026#39;, \u0026#39;covfefe\u0026#39;, # 马斯克+川普联动 \u0026#39;doge department\u0026#39;, \u0026#39;d.o.g.e\u0026#39;, \u0026#39;government efficiency\u0026#39;, } # 马斯克/川普正则（捕捉变体） MUSK_TRUMP_PATTERNS = [ r\u0026#39;\\belon\\b\u0026#39;, r\u0026#39;\\bmusk\\b\u0026#39;, r\u0026#39;\\btrump\\b\u0026#39;, r\u0026#39;\\bmaga\\b\u0026#39;, r\u0026#39;\\bspacex\\b\u0026#39;, r\u0026#39;\\bstarship\\b\u0026#39;, r\u0026#39;\\btesla\\b\u0026#39;, r\u0026#39;\\bgrok\\b\u0026#39;, r\u0026#39;\\bmelania\\b\u0026#39;, r\u0026#39;\\bbarron\\b\u0026#39;, r\u0026#39;\\bdoge\\s*department\\b\u0026#39;, r\u0026#39;\\bd\\.?o\\.?g\\.?e\\b\u0026#39;, # D.O.G.E变体 r\u0026#39;\\bx\\s*ai\\b\u0026#39;, r\u0026#39;\\bneuralink\\b\u0026#39;, ] # ============================================================ # 币安/CZ关键词库 # ============================================================ BINANCE_CZ_KEYWORDS = { # CZ核心 \u0026#39;cz\u0026#39;, \u0026#39;changpeng\u0026#39;, \u0026#39;zhao\u0026#39;, \u0026#39;czb\u0026#39;, \u0026#39;czbinance\u0026#39;, # 何一（BSC现在的核心推手！） \u0026#39;heyi\u0026#39;, \u0026#39;yi he\u0026#39;, \u0026#39;he yi\u0026#39;, \u0026#39;何一\u0026#39;, \u0026#39;yihe\u0026#39;, \u0026#39;sister yi\u0026#39;, \u0026#39;yi jie\u0026#39;, \u0026#39;一姐\u0026#39;, \u0026#39;何一姐\u0026#39;, # 币安品牌 \u0026#39;binance\u0026#39;, \u0026#39;bnb\u0026#39;, \u0026#39;pancake\u0026#39;, \u0026#39;pancakeswap\u0026#39;, # CZ相关动态词（书、活动、推特高频词） \u0026#39;giggle academy\u0026#39;, \u0026#39;binance life\u0026#39;, \u0026#39;bnb chain\u0026#39;, \u0026#39;principles\u0026#39;, \u0026#39;cz book\u0026#39;, # YZi Labs (原Binance Labs) \u0026#39;yzi\u0026#39;, \u0026#39;yzi labs\u0026#39;, # 中文关键词（BSC上常见） \u0026#39;赵长鹏\u0026#39;, \u0026#39;币安\u0026#39;, \u0026#39;长鹏\u0026#39;, \u0026#39;cz的\u0026#39;, \u0026#39;何一的\u0026#39;, # Four.meme平台相关 \u0026#39;fourmeme\u0026#39;, \u0026#39;four meme\u0026#39;, \u0026#39;4meme\u0026#39;, # CZ/何一推特互动高频词 \u0026#39;czs dog\u0026#39;, \u0026#39;cz dog\u0026#39;, \u0026#39;bnb dog\u0026#39;, \u0026#39;build on bnb\u0026#39;, \u0026#39;bnb ecosystem\u0026#39;, } BINANCE_CZ_PATTERNS = [ r\u0026#39;\\bcz\\b\u0026#39;, r\u0026#39;\\bbinance\\b\u0026#39;, r\u0026#39;\\bbnb\\b\u0026#39;, r\u0026#39;\\bheyi\\b\u0026#39;, r\u0026#39;\\byi\\s*he\\b\u0026#39;, r\u0026#39;\\bhe\\s*yi\\b\u0026#39;, r\u0026#39;\\b何一\\b\u0026#39;, r\u0026#39;\\b一姐\\b\u0026#39;, r\u0026#39;\\bpancake\\b\u0026#39;, r\u0026#39;\\bgiggle\\b\u0026#39;, r\u0026#39;\\byzi\\b\u0026#39;, r\u0026#39;\\bfourmeme\\b\u0026#39;, r\u0026#39;\\b4meme\\b\u0026#39;, ] # ============================================================ # 推特热点/名人关键词库（★★级别） # ============================================================ CELEBRITY_VIRAL_KEYWORDS = { # 科技名人 \u0026#39;vitalik\u0026#39;, \u0026#39;buterin\u0026#39;, \u0026#39;sam altman\u0026#39;, \u0026#39;satoshi\u0026#39;, \u0026#39;michael saylor\u0026#39;, \u0026#39;saylor\u0026#39;, \u0026#39;cathie wood\u0026#39;, \u0026#39;jack dorsey\u0026#39;, \u0026#39;zuckerberg\u0026#39;, \u0026#39;bezos\u0026#39;, \u0026#39;jensen huang\u0026#39;, \u0026#39;nvidia\u0026#39;, \u0026#39;tim cook\u0026#39;, # 币圈名人 \u0026#39;justin sun\u0026#39;, \u0026#39;sun yuchen\u0026#39;, \u0026#39;孙宇晨\u0026#39;, \u0026#39;tron\u0026#39;, \u0026#39;arthur hayes\u0026#39;, \u0026#39;su zhu\u0026#39;, \u0026#39;3ac\u0026#39;, \u0026#39;brian armstrong\u0026#39;, \u0026#39;coinbase\u0026#39;, \u0026#39;larry fink\u0026#39;, \u0026#39;blackrock\u0026#39;, \u0026#39;gary gensler\u0026#39;, \u0026#39;sec\u0026#39;, \u0026#39;michael novogratz\u0026#39;, \u0026#39;galaxy\u0026#39;, # 政治/社会名人 \u0026#39;biden\u0026#39;, \u0026#39;obama\u0026#39;, \u0026#39;putin\u0026#39;, \u0026#39;xi jinping\u0026#39;, \u0026#39;kanye\u0026#39;, \u0026#39;drake\u0026#39;, \u0026#39;snoop dogg\u0026#39;, \u0026#39;paris hilton\u0026#39;, \u0026#39;mark cuban\u0026#39;, \u0026#39;mr beast\u0026#39;, \u0026#39;mrbeast\u0026#39;, # 病毒式传播热词（龙虾级别的梗） \u0026#39;lobster\u0026#39;, \u0026#39;龙虾\u0026#39;, \u0026#39;lobsta\u0026#39;, \u0026#39;hawk tuah\u0026#39;, \u0026#39;griddy\u0026#39;, \u0026#39;skibidi\u0026#39;, \u0026#39;rizz\u0026#39;, \u0026#39;sigma\u0026#39;, \u0026#39;gyatt\u0026#39;, # 重大事件关键词 \u0026#39;etf\u0026#39;, \u0026#39;halving\u0026#39;, \u0026#39;减半\u0026#39;, \u0026#39;world war\u0026#39;, \u0026#39;wwiii\u0026#39;, \u0026#39;fed\u0026#39;, \u0026#39;rate cut\u0026#39;, \u0026#39;降息\u0026#39;, \u0026#39;tiktok ban\u0026#39;, \u0026#39;tiktok\u0026#39;, } CELEBRITY_VIRAL_PATTERNS = [ r\u0026#39;\\bvitalik\\b\u0026#39;, r\u0026#39;\\bsaylor\\b\u0026#39;, r\u0026#39;\\bblackrock\\b\u0026#39;, r\u0026#39;\\bcoinbase\\b\u0026#39;, r\u0026#39;\\bjustin\\s*sun\\b\u0026#39;, r\u0026#39;\\blobster\\b\u0026#39;, r\u0026#39;\\betf\\b\u0026#39;, r\u0026#39;\\bhalving\\b\u0026#39;, r\u0026#39;\\bmrbeast\\b\u0026#39;, r\u0026#39;\\bsnoop\\b\u0026#39;, r\u0026#39;\\bkanye\\b\u0026#39;, r\u0026#39;\\bdrake\\b\u0026#39;, ] # ============================================================ # 通用垃圾词（过滤明显的骗局/低质量币） # ============================================================ SPAM_PATTERNS = [ r\u0026#39;airdrop\u0026#39;, r\u0026#39;presale\u0026#39;, r\u0026#39;pre\\s*sale\u0026#39;, r\u0026#39;1000x\u0026#39;, r\u0026#39;100x guaranteed\u0026#39;, r\u0026#39;safe\\s*moon\u0026#39;, r\u0026#39;baby\\s*\\w+\u0026#39;, # babydoge等仿盘 r\u0026#39;pornhub\u0026#39;, r\u0026#39;porn\u0026#39;, r\u0026#39;xxx\u0026#39;, r\u0026#39;nsfw\u0026#39;, r\u0026#39;nigga\u0026#39;, r\u0026#39;nigger\u0026#39;, r\u0026#39;faggot\u0026#39;, r\u0026#39;scam\u0026#39;, r\u0026#39;rugpull\u0026#39;, r\u0026#39;rug\\s*pull\u0026#39;, r\u0026#39;official\\s*token\u0026#39;, r\u0026#39;official\\s*coin\u0026#39;, ] # 常见无叙事意义的单词（过滤单词名币） COMMON_NOISE_WORDS = { \u0026#39;nice\u0026#39;, \u0026#39;good\u0026#39;, \u0026#39;bad\u0026#39;, \u0026#39;cool\u0026#39;, \u0026#39;hot\u0026#39;, \u0026#39;big\u0026#39;, \u0026#39;small\u0026#39;, \u0026#39;life\u0026#39;, \u0026#39;love\u0026#39;, \u0026#39;hate\u0026#39;, \u0026#39;happy\u0026#39;, \u0026#39;sad\u0026#39;, \u0026#39;fun\u0026#39;, \u0026#39;lol\u0026#39;, \u0026#39;cat\u0026#39;, \u0026#39;dog\u0026#39;, \u0026#39;moon\u0026#39;, \u0026#39;sun\u0026#39;, \u0026#39;star\u0026#39;, \u0026#39;king\u0026#39;, \u0026#39;queen\u0026#39;, \u0026#39;gold\u0026#39;, \u0026#39;rich\u0026#39;, \u0026#39;cash\u0026#39;, \u0026#39;money\u0026#39;, \u0026#39;pay\u0026#39;, \u0026#39;buy\u0026#39;, \u0026#39;sell\u0026#39;, \u0026#39;pump\u0026#39;, \u0026#39;dump\u0026#39;, \u0026#39;bull\u0026#39;, \u0026#39;bear\u0026#39;, \u0026#39;green\u0026#39;, \u0026#39;red\u0026#39;, \u0026#39;hello\u0026#39;, \u0026#39;world\u0026#39;, \u0026#39;yes\u0026#39;, \u0026#39;no\u0026#39;, \u0026#39;wow\u0026#39;, \u0026#39;omg\u0026#39;, \u0026#39;lmao\u0026#39;, \u0026#39;simp\u0026#39;, \u0026#39;chad\u0026#39;, \u0026#39;based\u0026#39;, \u0026#39;cope\u0026#39;, \u0026#39;seethe\u0026#39;, \u0026#39;test\u0026#39;, \u0026#39;new\u0026#39;, \u0026#39;old\u0026#39;, \u0026#39;real\u0026#39;, \u0026#39;fake\u0026#39;, # 垃圾币名常见词 \u0026#39;shit\u0026#39;, \u0026#39;shitcoin\u0026#39;, \u0026#39;fuck\u0026#39;, \u0026#39;fart\u0026#39;, \u0026#39;poop\u0026#39;, \u0026#39;pee\u0026#39;, \u0026#39;cum\u0026#39;, \u0026#39;dick\u0026#39;, \u0026#39;ass\u0026#39;, \u0026#39;boob\u0026#39;, \u0026#39;tit\u0026#39;, \u0026#39;nigga\u0026#39;, \u0026#39;retard\u0026#39;, \u0026#39;slop\u0026#39;, # 超通用币名 \u0026#39;the\u0026#39;, \u0026#39;and\u0026#39;, \u0026#39;for\u0026#39;, \u0026#39;from\u0026#39;, \u0026#39;with\u0026#39;, \u0026#39;this\u0026#39;, \u0026#39;that\u0026#39;, \u0026#39;coin\u0026#39;, \u0026#39;token\u0026#39;, \u0026#39;meme\u0026#39;, \u0026#39;pepe\u0026#39;, \u0026#39;wojak\u0026#39;, \u0026#39;peg\u0026#39;, \u0026#39;usd\u0026#39;, \u0026#39;usdt\u0026#39;, \u0026#39;usdc\u0026#39;, \u0026#39;dai\u0026#39;, } # ============================================================ # 工具函数 # ============================================================ def log(msg): ts = datetime.now().strftime(\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;) line = f\u0026#34;[{ts}] {msg}\u0026#34; print(line) os.makedirs(DATA_DIR, exist_ok=True) with open(LOG_FILE, \u0026#39;a\u0026#39;) as f: f.write(line + \u0026#39;\\n\u0026#39;) def load_flap_seen(): if os.path.exists(FLAP_SEEN_FILE): try: with open(FLAP_SEEN_FILE) as f: return json.load(f) except: pass return {} def save_flap_seen(data): # 只保留7天内的 cutoff = int(time.time()) - 86400 * 7 data = {k: v for k, v in data.items() if v \u0026gt; cutoff} with open(FLAP_SEEN_FILE, \u0026#39;w\u0026#39;) as f: json.dump(data, f) def tg_send(text, parse_mode=\u0026#39;Markdown\u0026#39;): if not TG_TOKEN: log(f\u0026#34;[TG] No token, skip: {text[:80]}\u0026#34;) return False try: resp = requests.post( f\u0026#39;https://api.telegram.org/bot{TG_TOKEN}/sendMessage\u0026#39;, json={\u0026#39;chat_id\u0026#39;: TG_CHAT_ID, \u0026#39;text\u0026#39;: text, \u0026#39;parse_mode\u0026#39;: parse_mode}, timeout=10 ) result = resp.json() if not result.get(\u0026#39;ok\u0026#39;): # Markdown失败时降级到纯文本 if \u0026#39;can\\\u0026#39;t parse\u0026#39; in str(result.get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;)).lower(): resp = requests.post( f\u0026#39;https://api.telegram.org/bot{TG_TOKEN}/sendMessage\u0026#39;, json={\u0026#39;chat_id\u0026#39;: TG_CHAT_ID, \u0026#39;text\u0026#39;: text}, timeout=10 ) else: log(f\u0026#34;[TG] Error: {result.get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;)}\u0026#34;) return False return True except Exception as e: log(f\u0026#34;[TG] Send error: {e}\u0026#34;) return False # ============================================================ # 叙事历史数据库 # ============================================================ def init_db(): \u0026#34;\u0026#34;\u0026#34;初始化SQLite叙事历史库\u0026#34;\u0026#34;\u0026#34; conn = sqlite3.connect(DB_FILE) c = conn.cursor() # 所有见过的叙事主题 c.execute(\u0026#39;\u0026#39;\u0026#39;CREATE TABLE IF NOT EXISTS narratives ( id INTEGER PRIMARY KEY AUTOINCREMENT, theme TEXT NOT NULL, -- 归一化的叙事主题（小写） first_token_name TEXT, -- 第一次出现时的代币名 first_token_address TEXT, -- 第一次出现时的地址 first_chain TEXT, -- 第一次出现的链 first_seen_at INTEGER, -- 第一次看到的时间戳 token_count INTEGER DEFAULT 1, -- 出现过多少次 last_seen_at INTEGER -- 最近一次看到 )\u0026#39;\u0026#39;\u0026#39;) # 所有扫描过的代币 c.execute(\u0026#39;\u0026#39;\u0026#39;CREATE TABLE IF NOT EXISTS tokens_seen ( address TEXT PRIMARY KEY, chain TEXT, name TEXT, symbol TEXT, narrative_theme TEXT, category TEXT, -- \u0026#39;musk_trump\u0026#39; / \u0026#39;binance_cz\u0026#39; / \u0026#39;novel\u0026#39; / \u0026#39;common\u0026#39; first_seen_at INTEGER, market_cap REAL, pushed INTEGER DEFAULT 0, -- 是否已推送 seen_count INTEGER DEFAULT 1 -- 出现次数 )\u0026#39;\u0026#39;\u0026#39;) # 索引 c.execute(\u0026#39;CREATE INDEX IF NOT EXISTS idx_theme ON narratives(theme)\u0026#39;) c.execute(\u0026#39;CREATE INDEX IF NOT EXISTS idx_addr ON tokens_seen(address)\u0026#39;) conn.commit() return conn def normalize_theme(name, symbol): \u0026#34;\u0026#34;\u0026#34; 从代币名称+符号提取归一化的叙事主题 例如：\u0026#39;Elon Mars Colony\u0026#39; → \u0026#39;elon mars colony\u0026#39; \u0026#39;TRUMP2028\u0026#39; → \u0026#39;trump\u0026#39; \u0026#39;PancakeBunny\u0026#39; → \u0026#39;pancake bunny\u0026#39; \u0026#34;\u0026#34;\u0026#34; # 合并name和symbol text = f\u0026#34;{name} {symbol}\u0026#34;.lower().strip() # 去除常见后缀/前缀 noise = [\u0026#39;token\u0026#39;, \u0026#39;coin\u0026#39;, \u0026#39;inu\u0026#39;, \u0026#39;swap\u0026#39;, \u0026#39;finance\u0026#39;, \u0026#39;protocol\u0026#39;, \u0026#39;dao\u0026#39;, \u0026#39;defi\u0026#39;, \u0026#39;nft\u0026#39;, \u0026#39;meta\u0026#39;, \u0026#39;verse\u0026#39;, \u0026#39;fi\u0026#39;, \u0026#39;ai\u0026#39;, \u0026#39;pepe\u0026#39;, \u0026#39;wojak\u0026#39;, \u0026#39;chad\u0026#39;, \u0026#39;based\u0026#39;] # 分割camelCase text = re.sub(r\u0026#39;([a-z])([A-Z])\u0026#39;, r\u0026#39;\\1 \\2\u0026#39;, text) # 去除数字（如2028、1000x） text = re.sub(r\u0026#39;\\d+x?\u0026#39;, \u0026#39;\u0026#39;, text) # 只保留字母和空格 text = re.sub(r\u0026#39;[^a-z\\s]\u0026#39;, \u0026#39; \u0026#39;, text) # 去噪 words = [w for w in text.split() if w and len(w) \u0026gt; 1 and w not in noise] if not words: return name.lower().strip() return \u0026#39; \u0026#39;.join(sorted(set(words))) def is_similar_theme(theme1, theme2, threshold=0.7): \u0026#34;\u0026#34;\u0026#34;模糊匹配两个叙事主题\u0026#34;\u0026#34;\u0026#34; if theme1 == theme2: return True # 子串匹配 if theme1 in theme2 or theme2 in theme1: return True # 词重叠 words1 = set(theme1.split()) words2 = set(theme2.split()) if words1 and words2: overlap = len(words1 \u0026amp; words2) / min(len(words1), len(words2)) if overlap \u0026gt;= 0.6: return True # 序列匹配 return SequenceMatcher(None, theme1, theme2).ratio() \u0026gt;= threshold def check_narrative_novelty(conn, theme, name, symbol, address, chain): \u0026#34;\u0026#34;\u0026#34; 检查叙事状态 返回： (\u0026#39;novel\u0026#39;, None) — 第一次见到 (\u0026#39;heating\u0026#39;, narrative_row) — 短时间内持续出现新币！热点信号！ (\u0026#39;existing\u0026#39;, existing_theme_row) — 已有叙事，不热 核心逻辑：同一主题在30分钟内出现2+个不同的币 = 热点 \u0026#34;\u0026#34;\u0026#34; c = conn.cursor() now = int(time.time()) HEAT_WINDOW = 1800 # 30分钟窗口 HEAT_THRESHOLD = 2 # 窗口内出现2个以上同主题币就是热点 # 精确匹配 c.execute(\u0026#39;SELECT id, theme, first_token_name, first_token_address, first_chain, first_seen_at, token_count, last_seen_at FROM narratives WHERE theme = ?\u0026#39;, (theme,)) exact = c.fetchone() if exact: row_id, _, _, _, _, first_seen, count, last_seen = exact # 更新计数 new_count = count + 1 c.execute(\u0026#39;UPDATE narratives SET token_count = ?, last_seen_at = ? WHERE theme = ?\u0026#39;, (new_count, now, theme)) conn.commit() # 热点判断：在HEAT_WINDOW内出现了多个币 if now - first_seen \u0026lt; HEAT_WINDOW and new_count \u0026gt;= HEAT_THRESHOLD: return (\u0026#39;heating\u0026#39;, exact) # 或者：最近一次和这次间隔很短（说明持续在冒） if now - last_seen \u0026lt; HEAT_WINDOW and new_count \u0026gt;= HEAT_THRESHOLD: return (\u0026#39;heating\u0026#39;, exact) return (\u0026#39;existing\u0026#39;, exact) # 模糊匹配 — 取最近1000个主题比对 c.execute(\u0026#39;SELECT id, theme, first_token_name, first_token_address, first_chain, first_seen_at, token_count, last_seen_at FROM narratives ORDER BY last_seen_at DESC LIMIT 1000\u0026#39;) for row in c.fetchall(): if is_similar_theme(theme, row[1]): row_id, _, _, _, _, first_seen, count, last_seen = row new_count = count + 1 c.execute(\u0026#39;UPDATE narratives SET token_count = ?, last_seen_at = ? WHERE id = ?\u0026#39;, (new_count, now, row[0])) conn.commit() # 热点判断 if now - last_seen \u0026lt; HEAT_WINDOW and new_count \u0026gt;= HEAT_THRESHOLD: return (\u0026#39;heating\u0026#39;, row) return (\u0026#39;existing\u0026#39;, row) # 第一次见到 — 记录 c.execute(\u0026#39;\u0026#39;\u0026#39;INSERT INTO narratives (theme, first_token_name, first_token_address, first_chain, first_seen_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)\u0026#39;\u0026#39;\u0026#39;, (theme, name, address, chain, now, now)) conn.commit() return (\u0026#39;novel\u0026#39;, None) def get_token_seen_count(conn, address): \u0026#34;\u0026#34;\u0026#34;获取代币出现次数\u0026#34;\u0026#34;\u0026#34; c = conn.cursor() c.execute(\u0026#39;SELECT seen_count FROM tokens_seen WHERE address = ?\u0026#39;, (address,)) row = c.fetchone() return row[0] if row else 0 def is_token_seen(conn, address): \u0026#34;\u0026#34;\u0026#34;检查代币是否已经扫描过\u0026#34;\u0026#34;\u0026#34; c = conn.cursor() c.execute(\u0026#39;SELECT address FROM tokens_seen WHERE address = ?\u0026#39;, (address,)) return c.fetchone() is not None def record_token(conn, address, chain, name, symbol, theme, category, mc, pushed=False): \u0026#34;\u0026#34;\u0026#34;记录已扫描的代币 — 重复出现时计数+1\u0026#34;\u0026#34;\u0026#34; c = conn.cursor() # 检查是否已存在 c.execute(\u0026#39;SELECT seen_count FROM tokens_seen WHERE address = ?\u0026#39;, (address,)) existing = c.fetchone() if existing: # 已存在：计数+1，更新市值 new_count = existing[0] + 1 c.execute(\u0026#39;\u0026#39;\u0026#39;UPDATE tokens_seen SET seen_count = ?, market_cap = ?, category = ? WHERE address = ?\u0026#39;\u0026#39;\u0026#39;, (new_count, mc, category, address)) else: # 新记录 c.execute(\u0026#39;\u0026#39;\u0026#39;INSERT INTO tokens_seen (address, chain, name, symbol, narrative_theme, category, first_seen_at, market_cap, pushed, seen_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)\u0026#39;\u0026#39;\u0026#39;, (address, chain, name, symbol, theme, category, int(time.time()), mc, 1 if pushed else 0)) conn.commit() # ============================================================ # 叙事分类引擎 # ============================================================ def classify_narrative(name, symbol, chain): \u0026#34;\u0026#34;\u0026#34; 分类代币叙事 返回：(\u0026#39;musk_trump\u0026#39;, matched_keywords) / (\u0026#39;binance_cz\u0026#39;, matched_keywords) / (\u0026#39;novel\u0026#39;, None) / (\u0026#39;common\u0026#39;, None) \u0026#34;\u0026#34;\u0026#34; text = f\u0026#34;{name} {symbol}\u0026#34;.lower() # 1. 检查是否是垃圾币 for pat in SPAM_PATTERNS: if re.search(pat, text, re.IGNORECASE): return (\u0026#39;spam\u0026#39;, None) # 2. 马斯克/川普检测 matched_mt = [] for kw in MUSK_TRUMP_KEYWORDS: if kw.lower() in text: matched_mt.append(kw) if not matched_mt: for pat in MUSK_TRUMP_PATTERNS: m = re.search(pat, text, re.IGNORECASE) if m: matched_mt.append(m.group()) if matched_mt: # 马斯克/川普：重点ETH+SOL，BSC也可以 chain_lower = chain.lower() if chain_lower in (\u0026#39;eth\u0026#39;, \u0026#39;ethereum\u0026#39;, \u0026#39;sol\u0026#39;, \u0026#39;solana\u0026#39;, \u0026#39;bsc\u0026#39;, \u0026#39;base\u0026#39;): return (\u0026#39;musk_trump\u0026#39;, matched_mt) # 3. 币安/CZ检测 — 只在BSC上推 matched_bc = [] for kw in BINANCE_CZ_KEYWORDS: if kw.lower() in text: matched_bc.append(kw) if not matched_bc: for pat in BINANCE_CZ_PATTERNS: m = re.search(pat, text, re.IGNORECASE) if m: matched_bc.append(m.group()) if matched_bc: chain_lower = chain.lower() if chain_lower in (\u0026#39;bsc\u0026#39;,): return (\u0026#39;binance_cz\u0026#39;, matched_bc) else: return (\u0026#39;binance_cz_wrong_chain\u0026#39;, matched_bc) # 4. 名人/推特热点检测（★★级别） matched_cv = [] for kw in CELEBRITY_VIRAL_KEYWORDS: if kw.lower() in text: matched_cv.append(kw) if not matched_cv: for pat in CELEBRITY_VIRAL_PATTERNS: m = re.search(pat, text, re.IGNORECASE) if m: matched_cv.append(m.group()) if matched_cv: return (\u0026#39;celebrity_viral\u0026#39;, matched_cv) # 5. 都不匹配 → 需要进一步检查是否全新叙事 return (\u0026#39;check_novelty\u0026#39;, None) # ============================================================ # 安全检查（复用现有逻辑） # ============================================================ def check_token_safety(chain, address): \u0026#34;\u0026#34;\u0026#34;快速安全检查 — 只拦硬伤（蜜罐/可增发），卖税不作为否决条件\u0026#34;\u0026#34;\u0026#34; if chain in (\u0026#39;sol\u0026#39;, \u0026#39;solana\u0026#39;): try: r = requests.get(f\u0026#39;https://api.rugcheck.xyz/v1/tokens/{address}/report\u0026#39;, timeout=10) if r.status_code == 200: data = r.json() score = data.get(\u0026#39;score\u0026#39;, 999) mint = data.get(\u0026#39;mintAuthority\u0026#39;) freeze = data.get(\u0026#39;freezeAuthority\u0026#39;) return { \u0026#39;safe\u0026#39;: not mint and not freeze, \u0026#39;score\u0026#39;: score, \u0026#39;mint\u0026#39;: mint is not None, \u0026#39;freeze\u0026#39;: freeze is not None } except: pass else: chain_map = {\u0026#39;ethereum\u0026#39;: \u0026#39;1\u0026#39;, \u0026#39;eth\u0026#39;: \u0026#39;1\u0026#39;, \u0026#39;bsc\u0026#39;: \u0026#39;56\u0026#39;, \u0026#39;base\u0026#39;: \u0026#39;8453\u0026#39;} cid = chain_map.get(chain, \u0026#39;1\u0026#39;) try: r = requests.get(f\u0026#39;https://api.gopluslabs.io/api/v1/token_security/{cid}?contract_addresses={address}\u0026#39;, timeout=10) if r.status_code == 200: result = r.json().get(\u0026#39;result\u0026#39;, {}) data = result.get(address.lower(), {}) if data: honeypot = data.get(\u0026#39;is_honeypot\u0026#39;, \u0026#39;0\u0026#39;) == \u0026#39;1\u0026#39; mintable = data.get(\u0026#39;is_mintable\u0026#39;, \u0026#39;0\u0026#39;) == \u0026#39;1\u0026#39; sell_tax = float(data.get(\u0026#39;sell_tax\u0026#39;, \u0026#39;0\u0026#39;) or \u0026#39;0\u0026#39;) buy_tax = float(data.get(\u0026#39;buy_tax\u0026#39;, \u0026#39;0\u0026#39;) or \u0026#39;0\u0026#39;) return { \u0026#39;safe\u0026#39;: not honeypot and not mintable, # 卖税不作为否决 \u0026#39;honeypot\u0026#39;: honeypot, \u0026#39;mintable\u0026#39;: mintable, \u0026#39;sell_tax\u0026#39;: sell_tax, \u0026#39;buy_tax\u0026#39;: buy_tax } except: pass return {\u0026#39;safe\u0026#39;: False, \u0026#39;reason\u0026#39;: \u0026#39;无法检查\u0026#39;} # 无法检查时不推，宁可错过不踩坑 # ============================================================ # GMGN数据获取 # ============================================================ def gmgn_get(url): try: resp = requests.get(url, headers=GMGN_HEADERS, timeout=15) if resp.status_code == 200: return resp.json().get(\u0026#39;data\u0026#39;, {}) except: pass return {} def fetch_token_description(chain, address): \u0026#34;\u0026#34;\u0026#34;获取代币描述/故事 — 叙事雷达核心信息\u0026#34;\u0026#34;\u0026#34; desc = \u0026#39;\u0026#39; # SOL链：Pump.fun有最完整的description if chain in (\u0026#39;sol\u0026#39;, \u0026#39;solana\u0026#39;): try: r = requests.get(f\u0026#39;https://frontend-api-v3.pump.fun/coins/{address}\u0026#39;, timeout=8) if r.status_code == 200: data = r.json() desc = data.get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;) or \u0026#39;\u0026#39; twitter = data.get(\u0026#39;twitter\u0026#39;, \u0026#39;\u0026#39;) or \u0026#39;\u0026#39; telegram = data.get(\u0026#39;telegram\u0026#39;, \u0026#39;\u0026#39;) or \u0026#39;\u0026#39; website = data.get(\u0026#39;website\u0026#39;, \u0026#39;\u0026#39;) or \u0026#39;\u0026#39; return { \u0026#39;description\u0026#39;: desc.strip(), \u0026#39;twitter\u0026#39;: twitter, \u0026#39;telegram\u0026#39;: telegram, \u0026#39;website\u0026#39;: website, } except: pass # 所有链：DEXScreener info字段（网站+社交链接） try: chain_dex = {\u0026#39;sol\u0026#39;: \u0026#39;solana\u0026#39;, \u0026#39;eth\u0026#39;: \u0026#39;ethereum\u0026#39;, \u0026#39;bsc\u0026#39;: \u0026#39;bsc\u0026#39;, \u0026#39;base\u0026#39;: \u0026#39;base\u0026#39;, \u0026#39;solana\u0026#39;: \u0026#39;solana\u0026#39;, \u0026#39;ethereum\u0026#39;: \u0026#39;ethereum\u0026#39;}.get(chain, chain) r = requests.get(f\u0026#39;https://api.dexscreener.com/latest/dex/tokens/{address}\u0026#39;, timeout=8) if r.status_code == 200: pairs = r.json().get(\u0026#39;pairs\u0026#39;, []) if pairs: info = pairs[0].get(\u0026#39;info\u0026#39;, {}) websites = info.get(\u0026#39;websites\u0026#39;, []) socials = info.get(\u0026#39;socials\u0026#39;, []) twitter = \u0026#39;\u0026#39; telegram = \u0026#39;\u0026#39; website = \u0026#39;\u0026#39; for s in socials: if s.get(\u0026#39;type\u0026#39;) == \u0026#39;twitter\u0026#39;: twitter = s.get(\u0026#39;url\u0026#39;, \u0026#39;\u0026#39;) elif s.get(\u0026#39;type\u0026#39;) == \u0026#39;telegram\u0026#39;: telegram = s.get(\u0026#39;url\u0026#39;, \u0026#39;\u0026#39;) for w in websites: if w.get(\u0026#39;label\u0026#39;, \u0026#39;\u0026#39;).lower() == \u0026#39;website\u0026#39;: website = w.get(\u0026#39;url\u0026#39;, \u0026#39;\u0026#39;) if not desc: # DEXScreener没有description但有社交信息 return { \u0026#39;description\u0026#39;: desc, \u0026#39;twitter\u0026#39;: twitter, \u0026#39;telegram\u0026#39;: telegram, \u0026#39;website\u0026#39;: website, } except: pass return {\u0026#39;description\u0026#39;: desc, \u0026#39;twitter\u0026#39;: \u0026#39;\u0026#39;, \u0026#39;telegram\u0026#39;: \u0026#39;\u0026#39;, \u0026#39;website\u0026#39;: \u0026#39;\u0026#39;} def fetch_new_tokens(): \u0026#34;\u0026#34;\u0026#34;从GMGN获取各链新币 + 多维度覆盖\u0026#34;\u0026#34;\u0026#34; all_tokens = [] seen_addrs = set() for chain in [\u0026#39;eth\u0026#39;, \u0026#39;bsc\u0026#39;, \u0026#39;base\u0026#39;]: # 多维度拉数据，避免漏掉 urls = [ # 按创建时间 — 最新的币 f\u0026#39;https://gmgn.ai/defi/quotation/v1/rank/{chain}/swaps/1h?orderby=open_timestamp\u0026amp;direction=desc\u0026amp;limit=100\u0026#39;, # 按交易量 — 最活跃的币 f\u0026#39;https://gmgn.ai/defi/quotation/v1/rank/{chain}/swaps/1h?orderby=swaps\u0026amp;direction=desc\u0026amp;limit=50\u0026#39;, ] for url in urls: data = gmgn_get(url) tokens = data.get(\u0026#39;rank\u0026#39;, []) for t in tokens: addr = t.get(\u0026#39;address\u0026#39;, \u0026#39;\u0026#39;) if not addr or addr in seen_addrs: continue mc = t.get(\u0026#39;market_cap\u0026#39;, 0) or t.get(\u0026#39;fdv\u0026#39;, 0) or 0 liq = t.get(\u0026#39;liquidity\u0026#39;, 0) or 0 # 基本过滤：太小的不看 if mc \u0026lt; 1000 or liq \u0026lt; 500 or mc \u0026gt; 10000000: continue age_ts = t.get(\u0026#39;open_timestamp\u0026#39;, 0) age_h = (time.time() - age_ts) / 3600 if age_ts \u0026gt; 0 else 999 # 不限年龄 — 动量追踪核心逻辑：涨就推，不管新旧 seen_addrs.add(addr) all_tokens.append({ \u0026#39;address\u0026#39;: addr, \u0026#39;chain\u0026#39;: chain, \u0026#39;name\u0026#39;: t.get(\u0026#39;name\u0026#39;, \u0026#39;?\u0026#39;), \u0026#39;symbol\u0026#39;: t.get(\u0026#39;symbol\u0026#39;, \u0026#39;?\u0026#39;), \u0026#39;mc\u0026#39;: mc, \u0026#39;liq\u0026#39;: liq, \u0026#39;volume\u0026#39;: t.get(\u0026#39;volume\u0026#39;, 0) or 0, \u0026#39;holders\u0026#39;: t.get(\u0026#39;holder_count\u0026#39;, 0) or 0, \u0026#39;sm\u0026#39;: t.get(\u0026#39;smart_degen_count\u0026#39;, 0) or 0, \u0026#39;chg_1h\u0026#39;: t.get(\u0026#39;price_change_percent1h\u0026#39;, 0) or 0, \u0026#39;chg_24h\u0026#39;: t.get(\u0026#39;price_change_percent\u0026#39;, 0) or 0, \u0026#39;age_h\u0026#39;: age_h, \u0026#39;price\u0026#39;: t.get(\u0026#39;price\u0026#39;, 0), \u0026#39;buys_1h\u0026#39;: t.get(\u0026#39;buys\u0026#39;, 0) or 0, \u0026#39;sells_1h\u0026#39;: t.get(\u0026#39;sells\u0026#39;, 0) or 0, }) time.sleep(0.3) return all_tokens def fetch_flap_tokens(): \u0026#34;\u0026#34;\u0026#34; FLAP平台扫描 — BSC社区驱动型发射台 找形态：跌下来但有底部支撑（有庄在低位推） 特征：24h跌了，但1h企稳/反弹，买入\u0026gt;卖出，holders在涨 \u0026#34;\u0026#34;\u0026#34; data = gmgn_get( \u0026#39;https://gmgn.ai/defi/quotation/v1/rank/bsc/swaps/24h?launchpad=flap\u0026amp;orderby=volume\u0026amp;direction=desc\u0026amp;limit=30\u0026#39; ) tokens = data.get(\u0026#39;rank\u0026#39;, []) candidates = [] for t in tokens: addr = t.get(\u0026#39;address\u0026#39;, \u0026#39;\u0026#39;) if not addr: continue mc = t.get(\u0026#39;market_cap\u0026#39;, 0) or 0 liq = t.get(\u0026#39;liquidity\u0026#39;, 0) or 0 vol = t.get(\u0026#39;volume\u0026#39;, 0) or 0 holders = t.get(\u0026#39;holder_count\u0026#39;, 0) or 0 buys = t.get(\u0026#39;buys\u0026#39;, 0) or 0 sells = t.get(\u0026#39;sells\u0026#39;, 0) or 0 chg_1h = t.get(\u0026#39;price_change_percent1h\u0026#39;, 0) or 0 chg_24h = t.get(\u0026#39;price_change_percent\u0026#39;, 0) or 0 age_ts = t.get(\u0026#39;open_timestamp\u0026#39;, 0) age_h = (time.time() - age_ts) / 3600 if age_ts \u0026gt; 0 else 0 # 基本门槛 if mc \u0026lt; 1000 or liq \u0026lt; 500: continue if holders \u0026lt; 5: continue # 底部支撑形态判断： # 条件1: 24h跌了（或者涨幅有限），说明不是刚拉的 # 条件2: 1h跌幅小于24h跌幅，说明在企稳 # 条件3: 买入 \u0026gt; 卖出，有人在接 buy_ratio = buys / max(sells, 1) is_support = False reason = \u0026#39;\u0026#39; # 形态A: 24h跌了，1h在企稳/反弹 if chg_24h \u0026lt; -10 and chg_1h \u0026gt; chg_24h * 0.3: is_support = True reason = f\u0026#39;24h跌{chg_24h:.0f}%但1h企稳{chg_1h:+.0f}%\u0026#39; # 形态B: 24h微跌或横盘，1h微涨，买卖比健康 if -10 \u0026lt;= chg_24h \u0026lt;= 30 and chg_1h \u0026gt; -5 and buy_ratio \u0026gt; 1.1: is_support = True reason = f\u0026#39;底部横盘 买卖比{buy_ratio:.2f}\u0026#39; # 形态C: 大跌后强反弹 if chg_24h \u0026lt; -30 and chg_1h \u0026gt; 10: is_support = True reason = f\u0026#39;大跌{chg_24h:.0f}%后反弹{chg_1h:+.0f}%\u0026#39; if is_support and buy_ratio \u0026gt;= 1.0: candidates.append({ \u0026#39;address\u0026#39;: addr, \u0026#39;chain\u0026#39;: \u0026#39;bsc\u0026#39;, \u0026#39;name\u0026#39;: t.get(\u0026#39;name\u0026#39;, \u0026#39;?\u0026#39;), \u0026#39;symbol\u0026#39;: t.get(\u0026#39;symbol\u0026#39;, \u0026#39;?\u0026#39;), \u0026#39;mc\u0026#39;: mc, \u0026#39;liq\u0026#39;: liq, \u0026#39;volume\u0026#39;: vol, \u0026#39;holders\u0026#39;: holders, \u0026#39;sm\u0026#39;: 0, \u0026#39;chg_1h\u0026#39;: chg_1h, \u0026#39;chg_24h\u0026#39;: chg_24h, \u0026#39;age_h\u0026#39;: age_h, \u0026#39;price\u0026#39;: t.get(\u0026#39;price\u0026#39;, 0), \u0026#39;buys\u0026#39;: buys, \u0026#39;sells\u0026#39;: sells, \u0026#39;buy_ratio\u0026#39;: buy_ratio, \u0026#39;support_reason\u0026#39;: reason, \u0026#39;launchpad\u0026#39;: \u0026#39;flap\u0026#39;, }) # 按市值排序 candidates.sort(key=lambda x: x[\u0026#39;mc\u0026#39;], reverse=True) return candidates def format_flap_alert(token, desc_info=None): \u0026#34;\u0026#34;\u0026#34;FLAP低吸信号推送\u0026#34;\u0026#34;\u0026#34; msg = f\u0026#34;链上雷达 — FLAP低吸信号\\n\u0026#34; msg += f\u0026#34;链: BSC | 平台: FLAP\\n\\n\u0026#34; msg += f\u0026#34;{token[\u0026#39;name\u0026#39;]} ({token[\u0026#39;symbol\u0026#39;]})\\n\u0026#34; msg += f\u0026#34;`{token[\u0026#39;address\u0026#39;]}`\\n\\n\u0026#34; # 故事描述 desc = (desc_info or {}).get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;) if desc: if len(desc) \u0026gt; 200: desc = desc[:200] + \u0026#39;...\u0026#39; msg += f\u0026#34;故事: {desc}\\n\\n\u0026#34; msg += f\u0026#34;形态: {token[\u0026#39;support_reason\u0026#39;]}\\n\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; msg += f\u0026#34;市值 ${token[\u0026#39;mc\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;流动性 ${token[\u0026#39;liq\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;24h量 ${token[\u0026#39;volume\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;持有人 {token[\u0026#39;holders\u0026#39;]:\u0026gt;12,d}\\n\u0026#34; msg += f\u0026#34;买/卖 {token[\u0026#39;buys\u0026#39;]:\u0026gt;6,d}/{token[\u0026#39;sells\u0026#39;]:\u0026gt;6,d}\\n\u0026#34; msg += f\u0026#34;买卖比 {token[\u0026#39;buy_ratio\u0026#39;]:\u0026gt;12.2f}\\n\u0026#34; msg += f\u0026#34;1h涨幅 {token[\u0026#39;chg_1h\u0026#39;]:\u0026gt;+11.1f}%\\n\u0026#34; msg += f\u0026#34;24h涨幅 {token[\u0026#39;chg_24h\u0026#39;]:\u0026gt;+11.1f}%\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; msg += \u0026#34;\\nFLAP社区币 — 低吸进场信号\u0026#34; # 社交链接 links = [] if (desc_info or {}).get(\u0026#39;twitter\u0026#39;): links.append(f\u0026#34;\\nTwitter: {desc_info[\u0026#39;twitter\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;telegram\u0026#39;): links.append(f\u0026#34;TG: {desc_info[\u0026#39;telegram\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;website\u0026#39;): links.append(f\u0026#34;Web: {desc_info[\u0026#39;website\u0026#39;]}\u0026#34;) if links: msg += \u0026#39;\\n\u0026#39;.join(links) return msg # ============================================================ # 推送格式 # ============================================================ def format_musk_trump_alert(token, matched_kw, desc_info=None): \u0026#34;\u0026#34;\u0026#34;马斯克/川普叙事推送\u0026#34;\u0026#34;\u0026#34; chain_map = {\u0026#39;sol\u0026#39;: \u0026#39;SOL\u0026#39;, \u0026#39;eth\u0026#39;: \u0026#39;ETH\u0026#39;, \u0026#39;bsc\u0026#39;: \u0026#39;BSC\u0026#39;, \u0026#39;base\u0026#39;: \u0026#39;BASE\u0026#39;} ch = chain_map.get(token[\u0026#39;chain\u0026#39;], token[\u0026#39;chain\u0026#39;].upper()) msg = f\u0026#34;链上雷达 — 马斯克/川普概念\\n\u0026#34; msg += f\u0026#34;链: {ch}\\n\\n\u0026#34; msg += f\u0026#34;{token[\u0026#39;name\u0026#39;]} ({token[\u0026#39;symbol\u0026#39;]})\\n\u0026#34; msg += f\u0026#34;`{token[\u0026#39;address\u0026#39;]}`\\n\\n\u0026#34; # 叙事故事（核心！） desc = (desc_info or {}).get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;) if desc: # 截取前200字符，避免太长 if len(desc) \u0026gt; 200: desc = desc[:200] + \u0026#39;...\u0026#39; msg += f\u0026#34;故事: {desc}\\n\\n\u0026#34; msg += f\u0026#34;命中关键词: {\u0026#39;, \u0026#39;.join(matched_kw[:5])}\\n\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; msg += f\u0026#34;市值 ${token[\u0026#39;mc\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;流动性 ${token[\u0026#39;liq\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;1h涨幅 {token[\u0026#39;chg_1h\u0026#39;]:\u0026gt;+11.1f}%\\n\u0026#34; if token.get(\u0026#39;sm\u0026#39;, 0) \u0026gt; 0: msg += f\u0026#34;聪明钱 {token[\u0026#39;sm\u0026#39;]:\u0026gt;12d}\\n\u0026#34; msg += f\u0026#34;币龄 {token[\u0026#39;age_h\u0026#39;]:\u0026gt;10.1f}h\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; # 社交链接 links = [] if (desc_info or {}).get(\u0026#39;twitter\u0026#39;): links.append(f\u0026#34;Twitter: {desc_info[\u0026#39;twitter\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;telegram\u0026#39;): links.append(f\u0026#34;TG: {desc_info[\u0026#39;telegram\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;website\u0026#39;): links.append(f\u0026#34;Web: {desc_info[\u0026#39;website\u0026#39;]}\u0026#34;) if links: msg += \u0026#39;\\n\u0026#39; + \u0026#39;\\n\u0026#39;.join(links) return msg def format_binance_cz_alert(token, matched_kw, desc_info=None): \u0026#34;\u0026#34;\u0026#34;币安/CZ叙事推送\u0026#34;\u0026#34;\u0026#34; msg = f\u0026#34;链上雷达 — 币安/CZ概念\\n\u0026#34; msg += f\u0026#34;链: BSC\\n\\n\u0026#34; msg += f\u0026#34;{token[\u0026#39;name\u0026#39;]} ({token[\u0026#39;symbol\u0026#39;]})\\n\u0026#34; msg += f\u0026#34;`{token[\u0026#39;address\u0026#39;]}`\\n\\n\u0026#34; # 叙事故事 desc = (desc_info or {}).get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;) if desc: if len(desc) \u0026gt; 200: desc = desc[:200] + \u0026#39;...\u0026#39; msg += f\u0026#34;故事: {desc}\\n\\n\u0026#34; msg += f\u0026#34;命中关键词: {\u0026#39;, \u0026#39;.join(matched_kw[:5])}\\n\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; msg += f\u0026#34;市值 ${token[\u0026#39;mc\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;流动性 ${token[\u0026#39;liq\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;1h涨幅 {token[\u0026#39;chg_1h\u0026#39;]:\u0026gt;+11.1f}%\\n\u0026#34; msg += f\u0026#34;币龄 {token[\u0026#39;age_h\u0026#39;]:\u0026gt;10.1f}h\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; # 社交链接 links = [] if (desc_info or {}).get(\u0026#39;twitter\u0026#39;): links.append(f\u0026#34;Twitter: {desc_info[\u0026#39;twitter\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;telegram\u0026#39;): links.append(f\u0026#34;TG: {desc_info[\u0026#39;telegram\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;website\u0026#39;): links.append(f\u0026#34;Web: {desc_info[\u0026#39;website\u0026#39;]}\u0026#34;) if links: msg += \u0026#39;\\n\u0026#39; + \u0026#39;\\n\u0026#39;.join(links) return msg def format_novel_narrative_alert(token, theme, desc_info=None): \u0026#34;\u0026#34;\u0026#34;全新叙事推送 — 保留备用\u0026#34;\u0026#34;\u0026#34; return format_heating_narrative_alert(token, theme, 1, desc_info) def format_heating_narrative_alert(token, theme, count, desc_info=None): \u0026#34;\u0026#34;\u0026#34;叙事热点推送 — 同主题持续冒新币\u0026#34;\u0026#34;\u0026#34; chain_map = {\u0026#39;sol\u0026#39;: \u0026#39;SOL\u0026#39;, \u0026#39;eth\u0026#39;: \u0026#39;ETH\u0026#39;, \u0026#39;bsc\u0026#39;: \u0026#39;BSC\u0026#39;, \u0026#39;base\u0026#39;: \u0026#39;BASE\u0026#39;} ch = chain_map.get(token[\u0026#39;chain\u0026#39;], token[\u0026#39;chain\u0026#39;].upper()) msg = f\u0026#34;链上雷达 — 叙事热点\\n\u0026#34; msg += f\u0026#34;链: {ch}\\n\\n\u0026#34; msg += f\u0026#34;{token[\u0026#39;name\u0026#39;]} ({token[\u0026#39;symbol\u0026#39;]})\\n\u0026#34; msg += f\u0026#34;`{token[\u0026#39;address\u0026#39;]}`\\n\\n\u0026#34; # 叙事故事 desc = (desc_info or {}).get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;) if desc: if len(desc) \u0026gt; 300: desc = desc[:300] + \u0026#39;...\u0026#39; msg += f\u0026#34;故事: {desc}\\n\\n\u0026#34; else: msg += f\u0026#34;叙事主题: {theme}\\n\\n\u0026#34; msg += f\u0026#34;同类概念已出现{count}个币 — 持续有人做\\n\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; msg += f\u0026#34;市值 ${token[\u0026#39;mc\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;流动性 ${token[\u0026#39;liq\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;1h涨幅 {token[\u0026#39;chg_1h\u0026#39;]:\u0026gt;+11.1f}%\\n\u0026#34; if token.get(\u0026#39;sm\u0026#39;, 0) \u0026gt; 0: msg += f\u0026#34;聪明钱 {token[\u0026#39;sm\u0026#39;]:\u0026gt;12d}\\n\u0026#34; msg += f\u0026#34;持有人 {token[\u0026#39;holders\u0026#39;]:\u0026gt;12d}\\n\u0026#34; msg += f\u0026#34;币龄 {token[\u0026#39;age_h\u0026#39;]:\u0026gt;10.1f}h\\n\u0026#34; msg += f\u0026#34;```\u0026#34; # 社交链接 links = [] if (desc_info or {}).get(\u0026#39;twitter\u0026#39;): links.append(f\u0026#34;\\nTwitter: {desc_info[\u0026#39;twitter\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;telegram\u0026#39;): links.append(f\u0026#34;TG: {desc_info[\u0026#39;telegram\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;website\u0026#39;): links.append(f\u0026#34;Web: {desc_info[\u0026#39;website\u0026#39;]}\u0026#34;) if links: msg += \u0026#39;\\n\u0026#39;.join(links) return msg # ============================================================ # 动量追踪器 — 持续上涨+放量检测 # ============================================================ def track_momentum(tokens): \u0026#34;\u0026#34;\u0026#34; 每轮扫描更新币的快照。 连续多轮市值上涨+成交量增加 = 动量信号，直接推。 \u0026#34;\u0026#34;\u0026#34; global MOMENTUM_TRACKER, MOMENTUM_PUSHED now = time.time() alerts = [] # 当前轮所有地址 current_addrs = set() for token in tokens: addr = token[\u0026#39;address\u0026#39;] mc = token[\u0026#39;mc\u0026#39;] vol = token.get(\u0026#39;volume\u0026#39;, 0) or 0 price = token.get(\u0026#39;price\u0026#39;, 0) or 0 buys = token.get(\u0026#39;buys_1h\u0026#39;, 0) or token.get(\u0026#39;buys\u0026#39;, 0) or 0 current_addrs.add(addr) # 基本门槛 if mc \u0026lt; 1000 or token.get(\u0026#39;liq\u0026#39;, 0) \u0026lt; 500 or mc \u0026gt; 10000000: continue # 记录快照 — 只有数据真正变化时才记录（GMGN有缓存） if addr not in MOMENTUM_TRACKER: MOMENTUM_TRACKER[addr] = [] snapshots = MOMENTUM_TRACKER[addr] # 跳过重复数据（跟上一次完全一样就不记录） if snapshots and snapshots[-1][\u0026#39;mc\u0026#39;] == mc and snapshots[-1][\u0026#39;vol\u0026#39;] == vol: continue # 数据没变，跳过 snapshots.append({ \u0026#39;ts\u0026#39;: now, \u0026#39;mc\u0026#39;: mc, \u0026#39;vol\u0026#39;: vol, \u0026#39;price\u0026#39;: price, \u0026#39;buys\u0026#39;: buys, }) # 只保留最近20个快照（约200秒） if len(snapshots) \u0026gt; 20: snapshots[:] = snapshots[-20:] # 至少需要3个快照才能判断 if len(snapshots) \u0026lt; MOMENTUM_CONSECUTIVE_UP: continue # 检测最近N轮是否持续涨 recent = snapshots[-MOMENTUM_CONSECUTIVE_UP:] consecutive_up = True total_gain = 0 for i in range(1, len(recent)): prev_mc = recent[i-1][\u0026#39;mc\u0026#39;] curr_mc = recent[i][\u0026#39;mc\u0026#39;] if prev_mc \u0026lt;= 0: consecutive_up = False break gain = (curr_mc - prev_mc) / prev_mc if gain \u0026lt;= 0: # 任何一轮没涨就不算 consecutive_up = False break total_gain += gain if not consecutive_up: continue # 连续涨了！检查放量（成交量在增） vol_increasing = True for i in range(1, len(recent)): if recent[i][\u0026#39;buys\u0026#39;] \u0026lt; recent[i-1][\u0026#39;buys\u0026#39;] * 0.8: # 允许小幅波动 vol_increasing = False break # 计算总涨幅 first_mc = recent[0][\u0026#39;mc\u0026#39;] last_mc = recent[-1][\u0026#39;mc\u0026#39;] pct_gain = ((last_mc - first_mc) / first_mc * 100) if first_mc \u0026gt; 0 else 0 # 推送条件：连续涨 + 涨幅\u0026gt;5% if pct_gain \u0026lt; 5: continue # 信号计数：同一个币每次触发信号，计数+1 push_info = MOMENTUM_PUSHED.get(addr, {\u0026#39;count\u0026#39;: 0, \u0026#39;last_ts\u0026#39;: 0, \u0026#39;last_mc\u0026#39;: 0}) # 必须比上次推送时市值还高才推（真的还在涨） if push_info[\u0026#39;count\u0026#39;] \u0026gt; 0 and last_mc \u0026lt;= push_info[\u0026#39;last_mc\u0026#39;]: continue push_info[\u0026#39;count\u0026#39;] += 1 push_info[\u0026#39;last_ts\u0026#39;] = now push_info[\u0026#39;last_mc\u0026#39;] = last_mc signal_count = push_info[\u0026#39;count\u0026#39;] # 安全检查 safety = check_token_safety(token[\u0026#39;chain\u0026#39;], addr) if not safety.get(\u0026#39;safe\u0026#39;): continue # 叙事分类 → 星级评分 category, matched_kw = classify_narrative(token[\u0026#39;name\u0026#39;], token[\u0026#39;symbol\u0026#39;], token[\u0026#39;chain\u0026#39;]) is_flap = token.get(\u0026#39;launchpad\u0026#39;) == \u0026#39;flap\u0026#39; if category == \u0026#39;musk_trump\u0026#39;: stars = 3 narrative_tag = f\u0026#34;马斯克/川普概念 ({\u0026#39;, \u0026#39;.join(matched_kw[:3])})\u0026#34; elif category == \u0026#39;binance_cz\u0026#39;: stars = 3 narrative_tag = f\u0026#34;币安/CZ概念 ({\u0026#39;, \u0026#39;.join(matched_kw[:3])})\u0026#34; elif category == \u0026#39;celebrity_viral\u0026#39;: stars = 2 narrative_tag = f\u0026#34;名人/热点 ({\u0026#39;, \u0026#39;.join(matched_kw[:3])})\u0026#34; elif is_flap: stars = 2 narrative_tag = \u0026#34;FLAP社区币\u0026#34; else: # 检查是否全新叙事 theme = normalize_theme(token[\u0026#39;name\u0026#39;], token[\u0026#39;symbol\u0026#39;]) theme_words = [w for w in theme.split() if w not in COMMON_NOISE_WORDS and len(w) \u0026gt; 2] if len(theme_words) \u0026gt;= 2: stars = 2 narrative_tag = f\u0026#34;叙事: {theme}\u0026#34; else: stars = 1 narrative_tag = \u0026#34;无明确叙事\u0026#34; # 生成推送 desc_info = fetch_token_description(token[\u0026#39;chain\u0026#39;], addr) # FLAP币额外标注社区/CTO信息 if is_flap: has_twitter = bool(desc_info.get(\u0026#39;twitter\u0026#39;)) has_tg = bool(desc_info.get(\u0026#39;telegram\u0026#39;)) has_web = bool(desc_info.get(\u0026#39;website\u0026#39;)) community_tags = [] if has_twitter: community_tags.append(\u0026#34;有推特\u0026#34;) if has_tg: community_tags.append(\u0026#34;有TG群\u0026#34;) if has_web: community_tags.append(\u0026#34;有官网\u0026#34;) if community_tags: narrative_tag += f\u0026#34; | {\u0026#39; \u0026#39;.join(community_tags)}\u0026#34; stars = min(3, stars + 1) # 有社区加一星 else: narrative_tag += \u0026#34; | 无社区链接\u0026#34; msg = format_momentum_alert(token, pct_gain, len(recent), vol_increasing, stars, narrative_tag, desc_info, signal_count) alerts.append({\u0026#39;msg\u0026#39;: msg, \u0026#39;token\u0026#39;: token}) MOMENTUM_PUSHED[addr] = push_info log(f\u0026#34;[动量信号{signal_count}] {token[\u0026#39;name\u0026#39;]} ({token[\u0026#39;symbol\u0026#39;]}) on {token[\u0026#39;chain\u0026#39;]} — 连涨{len(recent)}轮 +{pct_gain:.1f}%\u0026#34;) # 清理不再出现的币 stale = [a for a in MOMENTUM_TRACKER if a not in current_addrs] for a in stale: if now - MOMENTUM_TRACKER[a][-1][\u0026#39;ts\u0026#39;] \u0026gt; 600: # 10分钟没出现就清理 del MOMENTUM_TRACKER[a] # 清理推送记录 — 1小时没出现的清掉 MOMENTUM_PUSHED = {k: v for k, v in MOMENTUM_PUSHED.items() if now - v.get(\u0026#39;last_ts\u0026#39;, 0) \u0026lt; 3600} return alerts def format_momentum_alert(token, pct_gain, rounds, vol_up, stars, narrative_tag, desc_info=None, seen_count=0): \u0026#34;\u0026#34;\u0026#34;持续上涨动量推送 — 带叙事星级\u0026#34;\u0026#34;\u0026#34; chain_map = {\u0026#39;sol\u0026#39;: \u0026#39;SOL\u0026#39;, \u0026#39;eth\u0026#39;: \u0026#39;ETH\u0026#39;, \u0026#39;bsc\u0026#39;: \u0026#39;BSC\u0026#39;, \u0026#39;base\u0026#39;: \u0026#39;BASE\u0026#39;} ch = chain_map.get(token[\u0026#39;chain\u0026#39;], token[\u0026#39;chain\u0026#39;].upper()) vol_tag = \u0026#34;放量\u0026#34; if vol_up else \u0026#34;\u0026#34; star_str = \u0026#34;★\u0026#34; * stars + \u0026#34;☆\u0026#34; * (3 - stars) # 信号编号从标题移到下面 msg = f\u0026#34;链上雷达\\n\u0026#34; msg += f\u0026#34;链: {ch}\\n\\n\u0026#34; msg += f\u0026#34;{token[\u0026#39;name\u0026#39;]} ({token[\u0026#39;symbol\u0026#39;]})\\n\u0026#34; msg += f\u0026#34;`{token[\u0026#39;address\u0026#39;]}`\\n\\n\u0026#34; # 故事描述 desc = (desc_info or {}).get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;) if desc: if len(desc) \u0026gt; 200: desc = desc[:200] + \u0026#39;...\u0026#39; msg += f\u0026#34;故事: {desc}\\n\\n\u0026#34; msg += f\u0026#34;叙事: {narrative_tag}\\n\u0026#34; msg += f\u0026#34;连涨{rounds}轮 +{pct_gain:.1f}% {vol_tag}\\n\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; msg += f\u0026#34;市值 ${token[\u0026#39;mc\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;流动性 ${token[\u0026#39;liq\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;1h涨幅 {token[\u0026#39;chg_1h\u0026#39;]:\u0026gt;+11.1f}%\\n\u0026#34; if token.get(\u0026#39;sm\u0026#39;, 0) \u0026gt; 0: msg += f\u0026#34;聪明钱 {token[\u0026#39;sm\u0026#39;]:\u0026gt;12d}\\n\u0026#34; msg += f\u0026#34;币龄 {token[\u0026#39;age_h\u0026#39;]:\u0026gt;10.1f}h\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; msg += f\u0026#34;评星: {star_str} 出现次数: {seen_count}\u0026#34; # 社交链接 links = [] if (desc_info or {}).get(\u0026#39;twitter\u0026#39;): links.append(f\u0026#34;\\nTwitter: {desc_info[\u0026#39;twitter\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;telegram\u0026#39;): links.append(f\u0026#34;TG: {desc_info[\u0026#39;telegram\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;website\u0026#39;): links.append(f\u0026#34;Web: {desc_info[\u0026#39;website\u0026#39;]}\u0026#34;) if links: msg += \u0026#39;\\n\u0026#39;.join(links) return msg def format_celebrity_alert(token, matched_kw, desc_info=None): \u0026#34;\u0026#34;\u0026#34;名人/推特热点推送 ★★\u0026#34;\u0026#34;\u0026#34; chain_map = {\u0026#39;sol\u0026#39;: \u0026#39;SOL\u0026#39;, \u0026#39;eth\u0026#39;: \u0026#39;ETH\u0026#39;, \u0026#39;bsc\u0026#39;: \u0026#39;BSC\u0026#39;, \u0026#39;base\u0026#39;: \u0026#39;BASE\u0026#39;} ch = chain_map.get(token[\u0026#39;chain\u0026#39;], token[\u0026#39;chain\u0026#39;].upper()) msg = f\u0026#34;链上雷达 — 名人/热点 ★★\\n\u0026#34; msg += f\u0026#34;链: {ch}\\n\\n\u0026#34; msg += f\u0026#34;{token[\u0026#39;name\u0026#39;]} ({token[\u0026#39;symbol\u0026#39;]})\\n\u0026#34; msg += f\u0026#34;`{token[\u0026#39;address\u0026#39;]}`\\n\\n\u0026#34; desc = (desc_info or {}).get(\u0026#39;description\u0026#39;, \u0026#39;\u0026#39;) if desc: if len(desc) \u0026gt; 200: desc = desc[:200] + \u0026#39;...\u0026#39; msg += f\u0026#34;故事: {desc}\\n\\n\u0026#34; msg += f\u0026#34;命中关键词: {\u0026#39;, \u0026#39;.join(matched_kw[:5])}\\n\\n\u0026#34; msg += f\u0026#34;```\\n\u0026#34; msg += f\u0026#34;市值 ${token[\u0026#39;mc\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;流动性 ${token[\u0026#39;liq\u0026#39;]:\u0026gt;12,.0f}\\n\u0026#34; msg += f\u0026#34;1h涨幅 {token[\u0026#39;chg_1h\u0026#39;]:\u0026gt;+11.1f}%\\n\u0026#34; if token.get(\u0026#39;sm\u0026#39;, 0) \u0026gt; 0: msg += f\u0026#34;聪明钱 {token[\u0026#39;sm\u0026#39;]:\u0026gt;12d}\\n\u0026#34; msg += f\u0026#34;币龄 {token[\u0026#39;age_h\u0026#39;]:\u0026gt;10.1f}h\\n\u0026#34; msg += f\u0026#34;```\u0026#34; links = [] if (desc_info or {}).get(\u0026#39;twitter\u0026#39;): links.append(f\u0026#34;\\nTwitter: {desc_info[\u0026#39;twitter\u0026#39;]}\u0026#34;) if (desc_info or {}).get(\u0026#39;telegram\u0026#39;): links.append(f\u0026#34;TG: {desc_info[\u0026#39;telegram\u0026#39;]}\u0026#34;) if links: msg += \u0026#39;\\n\u0026#39;.join(links) return msg # ============================================================ # 核心扫描逻辑 # ============================================================ def scan_narratives(): \u0026#34;\u0026#34;\u0026#34;主扫描函数\u0026#34;\u0026#34;\u0026#34; conn = init_db() tokens = fetch_new_tokens() log(f\u0026#34;扫描 {len(tokens)} 个新币...\u0026#34;) # === 动量追踪 — 每轮更新所有币的快照，检测持续上涨 === # 拉FLAP币一起喂进动量追踪器 flap_tokens = [] try: flap_tokens = fetch_flap_tokens() except: pass all_momentum_tokens = tokens + flap_tokens momentum_alerts = track_momentum(all_momentum_tokens) for token in tokens: addr = token[\u0026#39;address\u0026#39;] chain = token[\u0026#39;chain\u0026#39;] name = token[\u0026#39;name\u0026#39;] symbol = token[\u0026#39;symbol\u0026#39;] # 已扫描过的 — 更新seen_count和narratives的token_count，但不重复推 if is_token_seen(conn, addr): # 更新seen_count c = conn.cursor() c.execute(\u0026#39;UPDATE tokens_seen SET seen_count = seen_count + 1, market_cap = ? WHERE address = ?\u0026#39;, (token[\u0026#39;mc\u0026#39;], addr)) # 更新narratives表的token_count（按主题） theme_tmp = normalize_theme(name, symbol) if theme_tmp: c.execute(\u0026#39;UPDATE narratives SET token_count = token_count + 1, last_seen_at = ? WHERE theme = ?\u0026#39;, (int(time.time()), theme_tmp)) conn.commit() continue # 分类叙事 category, matched_kw = classify_narrative(name, symbol, chain) if category == \u0026#39;spam\u0026#39;: record_token(conn, addr, chain, name, symbol, \u0026#39;\u0026#39;, \u0026#39;spam\u0026#39;, token[\u0026#39;mc\u0026#39;]) continue # 基本质量门槛（防止推太多垃圾） min_mc = 1000 min_liq = 500 if token[\u0026#39;mc\u0026#39;] \u0026lt; min_mc or token[\u0026#39;liq\u0026#39;] \u0026lt; min_liq: record_token(conn, addr, chain, name, symbol, \u0026#39;\u0026#39;, \u0026#39;too_small\u0026#39;, token[\u0026#39;mc\u0026#39;]) continue theme = normalize_theme(name, symbol) # 所有分类只记录，不直接推送 — 推送统一走动量引擎 record_token(conn, addr, chain, name, symbol, theme, category, token[\u0026#39;mc\u0026#39;]) check_narrative_novelty(conn, theme, name, symbol, addr, chain) conn.close() # === 推送动量信号 === pushed = 0 for ma in momentum_alerts[:8]: # 单轮最多推8个 if tg_send(ma[\u0026#39;msg\u0026#39;]): pushed += 1 time.sleep(1) # 避免TG限流 return pushed, len(momentum_alerts) # ============================================================ # 主循环 # ============================================================ def main(): log(\u0026#34;=\u0026#34; * 50) log(\u0026#34;链上雷达 v1 启动\u0026#34;) log(f\u0026#34;扫描间隔: {SCAN_INTERVAL}s\u0026#34;) log(f\u0026#34;推送逻辑: 动量优先 — 连涨才推，叙事只做分类标签\u0026#34;) log(\u0026#34;=\u0026#34; * 50) # 初始化DB init_db() # 启动通知 tg_send( \u0026#34;链上雷达 v1 已启动\\n\\n\u0026#34; \u0026#34;核心逻辑: 动量优先\\n\u0026#34; \u0026#34;连涨3轮+涨幅\u0026gt;5%才推送\\n\u0026#34; \u0026#34;叙事只做分类标签:\\n\u0026#34; \u0026#34;★★★ 马斯克/川普 | 币安/CZ | FLAP有社区\\n\u0026#34; \u0026#34;★★ 名人热点 | FLAP无社区 | 有叙事\\n\u0026#34; \u0026#34;★ 无明确叙事\\n\\n\u0026#34; f\u0026#34;扫描频率: 每{SCAN_INTERVAL}秒\u0026#34; ) scan_count = 0 total_pushed = 0 while True: try: scan_count += 1 pushed, found = scan_narratives() total_pushed += pushed if pushed \u0026gt; 0: log(f\u0026#34;第{scan_count}轮: 发现{found}个, 推送{pushed}个 (累计推送{total_pushed})\u0026#34;) else: if scan_count % 20 == 0: # 每20轮报一次无信号 log(f\u0026#34;第{scan_count}轮: 无新信号 (累计推送{total_pushed})\u0026#34;) except Exception as e: log(f\u0026#34;扫描异常: {e}\u0026#34;) time.sleep(SCAN_INTERVAL) if __name__ == \u0026#39;__main__\u0026#39;: main() OI + Funding Rate Scanner Date: 2026.04.25　Tags: Python · Binance Futures · Telegram\nFunding rate flip detection + OI surge\nSnapshot-based scanner: detects funding rate flipping from positive to negative while OI is rising. Runs every 5 minutes.\nFull source code #!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; OI持续放大 + 费率由正转负 扫描器 - 每分钟运行一次 - 检测: OI持续放大(4段递增, 总涨幅\u0026gt;8%) + 费率由正转负 - 去重: 同一币种24小时内只推一次 - 纯API零成本 \u0026#34;\u0026#34;\u0026#34; import requests import json import os import time import sys from datetime import datetime, timedelta from pathlib import Path # ============ 配置 ============ SCRIPT_DIR = Path(__file__).parent ENV_FILE = SCRIPT_DIR / \u0026#34;.env.oi\u0026#34; ALERT_HISTORY_FILE = SCRIPT_DIR / \u0026#34;oi_funding_alerts.json\u0026#34; FR_SNAPSHOT_FILE = SCRIPT_DIR / \u0026#34;fr_snapshot.json\u0026#34; # 上一次费率快照 # 信号参数 MIN_OI_CHANGE_PCT = 8 # OI总涨幅最低8% MIN_VOLUME_USDT = 0 # 无门槛，全扫 MIN_FR_PERIODS_POSITIVE = 2 # 转负前至少2期为正 DEDUP_HOURS = 24 # 去重窗口24小时 # ============ 加载TG配置 ============ def load_env(): env = {} if ENV_FILE.exists(): for line in ENV_FILE.read_text().strip().split(\u0026#39;\\n\u0026#39;): if \u0026#39;=\u0026#39; in line and not line.startswith(\u0026#39;#\u0026#39;): k, v = line.split(\u0026#39;=\u0026#39;, 1) env[k.strip()] = v.strip() return env env = load_env() TG_BOT_TOKEN = env.get(\u0026#39;TG_BOT_TOKEN\u0026#39;, \u0026#39;\u0026#39;) TG_CHAT_ID = env.get(\u0026#39;TG_CHAT_ID\u0026#39;, \u0026#39;\u0026#39;) # ============ TG推送 ============ def send_tg(text): if not TG_BOT_TOKEN or not TG_CHAT_ID: print(\u0026#34;[TG] 未配置, 仅打印:\u0026#34;) print(text) return url = f\u0026#34;https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage\u0026#34; # 分段发送(TG限制4096字) chunks = [text[i:i+4000] for i in range(0, len(text), 4000)] for chunk in chunks: try: resp = requests.post(url, json={ \u0026#39;chat_id\u0026#39;: TG_CHAT_ID, \u0026#39;text\u0026#39;: chunk, \u0026#39;parse_mode\u0026#39;: \u0026#39;Markdown\u0026#39; }, timeout=10) if resp.status_code != 200: # fallback无格式 requests.post(url, json={ \u0026#39;chat_id\u0026#39;: TG_CHAT_ID, \u0026#39;text\u0026#39;: chunk }, timeout=10) except Exception as e: print(f\u0026#34;[TG] 发送失败: {e}\u0026#34;) # ============ 去重 ============ def load_alert_history(): if ALERT_HISTORY_FILE.exists(): try: return json.loads(ALERT_HISTORY_FILE.read_text()) except: return {} return {} def save_alert_history(history): ALERT_HISTORY_FILE.write_text(json.dumps(history)) def is_duplicate(symbol, history): if symbol not in history: return False last_alert = datetime.fromisoformat(history[symbol]) return (datetime.now() - last_alert).total_seconds() \u0026lt; DEDUP_HOURS * 3600 def mark_alerted(symbol, history): history[symbol] = datetime.now().isoformat() # 清理过期记录 cutoff = datetime.now() - timedelta(hours=DEDUP_HOURS * 2) history = {k: v for k, v in history.items() if datetime.fromisoformat(v) \u0026gt; cutoff} return history # ============ 费率快照 ============ def load_fr_snapshot(): if FR_SNAPSHOT_FILE.exists(): try: return json.loads(FR_SNAPSHOT_FILE.read_text()) except: pass return {} def save_fr_snapshot(snapshot): FR_SNAPSHOT_FILE.write_text(json.dumps(snapshot)) # ============ 核心扫描 ============ def scan(): ts_start = time.time() # 1. 获取所有永续合约 try: info = requests.get(\u0026#39;https://fapi.binance.com/fapi/v1/exchangeInfo\u0026#39;, timeout=10).json() symbols = [s[\u0026#39;symbol\u0026#39;] for s in info[\u0026#39;symbols\u0026#39;] if s[\u0026#39;contractType\u0026#39;] == \u0026#39;PERPETUAL\u0026#39; and s[\u0026#39;quoteAsset\u0026#39;] == \u0026#39;USDT\u0026#39; and s[\u0026#39;status\u0026#39;] == \u0026#39;TRADING\u0026#39;] except Exception as e: print(f\u0026#34;[ERROR] exchangeInfo: {e}\u0026#34;) return [] # 2. 批量获取24h行情(过滤低量币) try: tickers = requests.get(\u0026#39;https://fapi.binance.com/fapi/v1/ticker/24hr\u0026#39;, timeout=10).json() ticker_map = {t[\u0026#39;symbol\u0026#39;]: t for t in tickers} except Exception as e: print(f\u0026#34;[ERROR] ticker: {e}\u0026#34;) return [] active = [s for s in symbols if float(ticker_map.get(s, {}).get(\u0026#39;quoteVolume\u0026#39;, 0)) \u0026gt; MIN_VOLUME_USDT] # 3. 批量获取当前费率 (一次拿全部) try: fr_all = requests.get(\u0026#39;https://fapi.binance.com/fapi/v1/premiumIndex\u0026#39;, timeout=10).json() fr_current = {item[\u0026#39;symbol\u0026#39;]: float(item[\u0026#39;lastFundingRate\u0026#39;]) for item in fr_all} except: fr_current = {} # 4. 加载上次快照，对比找\u0026#34;刚转负\u0026#34;的 prev_snapshot = load_fr_snapshot() # 保存本次快照(供下次对比) save_fr_snapshot(fr_current) if not prev_snapshot: print(f\u0026#34;[{datetime.now().strftime(\u0026#39;%H:%M:%S\u0026#39;)}] 首次运行，保存快照，下次开始对比\u0026#34;) return [] # 找出: 上次\u0026gt;=0, 这次\u0026lt;0 的币 just_turned_negative = [] for sym in active: prev_fr = prev_snapshot.get(sym) curr_fr = fr_current.get(sym) if prev_fr is None or curr_fr is None: continue if prev_fr \u0026gt;= 0 and curr_fr \u0026lt; 0: just_turned_negative.append(sym) if not just_turned_negative: elapsed = time.time() - ts_start print(f\u0026#34;[{datetime.now().strftime(\u0026#39;%H:%M:%S\u0026#39;)}] 扫描完成: {len(active)}币/{elapsed:.1f}s, 无新转负\u0026#34;) return [] print(f\u0026#34;[{datetime.now().strftime(\u0026#39;%H:%M:%S\u0026#39;)}] 发现 {len(just_turned_negative)} 个刚转负: {just_turned_negative}\u0026#34;) # 5. 只对刚转负的币查OI signals = [] for sym in just_turned_negative: try: # OI历史 oi_hist = requests.get(\u0026#39;https://fapi.binance.com/futures/data/openInterestHist\u0026#39;, params={\u0026#39;symbol\u0026#39;: sym, \u0026#39;period\u0026#39;: \u0026#39;1h\u0026#39;, \u0026#39;limit\u0026#39;: 48}, timeout=10).json() oi_chg = 0 segs = [] oi_rising = False if oi_hist and len(oi_hist) \u0026gt;= 12: oi_values = [float(x[\u0026#39;sumOpenInterestValue\u0026#39;]) for x in oi_hist] seg_len = len(oi_values) // 4 if seg_len \u0026gt;= 3: segs = [ sum(oi_values[:seg_len]) / seg_len, sum(oi_values[seg_len:seg_len*2]) / seg_len, sum(oi_values[seg_len*2:seg_len*3]) / seg_len, sum(oi_values[seg_len*3:]) / max(1, len(oi_values[seg_len*3:])) ] oi_chg = (segs[3] - segs[0]) / segs[0] * 100 if segs[0] \u0026gt; 0 else 0 oi_rising = oi_chg \u0026gt; 0 t = ticker_map.get(sym, {}) signals.append({ \u0026#39;symbol\u0026#39;: sym, \u0026#39;price\u0026#39;: float(t.get(\u0026#39;lastPrice\u0026#39;, 0)), \u0026#39;price_chg_24h\u0026#39;: float(t.get(\u0026#39;priceChangePercent\u0026#39;, 0)), \u0026#39;volume\u0026#39;: float(t.get(\u0026#39;quoteVolume\u0026#39;, 0)), \u0026#39;oi_change\u0026#39;: oi_chg, \u0026#39;oi_segments\u0026#39;: segs, \u0026#39;oi_rising\u0026#39;: oi_rising, \u0026#39;current_fr\u0026#39;: fr_current.get(sym, 0), \u0026#39;prev_fr\u0026#39;: prev_snapshot.get(sym, 0), }) except: continue elapsed = time.time() - ts_start print(f\u0026#34;[{datetime.now().strftime(\u0026#39;%H:%M:%S\u0026#39;)}] 扫描完成: {len(active)}币/{elapsed:.1f}s, 信号: {len(signals)}\u0026#34;) return signals # ============ 附加信息 ============ def get_square_discussion(coin): \u0026#34;\u0026#34;\u0026#34;查询币安广场该币的帖子数和浏览量\u0026#34;\u0026#34;\u0026#34; try: r = requests.get( \u0026#34;https://www.binance.com/bapi/composite/v4/friendly/pgc/content/queryByHashtag\u0026#34;, params={\u0026#34;hashtag\u0026#34;: f\u0026#34;#{coin.lower()}\u0026#34;, \u0026#34;pageIndex\u0026#34;: 1, \u0026#34;pageSize\u0026#34;: 1, \u0026#34;orderBy\u0026#34;: \u0026#34;HOT\u0026#34;}, headers={\u0026#34;User-Agent\u0026#34;: \u0026#34;Mozilla/5.0\u0026#34;, \u0026#34;Referer\u0026#34;: \u0026#34;https://www.binance.com/en/square\u0026#34;}, timeout=8 ) if r.status_code == 200: ht = r.json().get(\u0026#34;data\u0026#34;, {}).get(\u0026#34;hashtag\u0026#34;, {}) return ht.get(\u0026#34;contentCount\u0026#34;, 0), ht.get(\u0026#34;viewCount\u0026#34;, 0) except: pass return 0, 0 def get_market_caps(): \u0026#34;\u0026#34;\u0026#34;获取币安流通市值\u0026#34;\u0026#34;\u0026#34; mcap = {} try: r = requests.get( \u0026#34;https://www.binance.com/bapi/composite/v1/public/marketing/symbol/list\u0026#34;, timeout=10 ) if r.status_code == 200: for item in r.json().get(\u0026#34;data\u0026#34;, []): name = item.get(\u0026#34;name\u0026#34;, \u0026#34;\u0026#34;) mc = item.get(\u0026#34;marketCap\u0026#34;, 0) if name and mc: mcap[name] = float(mc) except: pass return mcap def get_spot_symbols(): \u0026#34;\u0026#34;\u0026#34;获取有现货的币种\u0026#34;\u0026#34;\u0026#34; try: info = requests.get(\u0026#34;https://api.binance.com/api/v3/exchangeInfo\u0026#34;, timeout=10).json() return {s[\u0026#34;baseAsset\u0026#34;] for s in info[\u0026#34;symbols\u0026#34;] if s[\u0026#34;quoteAsset\u0026#34;] == \u0026#34;USDT\u0026#34; and s[\u0026#34;status\u0026#34;] == \u0026#34;TRADING\u0026#34;} except: return set() def fmt_mcap(v): if v \u0026gt;= 1e9: return f\u0026#34;${v/1e9:.2f}B\u0026#34; if v \u0026gt;= 1e6: return f\u0026#34;${v/1e6:.1f}M\u0026#34; if v \u0026gt;= 1e3: return f\u0026#34;${v/1e3:.0f}K\u0026#34; return f\u0026#34;${v:.0f}\u0026#34; def fmt_views(v): if v \u0026gt;= 1e6: return f\u0026#34;{v/1e6:.1f}M\u0026#34; if v \u0026gt;= 1e3: return f\u0026#34;{v/1e3:.0f}K\u0026#34; return str(v) # ============ 格式化推送 ============ def format_alert(signals): if not signals: return None # OI在涨的排前面，同组内按费率绝对值排序 signals.sort(key=lambda x: (-int(x.get(\u0026#39;oi_rising\u0026#39;, False)), x[\u0026#39;current_fr\u0026#39;])) # 批量获取附加信息 mcap_map = get_market_caps() spot_set = get_spot_symbols() now = datetime.now().strftime(\u0026#39;%m-%d %H:%M\u0026#39;) lines = [f\u0026#34;*[ 费率刚转负+OI涨 ]* {now}\\n\u0026#34;] for s in signals: coin = s[\u0026#39;symbol\u0026#39;].replace(\u0026#39;USDT\u0026#39;, \u0026#39;\u0026#39;) # 费率: 上期→本期 fr_change = f\u0026#34;{s[\u0026#39;prev_fr\u0026#39;]:+.4%} -\u0026gt; {s[\u0026#39;current_fr\u0026#39;]:+.4%}\u0026#34; # 附加信息 mcap = mcap_map.get(coin, 0) has_spot = coin in spot_set sq_posts, sq_views = get_square_discussion(coin) lines.append(f\u0026#34;```\u0026#34;) lines.append(f\u0026#34;{coin}\u0026#34;) lines.append(f\u0026#34; 价格: {s[\u0026#39;price\u0026#39;]:.4f} 24h: {s[\u0026#39;price_chg_24h\u0026#39;]:+.1f}%\u0026#34;) lines.append(f\u0026#34; 费率: {fr_change}\u0026#34;) if s[\u0026#39;oi_segments\u0026#39;]: oi_segs = \u0026#39; \u0026gt; \u0026#39;.join([f\u0026#34;{v/1e6:.1f}M\u0026#34; for v in s[\u0026#39;oi_segments\u0026#39;]]) lines.append(f\u0026#34; OI: +{s[\u0026#39;oi_change\u0026#39;]:.1f}% ({oi_segs})\u0026#34;) lines.append(f\u0026#34; 成交额: ${s[\u0026#39;volume\u0026#39;]/1e6:.1f}M\u0026#34;) lines.append(f\u0026#34; 市值: {fmt_mcap(mcap) if mcap \u0026gt; 0 else \u0026#39;未知\u0026#39;} 现货: {\u0026#39;有\u0026#39; if has_spot else \u0026#39;仅合约\u0026#39;}\u0026#34;) if sq_posts \u0026gt; 0: lines.append(f\u0026#34; 广场: {sq_posts}帖 / {fmt_views(sq_views)}浏览\u0026#34;) else: lines.append(f\u0026#34; 广场: 无讨论\u0026#34;) lines.append(f\u0026#34;```\u0026#34;) return \u0026#39;\\n\u0026#39;.join(lines) # ============ 主逻辑 ============ def main(): signals = scan() if signals: # 只推: 费率当前为负 + OI在涨 (最强组合) strong = [s for s in signals if s[\u0026#39;current_fr\u0026#39;] \u0026lt; 0 and s.get(\u0026#39;oi_rising\u0026#39;)] if strong: msg = format_alert(strong) if msg: send_tg(msg) print(f\u0026#34; 推送 {len(strong)} 个信号 (总{len(signals)}个转负, {len(strong)}个OI也涨)\u0026#34;) else: print(f\u0026#34; {len(signals)} 个转负但无OI在涨的, 跳过\u0026#34;) else: print(f\u0026#34; 无信号\u0026#34;) if __name__ == \u0026#39;__main__\u0026#39;: main() Accumulation Radar Date: 2026.04.25　Tags: Python · Binance · CoinGlass · Telegram\nMomentum + OI anomaly + smart alerts\nHourly scan: top gainers momentum tracking, OI anomaly detection, and Telegram push. Pure Python, zero AI cost.\nFull source code #!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; 热度做多雷达 v2 — 热度+费率+OI 三维扫描 核心逻辑（拉哪模式）： 1. 热度先行 → CG热搜+放量=资金涌入信号 2. 负费率=空头燃料，庄家拉盘爆空单 3. OI暴涨=大资金建仓=即将拉盘 单策略：发现热度→小仓做多→严格止损→拿住赢家 数据源：币安合约API + CoinGecko Trending（零成本） \u0026#34;\u0026#34;\u0026#34; import json import os import sys import time import requests from datetime import datetime, timezone, timedelta from pathlib import Path from square_heat import get_square_heat # === 加载 .env === env_file = Path(__file__).parent / \u0026#34;.env.oi\u0026#34; if env_file.exists(): with open(env_file) as f: for line in f: line = line.strip() if line and not line.startswith(\u0026#34;#\u0026#34;) and \u0026#34;=\u0026#34; in line: k, v = line.split(\u0026#34;=\u0026#34;, 1) os.environ.setdefault(k.strip(), v.strip()) # === 配置 === TG_BOT_TOKEN = os.getenv(\u0026#34;TG_BOT_TOKEN\u0026#34;, \u0026#34;\u0026#34;) TG_CHAT_ID = os.getenv(\u0026#34;TG_CHAT_ID\u0026#34;, \u0026#34;YOUR_CHAT_ID\u0026#34;) FAPI = \u0026#34;https://fapi.binance.com\u0026#34; # 热度历史记录（用于检测首次上榜） HEAT_HISTORY_FILE = Path(__file__).parent / \u0026#34;heat_history.json\u0026#34; # 热度参数 VOL_SURGE_MULT = 2.5 # 成交量放大2.5倍以上=放量 MIN_VOL_USD = 20_000_000 # 日均成交\u0026gt;$20M才检测放量 # OI异动参数 MIN_OI_DELTA_PCT = 3.0 # OI变化至少3% MIN_OI_USD = 2_000_000 # 最低OI门槛 $2M def api_get(endpoint, params=None): \u0026#34;\u0026#34;\u0026#34;币安API请求\u0026#34;\u0026#34;\u0026#34; url = f\u0026#34;{FAPI}{endpoint}\u0026#34; for attempt in range(3): try: resp = requests.get(url, params=params, timeout=10) if resp.status_code == 200: return resp.json() elif resp.status_code == 429: time.sleep(2) else: return None except: time.sleep(1) return None def format_usd(v): if v \u0026gt;= 1e9: return f\u0026#34;${v/1e9:.1f}B\u0026#34; if v \u0026gt;= 1e6: return f\u0026#34;${v/1e6:.1f}M\u0026#34; if v \u0026gt;= 1e3: return f\u0026#34;${v/1e3:.0f}K\u0026#34; return f\u0026#34;${v:.0f}\u0026#34; def mcap_str(v): if v \u0026gt;= 1e9: return f\u0026#34;${v/1e9:.1f}B\u0026#34; if v \u0026gt;= 1e6: return f\u0026#34;${v/1e6:.0f}M\u0026#34; if v \u0026gt;= 1e3: return f\u0026#34;${v/1e3:.0f}K\u0026#34; return f\u0026#34;${v:.0f}\u0026#34; def send_telegram(text): \u0026#34;\u0026#34;\u0026#34;发送TG消息\u0026#34;\u0026#34;\u0026#34; if not TG_BOT_TOKEN: print(\u0026#34;\\n[TG] No token, stdout:\\n\u0026#34;) print(text) return url = f\u0026#34;https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage\u0026#34; # 分段发送（TG限制4096字） chunks = [] current = \u0026#34;\u0026#34; for line in text.split(\u0026#34;\\n\u0026#34;): if len(current) + len(line) + 1 \u0026gt; 3800: chunks.append(current) current = line else: current += \u0026#34;\\n\u0026#34; + line if current else line if current: chunks.append(current) for chunk in chunks: try: resp = requests.post(url, json={ \u0026#34;chat_id\u0026#34;: TG_CHAT_ID, \u0026#34;text\u0026#34;: chunk, \u0026#34;parse_mode\u0026#34;: \u0026#34;Markdown\u0026#34; }, timeout=10) if resp.status_code == 200: print(f\u0026#34;[TG] Sent ✓ ({len(chunk)} chars)\u0026#34;) else: # Markdown失败就用纯文本 resp2 = requests.post(url, json={ \u0026#34;chat_id\u0026#34;: TG_CHAT_ID, \u0026#34;text\u0026#34;: chunk.replace(\u0026#34;*\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;_\u0026#34;, \u0026#34;\u0026#34;), }, timeout=10) print(f\u0026#34;[TG] Sent plain ({\u0026#39;✓\u0026#39; if resp2.status_code == 200 else \u0026#39;✗\u0026#39;})\u0026#34;) except Exception as e: print(f\u0026#34;[TG] Error: {e}\u0026#34;) time.sleep(0.5) def main(): print(f\u0026#34;🔥 热度做多雷达 v2 — {datetime.now().strftime(\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)}\\n\u0026#34;) # 1. 全市场行情+费率 tickers_raw = api_get(\u0026#34;/fapi/v1/ticker/24hr\u0026#34;) premiums_raw = api_get(\u0026#34;/fapi/v1/premiumIndex\u0026#34;) if not tickers_raw or not premiums_raw: print(\u0026#34;❌ API失败\u0026#34;) return ticker_map = {} for t in tickers_raw: if t[\u0026#34;symbol\u0026#34;].endswith(\u0026#34;USDT\u0026#34;): ticker_map[t[\u0026#34;symbol\u0026#34;]] = { \u0026#34;px_chg\u0026#34;: float(t[\u0026#34;priceChangePercent\u0026#34;]), \u0026#34;vol\u0026#34;: float(t[\u0026#34;quoteVolume\u0026#34;]), \u0026#34;price\u0026#34;: float(t[\u0026#34;lastPrice\u0026#34;]), } funding_map = {} for p in premiums_raw: if p[\u0026#34;symbol\u0026#34;].endswith(\u0026#34;USDT\u0026#34;): funding_map[p[\u0026#34;symbol\u0026#34;]] = float(p[\u0026#34;lastFundingRate\u0026#34;]) # 2. 真实流通市值（币安现货API） mcap_map = {} try: r = requests.get( \u0026#34;https://www.binance.com/bapi/composite/v1/public/marketing/symbol/list\u0026#34;, timeout=10 ) if r.status_code == 200: for item in r.json().get(\u0026#34;data\u0026#34;, []): name = item.get(\u0026#34;name\u0026#34;, \u0026#34;\u0026#34;) mc = item.get(\u0026#34;marketCap\u0026#34;, 0) if name and mc: mcap_map[name] = float(mc) print(f\u0026#34;✅ 真实市值: {len(mcap_map)}个币\u0026#34;) except Exception as e: print(f\u0026#34;⚠️ 市值API失败: {e}\u0026#34;) # 3. 热度检测：币安广场热搜 + CoinGecko Trending + 成交量暴增 heat_map = {} cg_trending = set() square_trending = set() # 3a. 币安广场热搜（6H）— 最重要！币安用户=交易用户 sq_coins = get_square_heat() if sq_coins: for i, c in enumerate(sq_coins): coin = c[\u0026#34;coin\u0026#34;] square_trending.add(coin) # 排名越靠前分越高，急速上升额外加分 rank_score = max(50 - i * 4, 10) if c.get(\u0026#34;rapidRiser\u0026#34;): rank_score += 15 heat_map[coin] = heat_map.get(coin, 0) + rank_score print(f\u0026#34;🏦 广场热搜: {len(square_trending)}个币 {[c[\u0026#39;coin\u0026#39;] for c in sq_coins[:5]]}\u0026#34;) # 3b. CoinGecko Trending try: r = requests.get(\u0026#34;https://api.coingecko.com/api/v3/search/trending\u0026#34;, timeout=10) if r.status_code == 200: for item in r.json().get(\u0026#34;coins\u0026#34;, []): sym = item[\u0026#34;item\u0026#34;][\u0026#34;symbol\u0026#34;].upper() rank = item[\u0026#34;item\u0026#34;].get(\u0026#34;score\u0026#34;, 99) cg_trending.add(sym) heat_map[sym] = heat_map.get(sym, 0) + max(50 - rank * 3, 10) print(f\u0026#34;🌐 CG Trending: {len(cg_trending)}个币\u0026#34;) except Exception as e: print(f\u0026#34;⚠️ CG Trending失败: {e}\u0026#34;) # 成交量暴增检测 vol_surge_coins = set() for sym, tk in ticker_map.items(): coin = sym.replace(\u0026#34;USDT\u0026#34;, \u0026#34;\u0026#34;) vol_24h = tk[\u0026#34;vol\u0026#34;] if vol_24h \u0026gt; MIN_VOL_USD: kl = api_get(\u0026#34;/fapi/v1/klines\u0026#34;, {\u0026#34;symbol\u0026#34;: sym, \u0026#34;interval\u0026#34;: \u0026#34;1d\u0026#34;, \u0026#34;limit\u0026#34;: 8}) if kl and len(kl) \u0026gt;= 5: avg_prev = sum(float(k[7]) for k in kl[:-1]) / (len(kl) - 1) if avg_prev \u0026gt; 0: ratio = vol_24h / avg_prev if ratio \u0026gt;= VOL_SURGE_MULT: vol_surge_coins.add(coin) heat_map[coin] = heat_map.get(coin, 0) + min(ratio * 10, 50) time.sleep(0.05) print(f\u0026#34;📈 放量(≥{VOL_SURGE_MULT}x): {len(vol_surge_coins)}个币\u0026#34;) # 双重/三重热度 dual_heat = cg_trending \u0026amp; vol_surge_coins square_vol = square_trending \u0026amp; vol_surge_coins triple_heat = cg_trending \u0026amp; vol_surge_coins \u0026amp; square_trending all_multi_heat = dual_heat | square_vol if all_multi_heat: for coin in all_multi_heat: heat_map[coin] = heat_map.get(coin, 0) + 20 if triple_heat: for coin in triple_heat: heat_map[coin] = heat_map.get(coin, 0) + 30 # 三重热度超级加分 print(f\u0026#34;🔥🔥🔥 三重热度: {triple_heat}\u0026#34;) else: print(f\u0026#34;🔥🔥 双重热度: {all_multi_heat}\u0026#34;) # 4. OI扫描（Top100成交量 + 热度币） scan_syms = set() # 热度币必扫 for coin in heat_map: sym = coin + \u0026#34;USDT\u0026#34; if sym in ticker_map: scan_syms.add(sym) # Top100成交量 top_by_vol = sorted(ticker_map.items(), key=lambda x: x[1][\u0026#34;vol\u0026#34;], reverse=True)[:100] for sym, _ in top_by_vol: scan_syms.add(sym) oi_map = {} for i, sym in enumerate(scan_syms): oi_hist = api_get(\u0026#34;/futures/data/openInterestHist\u0026#34;, { \u0026#34;symbol\u0026#34;: sym, \u0026#34;period\u0026#34;: \u0026#34;1h\u0026#34;, \u0026#34;limit\u0026#34;: 6 }) if oi_hist and len(oi_hist) \u0026gt;= 2: curr = float(oi_hist[-1][\u0026#34;sumOpenInterestValue\u0026#34;]) prev_1h = float(oi_hist[-2][\u0026#34;sumOpenInterestValue\u0026#34;]) prev_6h = float(oi_hist[0][\u0026#34;sumOpenInterestValue\u0026#34;]) d1h = ((curr - prev_1h) / prev_1h * 100) if prev_1h \u0026gt; 0 else 0 d6h = ((curr - prev_6h) / prev_6h * 100) if prev_6h \u0026gt; 0 else 0 oi_map[sym] = {\u0026#34;oi_usd\u0026#34;: curr, \u0026#34;d1h\u0026#34;: d1h, \u0026#34;d6h\u0026#34;: d6h} if (i + 1) % 10 == 0: time.sleep(0.5) print(f\u0026#34;📊 OI扫描: {len(oi_map)}个币\u0026#34;) # 5. 整合所有数据 all_syms = set(list(ticker_map.keys())) coin_data = {} for sym in all_syms: tk = ticker_map.get(sym, {}) if not tk: continue oi = oi_map.get(sym, {}) fr = funding_map.get(sym, 0) coin = sym.replace(\u0026#34;USDT\u0026#34;, \u0026#34;\u0026#34;) d6h = oi.get(\u0026#34;d6h\u0026#34;, 0) fr_pct = fr * 100 oi_usd = oi.get(\u0026#34;oi_usd\u0026#34;, 0) # 真实市值优先，fallback粗估 if coin in mcap_map: est_mcap = mcap_map[coin] else: est_mcap = max(tk[\u0026#34;vol\u0026#34;] * 0.3, oi_usd * 2) if oi_usd \u0026gt; 0 else tk[\u0026#34;vol\u0026#34;] * 0.3 heat = heat_map.get(coin, 0) coin_data[sym] = { \u0026#34;coin\u0026#34;: coin, \u0026#34;sym\u0026#34;: sym, \u0026#34;px_chg\u0026#34;: tk[\u0026#34;px_chg\u0026#34;], \u0026#34;vol\u0026#34;: tk[\u0026#34;vol\u0026#34;], \u0026#34;fr_pct\u0026#34;: fr_pct, \u0026#34;d6h\u0026#34;: d6h, \u0026#34;oi_usd\u0026#34;: oi_usd, \u0026#34;est_mcap\u0026#34;: est_mcap, \u0026#34;heat\u0026#34;: heat, \u0026#34;in_cg\u0026#34;: coin in cg_trending, \u0026#34;in_sq\u0026#34;: coin in square_trending, \u0026#34;vol_surge\u0026#34;: coin in vol_surge_coins, } # ═══════════════════════════════════════ # 热度榜 # ═══════════════════════════════════════ hot_coins = sorted( [d for d in coin_data.values() if d[\u0026#34;heat\u0026#34;] \u0026gt; 0], key=lambda x: x[\u0026#34;heat\u0026#34;], reverse=True ) # 检测首次上榜 heat_history = {} if HEAT_HISTORY_FILE.exists(): try: heat_history = json.loads(HEAT_HISTORY_FILE.read_text()) except: pass now_ts = datetime.now(timezone(timedelta(hours=8))).strftime(\u0026#34;%Y-%m-%d %H:%M\u0026#34;) new_entries = [] # 首次上榜的币 for s in hot_coins: coin = s[\u0026#34;coin\u0026#34;] if coin not in heat_history: # 首次上榜！ heat_history[coin] = {\u0026#34;first_seen\u0026#34;: now_ts, \u0026#34;price\u0026#34;: s.get(\u0026#34;px_chg\u0026#34;, 0)} sources = [] if s[\u0026#34;in_sq\u0026#34;]: sources.append(\u0026#34;广场\u0026#34;) if s[\u0026#34;in_cg\u0026#34;]: sources.append(\u0026#34;CG\u0026#34;) if s[\u0026#34;vol_surge\u0026#34;]: sources.append(\u0026#34;放量\u0026#34;) new_entries.append({\u0026#34;coin\u0026#34;: coin, \u0026#34;sources\u0026#34;: sources, \u0026#34;data\u0026#34;: s}) # 清理超过7天的历史（避免文件无限增长） cutoff = (datetime.now(timezone(timedelta(hours=8))) - timedelta(days=7)).strftime(\u0026#34;%Y-%m-%d\u0026#34;) heat_history = {k: v for k, v in heat_history.items() if v.get(\u0026#34;first_seen\u0026#34;, \u0026#34;9999\u0026#34;) \u0026gt;= cutoff} # 保存历史 HEAT_HISTORY_FILE.write_text(json.dumps(heat_history, indent=2, ensure_ascii=False)) # ═══════════════════════════════════════ # 追多：负费率+在涨 # ═══════════════════════════════════════ chase = [] for sym, d in coin_data.items(): if d[\u0026#34;px_chg\u0026#34;] \u0026gt; 3 and d[\u0026#34;fr_pct\u0026#34;] \u0026lt; -0.005 and d[\u0026#34;vol\u0026#34;] \u0026gt; 1_000_000: fr_hist = api_get(\u0026#34;/fapi/v1/fundingRate\u0026#34;, {\u0026#34;symbol\u0026#34;: sym, \u0026#34;limit\u0026#34;: 5}) fr_rates = [float(f[\u0026#34;fundingRate\u0026#34;]) * 100 for f in fr_hist] if fr_hist else [d[\u0026#34;fr_pct\u0026#34;]] fr_prev = fr_rates[-2] if len(fr_rates) \u0026gt;= 2 else d[\u0026#34;fr_pct\u0026#34;] fr_delta = d[\u0026#34;fr_pct\u0026#34;] - fr_prev trend = \u0026#34;加速恶化\u0026#34; if fr_delta \u0026lt; -0.05 else \u0026#34;转负\u0026#34; if fr_delta \u0026lt; -0.01 else \u0026#34;持平\u0026#34; if abs(fr_delta) \u0026lt; 0.01 else \u0026#34;回升\u0026#34; chase.append({**d, \u0026#34;fr_delta\u0026#34;: fr_delta, \u0026#34;trend\u0026#34;: trend, \u0026#34;rates\u0026#34;: \u0026#34; → \u0026#34;.join([f\u0026#34;{x:.3f}\u0026#34; for x in fr_rates[-3:]])}) time.sleep(0.2) chase.sort(key=lambda x: x[\u0026#34;fr_pct\u0026#34;]) # ═══════════════════════════════════════ # 生成推送 # ═══════════════════════════════════════ now = datetime.now(timezone(timedelta(hours=8))) lines = [ f\u0026#34;**热度做多雷达**\u0026#34;, f\u0026#34;{now.strftime(\u0026#39;%Y-%m-%d %H:%M\u0026#39;)} CST\u0026#34;, ] # 热度榜（表格） # 首次上榜放最前面 if new_entries: lines.append(f\u0026#34;\\n**[ 首次上榜 ]** 新出现的热度币，重点关注\u0026#34;) tbl = [\u0026#34;```\u0026#34;] tbl.append(f\u0026#34;{\u0026#39;币种\u0026#39;:\u0026lt;10} {\u0026#39;市值\u0026#39;:\u0026gt;8} {\u0026#39;涨幅\u0026#39;:\u0026gt;7} {\u0026#39;来源\u0026#39;}\u0026#34;) tbl.append(f\u0026#34;{\u0026#39;-\u0026#39;*10} {\u0026#39;-\u0026#39;*8} {\u0026#39;-\u0026#39;*7} {\u0026#39;-\u0026#39;*20}\u0026#34;) for e in new_entries: s = e[\u0026#34;data\u0026#34;] src_str = \u0026#34;/\u0026#34;.join(e[\u0026#34;sources\u0026#34;]) tbl.append(f\u0026#34;{s[\u0026#39;coin\u0026#39;]:\u0026lt;10} {mcap_str(s[\u0026#39;est_mcap\u0026#39;]):\u0026gt;8} {s[\u0026#39;px_chg\u0026#39;]:\u0026gt;+6.0f}% {src_str}\u0026#34;) tbl.append(\u0026#34;```\u0026#34;) lines.append(\u0026#34;\\n\u0026#34;.join(tbl)) if hot_coins: lines.append(f\u0026#34;\\n**[ 热度榜 ]**\u0026#34;) tbl = [\u0026#34;```\u0026#34;] tbl.append(f\u0026#34;{\u0026#39;币种\u0026#39;:\u0026lt;10} {\u0026#39;市值\u0026#39;:\u0026gt;8} {\u0026#39;涨幅\u0026#39;:\u0026gt;7} {\u0026#39;来源\u0026#39;}\u0026#34;) tbl.append(f\u0026#34;{\u0026#39;-\u0026#39;*10} {\u0026#39;-\u0026#39;*8} {\u0026#39;-\u0026#39;*7} {\u0026#39;-\u0026#39;*20}\u0026#34;) for s in hot_coins[:10]: sources = [] if s[\u0026#34;in_sq\u0026#34;]: sources.append(\u0026#34;广场\u0026#34;) if s[\u0026#34;in_cg\u0026#34;]: sources.append(\u0026#34;CG\u0026#34;) if s[\u0026#34;vol_surge\u0026#34;]: sources.append(\u0026#34;放量\u0026#34;) extra = [] if abs(s[\u0026#34;d6h\u0026#34;]) \u0026gt;= 3: extra.append(f\u0026#34;OI{s[\u0026#39;d6h\u0026#39;]:+.0f}%\u0026#34;) if s[\u0026#34;fr_pct\u0026#34;] \u0026lt; -0.03: extra.append(f\u0026#34;费率{s[\u0026#39;fr_pct\u0026#39;]:.2f}%\u0026#34;) src_str = \u0026#34;/\u0026#34;.join(sources) if extra: src_str += \u0026#34; \u0026#34; + \u0026#34; \u0026#34;.join(extra) coin_name = s[\u0026#39;coin\u0026#39;] tbl.append(f\u0026#34;{coin_name:\u0026lt;10} {mcap_str(s[\u0026#39;est_mcap\u0026#39;]):\u0026gt;8} {s[\u0026#39;px_chg\u0026#39;]:\u0026gt;+6.0f}% {src_str}\u0026#34;) tbl.append(\u0026#34;```\u0026#34;) lines.append(\u0026#34;\\n\u0026#34;.join(tbl)) else: lines.append(\u0026#34;\\n**[ 热度榜 ]** 暂无热点\u0026#34;) # 追多（表格） lines.append(f\u0026#34;\\n**[ 追多 ]** 涨了+费率负=空头燃料\u0026#34;) if chase: tbl = [\u0026#34;```\u0026#34;] tbl.append(f\u0026#34;{\u0026#39;币种\u0026#39;:\u0026lt;10} {\u0026#39;费率\u0026#39;:\u0026gt;10} {\u0026#39;趋势\u0026#39;:\u0026gt;8} {\u0026#39;涨幅\u0026#39;:\u0026gt;7} {\u0026#39;市值\u0026#39;:\u0026gt;8}\u0026#34;) tbl.append(f\u0026#34;{\u0026#39;-\u0026#39;*10} {\u0026#39;-\u0026#39;*10} {\u0026#39;-\u0026#39;*8} {\u0026#39;-\u0026#39;*7} {\u0026#39;-\u0026#39;*8}\u0026#34;) for s in chase[:8]: tbl.append( f\u0026#34;{s[\u0026#39;coin\u0026#39;]:\u0026lt;10} {s[\u0026#39;fr_pct\u0026#39;]:\u0026gt;+9.3f}% {s[\u0026#39;trend\u0026#39;]:\u0026gt;8} {s[\u0026#39;px_chg\u0026#39;]:\u0026gt;+6.0f}% {mcap_str(s[\u0026#39;est_mcap\u0026#39;]):\u0026gt;7}\u0026#34; ) tbl.append(\u0026#34;```\u0026#34;) lines.append(\u0026#34;\\n\u0026#34;.join(tbl)) else: lines.append(\u0026#34; 暂无符合条件的标的\u0026#34;) # OI异动（表格） oi_alerts = [] for sym, oi in oi_map.items(): if abs(oi[\u0026#34;d6h\u0026#34;]) \u0026gt;= 8: d = coin_data.get(sym) if d and d[\u0026#34;heat\u0026#34;] == 0: oi_alerts.append(d) oi_alerts.sort(key=lambda x: abs(x[\u0026#34;d6h\u0026#34;]), reverse=True) if oi_alerts: lines.append(f\u0026#34;\\n**[ OI异动 ]** 6小时持仓变化\u0026gt;=8%\u0026#34;) tbl = [\u0026#34;```\u0026#34;] tbl.append(f\u0026#34;{\u0026#39;币种\u0026#39;:\u0026lt;10} {\u0026#39;方向\u0026#39;:\u0026gt;4} {\u0026#39;OI变化\u0026#39;:\u0026gt;8} {\u0026#39;涨幅\u0026#39;:\u0026gt;7} {\u0026#39;市值\u0026#39;:\u0026gt;8}\u0026#34;) tbl.append(f\u0026#34;{\u0026#39;-\u0026#39;*10} {\u0026#39;-\u0026#39;*4} {\u0026#39;-\u0026#39;*8} {\u0026#39;-\u0026#39;*7} {\u0026#39;-\u0026#39;*8}\u0026#34;) for s in oi_alerts[:6]: direction = \u0026#34;增仓\u0026#34; if s[\u0026#34;d6h\u0026#34;] \u0026gt; 0 else \u0026#34;减仓\u0026#34; tbl.append( f\u0026#34;{s[\u0026#39;coin\u0026#39;]:\u0026lt;10} {direction:\u0026gt;4} {s[\u0026#39;d6h\u0026#39;]:\u0026gt;+7.1f}% {s[\u0026#39;px_chg\u0026#39;]:\u0026gt;+6.0f}% {mcap_str(s[\u0026#39;est_mcap\u0026#39;]):\u0026gt;7}\u0026#34; ) tbl.append(\u0026#34;```\u0026#34;) lines.append(\u0026#34;\\n\u0026#34;.join(tbl)) # 值得关注 highlights = [] hot_oi = [d for d in coin_data.values() if d[\u0026#34;heat\u0026#34;] \u0026gt; 0 and d[\u0026#34;d6h\u0026#34;] \u0026gt; 5] for s in sorted(hot_oi, key=lambda x: x[\u0026#34;d6h\u0026#34;], reverse=True)[:3]: highlights.append(f\u0026#34;{s[\u0026#39;coin\u0026#39;]} — 热度高+OI涨{s[\u0026#39;d6h\u0026#39;]:+.0f}%，资金涌入\u0026#34;) hot_fuel = [d for d in coin_data.values() if d[\u0026#34;heat\u0026#34;] \u0026gt; 0 and d[\u0026#34;fr_pct\u0026#34;] \u0026lt; -0.03] for s in sorted(hot_fuel, key=lambda x: x[\u0026#34;fr_pct\u0026#34;])[:2]: if s[\u0026#34;coin\u0026#34;] not in \u0026#34; \u0026#34;.join(highlights): highlights.append(f\u0026#34;{s[\u0026#39;coin\u0026#39;]} — 热度高+费率{s[\u0026#39;fr_pct\u0026#39;]:.2f}%，空头燃料足\u0026#34;) chase_fire = [s for s in chase[:5] if \u0026#34;加速\u0026#34; in s.get(\u0026#34;trend\u0026#34;, \u0026#34;\u0026#34;)] for s in chase_fire[:2]: if s[\u0026#34;coin\u0026#34;] not in \u0026#34; \u0026#34;.join(highlights): highlights.append(f\u0026#34;{s[\u0026#39;coin\u0026#39;]} — 费率{s[\u0026#39;fr_pct\u0026#39;]:.3f}%持续恶化，逼空在即\u0026#34;) if highlights: lines.append(f\u0026#34;\\n**[ 值得关注 ]**\u0026#34;) for h in highlights[:5]: lines.append(f\u0026#34; {h}\u0026#34;) lines.append(f\u0026#34;\\n广场=币安站内搜索 / CG=CoinGecko全球热度\u0026#34;) lines.append(f\u0026#34;费率负=做空多，庄家拉盘爆空头\u0026#34;) report = \u0026#34;\\n\u0026#34;.join(lines) send_telegram(report) print(\u0026#34;\\n✅ 完成\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: main() Binance Alpha Monitor Date: 2026.04.23　Tags: Python · Binance · Claude AI · Telegram\nWebSocket + AI analysis + Telegram alerts\nAuto-detects new Binance Alpha listings, analyzes token quality with Claude AI, and sends alerts via Telegram.\nFull source code #!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; Binance Alpha Monitor v2 — 稳定版 REST轮询 + 智能过滤 + 评级 + TG推送 零API Key，零AI成本（评级用规则引擎） 运行: python3 alpha_monitor.py \u0026#34;\u0026#34;\u0026#34; import asyncio import hashlib import json import logging import os import re import sqlite3 import time from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Optional import httpx # ============================================================ # 配置 # ============================================================ BASE_DIR = Path(__file__).parent DB_PATH = str(BASE_DIR / \u0026#34;data\u0026#34; / \u0026#34;alpha.db\u0026#34;) # TG TG_BOT_TOKEN = os.environ.get(\u0026#34;TG_BOT_TOKEN\u0026#34;, \u0026#34;\u0026#34;) TG_CHAT_ID = os.environ.get(\u0026#34;TG_CHAT_ID\u0026#34;, \u0026#34;\u0026#34;) # LLM (可选，用于抽取叙事，不配置则降级为规则) # 优先从hermes auth.json读credential pool（与scanner一致） def _load_anthropic_key(): \u0026#34;\u0026#34;\u0026#34;从auth.json或环境变量获取API key\u0026#34;\u0026#34;\u0026#34; # 1. hermes auth.json credential pool try: auth_file = os.path.expanduser(\u0026#34;~/.hermes/auth.json\u0026#34;) if os.path.exists(auth_file): with open(auth_file) as f: auth = json.load(f) pool = auth.get(\u0026#34;credential_pool\u0026#34;, {}).get(\u0026#34;anthropic\u0026#34;, []) if pool: key = pool[0].get(\u0026#34;access_token\u0026#34;, \u0026#34;\u0026#34;) if key and key != \u0026#34;***\u0026#34;: return key except Exception: pass # 2. 环境变量 fallback return os.environ.get(\u0026#34;ANTHROPIC_API_KEY\u0026#34;, \u0026#34;\u0026#34;) ANTHROPIC_API_KEY = _load_anthropic_key() ANTHROPIC_BASE_URL = os.environ.get(\u0026#34;ANTHROPIC_BASE_URL\u0026#34;, \u0026#34;https://api.anthropic.com\u0026#34;) ANTHROPIC_MODEL = os.environ.get(\u0026#34;ANTHROPIC_MODEL\u0026#34;, \u0026#34;claude-sonnet-4-6\u0026#34;) # 轮询间隔 ANNOUNCEMENT_POLL_INTERVAL = 30 # 公告轮询30秒 AGGREGATION_POLL_INTERVAL = 15 # 聚合工作者15秒 MONITOR_POLL_INTERVAL = 120 # 上线后监控2分钟 # HTTP HEADERS = { \u0026#34;User-Agent\u0026#34;: \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\u0026#34;, \u0026#34;Accept\u0026#34;: \u0026#34;application/json\u0026#34;, \u0026#34;Accept-Language\u0026#34;: \u0026#34;en-US,en;q=0.9\u0026#34;, } BINANCE_ANNOUNCEMENT_API = \u0026#34;https://www.binance.com/bapi/composite/v1/public/cms/article/list/query\u0026#34; # ============================================================ # 日志 # ============================================================ logging.basicConfig( level=logging.INFO, format=\u0026#34;%(asctime)s | %(levelname)s | %(name)s | %(message)s\u0026#34;, datefmt=\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;, ) logger = logging.getLogger(\u0026#34;alpha\u0026#34;) # ============================================================ # 过滤规则 # ============================================================ # 触发关键词 — 命中任意一个就触发 TRIGGER_KEYWORDS = [ \u0026#34;alpha\u0026#34;, \u0026#34;空投\u0026#34;, \u0026#34;airdrop\u0026#34;, \u0026#34;tge\u0026#34;, \u0026#34;token generation\u0026#34;, \u0026#34;将上线\u0026#34;, \u0026#34;will list\u0026#34;, \u0026#34;will launch\u0026#34;, \u0026#34;独家\u0026#34;, \u0026#34;exclusive\u0026#34;, \u0026#34;binance wallet\u0026#34;, \u0026#34;hodler\u0026#34;, ] # 排除关键词 — 命中任意一个直接排除 EXCLUDE_KEYWORDS = [ \u0026#34;delisting\u0026#34;, \u0026#34;delist\u0026#34;, \u0026#34;下架\u0026#34;, \u0026#34;deprecate\u0026#34;, \u0026#34;退市\u0026#34;, \u0026#34;maintenance\u0026#34;, \u0026#34;维护\u0026#34;, \u0026#34;launchpool\u0026#34;, \u0026#34;megadrop\u0026#34;, \u0026#34;buyback\u0026#34;, \u0026#34;回购\u0026#34;, \u0026#34;已完成\u0026#34;, \u0026#34;完成结算\u0026#34;, \u0026#34;perpetual contract\u0026#34;, # 永续合约 \u0026#34;futures will launch\u0026#34;, # 期货上新 \u0026#34;usdⓢ-margined\u0026#34;, # U本位合约 \u0026#34;coin-margined\u0026#34;, # 币本位合约 \u0026#34;margin will add\u0026#34;, # 杠杆上新 \u0026#34;trading bots services\u0026#34;, # 交易机器人 \u0026#34;trading pairs\u0026#34;, # 交易对调整(非新币) ] # Alpha Box 盲盒 ALPHA_BOX_KEYWORDS = [\u0026#34;alpha box\u0026#34;, \u0026#34;盲盒\u0026#34;, \u0026#34;mystery box\u0026#34;] # ============================================================ # VC 白名单 # ============================================================ TIER1_VCS = [ \u0026#34;binance labs\u0026#34;, \u0026#34;yzi labs\u0026#34;, \u0026#34;coinbase ventures\u0026#34;, \u0026#34;a16z\u0026#34;, \u0026#34;andreessen horowitz\u0026#34;, \u0026#34;paradigm\u0026#34;, \u0026#34;polychain\u0026#34;, \u0026#34;polychain capital\u0026#34;, \u0026#34;sequoia\u0026#34;, \u0026#34;sequoia china\u0026#34;, \u0026#34;sequoia capital\u0026#34;, \u0026#34;multicoin\u0026#34;, \u0026#34;multicoin capital\u0026#34;, \u0026#34;pantera\u0026#34;, \u0026#34;pantera capital\u0026#34;, \u0026#34;dragonfly\u0026#34;, \u0026#34;dragonfly capital\u0026#34;, \u0026#34;founders fund\u0026#34;, ] TIER2_VCS = [ \u0026#34;abcde\u0026#34;, \u0026#34;iosg\u0026#34;, \u0026#34;hashkey\u0026#34;, \u0026#34;okx ventures\u0026#34;, \u0026#34;sevenx\u0026#34;, \u0026#34;folius\u0026#34;, \u0026#34;foresight\u0026#34;, \u0026#34;hashed\u0026#34;, \u0026#34;bitkraft\u0026#34;, \u0026#34;framework\u0026#34;, \u0026#34;framework ventures\u0026#34;, \u0026#34;delphi\u0026#34;, \u0026#34;delphi digital\u0026#34;, \u0026#34;electric capital\u0026#34;, \u0026#34;variant\u0026#34;, \u0026#34;1kx\u0026#34;, \u0026#34;placeholder\u0026#34;, \u0026#34;animoca\u0026#34;, \u0026#34;animoca brands\u0026#34;, \u0026#34;jump\u0026#34;, \u0026#34;jump crypto\u0026#34;, \u0026#34;hack vc\u0026#34;, \u0026#34;bain capital\u0026#34;, ] # 叙事热度 HOT_NARRATIVES = [\u0026#34;defi_perp\u0026#34;, \u0026#34;ai_agent\u0026#34;, \u0026#34;ai_defi\u0026#34;, \u0026#34;defai\u0026#34;, \u0026#34;zk_proof\u0026#34;] WEAK_NARRATIVES = [\u0026#34;gamefi\u0026#34;, \u0026#34;meme\u0026#34;, \u0026#34;social\u0026#34;] # 币安亲儿子信号 BINANCE_DARLING_KEYWORDS = [\u0026#34;yzi labs\u0026#34;, \u0026#34;binance labs\u0026#34;] # ============================================================ # 评级引擎 # ============================================================ TIER_ICONS = {\u0026#34;S\u0026#34;: \u0026#34;🟢🟢🟢\u0026#34;, \u0026#34;A\u0026#34;: \u0026#34;🟡🟡\u0026#34;, \u0026#34;B\u0026#34;: \u0026#34;🟠\u0026#34;, \u0026#34;C\u0026#34;: \u0026#34;⚪\u0026#34;} TIER_LABELS = {\u0026#34;S\u0026#34;: \u0026#34;S 级(必研究)\u0026#34;, \u0026#34;A\u0026#34;: \u0026#34;A 级(值得看)\u0026#34;, \u0026#34;B\u0026#34;: \u0026#34;B 级(正常)\u0026#34;, \u0026#34;C\u0026#34;: \u0026#34;C 级(了解)\u0026#34;} def count_vc_tier(vcs: list, vc_list: list) -\u0026gt; int: count = 0 vcs_lower = [v.lower() for v in vcs] for t in vc_list: if any(t in v for v in vcs_lower): count += 1 return count def rate_project(circ_mcap: float, fdv: float, vcs: list, narrative: str, is_darling: bool) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;S/A/B/C 评级\u0026#34;\u0026#34;\u0026#34; t1 = count_vc_tier(vcs, TIER1_VCS) t2 = count_vc_tier(vcs, TIER2_VCS) hot = narrative in HOT_NARRATIVES weak = narrative in WEAK_NARRATIVES circ_mcap = circ_mcap or 0 fdv = fdv or 0 warnings = [] if weak: warnings.append(f\u0026#34;⚠️ {narrative} 历史破发率较高\u0026#34;) # S级 5条路径 if is_darling: return {\u0026#34;tier\u0026#34;: \u0026#34;S\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;币安亲儿子(YZi/Binance Labs/CZ)\u0026#34;, \u0026#34;warnings\u0026#34;: warnings} if hot and t1 \u0026gt;= 1 and fdv \u0026lt; 500_000_000: return {\u0026#34;tier\u0026#34;: \u0026#34;S\u0026#34;, \u0026#34;reason\u0026#34;: f\u0026#34;热叙事({narrative})+ Tier1 VC\u0026#34;, \u0026#34;warnings\u0026#34;: warnings} if t1 \u0026gt;= 2 and circ_mcap \u0026lt; 50_000_000 and fdv \u0026lt; 300_000_000: return {\u0026#34;tier\u0026#34;: \u0026#34;S\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;≥2家 Tier1 中盘\u0026#34;, \u0026#34;warnings\u0026#34;: warnings} if t1 \u0026gt;= 1 and circ_mcap \u0026lt; 10_000_000 and fdv \u0026lt; 100_000_000: return {\u0026#34;tier\u0026#34;: \u0026#34;S\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;Tier1 微盘\u0026#34;, \u0026#34;warnings\u0026#34;: warnings} if hot and circ_mcap \u0026lt; 10_000_000 and fdv \u0026lt; 50_000_000: return {\u0026#34;tier\u0026#34;: \u0026#34;S\u0026#34;, \u0026#34;reason\u0026#34;: f\u0026#34;热叙事({narrative})微盘\u0026#34;, \u0026#34;warnings\u0026#34;: warnings} # A级 if t1 \u0026gt;= 1 and circ_mcap \u0026lt; 20_000_000 and fdv \u0026lt; 200_000_000: return {\u0026#34;tier\u0026#34;: \u0026#34;A\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;Tier1 小盘\u0026#34;, \u0026#34;warnings\u0026#34;: warnings} # B级 if circ_mcap \u0026lt; 50_000_000 and fdv \u0026lt; 500_000_000: return {\u0026#34;tier\u0026#34;: \u0026#34;B\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;中盘\u0026#34;, \u0026#34;warnings\u0026#34;: warnings} return {\u0026#34;tier\u0026#34;: \u0026#34;C\u0026#34;, \u0026#34;reason\u0026#34;: \u0026#34;大盘/弱信号\u0026#34;, \u0026#34;warnings\u0026#34;: warnings} # ============================================================ # 数据库 # ============================================================ def init_db(): Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute(\u0026#34;\u0026#34;\u0026#34; CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, symbol TEXT NOT NULL, name TEXT, launch_time TEXT, source TEXT, raw_text TEXT, tier TEXT DEFAULT \u0026#39;PENDING\u0026#39;, tier_reason TEXT, narrative TEXT, narrative_desc TEXT, vcs_json TEXT DEFAULT \u0026#39;[]\u0026#39;, is_darling INTEGER DEFAULT 0, open_price REAL, total_supply REAL, circulating_supply REAL, fdv REAL, circulating_mcap REAL, excluded INTEGER DEFAULT 0, exclude_reason TEXT, discovered_at TEXT, updated_at TEXT )\u0026#34;\u0026#34;\u0026#34;) c.execute(\u0026#34;\u0026#34;\u0026#34; CREATE TABLE IF NOT EXISTS pushes ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL, push_type TEXT, sent_at TEXT, content TEXT )\u0026#34;\u0026#34;\u0026#34;) c.execute(\u0026#34;\u0026#34;\u0026#34; CREATE TABLE IF NOT EXISTS snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL, timestamp TEXT NOT NULL, price REAL, circulating_mcap REAL, fdv REAL )\u0026#34;\u0026#34;\u0026#34;) conn.commit() conn.close() def project_id(symbol: str, date_str: str) -\u0026gt; str: return hashlib.md5(f\u0026#34;{symbol.upper()}_{date_str}\u0026#34;.encode()).hexdigest()[:16] def project_exists(pid: str) -\u0026gt; bool: conn = sqlite3.connect(DB_PATH) exists = conn.execute(\u0026#34;SELECT 1 FROM projects WHERE id=?\u0026#34;, (pid,)).fetchone() is not None conn.close() return exists def save_project(project: dict): conn = sqlite3.connect(DB_PATH) now = datetime.utcnow().isoformat() conn.execute(\u0026#34;\u0026#34;\u0026#34; INSERT OR IGNORE INTO projects (id, symbol, name, launch_time, source, raw_text, tier, tier_reason, narrative, narrative_desc, vcs_json, is_darling, open_price, total_supply, circulating_supply, fdv, circulating_mcap, excluded, exclude_reason, discovered_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) \u0026#34;\u0026#34;\u0026#34;, ( project[\u0026#34;id\u0026#34;], project[\u0026#34;symbol\u0026#34;], project.get(\u0026#34;name\u0026#34;), project.get(\u0026#34;launch_time\u0026#34;), project.get(\u0026#34;source\u0026#34;), project.get(\u0026#34;raw_text\u0026#34;), project.get(\u0026#34;tier\u0026#34;, \u0026#34;PENDING\u0026#34;), project.get(\u0026#34;tier_reason\u0026#34;), project.get(\u0026#34;narrative\u0026#34;), project.get(\u0026#34;narrative_desc\u0026#34;), json.dumps(project.get(\u0026#34;vcs\u0026#34;, [])), int(project.get(\u0026#34;is_darling\u0026#34;, False)), project.get(\u0026#34;open_price\u0026#34;), project.get(\u0026#34;total_supply\u0026#34;), project.get(\u0026#34;circulating_supply\u0026#34;), project.get(\u0026#34;fdv\u0026#34;), project.get(\u0026#34;circulating_mcap\u0026#34;), int(project.get(\u0026#34;excluded\u0026#34;, 0)), project.get(\u0026#34;exclude_reason\u0026#34;), now, now, )) conn.commit() conn.close() def update_project(pid: str, fields: dict): if not fields: return conn = sqlite3.connect(DB_PATH) fields[\u0026#34;updated_at\u0026#34;] = datetime.utcnow().isoformat() set_parts = [f\u0026#34;{k}=?\u0026#34; for k in fields] values = list(fields.values()) + [pid] conn.execute(f\u0026#34;UPDATE projects SET {\u0026#39;,\u0026#39;.join(set_parts)} WHERE id=?\u0026#34;, values) conn.commit() conn.close() def get_project(pid: str) -\u0026gt; Optional[dict]: conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row row = conn.execute(\u0026#34;SELECT * FROM projects WHERE id=?\u0026#34;, (pid,)).fetchone() conn.close() return dict(row) if row else None def list_pending() -\u0026gt; list: conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row rows = conn.execute( \u0026#34;SELECT * FROM projects WHERE excluded=0 AND tier=\u0026#39;PENDING\u0026#39; ORDER BY discovered_at\u0026#34; ).fetchall() conn.close() return [dict(r) for r in rows] def list_active() -\u0026gt; list: \u0026#34;\u0026#34;\u0026#34;上线后需要监控的项目（非PENDING非EXCLUDED，有launch_time）\u0026#34;\u0026#34;\u0026#34; conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row rows = conn.execute(\u0026#34;\u0026#34;\u0026#34; SELECT * FROM projects WHERE excluded=0 AND launch_time IS NOT NULL AND launch_time != \u0026#39;\u0026#39; AND tier NOT IN (\u0026#39;PENDING\u0026#39;, \u0026#39;EXCLUDED\u0026#39;, \u0026#39;ERROR\u0026#39;) \u0026#34;\u0026#34;\u0026#34;).fetchall() conn.close() return [dict(r) for r in rows] def has_pushed(pid: str, push_type: str) -\u0026gt; bool: conn = sqlite3.connect(DB_PATH) exists = conn.execute( \u0026#34;SELECT 1 FROM pushes WHERE project_id=? AND push_type=?\u0026#34;, (pid, push_type) ).fetchone() is not None conn.close() return exists def log_push(pid: str, push_type: str, content: str): conn = sqlite3.connect(DB_PATH) conn.execute( \u0026#34;INSERT INTO pushes (project_id, push_type, sent_at, content) VALUES (?,?,?,?)\u0026#34;, (pid, push_type, datetime.utcnow().isoformat(), content) ) conn.commit() conn.close() def save_snapshot(pid: str, price: float, mcap: float, fdv: float): conn = sqlite3.connect(DB_PATH) conn.execute( \u0026#34;INSERT INTO snapshots (project_id, timestamp, price, circulating_mcap, fdv) VALUES (?,?,?,?,?)\u0026#34;, (pid, datetime.utcnow().isoformat(), price, mcap, fdv) ) conn.commit() conn.close() # ============================================================ # TG 推送 # ============================================================ async def send_tg(text: str, silent: bool = False) -\u0026gt; bool: if not TG_BOT_TOKEN or not TG_CHAT_ID: logger.error(\u0026#34;TG_BOT_TOKEN 或 TG_CHAT_ID 未配置\u0026#34;) return False url = f\u0026#34;https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage\u0026#34; payload = { \u0026#34;chat_id\u0026#34;: TG_CHAT_ID, \u0026#34;text\u0026#34;: text, \u0026#34;parse_mode\u0026#34;: \u0026#34;HTML\u0026#34;, \u0026#34;disable_notification\u0026#34;: silent, } try: async with httpx.AsyncClient(timeout=15, headers=HEADERS) as client: resp = await client.post(url, json=payload) if resp.status_code != 200: logger.error(f\u0026#34;TG发送失败 {resp.status_code}: {resp.text[:200]}\u0026#34;) return False return True except Exception as e: logger.error(f\u0026#34;TG发送异常: {e}\u0026#34;) return False # ============================================================ # 公告标题解析 # ============================================================ def is_trigger(title: str) -\u0026gt; tuple[bool, Optional[str]]: t = title.lower() for kw in EXCLUDE_KEYWORDS: if kw.lower() in t: return False, f\u0026#34;排除: {kw}\u0026#34; for kw in ALPHA_BOX_KEYWORDS: if kw.lower() in t: return False, \u0026#34;Alpha Box 盲盒\u0026#34; for kw in TRIGGER_KEYWORDS: if kw.lower() in t: return True, None return False, None def extract_symbol(title: str) -\u0026gt; Optional[str]: # 英文括号: \u0026#34;Chip (CHIP)\u0026#34; m = re.search(r\u0026#34;\\(([A-Z0-9]{2,10})\\)\u0026#34;, title) if m: return m.group(1) # 中文括号 m = re.search(r\u0026#34;（([A-Z0-9]{2,10})）\u0026#34;, title) if m: return m.group(1) return None def extract_name(title: str) -\u0026gt; Optional[str]: patterns = [ r\u0026#34;(?:上线|List|list|Launch|launch|featured)\\s+([A-Za-z0-9 ]+?)\\s*[\\(（]\u0026#34;, ] for p in patterns: m = re.search(p, title, re.IGNORECASE) if m: return m.group(1).strip() return None # ============================================================ # 币安公告抓取 # ============================================================ async def fetch_announcements(limit: int = 20) -\u0026gt; list: \u0026#34;\u0026#34;\u0026#34;抓取币安最新公告\u0026#34;\u0026#34;\u0026#34; all_articles = [] # 48: New Cryptocurrency Listing, 161: Latest Activities, 93: Latest News for catalog_id in [48, 161, 93]: params = {\u0026#34;type\u0026#34;: 1, \u0026#34;catalogId\u0026#34;: catalog_id, \u0026#34;pageNo\u0026#34;: 1, \u0026#34;pageSize\u0026#34;: limit} try: async with httpx.AsyncClient(timeout=15, headers=HEADERS) as client: resp = await client.get(BINANCE_ANNOUNCEMENT_API, params=params) resp.raise_for_status() data = resp.json() for catalog in data.get(\u0026#34;data\u0026#34;, {}).get(\u0026#34;catalogs\u0026#34;, []): for a in catalog.get(\u0026#34;articles\u0026#34;, []): a[\u0026#34;_catalog_id\u0026#34;] = catalog_id all_articles.append(a) except Exception as e: logger.warning(f\u0026#34;抓取分类 {catalog_id} 失败: {e}\u0026#34;) # 去重 seen = set() unique = [] for a in all_articles: code = a.get(\u0026#34;code\u0026#34;) if code and code not in seen: seen.add(code) unique.append(a) return unique # ============================================================ # CoinGecko 数据 # ============================================================ async def fetch_coingecko(symbol: str, project_name: str = \u0026#34;\u0026#34;) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;查CoinGecko代币经济数据。双重匹配：symbol精确匹配 + 项目名模糊匹配\u0026#34;\u0026#34;\u0026#34; result = {\u0026#34;found\u0026#34;: False, \u0026#34;price\u0026#34;: None, \u0026#34;fdv\u0026#34;: None, \u0026#34;mcap\u0026#34;: None, \u0026#34;total_supply\u0026#34;: None, \u0026#34;circ_supply\u0026#34;: None, \u0026#34;chain\u0026#34;: None, \u0026#34;contract\u0026#34;: None} try: async with httpx.AsyncClient(timeout=15, headers=HEADERS) as client: # 搜索策略：先搜symbol，如果项目名不同再搜项目名 queries = [symbol] if project_name and project_name.upper() != symbol.upper(): queries.append(project_name) coin_id = None best_rank = 999999 name_exact_match = None # 项目名精确匹配优先 for query in queries: resp = await client.get(\u0026#34;https://api.coingecko.com/api/v3/search\u0026#34;, params={\u0026#34;query\u0026#34;: query}) if resp.status_code != 200: continue coins = resp.json().get(\u0026#34;coins\u0026#34;, []) for c in coins: c_sym = c.get(\u0026#34;symbol\u0026#34;, \u0026#34;\u0026#34;).upper() c_name = c.get(\u0026#34;name\u0026#34;, \u0026#34;\u0026#34;).lower() c_rank = c.get(\u0026#34;market_cap_rank\u0026#34;) or 999999 # 项目名精确匹配（最高优先级，如MegaETH -\u0026gt; megaeth） if project_name and c_name == project_name.lower(): name_exact_match = c[\u0026#34;id\u0026#34;] # 精确symbol匹配 — 取market_cap_rank最高（最小）的 if c_sym == symbol.upper(): if c_rank \u0026lt; best_rank: coin_id = c[\u0026#34;id\u0026#34;] best_rank = c_rank # 项目名模糊匹配（如MegaETH搜mega） if project_name and project_name.lower() in c_name: if c_rank \u0026lt; best_rank: coin_id = c[\u0026#34;id\u0026#34;] best_rank = c_rank # 项目名精确匹配 \u0026gt; 其他匹配 if name_exact_match: coin_id = name_exact_match if not coin_id: logger.info(f\u0026#34;CoinGecko未找到 {symbol}/{project_name}\u0026#34;) return result logger.info(f\u0026#34;CoinGecko匹配: {symbol} -\u0026gt; {coin_id} (rank={best_rank})\u0026#34;) resp2 = await client.get( f\u0026#34;https://api.coingecko.com/api/v3/coins/{coin_id}\u0026#34;, params={\u0026#34;localization\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;tickers\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;market_data\u0026#34;: \u0026#34;true\u0026#34;, \u0026#34;community_data\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;developer_data\u0026#34;: \u0026#34;false\u0026#34;} ) if resp2.status_code == 429: await asyncio.sleep(5) resp2 = await client.get( f\u0026#34;https://api.coingecko.com/api/v3/coins/{coin_id}\u0026#34;, params={\u0026#34;localization\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;tickers\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;market_data\u0026#34;: \u0026#34;true\u0026#34;, \u0026#34;community_data\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;developer_data\u0026#34;: \u0026#34;false\u0026#34;} ) if resp2.status_code != 200: return result d = resp2.json() md = d.get(\u0026#34;market_data\u0026#34;, {}) result.update({ \u0026#34;found\u0026#34;: True, \u0026#34;price\u0026#34;: (md.get(\u0026#34;current_price\u0026#34;) or {}).get(\u0026#34;usd\u0026#34;), \u0026#34;fdv\u0026#34;: (md.get(\u0026#34;fully_diluted_valuation\u0026#34;) or {}).get(\u0026#34;usd\u0026#34;), \u0026#34;mcap\u0026#34;: (md.get(\u0026#34;market_cap\u0026#34;) or {}).get(\u0026#34;usd\u0026#34;), \u0026#34;total_supply\u0026#34;: md.get(\u0026#34;total_supply\u0026#34;), \u0026#34;circ_supply\u0026#34;: md.get(\u0026#34;circulating_supply\u0026#34;), }) # 提取categories（含VC信息如\u0026#34;YZi Labs Portfolio\u0026#34;）和description result[\u0026#34;categories\u0026#34;] = d.get(\u0026#34;categories\u0026#34;, []) result[\u0026#34;description\u0026#34;] = (d.get(\u0026#34;description\u0026#34;) or {}).get(\u0026#34;en\u0026#34;, \u0026#34;\u0026#34;)[:500] platforms = d.get(\u0026#34;platforms\u0026#34;, {}) for chain, addr in platforms.items(): if addr: result[\u0026#34;chain\u0026#34;] = chain result[\u0026#34;contract\u0026#34;] = addr break # CoinGecko没价格时（新币常见），从币安合约/现货补充 if not result[\u0026#34;price\u0026#34;]: binance_price = await _fetch_binance_price(symbol, client) if binance_price: result[\u0026#34;price\u0026#34;] = binance_price ts = result.get(\u0026#34;total_supply\u0026#34;) or 0 cs = result.get(\u0026#34;circ_supply\u0026#34;) or 0 if ts \u0026gt; 0: result[\u0026#34;fdv\u0026#34;] = binance_price * ts if cs \u0026gt; 0: result[\u0026#34;mcap\u0026#34;] = binance_price * cs logger.info(f\u0026#34;币安补充价格 {symbol}: ${binance_price}, FDV=${result.get(\u0026#39;fdv\u0026#39;,0):,.0f}\u0026#34;) except Exception as e: logger.warning(f\u0026#34;CoinGecko查询失败 {symbol}: {e}\u0026#34;) return result async def _fetch_binance_price(symbol: str, client: httpx.AsyncClient) -\u0026gt; float: \u0026#34;\u0026#34;\u0026#34;从币安现货或合约获取价格\u0026#34;\u0026#34;\u0026#34; pair = f\u0026#34;{symbol.upper()}USDT\u0026#34; # 1. 现货 try: resp = await client.get(f\u0026#34;https://api.binance.com/api/v3/ticker/price\u0026#34;, params={\u0026#34;symbol\u0026#34;: pair}) if resp.status_code == 200: return float(resp.json()[\u0026#34;price\u0026#34;]) except Exception: pass # 2. 合约 try: resp = await client.get(f\u0026#34;https://fapi.binance.com/fapi/v1/ticker/price\u0026#34;, params={\u0026#34;symbol\u0026#34;: pair}) if resp.status_code == 200: return float(resp.json()[\u0026#34;price\u0026#34;]) except Exception: pass return 0.0 # ============================================================ # LLM 叙事抽取（可选，降级为规则） # ============================================================ async def llm_extract(raw_text: str, symbol: str, name: str = \u0026#34;\u0026#34;, cg_data: dict = None) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;用LLM从公告+CoinGecko数据抽取叙事/VC/是否亲儿子\u0026#34;\u0026#34;\u0026#34; fallback = { \u0026#34;narrative\u0026#34;: \u0026#34;unknown\u0026#34;, \u0026#34;narrative_desc\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;vcs\u0026#34;: [], \u0026#34;is_darling\u0026#34;: False, \u0026#34;exclude_reason\u0026#34;: None, } # 从CoinGecko categories自动提取信息 cg_data = cg_data or {} categories = cg_data.get(\u0026#34;categories\u0026#34;, []) description = cg_data.get(\u0026#34;description\u0026#34;, \u0026#34;\u0026#34;) # 自动检测亲儿子（从categories） darling_cats = [c for c in categories if any(kw in c.lower() for kw in [\u0026#34;yzi labs\u0026#34;, \u0026#34;binance labs\u0026#34;])] if darling_cats: fallback[\u0026#34;is_darling\u0026#34;] = True if not ANTHROPIC_API_KEY: # 降级：从标题+categories关键词猜 t = raw_text.lower() for kw in BINANCE_DARLING_KEYWORDS: if kw in t: fallback[\u0026#34;is_darling\u0026#34;] = True # 从categories猜叙事 cat_str = \u0026#34; \u0026#34;.join(categories).lower() if \u0026#34;defi\u0026#34; in cat_str: fallback[\u0026#34;narrative\u0026#34;] = \u0026#34;defi\u0026#34; elif \u0026#34;ai\u0026#34; in cat_str: fallback[\u0026#34;narrative\u0026#34;] = \u0026#34;ai_agent\u0026#34; elif \u0026#34;gaming\u0026#34; in cat_str or \u0026#34;gamefi\u0026#34; in cat_str: fallback[\u0026#34;narrative\u0026#34;] = \u0026#34;gamefi\u0026#34; elif \u0026#34;meme\u0026#34; in cat_str: fallback[\u0026#34;narrative\u0026#34;] = \u0026#34;meme\u0026#34; elif \u0026#34;rwa\u0026#34; in cat_str or \u0026#34;real world\u0026#34; in cat_str: fallback[\u0026#34;narrative\u0026#34;] = \u0026#34;rwa\u0026#34; return fallback # 构建丰富的上下文 extra_context = \u0026#34;\u0026#34; if categories: extra_context += f\u0026#34;\\nCoinGecko分类: {\u0026#39;, \u0026#39;.join(categories)}\u0026#34; if description: extra_context += f\u0026#34;\\n项目描述: {description[:300]}\u0026#34; if cg_data.get(\u0026#34;found\u0026#34;): extra_context += f\u0026#34;\\n市场数据: FDV=${cg_data.get(\u0026#39;fdv\u0026#39;,0):,.0f}, MCap=${cg_data.get(\u0026#39;mcap\u0026#39;,0):,.0f}, 价格=${cg_data.get(\u0026#39;price\u0026#39;,0)}\u0026#34; if cg_data.get(\u0026#34;chain\u0026#34;): extra_context += f\u0026#34;, 链={cg_data[\u0026#39;chain\u0026#39;]}\u0026#34; system = \u0026#34;你是加密货币研究员，从币安公告和项目数据中提取关键信息。只返回JSON，无其他文字。\u0026#34; user = f\u0026#34;\u0026#34;\u0026#34;分析这个币安上新项目： 代币: {symbol}, 项目名: {name or \u0026#34;未知\u0026#34;} 公告原文: {raw_text} {extra_context} 返回JSON: {{ \u0026#34;narrative\u0026#34;: \u0026#34;defi_perp|ai_agent|ai_defi|defai|zk_proof|infra|defi|rwa|gamefi|meme|social|stablecoin|unknown\u0026#34;, \u0026#34;narrative_desc\u0026#34;: \u0026#34;一句话中文描述这个项目做什么、有什么特点\u0026#34;, \u0026#34;vcs\u0026#34;: [\u0026#34;从CoinGecko分类和公告中提取的投资机构列表\u0026#34;], \u0026#34;is_darling\u0026#34;: true/false, \u0026#34;exclude_reason\u0026#34;: null|\u0026#34;already_tge\u0026#34;|\u0026#34;meme_only\u0026#34; }} 判断规则: - narrative: 选最主要的一个类别 - vcs: CoinGecko分类里如果有 \u0026#34;XXX Portfolio\u0026#34; 就提取XXX作为机构 - is_darling: 如果有YZi Labs/Binance Labs投资 或 CZ/何一站台 则true - exclude_reason: 只有当项目在其他主要CEX(如Coinbase/OKX/Bybit)上线超过3个月才算\u0026#34;already_tge\u0026#34;。如果只是在DEX或刚在币安上线，不算already_tge。CoinGecko有价格数据不代表already_tge。纯meme无叙事则\u0026#34;meme_only\u0026#34; \u0026#34;\u0026#34;\u0026#34; try: async with httpx.AsyncClient(timeout=30, headers=HEADERS) as client: resp = await client.post( f\u0026#34;{ANTHROPIC_BASE_URL.rstrip(\u0026#39;/\u0026#39;)}/v1/messages\u0026#34;, headers={ \u0026#34;x-api-key\u0026#34;: ANTHROPIC_API_KEY, \u0026#34;anthropic-version\u0026#34;: \u0026#34;2023-06-01\u0026#34;, \u0026#34;content-type\u0026#34;: \u0026#34;application/json\u0026#34;, }, json={ \u0026#34;model\u0026#34;: ANTHROPIC_MODEL, \u0026#34;max_tokens\u0026#34;: 800, \u0026#34;temperature\u0026#34;: 0, \u0026#34;system\u0026#34;: system, \u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user}], } ) if resp.status_code != 200: logger.warning(f\u0026#34;LLM调用失败 {resp.status_code}\u0026#34;) return fallback data = resp.json() text = \u0026#34;\u0026#34; for block in data.get(\u0026#34;content\u0026#34;, []): if block.get(\u0026#34;type\u0026#34;) == \u0026#34;text\u0026#34;: text = block.get(\u0026#34;text\u0026#34;, \u0026#34;\u0026#34;) break text = text.strip() if text.startswith(\u0026#34;```\u0026#34;): lines = text.split(\u0026#34;\\n\u0026#34;) text = \u0026#34;\\n\u0026#34;.join(lines[1:-1]) return json.loads(text) except Exception as e: logger.warning(f\u0026#34;LLM抽取异常: {e}\u0026#34;) return fallback # ============================================================ # 消息格式化 # ============================================================ def _fmt_mcap(v): if not v: return \u0026#34;N/A\u0026#34; if v \u0026gt;= 1e9: return f\u0026#34;${v/1e9:.1f}B\u0026#34; if v \u0026gt;= 1e6: return f\u0026#34;${v/1e6:.1f}M\u0026#34; if v \u0026gt;= 1e3: return f\u0026#34;${v/1e3:.0f}K\u0026#34; return f\u0026#34;${v:.0f}\u0026#34; def _fmt_price(v): if not v: return \u0026#34;N/A\u0026#34; if v \u0026gt;= 1: return f\u0026#34;${v:.2f}\u0026#34; if v \u0026gt;= 0.01: return f\u0026#34;${v:.4f}\u0026#34; return f\u0026#34;${v:.6f}\u0026#34; def fmt_discovery(p: dict) -\u0026gt; str: tier = p.get(\u0026#34;tier\u0026#34;, \u0026#34;C\u0026#34;) icon = TIER_ICONS.get(tier, \u0026#34;⚪\u0026#34;) label = TIER_LABELS.get(tier, \u0026#34;\u0026#34;) symbol = p[\u0026#34;symbol\u0026#34;] name = p.get(\u0026#34;name\u0026#34;) or \u0026#34;\u0026#34; vcs = json.loads(p.get(\u0026#34;vcs_json\u0026#34;, \u0026#34;[]\u0026#34;)) if isinstance(p.get(\u0026#34;vcs_json\u0026#34;), str) else p.get(\u0026#34;vcs\u0026#34;, []) lines = [ f\u0026#34;{icon} \u0026lt;b\u0026gt;Alpha 首发 · ${symbol}\u0026lt;/b\u0026gt; {icon}\u0026#34;, f\u0026#34;📋 {label}\u0026#34;, \u0026#34;\u0026#34;, f\u0026#34;\u0026lt;b\u0026gt;{name}\u0026lt;/b\u0026gt;\u0026#34; if name else \u0026#34;\u0026#34;, ] if p.get(\u0026#34;narrative_desc\u0026#34;): lines.append(f\u0026#34;💡 {p[\u0026#39;narrative_desc\u0026#39;]}\u0026#34;) if p.get(\u0026#34;narrative\u0026#34;) and p[\u0026#34;narrative\u0026#34;] != \u0026#34;unknown\u0026#34;: lines.append(f\u0026#34;🏷 叙事: {p[\u0026#39;narrative\u0026#39;]}\u0026#34;) lines.append(\u0026#34;\u0026#34;) if p.get(\u0026#34;fdv\u0026#34;): lines.append(f\u0026#34;📊 FDV: {_fmt_mcap(p[\u0026#39;fdv\u0026#39;])}\u0026#34;) if p.get(\u0026#34;circulating_mcap\u0026#34;): lines.append(f\u0026#34;📊 流通市值: {_fmt_mcap(p[\u0026#39;circulating_mcap\u0026#39;])}\u0026#34;) if p.get(\u0026#34;open_price\u0026#34;): lines.append(f\u0026#34;💰 预估开盘价: {_fmt_price(p[\u0026#39;open_price\u0026#39;])}\u0026#34;) if p.get(\u0026#34;total_supply\u0026#34;) and p.get(\u0026#34;circulating_supply\u0026#34;): pct = p[\u0026#34;circulating_supply\u0026#34;] / p[\u0026#34;total_supply\u0026#34;] * 100 lines.append(f\u0026#34;📦 初始流通: {pct:.1f}%\u0026#34;) if vcs: lines.append(\u0026#34;\u0026#34;) lines.append(\u0026#34;🏛 \u0026lt;b\u0026gt;机构\u0026lt;/b\u0026gt;\u0026#34;) for v in vcs[:5]: is_t1 = any(t in v.lower() for t in TIER1_VCS) lines.append(f\u0026#34; {\u0026#39;⭐\u0026#39; if is_t1 else \u0026#39;·\u0026#39;} {v}\u0026#34;) if p.get(\u0026#34;is_darling\u0026#34;): lines.append(\u0026#34;\u0026#34;) lines.append(\u0026#34;🔥 \u0026lt;b\u0026gt;币安亲儿子\u0026lt;/b\u0026gt;\u0026#34;) if p.get(\u0026#34;tier_reason\u0026#34;): lines.append(\u0026#34;\u0026#34;) lines.append(f\u0026#34;🎯 {p[\u0026#39;tier_reason\u0026#39;]}\u0026#34;) lines.append(\u0026#34;\u0026#34;) lines.append(f\u0026#34;\u0026lt;i\u0026gt;📌 来源: {p.get(\u0026#39;source\u0026#39;, \u0026#39;binance\u0026#39;)}\u0026lt;/i\u0026gt;\u0026#34;) if p.get(\u0026#34;raw_text\u0026#34;): lines.append(f\u0026#34;\u0026lt;i\u0026gt;{p[\u0026#39;raw_text\u0026#39;][:120]}\u0026lt;/i\u0026gt;\u0026#34;) return \u0026#34;\\n\u0026#34;.join(l for l in lines if l is not None) def fmt_countdown(p: dict, minutes: int) -\u0026gt; str: icon = TIER_ICONS.get(p.get(\u0026#34;tier\u0026#34;, \u0026#34;C\u0026#34;), \u0026#34;⚪\u0026#34;) t = f\u0026#34;{minutes//60}h{minutes%60}m\u0026#34; if minutes \u0026gt;= 60 else f\u0026#34;{minutes}m\u0026#34; lines = [ f\u0026#34;{icon} \u0026lt;b\u0026gt;倒计时提醒\u0026lt;/b\u0026gt;\u0026#34;, f\u0026#34;\u0026lt;b\u0026gt;${p[\u0026#39;symbol\u0026#39;]}\u0026lt;/b\u0026gt; · {p.get(\u0026#39;name\u0026#39;, \u0026#39;\u0026#39;)}\u0026#34;, f\u0026#34;⏰ 距上线还有 \u0026lt;b\u0026gt;{t}\u0026lt;/b\u0026gt;\u0026#34;, ] if p.get(\u0026#34;fdv\u0026#34;): lines.append(f\u0026#34;FDV: {_fmt_mcap(p[\u0026#39;fdv\u0026#39;])}\u0026#34;) if minutes \u0026lt;= 30: lines.append(\u0026#34;🔔 \u0026lt;b\u0026gt;准备下单\u0026lt;/b\u0026gt;\u0026#34;) return \u0026#34;\\n\u0026#34;.join(lines) def fmt_launch(p: dict, price: float, mcap: float, fdv: float) -\u0026gt; str: lines = [ f\u0026#34;🚀 \u0026lt;b\u0026gt;${p[\u0026#39;symbol\u0026#39;]} 已上线\u0026lt;/b\u0026gt;\u0026#34;, f\u0026#34;开盘价: \u0026lt;b\u0026gt;{_fmt_price(price)}\u0026lt;/b\u0026gt;\u0026#34;, f\u0026#34;流通市值: \u0026lt;b\u0026gt;{_fmt_mcap(mcap)}\u0026lt;/b\u0026gt;\u0026#34;, f\u0026#34;FDV: \u0026lt;b\u0026gt;{_fmt_mcap(fdv)}\u0026lt;/b\u0026gt;\u0026#34;, ] return \u0026#34;\\n\u0026#34;.join(lines) def fmt_periodic(p: dict, idx: int, price: float, mcap: float, change_pct: float) -\u0026gt; str: arrow = \u0026#34;📈\u0026#34; if change_pct \u0026gt; 0 else \u0026#34;📉\u0026#34; minutes = 30 * idx lines = [ f\u0026#34;⏱ \u0026lt;b\u0026gt;${p[\u0026#39;symbol\u0026#39;]} · +{minutes}min\u0026lt;/b\u0026gt;\u0026#34;, f\u0026#34;流通市值: {_fmt_mcap(mcap)} ({arrow} {change_pct:+.1f}%)\u0026#34;, f\u0026#34;当前价: {_fmt_price(price)}\u0026#34;, ] if change_pct \u0026gt;= 100: lines.append(\u0026#34;💡 \u0026lt;b\u0026gt;已翻倍，考虑分批止盈\u0026lt;/b\u0026gt;\u0026#34;) elif change_pct \u0026lt;= -30: lines.append(\u0026#34;⚠️ 跌幅较大，评估是否止损\u0026#34;) return \u0026#34;\\n\u0026#34;.join(lines) def fmt_anomaly(p: dict, atype: str, price: float, change_pct: float) -\u0026gt; str: emoji = {\u0026#34;double\u0026#34;: \u0026#34;🚀\u0026#34;, \u0026#34;halve\u0026#34;: \u0026#34;🔻\u0026#34;}.get(atype, \u0026#34;⚡\u0026#34;) desc = {\u0026#34;double\u0026#34;: \u0026#34;市值翻倍\u0026#34;, \u0026#34;halve\u0026#34;: \u0026#34;市值腰斩\u0026#34;}.get(atype, \u0026#34;异动\u0026#34;) return f\u0026#34;{emoji} \u0026lt;b\u0026gt;${p[\u0026#39;symbol\u0026#39;]} {desc}\u0026lt;/b\u0026gt;\\n变化: {change_pct:+.1f}%\\n当前价: {_fmt_price(price)}\u0026#34; # ============================================================ # 核心逻辑: 公告监听 # ============================================================ async def announcement_listener(): \u0026#34;\u0026#34;\u0026#34;轮询币安公告，发现新Alpha项目\u0026#34;\u0026#34;\u0026#34; logger.info(f\u0026#34;📡 公告监听启动 · 轮询 {ANNOUNCEMENT_POLL_INTERVAL}s\u0026#34;) while True: try: articles = await fetch_announcements() new_count = 0 for art in articles: title = art.get(\u0026#34;title\u0026#34;, \u0026#34;\u0026#34;) triggered, reason = is_trigger(title) if not triggered: continue symbol = extract_symbol(title) if not symbol: continue # 用发布日期去重 release_ts = art.get(\u0026#34;releaseDate\u0026#34;) release_iso = datetime.fromtimestamp(release_ts / 1000).isoformat() if release_ts else \u0026#34;\u0026#34; launch_date = release_iso[:10] if release_iso else datetime.utcnow().date().isoformat() pid = project_id(symbol, launch_date) if project_exists(pid): continue project = { \u0026#34;id\u0026#34;: pid, \u0026#34;symbol\u0026#34;: symbol, \u0026#34;name\u0026#34;: extract_name(title), \u0026#34;launch_time\u0026#34;: release_iso, \u0026#34;source\u0026#34;: \u0026#34;binance_announcement\u0026#34;, \u0026#34;raw_text\u0026#34;: title, \u0026#34;tier\u0026#34;: \u0026#34;PENDING\u0026#34;, \u0026#34;vcs\u0026#34;: [], \u0026#34;is_darling\u0026#34;: False, \u0026#34;excluded\u0026#34;: 0, } save_project(project) new_count += 1 logger.info(f\u0026#34;🆕 发现 ${symbol}: {title[:80]}\u0026#34;) if new_count: logger.info(f\u0026#34;本轮发现 {new_count} 个新项目\u0026#34;) except Exception as e: logger.error(f\u0026#34;公告监听异常: {e}\u0026#34;, exc_info=True) await asyncio.sleep(ANNOUNCEMENT_POLL_INTERVAL) # ============================================================ # 核心逻辑: 聚合 + 推送 # ============================================================ async def aggregation_worker(): \u0026#34;\u0026#34;\u0026#34;对PENDING项目做数据聚合、评级、推送\u0026#34;\u0026#34;\u0026#34; logger.info(f\u0026#34;🧠 聚合工作者启动 · 轮询 {AGGREGATION_POLL_INTERVAL}s\u0026#34;) while True: try: pending = list_pending() for p in pending: symbol = p[\u0026#34;symbol\u0026#34;] try: logger.info(f\u0026#34;📦 聚合 ${symbol}\u0026#34;) # 1. CoinGecko cg = await fetch_coingecko(symbol, project_name=p.get(\u0026#39;name\u0026#39;, \u0026#39;\u0026#39;)) await asyncio.sleep(1) # 避免限流 # 2. LLM抽取叙事（传入CoinGecko数据增强分析） llm = await llm_extract(p.get(\u0026#34;raw_text\u0026#34;, \u0026#34;\u0026#34;), symbol, p.get(\u0026#34;name\u0026#34;), cg_data=cg) await asyncio.sleep(1) # 3. 判断是否排除 if llm.get(\u0026#34;exclude_reason\u0026#34;) in (\u0026#34;already_tge\u0026#34;, \u0026#34;meme_only\u0026#34;): update_project(p[\u0026#34;id\u0026#34;], { \u0026#34;excluded\u0026#34;: 1, \u0026#34;exclude_reason\u0026#34;: llm[\u0026#34;exclude_reason\u0026#34;], \u0026#34;tier\u0026#34;: \u0026#34;EXCLUDED\u0026#34;, }) logger.info(f\u0026#34;⏭ ${symbol} 排除: {llm[\u0026#39;exclude_reason\u0026#39;]}\u0026#34;) continue # 4. 数据校验 — CoinGecko数据可能匹配错或项目太新没数据 cg_fdv = cg.get(\u0026#34;fdv\u0026#34;, 0) or 0 cg_mcap = cg.get(\u0026#34;mcap\u0026#34;, 0) or 0 data_suspect = False data_warnings = [] # 上币安的项目FDV不可能低于$100K（除非CoinGecko匹配错了） if cg.get(\u0026#34;found\u0026#34;) and cg_fdv \u0026gt; 0 and cg_fdv \u0026lt; 100_000: data_suspect = True data_warnings.append(f\u0026#34;FDV=${cg_fdv:,.0f}异常低，可能CoinGecko匹配错误\u0026#34;) logger.warning(f\u0026#34;⚠️ {symbol} FDV=${cg_fdv:,.0f} 异常低，数据可能不准\u0026#34;) # 流通100%的大项目也要警惕（新代币通常不会100%流通） if (cg.get(\u0026#34;circ_supply\u0026#34;) and cg.get(\u0026#34;total_supply\u0026#34;) and cg[\u0026#34;circ_supply\u0026#34;] == cg[\u0026#34;total_supply\u0026#34;] and cg_fdv \u0026lt; 1_000_000): data_suspect = True data_warnings.append(\u0026#34;流通=总量且FDV极低，可能是同名垃圾币\u0026#34;) if data_suspect: # 标记数据不可靠，评级时不使用CoinGecko的FDV/MCap cg_fdv = 0 cg_mcap = 0 logger.warning(f\u0026#34;⚠️ {symbol} CoinGecko数据不可靠，评级使用LLM判断为主\u0026#34;) # 5. 评级 is_darling = llm.get(\u0026#34;is_darling\u0026#34;, False) vcs = llm.get(\u0026#34;vcs\u0026#34;, []) narrative = llm.get(\u0026#34;narrative\u0026#34;, \u0026#34;unknown\u0026#34;) rating = rate_project( cg_mcap, cg_fdv, vcs, narrative, is_darling ) # 6. 更新DB（data_suspect时不存CoinGecko的错误数据） update_project(p[\u0026#34;id\u0026#34;], { \u0026#34;tier\u0026#34;: rating[\u0026#34;tier\u0026#34;], \u0026#34;tier_reason\u0026#34;: rating[\u0026#34;reason\u0026#34;], \u0026#34;narrative\u0026#34;: narrative, \u0026#34;narrative_desc\u0026#34;: llm.get(\u0026#34;narrative_desc\u0026#34;, \u0026#34;\u0026#34;), \u0026#34;vcs_json\u0026#34;: json.dumps(vcs), \u0026#34;is_darling\u0026#34;: int(is_darling), \u0026#34;open_price\u0026#34;: cg.get(\u0026#34;price\u0026#34;) if not data_suspect else None, \u0026#34;total_supply\u0026#34;: cg.get(\u0026#34;total_supply\u0026#34;) if not data_suspect else None, \u0026#34;circulating_supply\u0026#34;: cg.get(\u0026#34;circ_supply\u0026#34;) if not data_suspect else None, \u0026#34;fdv\u0026#34;: cg_fdv if cg_fdv else None, \u0026#34;circulating_mcap\u0026#34;: cg_mcap if cg_mcap else None, }) # 6. 推送discovery full = get_project(p[\u0026#34;id\u0026#34;]) if full and not has_pushed(p[\u0026#34;id\u0026#34;], \u0026#34;discovery\u0026#34;): text = fmt_discovery(full) silent = rating[\u0026#34;tier\u0026#34;] in (\u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;) ok = await send_tg(text, silent=silent) if ok: log_push(p[\u0026#34;id\u0026#34;], \u0026#34;discovery\u0026#34;, text) logger.info(f\u0026#34;✅ 推送 ${symbol} [{rating[\u0026#39;tier\u0026#39;]}]\u0026#34;) except Exception as e: logger.error(f\u0026#34;聚合 {symbol} 失败: {e}\u0026#34;, exc_info=True) update_project(p[\u0026#34;id\u0026#34;], {\u0026#34;tier\u0026#34;: \u0026#34;ERROR\u0026#34;, \u0026#34;tier_reason\u0026#34;: str(e)[:100]}) except Exception as e: logger.error(f\u0026#34;聚合循环异常: {e}\u0026#34;, exc_info=True) await asyncio.sleep(AGGREGATION_POLL_INTERVAL) # ============================================================ # 核心逻辑: 上线后监控 # ============================================================ async def post_launch_monitor(): \u0026#34;\u0026#34;\u0026#34;倒计时提醒 + 上线瞬间 + 30min×4跟踪 + 异动\u0026#34;\u0026#34;\u0026#34; logger.info(f\u0026#34;📊 上线监控启动 · 轮询 {MONITOR_POLL_INTERVAL}s\u0026#34;) while True: try: projects = list_active() for p in projects: try: await _monitor_project(p) except Exception as e: logger.error(f\u0026#34;监控 {p[\u0026#39;symbol\u0026#39;]} 异常: {e}\u0026#34;) except Exception as e: logger.error(f\u0026#34;监控循环异常: {e}\u0026#34;, exc_info=True) await asyncio.sleep(MONITOR_POLL_INTERVAL) async def _monitor_project(p: dict): pid = p[\u0026#34;id\u0026#34;] symbol = p[\u0026#34;symbol\u0026#34;] launch_str = p.get(\u0026#34;launch_time\u0026#34;, \u0026#34;\u0026#34;) if not launch_str: return try: launch = datetime.fromisoformat(launch_str.replace(\u0026#34;Z\u0026#34;, \u0026#34;\u0026#34;).split(\u0026#34;+\u0026#34;)[0]) except: return now = datetime.utcnow() delta_sec = (launch - now).total_seconds() # T-3h if 3*3600 - 300 \u0026lt;= delta_sec \u0026lt;= 3*3600 + 300: if not has_pushed(pid, \u0026#34;t_minus_3h\u0026#34;): text = fmt_countdown(p, int(delta_sec / 60)) ok = await send_tg(text, silent=p.get(\u0026#34;tier\u0026#34;) in (\u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;)) if ok: log_push(pid, \u0026#34;t_minus_3h\u0026#34;, text) # T-30m elif 30*60 - 150 \u0026lt;= delta_sec \u0026lt;= 30*60 + 150: if not has_pushed(pid, \u0026#34;t_minus_30m\u0026#34;): text = fmt_countdown(p, int(delta_sec / 60)) ok = await send_tg(text, silent=False) if ok: log_push(pid, \u0026#34;t_minus_30m\u0026#34;, text) # 上线瞬间 elif -300 \u0026lt;= delta_sec \u0026lt;= 0: if not has_pushed(pid, \u0026#34;at_launch\u0026#34;): cg = await fetch_coingecko(symbol, project_name=p.get(\u0026#39;name\u0026#39;, \u0026#39;\u0026#39;)) if cg.get(\u0026#34;price\u0026#34;): text = fmt_launch(p, cg[\u0026#34;price\u0026#34;], cg.get(\u0026#34;mcap\u0026#34;, 0), cg.get(\u0026#34;fdv\u0026#34;, 0)) ok = await send_tg(text, silent=False) if ok: log_push(pid, \u0026#34;at_launch\u0026#34;, text) save_snapshot(pid, cg[\u0026#34;price\u0026#34;], cg.get(\u0026#34;mcap\u0026#34;, 0), cg.get(\u0026#34;fdv\u0026#34;, 0)) # 上线后30min × 4 elif 0 \u0026lt; -delta_sec \u0026lt;= 2.5 * 3600: minutes_after = int(-delta_sec / 60) for idx, target in enumerate([30, 60, 90, 120], 1): if abs(minutes_after - target) \u0026lt;= 5: ptype = f\u0026#34;post_30m_{idx}\u0026#34; if not has_pushed(pid, ptype): cg = await fetch_coingecko(symbol, project_name=p.get(\u0026#39;name\u0026#39;, \u0026#39;\u0026#39;)) if cg.get(\u0026#34;price\u0026#34;): open_price = p.get(\u0026#34;open_price\u0026#34;) or cg[\u0026#34;price\u0026#34;] change = ((cg[\u0026#34;price\u0026#34;] - open_price) / open_price * 100) if open_price else 0 text = fmt_periodic(p, idx, cg[\u0026#34;price\u0026#34;], cg.get(\u0026#34;mcap\u0026#34;, 0), change) ok = await send_tg(text, silent=p.get(\u0026#34;tier\u0026#34;) in (\u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;) and idx \u0026gt; 1) if ok: log_push(pid, ptype, text) save_snapshot(pid, cg[\u0026#34;price\u0026#34;], cg.get(\u0026#34;mcap\u0026#34;, 0), cg.get(\u0026#34;fdv\u0026#34;, 0)) # 异动 if change \u0026gt;= 100 and not has_pushed(pid, \u0026#34;anomaly_double\u0026#34;): t = fmt_anomaly(p, \u0026#34;double\u0026#34;, cg[\u0026#34;price\u0026#34;], change) if await send_tg(t): log_push(pid, \u0026#34;anomaly_double\u0026#34;, t) elif change \u0026lt;= -50 and not has_pushed(pid, \u0026#34;anomaly_halve\u0026#34;): t = fmt_anomaly(p, \u0026#34;halve\u0026#34;, cg[\u0026#34;price\u0026#34;], change) if await send_tg(t): log_push(pid, \u0026#34;anomaly_halve\u0026#34;, t) break # ============================================================ # 启动 # ============================================================ async def main(): init_db() logger.info(f\u0026#34;📂 数据库: {DB_PATH}\u0026#34;) # 测试TG ok = await send_tg(\u0026#34;🎉 \u0026lt;b\u0026gt;Alpha Monitor v2 启动\u0026lt;/b\u0026gt;\\n\\n📡 币安公告监听中...\\n🔔 有新Alpha会立即推送\u0026#34;) if ok: logger.info(\u0026#34;✅ TG推送正常\u0026#34;) else: logger.warning(\u0026#34;⚠️ TG推送失败，检查配置\u0026#34;) tasks = [ asyncio.create_task(announcement_listener(), name=\u0026#34;announcements\u0026#34;), asyncio.create_task(aggregation_worker(), name=\u0026#34;aggregator\u0026#34;), asyncio.create_task(post_launch_monitor(), name=\u0026#34;monitor\u0026#34;), ] logger.info(\u0026#34;=\u0026#34; * 50) logger.info(\u0026#34;🚀 Alpha Monitor v2 启动完成\u0026#34;) logger.info(f\u0026#34; 📡 公告轮询: {ANNOUNCEMENT_POLL_INTERVAL}s\u0026#34;) logger.info(f\u0026#34; 🧠 LLM: {\u0026#39;Sonnet\u0026#39; if ANTHROPIC_API_KEY else \u0026#39;降级(规则)\u0026#39;}\u0026#34;) logger.info(f\u0026#34; 🔔 TG: {\u0026#39;✅\u0026#39; if TG_BOT_TOKEN else \u0026#39;❌\u0026#39;}\u0026#34;) logger.info(\u0026#34;=\u0026#34; * 50) try: await asyncio.gather(*tasks) except KeyboardInterrupt: for t in tasks: t.cancel() if __name__ == \u0026#34;__main__\u0026#34;: try: asyncio.run(main()) except KeyboardInterrupt: pass AI Autonomous Trading Futures + Alpha Autonomous Trading v1 Date: 2026.04.29　Tags: Python · Binance Futures · Autonomous · Telegram\nAI scans market → analyzes → virtual trades → monitors → reviews — fully autonomous\n⚠️ RISK WARNING: This code has only been tested on virtual/paper trading. It has NO real trading experience and should NOT be used as a basis for live trading. Not validated with real funds — use at your own risk.\nAn AI that autonomously scans the entire Binance futures market every 30 seconds, detects anomalies, and makes virtual trades. 4 signal detection strategies (extreme negative funding rate → long squeeze, extreme positive funding → short, post-pump short, crash bounce). Before opening any position, runs a multi-dimensional environment check (BTC trend + Fear\u0026amp;Greed sentiment + OI attention + volume activity) — needs score ≥3/7 to proceed. Auto stop-loss/take-profit monitoring every 30 seconds.\nCurrent results (honest): 4 closed trades, 75% win rate, +13.94U. But profit concentrated in one trade (IR short +45%), rest basically break-even. Position diversification insufficient — tends to stack same-direction same-logic trades. Still rule-based scoring, not true independent thinking.\nTraining path: scan → trade → close → review → find problems → improve → repeat. Goal: evolve from rule executor to independent-thinking trader.b:T5192,#!/usr/bin/env pytho\nFull source code #!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; 市场扫描器 - 每分钟运行 纯Python零AI成本，发现异常信号自动开仓 \u0026#34;\u0026#34;\u0026#34; import json import os import sys import time import requests from datetime import datetime, timezone, timedelta SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_FILE = os.path.join(SCRIPT_DIR, \u0026#34;trades.json\u0026#34;) SCANNER_STATE = os.path.join(SCRIPT_DIR, \u0026#34;scanner_state.json\u0026#34;) SCANNER_LOG = os.path.join(SCRIPT_DIR, \u0026#34;scanner.log\u0026#34;) INITIAL_BALANCE = 100.0 TZ_UTC8 = timezone(timedelta(hours=8)) # === 配置 === MAX_OPEN_POSITIONS = 3 # 最多同时持仓 POSITION_PCT = 30 # 每笔仓位占比% LEVERAGE = 3 # 杠杆 COOLDOWN_HOURS = 4 # 同一币种冷却时间 MIN_VOLUME_M = 10 # 最小24h成交额(百万U) # === TG推送 === def load_tg_config(): \u0026#34;\u0026#34;\u0026#34;Load TG config from environment variables or .env file\u0026#34;\u0026#34;\u0026#34; env = {} # Try .env in script directory, then current directory for env_path in [ os.path.join(SCRIPT_DIR, \u0026#34;.env\u0026#34;), os.path.join(os.getcwd(), \u0026#34;.env\u0026#34;), ]: if os.path.exists(env_path): with open(env_path) as f: for line in f: line = line.strip() if \u0026#39;=\u0026#39; in line and not line.startswith(\u0026#39;#\u0026#39;): k, v = line.split(\u0026#39;=\u0026#39;, 1) env[k] = v.strip().strip(\u0026#39;\u0026#34;\u0026#39;).strip(\u0026#34;\u0026#39;\u0026#34;) break # OS environment variables override file for key in [\u0026#39;TG_BOT_TOKEN\u0026#39;, \u0026#39;TELEGRAM_BOT_TOKEN\u0026#39;, \u0026#39;TG_CHAT_ID\u0026#39;]: val = os.environ.get(key) if val: env[key] = val return env def send_tg(text): try: env = load_tg_config() token = env.get(\u0026#39;TG_BOT_TOKEN\u0026#39;, env.get(\u0026#39;TELEGRAM_BOT_TOKEN\u0026#39;, \u0026#39;\u0026#39;)) if not token: return chat_id = env.get(\u0026#39;TG_CHAT_ID\u0026#39;, \u0026#39;\u0026#39;) if not chat_id: return url = f\u0026#34;https://api.telegram.org/bot{token}/sendMessage\u0026#34; requests.post(url, json={ \u0026#34;chat_id\u0026#34;: chat_id, \u0026#34;text\u0026#34;: text, \u0026#34;parse_mode\u0026#34;: \u0026#34;Markdown\u0026#34; }, timeout=10) except: pass # === 数据加载 === def load_trades(): if os.path.exists(DATA_FILE): with open(DATA_FILE, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: return json.load(f) return {\u0026#34;initial_balance\u0026#34;: INITIAL_BALANCE, \u0026#34;trades\u0026#34;: []} def save_trades(data): with open(DATA_FILE, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: json.dump(data, f, ensure_ascii=False, indent=2) def load_state(): if os.path.exists(SCANNER_STATE): with open(SCANNER_STATE, \u0026#34;r\u0026#34;) as f: return json.load(f) return {\u0026#34;last_opens\u0026#34;: {}, \u0026#34;signals_seen\u0026#34;: {}} def save_state(state): with open(SCANNER_STATE, \u0026#34;w\u0026#34;) as f: json.dump(state, f, ensure_ascii=False, indent=2) def get_balance(data): balance = data.get(\u0026#34;initial_balance\u0026#34;, INITIAL_BALANCE) for t in data[\u0026#34;trades\u0026#34;]: if t[\u0026#34;status\u0026#34;] == \u0026#34;closed\u0026#34; and t[\u0026#34;pnl_usd\u0026#34;] is not None: balance += t[\u0026#34;pnl_usd\u0026#34;] return balance def next_id(data): if not data[\u0026#34;trades\u0026#34;]: return \u0026#34;001\u0026#34; max_id = max(int(t[\u0026#34;id\u0026#34;]) for t in data[\u0026#34;trades\u0026#34;]) return f\u0026#34;{max_id + 1:03d}\u0026#34; def now_str(): return datetime.now(TZ_UTC8).strftime(\u0026#34;%Y-%m-%dT%H:%M:%S\u0026#34;) def log(msg): ts = datetime.now(TZ_UTC8).strftime(\u0026#34;%m-%d %H:%M:%S\u0026#34;) line = f\u0026#34;[{ts}] {msg}\u0026#34; print(line) with open(SCANNER_LOG, \u0026#34;a\u0026#34;) as f: f.write(line + \u0026#34;\\n\u0026#34;) # === 币安API === def get_all_tickers(): url = \u0026#34;https://fapi.binance.com/fapi/v1/ticker/24hr\u0026#34; resp = requests.get(url, timeout=10) return resp.json() def get_funding_rates(): \u0026#34;\u0026#34;\u0026#34;获取所有币种最新费率\u0026#34;\u0026#34;\u0026#34; url = \u0026#34;https://fapi.binance.com/fapi/v1/premiumIndex\u0026#34; resp = requests.get(url, timeout=10) return {item[\u0026#39;symbol\u0026#39;]: float(item[\u0026#39;lastFundingRate\u0026#39;]) * 100 for item in resp.json()} def get_funding_history(symbol, limit=8): url = f\u0026#34;https://fapi.binance.com/fapi/v1/fundingRate?symbol={symbol}\u0026amp;limit={limit}\u0026#34; resp = requests.get(url, timeout=10) return [float(item[\u0026#39;fundingRate\u0026#39;]) * 100 for item in resp.json()] def get_open_interest(symbol): url = f\u0026#34;https://fapi.binance.com/fapi/v1/openInterest?symbol={symbol}\u0026#34; resp = requests.get(url, timeout=10) data = resp.json() return float(data[\u0026#39;openInterest\u0026#39;]) def get_klines(symbol, interval=\u0026#34;4h\u0026#34;, limit=6): url = f\u0026#34;https://fapi.binance.com/fapi/v1/klines?symbol={symbol}\u0026amp;interval={interval}\u0026amp;limit={limit}\u0026#34; resp = requests.get(url, timeout=10) return resp.json() # === 信号检测 === def detect_extreme_negative_funding(symbol, funding_rate, funding_rates_map): \u0026#34;\u0026#34;\u0026#34; 策略1: 费率极端深负 → 做多(逼空) 条件: 当前费率\u0026lt;-0.08% 且 连续多期为负 \u0026#34;\u0026#34;\u0026#34; if funding_rate \u0026gt;= -0.08: return None try: history = get_funding_history(symbol, 8) neg_count = sum(1 for r in history if r \u0026lt; -0.03) if neg_count \u0026lt; 4: return None avg_rate = sum(history) / len(history) # 费率越极端，信号越强 strength = \u0026#34;S\u0026#34; if avg_rate \u0026lt; -0.15 else \u0026#34;A\u0026#34; if avg_rate \u0026lt; -0.10 else \u0026#34;B\u0026#34; return { \u0026#34;type\u0026#34;: \u0026#34;extreme_neg_funding\u0026#34;, \u0026#34;direction\u0026#34;: \u0026#34;long\u0026#34;, \u0026#34;strength\u0026#34;: strength, \u0026#34;reason\u0026#34;: f\u0026#34;费率极端深负 avg:{avg_rate:.4f}% 连续{neg_count}/8期为负 逼空概率高\u0026#34;, \u0026#34;sl_pct\u0026#34;: 0.08, # 止损8% \u0026#34;tp_pct\u0026#34;: 0.12, # 止盈12% } except: return None def detect_extreme_positive_funding(symbol, funding_rate, funding_rates_map): \u0026#34;\u0026#34;\u0026#34; 策略2: 费率极端正 → 做空(多头拥挤) 条件: 当前费率\u0026gt;0.10% 且 连续多期高正 \u0026#34;\u0026#34;\u0026#34; if funding_rate \u0026lt;= 0.10: return None try: history = get_funding_history(symbol, 8) pos_count = sum(1 for r in history if r \u0026gt; 0.05) if pos_count \u0026lt; 4: return None avg_rate = sum(history) / len(history) strength = \u0026#34;S\u0026#34; if avg_rate \u0026gt; 0.20 else \u0026#34;A\u0026#34; if avg_rate \u0026gt; 0.12 else \u0026#34;B\u0026#34; return { \u0026#34;type\u0026#34;: \u0026#34;extreme_pos_funding\u0026#34;, \u0026#34;direction\u0026#34;: \u0026#34;short\u0026#34;, \u0026#34;strength\u0026#34;: strength, \u0026#34;reason\u0026#34;: f\u0026#34;费率极端正 avg:{avg_rate:.4f}% 连续{pos_count}/8期高正 多头过度拥挤\u0026#34;, \u0026#34;sl_pct\u0026#34;: 0.10, \u0026#34;tp_pct\u0026#34;: 0.15, } except: return None def detect_crash_bounce(ticker): \u0026#34;\u0026#34;\u0026#34; 策略3: 暴跌后反弹(超跌反弹) 条件: 24h跌\u0026gt;25% 但最近4h企稳/反弹 \u0026#34;\u0026#34;\u0026#34; change_pct = float(ticker[\u0026#39;priceChangePercent\u0026#39;]) if change_pct \u0026gt;= -25: return None symbol = ticker[\u0026#39;symbol\u0026#39;] try: klines = get_klines(symbol, \u0026#34;1h\u0026#34;, 6) # 最近2根K线 recent_closes = [float(k[4]) for k in klines[-3:]] # 企稳: 最近K线收盘 \u0026gt;= 前一根 if len(recent_closes) \u0026gt;= 2 and recent_closes[-1] \u0026gt;= recent_closes[-2]: return { \u0026#34;type\u0026#34;: \u0026#34;crash_bounce\u0026#34;, \u0026#34;direction\u0026#34;: \u0026#34;long\u0026#34;, \u0026#34;strength\u0026#34;: \u0026#34;B\u0026#34;, # 风险较高给B级 \u0026#34;reason\u0026#34;: f\u0026#34;24h暴跌{change_pct:.1f}%后企稳 超跌反弹\u0026#34;, \u0026#34;sl_pct\u0026#34;: 0.10, \u0026#34;tp_pct\u0026#34;: 0.15, } except: pass return None def detect_pump_short(ticker): \u0026#34;\u0026#34;\u0026#34; 策略4: 暴涨后做空(ATH回落) 条件: 24h涨\u0026gt;40% — 根据生命周期模型，暴涨后回调概率\u0026gt;85% 需要确认已经开始回落(不在最高点做空) \u0026#34;\u0026#34;\u0026#34; change_pct = float(ticker[\u0026#39;priceChangePercent\u0026#39;]) if change_pct \u0026lt;= 40: return None symbol = ticker[\u0026#39;symbol\u0026#39;] try: klines = get_klines(symbol, \u0026#34;1h\u0026#34;, 6) highs = [float(k[2]) for k in klines] closes = [float(k[4]) for k in klines] current = closes[-1] peak = max(highs) # 从最高点回落超过10%才做空 pullback = (peak - current) / peak * 100 if pullback \u0026lt; 10: return None strength = \u0026#34;A\u0026#34; if change_pct \u0026gt; 80 else \u0026#34;B\u0026#34; return { \u0026#34;type\u0026#34;: \u0026#34;pump_short\u0026#34;, \u0026#34;direction\u0026#34;: \u0026#34;short\u0026#34;, \u0026#34;strength\u0026#34;: strength, \u0026#34;reason\u0026#34;: f\u0026#34;24h暴涨{change_pct:.1f}%后回落{pullback:.1f}% 历史回调概率\u0026gt;85%\u0026#34;, \u0026#34;sl_pct\u0026#34;: 0.15, # 暴涨币波动大，止损宽一些 \u0026#34;tp_pct\u0026#34;: 0.20, } except: pass return None # === 综合环境检查 === def check_environment(symbol, signal): \u0026#34;\u0026#34;\u0026#34; 开仓前综合检查：不是单一信号触发就开，要多维度对齐 返回 (pass/fail, analysis_dict, adjusted_strength) \u0026#34;\u0026#34;\u0026#34; analysis = { \u0026#34;btc_env\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sentiment\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;oi_check\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;volume_check\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;verdict\u0026#34;: \u0026#34;\u0026#34; } score = 0 # 综合得分，\u0026gt;=3才开仓 try: # 1. BTC环境 — 做多需要BTC不在暴跌，做空需要BTC不在暴涨 btc_url = \u0026#34;https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=BTCUSDT\u0026#34; btc = requests.get(btc_url, timeout=5).json() btc_chg = float(btc[\u0026#39;priceChangePercent\u0026#39;]) if signal[\u0026#34;direction\u0026#34;] == \u0026#34;long\u0026#34;: if btc_chg \u0026gt; -2: score += 1 analysis[\u0026#34;btc_env\u0026#34;] = f\u0026#34;BTC {btc_chg:+.1f}% 环境正常 +1\u0026#34; elif btc_chg \u0026lt; -5: score -= 1 analysis[\u0026#34;btc_env\u0026#34;] = f\u0026#34;BTC {btc_chg:+.1f}% 暴跌中做多危险 -1\u0026#34; else: analysis[\u0026#34;btc_env\u0026#34;] = f\u0026#34;BTC {btc_chg:+.1f}% 偏弱 0\u0026#34; else: # short if btc_chg \u0026lt; 2: score += 1 analysis[\u0026#34;btc_env\u0026#34;] = f\u0026#34;BTC {btc_chg:+.1f}% 环境正常 +1\u0026#34; elif btc_chg \u0026gt; 5: score -= 1 analysis[\u0026#34;btc_env\u0026#34;] = f\u0026#34;BTC {btc_chg:+.1f}% 暴涨中做空危险 -1\u0026#34; else: analysis[\u0026#34;btc_env\u0026#34;] = f\u0026#34;BTC {btc_chg:+.1f}% 偏强 0\u0026#34; # 2. 市场情绪(Fear \u0026amp; Greed) try: fng = requests.get(\u0026#34;https://api.alternative.me/fng/\u0026#34;, timeout=5).json() fng_val = int(fng[\u0026#39;data\u0026#39;][0][\u0026#39;value\u0026#39;]) if signal[\u0026#34;direction\u0026#34;] == \u0026#34;long\u0026#34;: if fng_val \u0026lt;= 25: score += 1 analysis[\u0026#34;sentiment\u0026#34;] = f\u0026#34;FGI={fng_val}极度恐惧 逆向做多 +1\u0026#34; elif fng_val \u0026gt;= 75: score -= 1 analysis[\u0026#34;sentiment\u0026#34;] = f\u0026#34;FGI={fng_val}极度贪婪 做多风险 -1\u0026#34; else: analysis[\u0026#34;sentiment\u0026#34;] = f\u0026#34;FGI={fng_val}中性 0\u0026#34; else: if fng_val \u0026gt;= 75: score += 1 analysis[\u0026#34;sentiment\u0026#34;] = f\u0026#34;FGI={fng_val}极度贪婪 逆向做空 +1\u0026#34; elif fng_val \u0026lt;= 25: score -= 1 analysis[\u0026#34;sentiment\u0026#34;] = f\u0026#34;FGI={fng_val}极度恐惧 做空风险 -1\u0026#34; else: analysis[\u0026#34;sentiment\u0026#34;] = f\u0026#34;FGI={fng_val}中性 0\u0026#34; except: analysis[\u0026#34;sentiment\u0026#34;] = \u0026#34;FGI获取失败 0\u0026#34; # 3. OI变化 — 看该币OI是否支持方向 try: oi = get_open_interest(symbol) ticker = requests.get(f\u0026#34;https://fapi.binance.com/fapi/v1/ticker/24hr?symbol={symbol}\u0026#34;, timeout=5).json() price = float(ticker[\u0026#39;lastPrice\u0026#39;]) oi_usd = oi * price if oi_usd \u0026gt; 5_000_000: # OI \u0026gt; 5M说明有关注度 score += 1 analysis[\u0026#34;oi_check\u0026#34;] = f\u0026#34;OI={oi_usd/1e6:.1f}M 有关注度 +1\u0026#34; else: analysis[\u0026#34;oi_check\u0026#34;] = f\u0026#34;OI={oi_usd/1e6:.1f}M 关注度低 0\u0026#34; except: analysis[\u0026#34;oi_check\u0026#34;] = \u0026#34;OI获取失败 0\u0026#34; # 4. 成交量 — 量能是否活跃 try: vol = float(ticker.get(\u0026#39;quoteVolume\u0026#39;, 0)) if vol \u0026gt; 50_000_000: score += 1 analysis[\u0026#34;volume_check\u0026#34;] = f\u0026#34;24h量={vol/1e6:.0f}M 活跃 +1\u0026#34; elif vol \u0026gt; 20_000_000: analysis[\u0026#34;volume_check\u0026#34;] = f\u0026#34;24h量={vol/1e6:.0f}M 一般 0\u0026#34; else: score -= 1 analysis[\u0026#34;volume_check\u0026#34;] = f\u0026#34;24h量={vol/1e6:.0f}M 冷清 -1\u0026#34; except: analysis[\u0026#34;volume_check\u0026#34;] = \u0026#34;量能获取失败 0\u0026#34; # 5. 信号本身的强度加分 if signal[\u0026#34;strength\u0026#34;] == \u0026#34;S\u0026#34;: score += 2 elif signal[\u0026#34;strength\u0026#34;] == \u0026#34;A\u0026#34;: score += 1 # 综合判定: \u0026gt;=3通过 analysis[\u0026#34;verdict\u0026#34;] = f\u0026#34;综合得分:{score}/7\u0026#34; if score \u0026gt;= 3: return True, analysis, signal[\u0026#34;strength\u0026#34;] else: return False, analysis, signal[\u0026#34;strength\u0026#34;] except Exception as e: analysis[\u0026#34;verdict\u0026#34;] = f\u0026#34;检查异常:{e} 保守不开\u0026#34; return False, analysis, signal[\u0026#34;strength\u0026#34;] # === 开仓执行 === def execute_open(data, state, symbol, price, signal): \u0026#34;\u0026#34;\u0026#34;执行虚拟开仓 — 先过综合环境检查\u0026#34;\u0026#34;\u0026#34; # 综合环境检查 passed, env_analysis, strength = check_environment(symbol, signal) env_summary = \u0026#34; | \u0026#34;.join(v for v in env_analysis.values() if v) if not passed: log(f\u0026#34;综合检查未通过 {symbol}: {env_summary}\u0026#34;) return log(f\u0026#34;综合检查通过 {symbol}: {env_summary}\u0026#34;) balance = get_balance(data) position_usd = balance * POSITION_PCT / 100 if signal[\u0026#34;direction\u0026#34;] == \u0026#34;long\u0026#34;: sl = round(price * (1 - signal[\u0026#34;sl_pct\u0026#34;]), 6) tp = round(price * (1 + signal[\u0026#34;tp_pct\u0026#34;]), 6) else: sl = round(price * (1 + signal[\u0026#34;sl_pct\u0026#34;]), 6) tp = round(price * (1 - signal[\u0026#34;tp_pct\u0026#34;]), 6) trade = { \u0026#34;id\u0026#34;: next_id(data), \u0026#34;symbol\u0026#34;: symbol, \u0026#34;direction\u0026#34;: signal[\u0026#34;direction\u0026#34;], \u0026#34;leverage\u0026#34;: LEVERAGE, \u0026#34;position_pct\u0026#34;: POSITION_PCT, \u0026#34;position_usd\u0026#34;: round(position_usd, 4), \u0026#34;notional_usd\u0026#34;: round(position_usd * LEVERAGE, 4), \u0026#34;entry_price\u0026#34;: price, \u0026#34;stop_loss\u0026#34;: sl, \u0026#34;take_profit\u0026#34;: tp, \u0026#34;entry_time\u0026#34;: now_str(), \u0026#34;exit_price\u0026#34;: None, \u0026#34;exit_time\u0026#34;: None, \u0026#34;exit_reason\u0026#34;: None, \u0026#34;pnl_pct\u0026#34;: None, \u0026#34;pnl_usd\u0026#34;: None, \u0026#34;status\u0026#34;: \u0026#34;open\u0026#34;, \u0026#34;pre_analysis\u0026#34;: { \u0026#34;btc_env\u0026#34;: env_analysis.get(\u0026#34;btc_env\u0026#34;, \u0026#34;\u0026#34;), \u0026#34;sentiment\u0026#34;: env_analysis.get(\u0026#34;sentiment\u0026#34;, \u0026#34;\u0026#34;), \u0026#34;oi\u0026#34;: env_analysis.get(\u0026#34;oi_check\u0026#34;, \u0026#34;\u0026#34;), \u0026#34;volume\u0026#34;: env_analysis.get(\u0026#34;volume_check\u0026#34;, \u0026#34;\u0026#34;), \u0026#34;key_reason\u0026#34;: f\u0026#34;[{signal[\u0026#39;strength\u0026#39;]}级] {signal[\u0026#39;reason\u0026#39;]}\u0026#34;, \u0026#34;risk\u0026#34;: f\u0026#34;综合得分:{env_analysis.get(\u0026#39;verdict\u0026#39;,\u0026#39;\u0026#39;)} 策略:{signal[\u0026#39;type\u0026#39;]}\u0026#34; }, \u0026#34;post_review\u0026#34;: None } data[\u0026#34;trades\u0026#34;].append(trade) save_trades(data) # 记录冷却 state[\u0026#34;last_opens\u0026#34;][symbol] = now_str() save_state(state) direction_cn = \u0026#34;做多\u0026#34; if signal[\u0026#34;direction\u0026#34;] == \u0026#34;long\u0026#34; else \u0026#34;做空\u0026#34; msg = f\u0026#34;\u0026#34;\u0026#34;``` [扫描开仓] #{trade[\u0026#39;id\u0026#39;]} 币种: {symbol} 方向: {direction_cn} {LEVERAGE}x 入场: {price} 止损: {sl} 止盈: {tp} 仓位: {position_usd:.2f}U 信号: [{signal[\u0026#39;strength\u0026#39;]}] {signal[\u0026#39;reason\u0026#39;]} 时间: {trade[\u0026#39;entry_time\u0026#39;]} ```\u0026#34;\u0026#34;\u0026#34; log(f\u0026#34;开仓 #{trade[\u0026#39;id\u0026#39;]} {symbol} {direction_cn} @ {price} | {signal[\u0026#39;reason\u0026#39;]}\u0026#34;) send_tg(msg) print(msg) # === 换仓逻辑 === def swap_weakest(data, state, open_positions, new_signal, tickers): \u0026#34;\u0026#34;\u0026#34;满仓时遇到S级信号，平掉浮亏最大的持仓，开新仓\u0026#34;\u0026#34;\u0026#34; ticker_map = {t[\u0026#39;symbol\u0026#39;]: float(t[\u0026#39;lastPrice\u0026#39;]) for t in tickers} # 计算每个持仓的浮盈% worst_trade = None worst_pnl = float(\u0026#39;inf\u0026#39;) for t in open_positions: price = ticker_map.get(t[\u0026#34;symbol\u0026#34;]) if price is None: continue if t[\u0026#34;direction\u0026#34;] == \u0026#34;long\u0026#34;: pnl_pct = (price - t[\u0026#34;entry_price\u0026#34;]) / t[\u0026#34;entry_price\u0026#34;] * 100 else: pnl_pct = (t[\u0026#34;entry_price\u0026#34;] - price) / t[\u0026#34;entry_price\u0026#34;] * 100 if pnl_pct \u0026lt; worst_pnl: worst_pnl = pnl_pct worst_trade = t worst_price = price if worst_trade is None: return # 只换掉亏损的仓位，盈利的不动 if worst_pnl \u0026gt; 0: log(f\u0026#34;满仓但所有持仓盈利，不换仓 | 新信号: {new_signal[\u0026#39;symbol\u0026#39;]}\u0026#34;) return # 平掉最弱的 if worst_trade[\u0026#34;direction\u0026#34;] == \u0026#34;long\u0026#34;: pnl_pct_lev = (worst_price - worst_trade[\u0026#34;entry_price\u0026#34;]) / worst_trade[\u0026#34;entry_price\u0026#34;] * 100 * worst_trade[\u0026#34;leverage\u0026#34;] else: pnl_pct_lev = (worst_trade[\u0026#34;entry_price\u0026#34;] - worst_price) / worst_trade[\u0026#34;entry_price\u0026#34;] * 100 * worst_trade[\u0026#34;leverage\u0026#34;] pos_usd = worst_trade.get(\u0026#34;position_usd\u0026#34;, worst_trade.get(\u0026#34;position_pct\u0026#34;, 30)) pnl_usd = round(pnl_pct_lev / 100 * pos_usd, 4) worst_trade[\u0026#34;exit_price\u0026#34;] = worst_price worst_trade[\u0026#34;exit_time\u0026#34;] = now_str() worst_trade[\u0026#34;exit_reason\u0026#34;] = f\u0026#34;换仓→{new_signal[\u0026#39;symbol\u0026#39;]}\u0026#34; worst_trade[\u0026#34;pnl_pct\u0026#34;] = round(pnl_pct_lev, 2) worst_trade[\u0026#34;pnl_usd\u0026#34;] = pnl_usd worst_trade[\u0026#34;status\u0026#34;] = \u0026#34;closed\u0026#34; save_trades(data) direction_cn = \u0026#34;多\u0026#34; if worst_trade[\u0026#34;direction\u0026#34;] == \u0026#34;long\u0026#34; else \u0026#34;空\u0026#34; msg = f\u0026#34;\u0026#34;\u0026#34;``` [换仓平仓] #{worst_trade[\u0026#39;id\u0026#39;]} 平掉: {worst_trade[\u0026#39;symbol\u0026#39;]} {direction_cn} 入场: {worst_trade[\u0026#39;entry_price\u0026#39;]} 出场: {worst_price} 盈亏: {pnl_pct_lev:+.2f}% ({pnl_usd:+.2f}U) 原因: S级信号{new_signal[\u0026#39;symbol\u0026#39;]}更强 ```\u0026#34;\u0026#34;\u0026#34; log(f\u0026#34;换仓平 #{worst_trade[\u0026#39;id\u0026#39;]} {worst_trade[\u0026#39;symbol\u0026#39;]} {pnl_usd:+.2f}U → 开 {new_signal[\u0026#39;symbol\u0026#39;]}\u0026#34;) send_tg(msg) # 开新仓 execute_open(data, state, new_signal[\u0026#34;symbol\u0026#34;], new_signal[\u0026#34;price\u0026#34;], new_signal) # === 主扫描逻辑 === def scan(): data = load_trades() state = load_state() now = datetime.now(TZ_UTC8) # 检查持仓数 open_positions = [t for t in data[\u0026#34;trades\u0026#34;] if t[\u0026#34;status\u0026#34;] == \u0026#34;open\u0026#34;] open_symbols = set(t[\u0026#34;symbol\u0026#34;] for t in open_positions) if len(open_positions) \u0026gt;= MAX_OPEN_POSITIONS: return # 获取市场数据 try: tickers = get_all_tickers() funding_rates = get_funding_rates() except Exception as e: log(f\u0026#34;API错误: {e}\u0026#34;) return # 过滤USDT合约 + 最小成交量 exclude = {\u0026#34;BTCUSDT\u0026#34;, \u0026#34;ETHUSDT\u0026#34;, \u0026#34;USDCUSDT\u0026#34;, \u0026#34;FDUSDUSDT\u0026#34;, \u0026#34;BTCDOMUSDT\u0026#34;, \u0026#34;BTCSTUSDT\u0026#34;} candidates = [t for t in tickers if t[\u0026#39;symbol\u0026#39;].endswith(\u0026#39;USDT\u0026#39;) and t[\u0026#39;symbol\u0026#39;] not in exclude and float(t[\u0026#39;quoteVolume\u0026#39;]) \u0026gt; MIN_VOLUME_M * 1e6] signals = [] for ticker in candidates: symbol = ticker[\u0026#39;symbol\u0026#39;] # 跳过已持仓的币 if symbol in open_symbols: continue # 冷却检查 last_open = state.get(\u0026#34;last_opens\u0026#34;, {}).get(symbol) if last_open: try: last_dt = datetime.fromisoformat(last_open) if last_dt.tzinfo is None: last_dt = last_dt.replace(tzinfo=TZ_UTC8) if (now - last_dt).total_seconds() \u0026lt; COOLDOWN_HOURS * 3600: continue except: pass fr = funding_rates.get(symbol, 0) # 运行所有策略检测 for detect_fn in [ lambda s, f, m: detect_extreme_negative_funding(s, f, m), lambda s, f, m: detect_extreme_positive_funding(s, f, m), lambda s, f, m: detect_crash_bounce(ticker), lambda s, f, m: detect_pump_short(ticker), ]: try: signal = detect_fn(symbol, fr, funding_rates) if signal: signal[\u0026#34;symbol\u0026#34;] = symbol signal[\u0026#34;price\u0026#34;] = float(ticker[\u0026#39;lastPrice\u0026#39;]) signal[\u0026#34;volume_m\u0026#34;] = float(ticker[\u0026#39;quoteVolume\u0026#39;]) / 1e6 signals.append(signal) except: continue if not signals: return # 按信号强度排序 S\u0026gt;A\u0026gt;B strength_order = {\u0026#34;S\u0026#34;: 0, \u0026#34;A\u0026#34;: 1, \u0026#34;B\u0026#34;: 2} signals.sort(key=lambda x: strength_order.get(x[\u0026#34;strength\u0026#34;], 3)) # 只取最强的信号开仓(一次最多开1笔) best = signals[0] # B级信号跳过，只开S和A级 if best[\u0026#34;strength\u0026#34;] == \u0026#34;B\u0026#34;: log(f\u0026#34;B级信号跳过: {best[\u0026#39;symbol\u0026#39;]} {best[\u0026#39;reason\u0026#39;]}\u0026#34;) return slots = MAX_OPEN_POSITIONS - len(open_positions) if slots \u0026gt; 0: execute_open(data, state, best[\u0026#34;symbol\u0026#34;], best[\u0026#34;price\u0026#34;], best) elif best[\u0026#34;strength\u0026#34;] == \u0026#34;S\u0026#34;: # 满仓但遇到S级信号 → 换掉最弱的持仓 swap_weakest(data, state, open_positions, best, tickers) if __name__ == \u0026#34;__main__\u0026#34;: scan() Utility Tools VoiceKey — Speaker Verification Date: 2026.04.27　Tags: Python · Security · Telegram · Speaker Verification\nProtect your AI agent with voiceprint authentication\nWhy this tool? More people use Telegram to control AI agents (servers, trading bots, smart home). If your TG account gets compromised, attackers can do anything with your AI. Passwords can be stolen or socially engineered — but your voice is unique and non-transferable. VoiceKey requires a voice message to verify identity before unlocking any commands. Zero AI cost, runs entirely on local CPU.\nFull source code #!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; VoiceKey — 声纹密钥 (Voiceprint Authentication) Speaker verification for Telegram bot security. Uses resemblyzer (GE2E model) for speaker embedding extraction and cosine similarity for verification. Zero AI cost — runs entirely on local CPU. Usage: # Register voiceprint from audio files python voicekey.py register --audio voice1.ogg voice2.ogg --owner \u0026#34;YourName\u0026#34; # Verify a voice against stored voiceprint python voicekey.py verify --audio test.ogg # As a Python module from voicekey import VoiceKey vk = VoiceKey() vk.register([\u0026#34;voice1.ogg\u0026#34;, \u0026#34;voice2.ogg\u0026#34;], owner=\u0026#34;YourName\u0026#34;) is_owner, score = vk.verify(\u0026#34;test.ogg\u0026#34;) \u0026#34;\u0026#34;\u0026#34; import os import json import tempfile import argparse import numpy as np from pathlib import Path from datetime import datetime # Lazy imports to speed up module load when not needed _encoder = None def _get_encoder(): \u0026#34;\u0026#34;\u0026#34;Lazy-load the voice encoder model (first call takes ~1s).\u0026#34;\u0026#34;\u0026#34; global _encoder if _encoder is None: from resemblyzer import VoiceEncoder _encoder = VoiceEncoder() return _encoder def _audio_to_wav(audio_path: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Convert any audio format to 16kHz mono WAV for processing.\u0026#34;\u0026#34;\u0026#34; from pydub import AudioSegment ext = Path(audio_path).suffix.lower() if ext in (\u0026#39;.ogg\u0026#39;, \u0026#39;.oga\u0026#39;): audio = AudioSegment.from_ogg(audio_path) elif ext == \u0026#39;.mp3\u0026#39;: audio = AudioSegment.from_mp3(audio_path) elif ext == \u0026#39;.wav\u0026#39;: return audio_path # already wav elif ext in (\u0026#39;.m4a\u0026#39;, \u0026#39;.aac\u0026#39;): audio = AudioSegment.from_file(audio_path, format=ext.lstrip(\u0026#39;.\u0026#39;)) else: audio = AudioSegment.from_file(audio_path) # Convert to 16kHz mono audio = audio.set_frame_rate(16000).set_channels(1) tmp = tempfile.NamedTemporaryFile(suffix=\u0026#39;.wav\u0026#39;, delete=False) audio.export(tmp.name, format=\u0026#39;wav\u0026#39;) return tmp.name def _extract_embedding(audio_path: str) -\u0026gt; np.ndarray: \u0026#34;\u0026#34;\u0026#34;Extract voice embedding from an audio file.\u0026#34;\u0026#34;\u0026#34; from resemblyzer import preprocess_wav encoder = _get_encoder() wav_path = _audio_to_wav(audio_path) wav = preprocess_wav(wav_path) # Cleanup temp file if wav_path != audio_path: os.unlink(wav_path) if len(wav) \u0026lt; 1600: # Less than 0.1s raise ValueError(f\u0026#34;Audio too short: {len(wav)/16000:.1f}s (need \u0026gt;0.1s)\u0026#34;) return encoder.embed_utterance(wav) class VoiceKey: \u0026#34;\u0026#34;\u0026#34; Speaker verification using voice embeddings. Attributes: data_dir: Directory to store voiceprint data threshold: Cosine similarity threshold for verification (default: 0.75) voiceprint: The registered owner\u0026#39;s voice embedding (256-dim vector) \u0026#34;\u0026#34;\u0026#34; def __init__(self, data_dir: str = None, threshold: float = 0.75): if data_dir is None: data_dir = os.path.expanduser(\u0026#34;~/.hermes/voiceprint\u0026#34;) self.data_dir = Path(data_dir) self.data_dir.mkdir(parents=True, exist_ok=True) self.threshold = threshold self.voiceprint = None self.metadata = {} self._load() def _load(self): \u0026#34;\u0026#34;\u0026#34;Load existing voiceprint if available.\u0026#34;\u0026#34;\u0026#34; vp_path = self.data_dir / \u0026#34;voiceprint.npy\u0026#34; meta_path = self.data_dir / \u0026#34;voiceprint_meta.json\u0026#34; if vp_path.exists(): self.voiceprint = np.load(str(vp_path)) if meta_path.exists(): with open(meta_path) as f: self.metadata = json.load(f) self.threshold = self.metadata.get(\u0026#34;threshold\u0026#34;, self.threshold) @property def is_registered(self) -\u0026gt; bool: \u0026#34;\u0026#34;\u0026#34;Check if a voiceprint is registered.\u0026#34;\u0026#34;\u0026#34; return self.voiceprint is not None def register(self, audio_paths: list, owner: str = \u0026#34;owner\u0026#34;) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34; Register a voiceprint from multiple audio samples. Args: audio_paths: List of audio file paths (ogg, mp3, wav, etc.) owner: Name of the voiceprint owner Returns: dict with registration results \u0026#34;\u0026#34;\u0026#34; embeddings = [] results = [] for path in audio_paths: try: embed = _extract_embedding(path) embeddings.append(embed) results.append({\u0026#34;file\u0026#34;: os.path.basename(path), \u0026#34;status\u0026#34;: \u0026#34;ok\u0026#34;}) except Exception as e: results.append({\u0026#34;file\u0026#34;: os.path.basename(path), \u0026#34;status\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;error\u0026#34;: str(e)}) if not embeddings: raise ValueError(\u0026#34;No valid audio samples provided\u0026#34;) # Average embeddings and normalize voiceprint = np.mean(embeddings, axis=0) voiceprint = voiceprint / np.linalg.norm(voiceprint) # Save np.save(str(self.data_dir / \u0026#34;voiceprint.npy\u0026#34;), voiceprint) self.metadata = { \u0026#34;owner\u0026#34;: owner, \u0026#34;samples_used\u0026#34;: len(embeddings), \u0026#34;embedding_dim\u0026#34;: int(voiceprint.shape[0]), \u0026#34;threshold\u0026#34;: self.threshold, \u0026#34;created\u0026#34;: datetime.now().isoformat(), } with open(self.data_dir / \u0026#34;voiceprint_meta.json\u0026#34;, \u0026#34;w\u0026#34;) as f: json.dump(self.metadata, f, indent=2) self.voiceprint = voiceprint # Self-test similarities = [] for embed in embeddings: sim = float(np.dot(voiceprint, embed / np.linalg.norm(embed))) similarities.append(sim) return { \u0026#34;owner\u0026#34;: owner, \u0026#34;samples\u0026#34;: len(embeddings), \u0026#34;self_test_scores\u0026#34;: similarities, \u0026#34;min_score\u0026#34;: min(similarities), \u0026#34;details\u0026#34;: results, } def verify(self, audio_path: str) -\u0026gt; tuple: \u0026#34;\u0026#34;\u0026#34; Verify if an audio sample matches the registered voiceprint. Args: audio_path: Path to audio file to verify Returns: tuple: (is_verified: bool, similarity_score: float) \u0026#34;\u0026#34;\u0026#34; if not self.is_registered: raise RuntimeError(\u0026#34;No voiceprint registered. Call register() first.\u0026#34;) embed = _extract_embedding(audio_path) embed = embed / np.linalg.norm(embed) similarity = float(np.dot(self.voiceprint, embed)) is_verified = similarity \u0026gt;= self.threshold return is_verified, similarity def get_info(self) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;Get voiceprint registration info.\u0026#34;\u0026#34;\u0026#34; if not self.is_registered: return {\u0026#34;registered\u0026#34;: False} return { \u0026#34;registered\u0026#34;: True, **self.metadata, } def main(): parser = argparse.ArgumentParser( description=\u0026#34;VoiceKey — Speaker verification for security\u0026#34; ) sub = parser.add_subparsers(dest=\u0026#34;command\u0026#34;) # Register reg = sub.add_parser(\u0026#34;register\u0026#34;, help=\u0026#34;Register voiceprint from audio files\u0026#34;) reg.add_argument(\u0026#34;--audio\u0026#34;, nargs=\u0026#34;+\u0026#34;, required=True, help=\u0026#34;Audio files (ogg/mp3/wav)\u0026#34;) reg.add_argument(\u0026#34;--owner\u0026#34;, default=\u0026#34;owner\u0026#34;, help=\u0026#34;Owner name\u0026#34;) reg.add_argument(\u0026#34;--data-dir\u0026#34;, default=None, help=\u0026#34;Data directory\u0026#34;) reg.add_argument(\u0026#34;--threshold\u0026#34;, type=float, default=0.75, help=\u0026#34;Verification threshold\u0026#34;) # Verify ver = sub.add_parser(\u0026#34;verify\u0026#34;, help=\u0026#34;Verify audio against voiceprint\u0026#34;) ver.add_argument(\u0026#34;--audio\u0026#34;, required=True, help=\u0026#34;Audio file to verify\u0026#34;) ver.add_argument(\u0026#34;--data-dir\u0026#34;, default=None, help=\u0026#34;Data directory\u0026#34;) # Info sub.add_parser(\u0026#34;info\u0026#34;, help=\u0026#34;Show voiceprint info\u0026#34;) args = parser.parse_args() if args.command == \u0026#34;register\u0026#34;: vk = VoiceKey(data_dir=args.data_dir, threshold=args.threshold) result = vk.register(args.audio, owner=args.owner) print(f\u0026#34;Registered voiceprint for: {result[\u0026#39;owner\u0026#39;]}\u0026#34;) print(f\u0026#34;Samples used: {result[\u0026#39;samples\u0026#39;]}\u0026#34;) print(f\u0026#34;Self-test scores: {[f\u0026#39;{s:.4f}\u0026#39; for s in result[\u0026#39;self_test_scores\u0026#39;]]}\u0026#34;) print(f\u0026#34;Min score: {result[\u0026#39;min_score\u0026#39;]:.4f} (threshold: {args.threshold})\u0026#34;) elif args.command == \u0026#34;verify\u0026#34;: vk = VoiceKey(data_dir=getattr(args, \u0026#39;data_dir\u0026#39;, None)) is_ok, score = vk.verify(args.audio) status = \u0026#34;PASS\u0026#34; if is_ok else \u0026#34;FAIL\u0026#34; print(f\u0026#34;[{status}] Similarity: {score:.4f} (threshold: {vk.threshold})\u0026#34;) elif args.command == \u0026#34;info\u0026#34;: vk = VoiceKey() info = vk.get_info() for k, v in info.items(): print(f\u0026#34; {k}: {v}\u0026#34;) else: parser.print_help() if __name__ == \u0026#34;__main__\u0026#34;: main() Closing notes All seven snippets above are pulled from connectfarm1.com on 2026-05-04. They share three philosophies: (1) zero or near-zero AI cost — most use rule engines and free public APIs instead of LLMs; (2) Python only, easy to run on a cheap VPS with crontab or a simple while True; (3) Telegram is the universal output — no dashboards, no front-end, just push messages where you actually read them. Combine the radars to triangulate signals, and treat the autonomous trader as a research sandbox, not a money printer.\n","permalink":"https://dibi8.com/posts/code-vault-7-open-source-crypto-radar-trading-tools/","summary":"\u003cp\u003eThis is a complete walkthrough of the seven snippets currently published in \u003ca href=\"https://connectfarm1.com/\"\u003eCode Vault\u003c/a\u003e — a personal repository of crypto trading radars, autonomous trading systems, and security utilities, all in pure Python with zero or near-zero API costs. Every snippet below includes the \u003cstrong\u003efull source code\u003c/strong\u003e so you can read, fork, and run them locally. Use the table of contents on the right to jump to a specific tool.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e⚠️ \u003cstrong\u003eRisk warning\u003c/strong\u003e — These tools touch live markets and on-chain data. Several of them push real-time alerts to Telegram and one of them (the AI autonomous trader) opens \u003cem\u003evirtual\u003c/em\u003e positions on Binance Futures. Read the per-tool notes carefully. \u003cstrong\u003eUse at your own risk; the author makes no warranty for trading outcomes.\u003c/strong\u003e\u003c/p\u003e","title":"Code Vault — 7 Open-Source Crypto Radar \u0026 Trading Tools"},{"content":"Discover TikCoin: A Revolutionary Way to Boost Our Earnings! I\u0026rsquo;ve just discovered an amazing method to increase our earnings together! By joining TikCoin, we all benefit. The more active partners we have, the higher our rewards become! 💰✨\nTikCoin is a cutting-edge cryptocurrency platform that combines mining, community rewards, and partnership incentives to create a win-win ecosystem for everyone involved.\nRegister now and start mining with me: GRP-NBUC3PAH\nWhy TikCoin Mining is Different Community-Powered Rewards Unlike traditional mining where you work alone, TikCoin rewards increase with community participation. When you join with my invitation code, both of us benefit from the growing network.\nPassive Income Generation Start earning cryptocurrency rewards through TikCoin\u0026rsquo;s innovative mining system. The more people join our network, the higher the rewards for everyone.\nEasy to Get Started No expensive hardware required. Simply register with the invitation link and start mining immediately.\nReferral Bonuses Earn additional rewards by inviting friends and family to join TikCoin. The larger our community grows, the more we all earn.\nHow TikCoin Mining Works Step 1: Register with Invitation Code Use my exclusive invitation code GRP-NBUC3PAH to join the TikCoin network.\nStep 2: Start Mining Begin earning TikCoin tokens through the platform\u0026rsquo;s mining system.\nStep 3: Invite Partners Share your invitation code to grow our community and increase rewards.\nStep 4: Earn Together Watch as our collective rewards grow with each new active member.\nReward Distribution Base mining rewards for active participation Community bonuses based on network growth Referral rewards for bringing in new members Special event bonuses and giveaways Benefits of Joining TikCoin Financial Advantages Earn cryptocurrency passively Potential for significant returns Multiple income streams Community Benefits Join a growing network of miners Access to exclusive community events Support from experienced members Security Features Secure wallet integration Protected transaction processing Regular platform audits TikCoin Mining Plans Starter Plan Low entry barrier Basic mining rewards Community access Pro Plan Enhanced mining capabilities Higher reward rates Priority support Enterprise Plan Maximum mining power Best reward multipliers VIP community access Success Stories from TikCoin Users Alex from New York: \u0026ldquo;Since joining with my friend\u0026rsquo;s code, my earnings have tripled! The community aspect makes it so much more rewarding.\u0026rdquo;\nSarah from London: \u0026ldquo;TikCoin changed how I think about crypto mining. It\u0026rsquo;s not just about earning, it\u0026rsquo;s about building something together.\u0026rdquo;\nMike from Tokyo: \u0026ldquo;The referral bonuses are incredible. Every new member I bring in increases everyone\u0026rsquo;s rewards.\u0026rdquo;\nGetting Started with TikCoin Ready to join the revolution? Here\u0026rsquo;s how:\nClick the registration link: Join TikCoin Now Enter the invitation code: GRP-NBUC3PAH Complete registration: Set up your account Start mining: Begin earning immediately Invite others: Share your success Why Choose My Invitation Code? When you use GRP-NBUC3PAH, you\u0026rsquo;re joining a proven network of active miners. This means:\nHigher initial rewards Faster community growth bonuses Priority access to new features Support from experienced community members TikCoin vs Traditional Mining Feature Traditional Mining TikCoin Mining Hardware Required Expensive GPUs/ASICS None Community Benefits None High Reward Scaling Fixed Increases with network Ease of Entry Complex Simple Passive Income Limited High Security and Transparency TikCoin prioritizes user security with:\nAdvanced encryption Transparent reward calculations Secure wallet connections Regular security updates The Future of Community Mining TikCoin represents the future of cryptocurrency mining - where individual success contributes to collective prosperity. As our community grows, so do the rewards for everyone.\nDon\u0026rsquo;t miss this opportunity to be part of something revolutionary!\nRegister Today with Code GRP-NBUC3PAH\nFrequently Asked Questions What is TikCoin? TikCoin is a community-driven cryptocurrency platform that rewards users for mining and network participation.\nHow do rewards work? Rewards increase based on community participation and network growth.\nIs TikCoin secure? Yes, with bank-level security and regular audits.\nCan I withdraw my earnings? Yes, earnings can be withdrawn to supported wallets anytime.\nHow do referrals work? Share your invitation code to earn bonus rewards when others join.\nJoin the TikCoin Community Now\nJoin TikCoin - Together We Earn More! The power of community mining is here. Join TikCoin today and let\u0026rsquo;s increase our earnings together!\nStart Mining with TikCoin\nDisclaimer: Cryptocurrency mining involves risks. Always research thoroughly before participating.\n","permalink":"https://dibi8.com/posts/join-tikcoin-mining-increase-our-earnings-together/","summary":"\u003ch2 id=\"discover-tikcoin-a-revolutionary-way-to-boost-our-earnings\"\u003eDiscover TikCoin: A Revolutionary Way to Boost Our Earnings!\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve just discovered an amazing method to increase our earnings together! By joining TikCoin, we all benefit. The more active partners we have, the higher our rewards become! 💰✨\u003c/p\u003e\n\u003cp\u003eTikCoin is a cutting-edge cryptocurrency platform that combines mining, community rewards, and partnership incentives to create a win-win ecosystem for everyone involved.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tikcoin.info/?invitationCode=GRP-NBUC3PAH\"\u003eRegister now and start mining with me: GRP-NBUC3PAH\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"why-tikcoin-mining-is-different\"\u003eWhy TikCoin Mining is Different\u003c/h2\u003e\n\u003ch3 id=\"community-powered-rewards\"\u003e\u003cstrong\u003eCommunity-Powered Rewards\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eUnlike traditional mining where you work alone, TikCoin rewards increase with community participation. When you join with my invitation code, both of us benefit from the growing network.\u003c/p\u003e","title":"Join TikCoin Mining - Increase Our Earnings Together!"},{"content":"Introducing TikChain - Revolutionizing Social Media with Blockchain In the dynamic world of social media and cryptocurrency, TikChain emerges as a groundbreaking platform that bridges the gap between content creators, viewers, and blockchain technology. Whether you\u0026rsquo;re a TikTok enthusiast, content creator, or crypto investor, TikChain offers innovative solutions for the modern digital economy.\nJoin TikChain Today\nWhat is TikChain? TikChain is a cutting-edge blockchain platform specifically designed for social media integration. It leverages advanced blockchain technology to create a decentralized ecosystem where users can monetize their social media presence, engage in community governance, and participate in token-based economies.\nKey Features: Social Media Integration: Seamless connection with major social platforms Token Rewards: Earn tokens for content creation and engagement Decentralized Governance: Community-driven platform development Secure Transactions: Blockchain-based payment systems Cross-Platform Compatibility: Works across multiple social media networks Why Choose TikChain? 1. Empower Content Creators Transform your social media influence into tangible value. TikChain allows creators to monetize their content through token rewards and exclusive features.\n2. Community-Driven Economy Participate in a decentralized economy where your engagement directly contributes to platform growth and governance.\n3. Secure and Transparent Built on robust blockchain technology, ensuring transparency, security, and immutability of all transactions.\n4. Multi-Platform Support Integrate seamlessly with TikTok, Instagram, Twitter, and other social media platforms.\n5. Future-Proof Technology Stay ahead of the curve with cutting-edge blockchain innovations and social media trends.\nHow TikChain Works Step-by-Step Guide: Sign Up: Create your account with the referral link Connect Platforms: Link your social media accounts Start Earning: Begin earning tokens through engagement Participate: Join community events and governance Withdraw Rewards: Transfer earnings to your wallet Tokenomics: Native Token: TikChain\u0026rsquo;s utility token for platform functions Reward System: Earn tokens for likes, shares, and content creation Staking Options: Lock tokens for additional benefits Governance Rights: Vote on platform decisions Benefits of Joining TikChain For Content Creators Monetize social media presence Build loyal communities Access exclusive creator tools Participate in platform governance For Users Earn rewards for engagement Access premium content Participate in community events Secure digital asset management For Investors Early access to innovative platform Potential token appreciation Governance participation Diversified crypto portfolio TikChain Ecosystem TikChain creates a comprehensive ecosystem that includes:\nContent Marketplace: Buy and sell digital content NFT Integration: Create and trade social media NFTs DeFi Features: Lending, borrowing, and yield farming Gaming Elements: Gamified social media experiences Getting Started with TikChain Ready to explore the future of social media blockchain? Follow these steps:\nVisit the Platform: Register with TikChain Complete Registration: Set up your profile Connect Accounts: Link your social media profiles Start Exploring: Discover features and earn rewards Engage Community: Join discussions and events Security and Compliance TikChain prioritizes user security with:\nEnd-to-end encryption Secure wallet integration Regular security audits Compliance with global regulations The Future of Social Media As social media continues to evolve, platforms like TikChain represent the next generation of digital interaction. By combining the best of social media with blockchain technology, TikChain creates new opportunities for creators, users, and investors alike.\nBecome Part of TikChain Today\nCommunity Features Join the growing TikChain community:\nDeveloper forums and discussions Regular AMAs with the team Community events and giveaways Educational resources and tutorials Frequently Asked Questions What makes TikChain unique? TikChain uniquely combines social media engagement with blockchain rewards, creating a seamless ecosystem for content monetization.\nHow do I earn tokens on TikChain? Earn tokens through content creation, social media engagement, community participation, and referral programs.\nIs TikChain secure? Yes, with blockchain-based security, encryption, and regular audits.\nWhich social media platforms are supported? TikChain supports integration with TikTok, Instagram, Twitter, YouTube, and more.\nHow do I withdraw my earnings? Withdrawals can be made to supported crypto wallets or exchanged for other assets.\nStart Your TikChain Journey\nJoin the TikChain Revolution Don\u0026rsquo;t miss out on this revolutionary platform that combines social media and blockchain. Whether you\u0026rsquo;re a creator, user, or investor, TikChain offers something for everyone.\nSign Up Now with Referral Link\nDisclaimer: Cryptocurrency and blockchain investments carry risks. Always conduct thorough research before participating.\n","permalink":"https://dibi8.com/posts/discover-tikchain-your-gateway-to-social-media-blockchain/","summary":"\u003ch2 id=\"introducing-tikchain---revolutionizing-social-media-with-blockchain\"\u003eIntroducing TikChain - Revolutionizing Social Media with Blockchain\u003c/h2\u003e\n\u003cp\u003eIn the dynamic world of social media and cryptocurrency, TikChain emerges as a groundbreaking platform that bridges the gap between content creators, viewers, and blockchain technology. Whether you\u0026rsquo;re a TikTok enthusiast, content creator, or crypto investor, TikChain offers innovative solutions for the modern digital economy.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tikchain.network/user/luckybbjason\"\u003eJoin TikChain Today\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"what-is-tikchain\"\u003eWhat is TikChain?\u003c/h2\u003e\n\u003cp\u003eTikChain is a cutting-edge blockchain platform specifically designed for social media integration. It leverages advanced blockchain technology to create a decentralized ecosystem where users can monetize their social media presence, engage in community governance, and participate in token-based economies.\u003c/p\u003e","title":"Discover TikChain - Your Gateway to Social Media Blockchain"},{"content":"Introducing Billions Wallet - Revolutionizing Crypto Management In the rapidly evolving world of cryptocurrency, having a reliable and feature-rich wallet is essential. Billions Wallet emerges as a comprehensive solution designed to meet all your digital asset management needs. Whether you\u0026rsquo;re a seasoned trader or just starting your crypto journey, Billions Wallet offers unparalleled security, functionality, and user experience.\nGet Started with Billions Wallet Today\nWhy Choose Billions Wallet? 1. Unmatched Security Features Billions Wallet prioritizes your assets\u0026rsquo; safety with advanced encryption, multi-signature support, and biometric authentication. Your cryptocurrencies are protected by bank-level security protocols.\n2. Multi-Asset Support Manage a wide range of cryptocurrencies including Bitcoin, Ethereum, stablecoins, and many altcoins. Billions Wallet supports over 1000+ digital assets across multiple blockchains.\n3. User-Friendly Interface Designed with simplicity in mind, the intuitive interface makes it easy for both beginners and experts to navigate and manage their portfolios.\n4. Advanced Trading Tools Built-in exchange features allow you to trade cryptocurrencies directly within the wallet, with competitive rates and fast execution.\n5. Staking and Earning Opportunities Earn passive income by staking your assets or participating in yield farming programs integrated into the wallet.\nKey Features of Billions Wallet Secure Storage Hardware wallet integration Cold storage options Encrypted private keys Recovery phrase backup DeFi Integration Decentralized exchange access Liquidity pool participation Yield farming opportunities NFT marketplace Analytics and Insights Real-time portfolio tracking Price alerts and notifications Performance analytics Tax reporting tools Getting Started with Billions Wallet Step 1: Download and Install Visit the official website and download Billions Wallet for your preferred platform - iOS, Android, Desktop, or Web.\nStep 2: Create Your Account Sign up using the referral link for exclusive bonuses and enhanced features.\nCreate Your Billions Wallet Account\nStep 3: Secure Your Wallet Set up biometric authentication and backup your recovery phrase in a safe location.\nStep 4: Add Assets Deposit cryptocurrencies or buy them directly through integrated exchanges.\nStep 5: Start Managing and Trading Explore the full range of features and start optimizing your crypto portfolio.\nBillions Wallet vs Traditional Wallets Feature Billions Wallet Traditional Wallets Multi-Asset Support 1000+ assets Limited DeFi Integration Full support None Built-in Exchange Yes No Staking Rewards High yields Low or none Security Military-grade Basic User Experience Intuitive Complex Community and Support Join the thriving Billions Wallet community:\nActive Discord and Telegram groups Comprehensive knowledge base 24/7 customer support Regular updates and new features Success Stories from Billions Users Alex Chen, Singapore: \u0026ldquo;Billions Wallet transformed my crypto experience. The staking rewards are incredible!\u0026rdquo;\nMaria Rodriguez, Spain: \u0026ldquo;Finally, a wallet that supports all my favorite altcoins. Highly recommended!\u0026rdquo;\nDavid Kim, South Korea: \u0026ldquo;The security features give me peace of mind. Best wallet I\u0026rsquo;ve used.\u0026rdquo;\nFuture Roadmap Billions Wallet is continuously evolving with planned features including:\nCross-chain interoperability Enhanced NFT support Institutional-grade tools AI-powered trading assistants Frequently Asked Questions Is Billions Wallet safe? Yes, with multiple security layers and regular audits.\nWhat cryptocurrencies are supported? Over 1000 assets including BTC, ETH, USDT, and many more.\nHow do I earn rewards? Through staking, yield farming, and referral programs.\nIs there a mobile app? Yes, available for iOS and Android.\nDownload Billions Wallet Now\nThe Future of Crypto Wallets is Here Billions Wallet represents the next generation of cryptocurrency management tools. With its comprehensive feature set, top-tier security, and user-centric design, it\u0026rsquo;s poised to become the go-to wallet for crypto enthusiasts worldwide.\nDon\u0026rsquo;t miss out on the opportunity to upgrade your crypto experience. Join millions of users who have already discovered the power of Billions Wallet.\nSign Up with Referral Code 33E57NWH45\nDisclaimer: Cryptocurrency investments carry risks. Always conduct thorough research before investing.\n","permalink":"https://dibi8.com/posts/discover-billions-wallet-your-ultimate-crypto-companion/","summary":"\u003ch2 id=\"introducing-billions-wallet---revolutionizing-crypto-management\"\u003eIntroducing Billions Wallet - Revolutionizing Crypto Management\u003c/h2\u003e\n\u003cp\u003eIn the rapidly evolving world of cryptocurrency, having a reliable and feature-rich wallet is essential. Billions Wallet emerges as a comprehensive solution designed to meet all your digital asset management needs. Whether you\u0026rsquo;re a seasoned trader or just starting your crypto journey, Billions Wallet offers unparalleled security, functionality, and user experience.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://official.app/rc/33E57NWH45\"\u003eGet Started with Billions Wallet Today\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"why-choose-billions-wallet\"\u003eWhy Choose Billions Wallet?\u003c/h2\u003e\n\u003ch3 id=\"1-unmatched-security-features\"\u003e1. \u003cstrong\u003eUnmatched Security Features\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eBillions Wallet prioritizes your assets\u0026rsquo; safety with advanced encryption, multi-signature support, and biometric authentication. Your cryptocurrencies are protected by bank-level security protocols.\u003c/p\u003e","title":"Discover Billions Wallet - Your Ultimate Crypto Companion"},{"content":"Here\u0026rsquo;s an interesting Twitter post that caught my attention:\nView the full Twitter post here\nWhat do you think about this post? Share your thoughts in the comments!\n","permalink":"https://dibi8.com/posts/interesting-twitter-post-check-it-out/","summary":"\u003cp\u003eHere\u0026rsquo;s an interesting Twitter post that caught my attention:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Twitter Image 1\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HHYvLD_bAAEtLCi?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Twitter Image 2\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HHZ2puFWUAEXRBs?format=jpg\u0026name=medium\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Twitter Image 3\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HGq5w8bW4AA6ShR?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Twitter Image 4\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HGnDWVOX0AANAWu?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://x.com/i/status/2047276073973346585\"\u003eView the full Twitter post here\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eWhat do you think about this post? Share your thoughts in the comments!\u003c/p\u003e","title":"Interesting Twitter Post - Check It Out"},{"content":"Discover the Future of Digital Asset Processing with Access Network Check out these related images and video from our community:\nWatch the community video\nIn the evolving landscape of blockchain and cryptocurrency, Access Network emerges as a revolutionary platform that empowers users to participate in digital asset processing through innovative mining technology. Whether you\u0026rsquo;re new to crypto or an experienced miner, Access Network offers a seamless way to contribute to the network and earn substantial rewards.\nJoin Access Network Today\nWhat is Access Network Mining? Access Network is a cutting-edge blockchain platform that utilizes advanced processing power to validate transactions, secure the network, and distribute digital assets. Unlike traditional mining that requires expensive hardware, Access Network employs cloud-based processing that makes mining accessible to everyone.\nKey Features: Cloud-Based Mining: No need for expensive GPUs or ASICs Instant Rewards: Earn tokens immediately upon processing Low Energy Consumption: Eco-friendly mining solution Global Accessibility: Join from anywhere in the world Multi-Asset Support: Process various digital assets simultaneously Why Choose Access Network for Mining? 1. Passive Income Generation Start earning digital rewards without constant monitoring. Access Network\u0026rsquo;s automated system handles the processing while you collect earnings.\n2. Low Barrier to Entry Unlike traditional mining rigs that cost thousands, Access Network mining requires minimal investment and technical knowledge.\n3. Sustainable Mining Environmentally conscious approach with significantly lower energy consumption compared to traditional mining methods.\n4. Community-Driven Growth Join a thriving community of miners and benefit from network effects as more users participate.\n5. Future-Proof Technology Built on next-generation blockchain technology that adapts to market changes and technological advancements.\nHow Access Network Mining Works Step-by-Step Process: Sign Up: Create your account with the invitation link Choose Plan: Select from various mining packages Start Processing: Begin earning rewards immediately Withdraw Earnings: Transfer rewards to your wallet anytime Mining Mechanics: Processing Power Allocation: System assigns processing tasks based on your plan Reward Distribution: Earnings calculated in real-time Staking Options: Lock tokens for additional rewards Referral Program: Earn bonus rewards by inviting others Benefits of Joining Access Network Financial Advantages Competitive reward rates Daily payouts Compound interest options Multiple withdrawal methods Technical Benefits User-friendly interface 24/7 system availability Secure transaction processing Advanced encryption Community Features Discussion forums Educational resources Regular updates and news Support from experienced members Getting Started with Access Network Mining Ready to start your mining journey? Follow these simple steps:\nVisit the Platform: Join with Invitation Complete Registration: Provide basic information Verify Account: Secure your account with 2FA Fund Account: Add initial investment Begin Mining: Start processing and earning Access Network Mining Plans Starter Plan Minimum investment: $50 Daily rewards: 1-2%% Processing power: 100 GH/s Professional Plan Minimum investment: $500 Daily rewards: 2-3%% Processing power: 1000 GH/s Enterprise Plan Minimum investment: $5000 Daily rewards: 3-5%% Processing power: 10000 GH/s Security and Transparency Access Network prioritizes security with:\nMilitary-grade encryption Regular security audits Transparent reward calculations Secure wallet integration The Future of Mining As traditional mining becomes increasingly difficult and expensive, Access Network represents the future of digital asset processing. By leveraging cloud technology and innovative algorithms, Access Network makes mining accessible to the masses while maintaining profitability.\nFrequently Asked Questions What makes Access Network different? Access Network combines cloud mining with blockchain processing, eliminating hardware requirements while maintaining profitability.\nHow often are rewards paid? Rewards are calculated and paid out daily, with instant withdrawals available.\nIs Access Network secure? Yes, with bank-level security, encryption, and regular audits.\nCan I withdraw anytime? Yes, withdrawals are processed instantly once requested.\nStart Your Mining Journey Now\nJoin the Access Network Revolution Don\u0026rsquo;t miss out on this opportunity to be part of the digital asset processing revolution. With Access Network, mining has never been easier or more rewarding.\nSign Up Today with Code YCM9D7\nDisclaimer: Digital asset processing involves risks. Always invest responsibly and conduct thorough research.\n","permalink":"https://dibi8.com/posts/join-access-network-mining-process-digital-assets-and-earn-rewards/","summary":"\u003ch2 id=\"discover-the-future-of-digital-asset-processing-with-access-network\"\u003eDiscover the Future of Digital Asset Processing with Access Network\u003c/h2\u003e\n\u003cp\u003eCheck out these related images and video from our community:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Mining Image 1\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HHYvLD_bAAEtLCi?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Mining Image 2\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HHZ2puFWUAEXRBs?format=jpg\u0026name=medium\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Mining Image 3\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HGq5w8bW4AA6ShR?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Mining Image 4\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HGnDWVOX0AANAWu?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://x.com/i/status/2047276073973346585\"\u003eWatch the community video\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eIn the evolving landscape of blockchain and cryptocurrency, Access Network emerges as a revolutionary platform that empowers users to participate in digital asset processing through innovative mining technology. Whether you\u0026rsquo;re new to crypto or an experienced miner, Access Network offers a seamless way to contribute to the network and earn substantial rewards.\u003c/p\u003e","title":"Join Access Network Mining - Process Digital Assets and Earn Rewards"},{"content":"In today\u0026rsquo;s interconnected global economy, businesses face the challenge of accepting payments from customers worldwide. Traditional payment methods often limit merchants to specific currencies or regions, but NowPayments revolutionizes this by enabling payment acceptance in all currencies. Whether your customers use fiat currencies like USD, EUR, or JPY, or cryptocurrencies such as Bitcoin, Ethereum, or stablecoins, NowPayments ensures seamless transactions.\nWhat is NowPayments? NowPayments is a leading cryptocurrency payment gateway that bridges the gap between traditional finance and digital assets. Founded with the vision of making crypto payments accessible to everyone, NowPayments provides merchants with a simple, secure, and efficient way to accept payments globally.\nKey features include:\nUniversal Currency Support: Process payments in over 100+ cryptocurrencies and major fiat currencies Instant Settlements: Receive funds directly to your wallet without intermediaries Low Transaction Fees: Competitive rates starting from 0.5%% per transaction Advanced Security: PCI DSS compliant with multi-signature wallets and encryption Easy Integration: API and plugins for popular e-commerce platforms Why Choose NowPayments for Global Payments? 1. Break Down Currency Barriers Traditional payment processors often restrict merchants to their local currency or a handful of major ones. NowPayments eliminates these barriers, allowing you to accept payments from anywhere in the world without currency conversion hassles.\n2. Embrace Cryptocurrency Adoption As digital currencies gain mainstream acceptance, NowPayments positions your business at the forefront of this trend. Accept Bitcoin, Ethereum, USDT, and other popular cryptocurrencies alongside traditional payments.\n3. Reduce Transaction Costs With fees as low as 0.5%%, NowPayments offers significant savings compared to traditional payment processors that charge 2-3%% or more. Plus, instant settlements mean faster access to your funds.\n4. Enhance Customer Experience Customers appreciate the flexibility of choosing their preferred payment method. Whether they prefer credit cards, bank transfers, or crypto, NowPayments caters to diverse preferences.\n5. Future-Proof Your Business As the world moves towards digital finance, integrating NowPayments ensures your business stays competitive and ready for emerging payment trends.\nHow to Get Started with NowPayments Getting started is straightforward:\nSign Up: Create your free account at NowPayments Choose Your Plan: Select from Starter, Business, or Enterprise plans Integrate: Use our API or plugins for WooCommerce, Shopify, and more Start Accepting Payments: Begin receiving payments immediately Real-World Applications NowPayments powers payments for:\nE-commerce Stores: Online retailers accepting global orders Freelancers: Professionals receiving payments from international clients Gaming Companies: In-game purchases and subscriptions Non-Profits: Donations from supporters worldwide Software Companies: SaaS subscriptions and licensing fees Security and Compliance NowPayments prioritizes security with:\nEnd-to-end encryption Cold storage for funds Regular security audits Compliance with international regulations The Future of Payments is Here As cryptocurrency adoption grows, payment gateways like NowPayments are essential for businesses wanting to thrive in the digital age. Don\u0026rsquo;t get left behind – embrace the future of payments today.\nReady to accept payments in all currencies? Create your NowPayments account now and start receiving global payments instantly.\nCreate Your Account\nJoin thousands of merchants already benefiting from seamless, borderless payments.\n","permalink":"https://dibi8.com/posts/accept-payments-in-all-currencies-with-nowpayments/","summary":"\u003cp\u003eIn today\u0026rsquo;s interconnected global economy, businesses face the challenge of accepting payments from customers worldwide. Traditional payment methods often limit merchants to specific currencies or regions, but NowPayments revolutionizes this by enabling \u003cstrong\u003epayment acceptance in all currencies\u003c/strong\u003e. Whether your customers use fiat currencies like USD, EUR, or JPY, or cryptocurrencies such as Bitcoin, Ethereum, or stablecoins, NowPayments ensures seamless transactions.\u003c/p\u003e\n\u003ch2 id=\"what-is-nowpayments\"\u003eWhat is NowPayments?\u003c/h2\u003e\n\u003cp\u003eNowPayments is a leading cryptocurrency payment gateway that bridges the gap between traditional finance and digital assets. Founded with the vision of making crypto payments accessible to everyone, NowPayments provides merchants with a simple, secure, and efficient way to accept payments globally.\u003c/p\u003e","title":"Accept Payments in All Currencies with NowPayments"},{"content":"Welcome to RR6958 - The Pinnacle of Online Entertainment In 2026, as the online casino industry continues to flourish, RR6958 stands out as a premier destination offering world-class gaming experiences. With over 10 years of expertise, RR6958 provides a fair, transparent, and highly entertaining platform for millions of players worldwide, featuring thousands of diverse games and top-tier customer service available 24/7.\nRegister now to claim your 100%% bonus\nWhy Choose RR6958? 1. Hundreds of Exciting Casino Games RR6958 offers an unparalleled gaming experience with:\nLive Baccarat with stunning dealers Roulette variations for all preferences Blackjack with simple rules and high wins Slot machines with massive jackpots Poker, Sic Bo, Dragon Tiger, and more 2. Generous Bonuses and Promotions First deposit bonus: Up to 200%% on your initial deposit Weekly cashback: 5-15%% depending on your level Loyalty program: Points that convert to real money Special events: Double bonuses during holidays 3. Absolute Security and Fairness SSL 256-bit encryption protecting all data RNG certified by international organizations Ultra-fast payouts, within 1-5 minutes 24/7 support from professional team 4. User-Friendly Interface Responsive design for mobile play Multi-language support including English Live chat support Convenient mobile apps for iOS/Android How to Register with RR6958 Getting started is simple - just 3 steps:\nVisit the registration page: Register at RR6958 Enter your details: Name, email, phone number Verify and deposit: Get instant bonuses Multiple Payment Methods at RR6958 RR6958 supports all popular payment options in Vietnam:\nVietnamese bank transfers (BIDV, Vietcombank, Techcombank) E-wallets (Momo, ZaloPay, ViettelPay) Phone cards (Vinaphone, Mobifone, Viettel) Cryptocurrency (Bitcoin, Ethereum, USDT) Local agents for in-person payments Exclusive Benefits of Playing at RR6958 Premium Gaming Experience Vietnamese servers for lightning-fast speed 4K graphics and immersive sound Real dealers from the Philippines Seamless gameplay without interruptions Continuous Promotion Programs Daily bonuses for all members Triple bonuses on weekends Monthly rewards for active players Exclusive events and tournaments 5-Star Customer Service Hotline: 1900 XXX XXX (free calls) Email: support@rr6958.com 24/7 live chat Vietnamese-speaking support staff RR6958 - The Number 1 Choice for Vietnamese Players With over 10 years of development, RR6958 proudly serves millions of players globally and has:\n10 million+ members worldwide 1000+ games across all categories Billions in jackpots paid out monthly 99.9%% payout success rate Don\u0026rsquo;t miss the opportunity to join the RR6958 community today!\nRegister for free and claim your 200%% bonus\nPopular Games at RR6958 2026 VIP Baccarat Premium game for professional players with payouts up to 9 times your bet.\nEuropean Roulette Classic wheel of fortune with high win rates and attractive bonuses.\nJackpot Slots Hundreds of slot machines waiting for you to hit the million-dollar jackpots.\nSafe Gambling Tips at RR6958 Always play responsibly, never exceed your budget Check daily promotions Join member groups to learn strategies Use referral codes for extra bonuses Register with referral code vbx2083\nFrequently Asked Questions Is RR6958 legal in Vietnam? RR6958 operates under international licenses and complies with Vietnamese data protection laws.\nHow long do withdrawals take? Withdrawals are processed within 1-5 minutes.\nAre there mobile apps? Yes, available on CH Play and App Store.\nWhat\u0026rsquo;s the first deposit bonus? 200%% bonus on your first deposit, applicable immediately.\nRegister now for 200%% bonus\nConclusion - RR6958 is the Future of Online Casinos In the digital age of 2026, RR6958 continues to lead with cutting-edge technology and exceptional service. Join now to experience the difference!\nJoin RR6958 - Vietnam\u0026rsquo;s Top Casino\nNote: Play responsibly. RR6958 encourages healthy gaming habits.\n","permalink":"https://dibi8.com/posts/introducing-rr6958-the-ultimate-online-casino-experience/","summary":"\u003ch2 id=\"welcome-to-rr6958---the-pinnacle-of-online-entertainment\"\u003eWelcome to RR6958 - The Pinnacle of Online Entertainment\u003c/h2\u003e\n\u003cp\u003eIn 2026, as the online casino industry continues to flourish, RR6958 stands out as a premier destination offering world-class gaming experiences. With over 10 years of expertise, RR6958 provides a fair, transparent, and highly entertaining platform for millions of players worldwide, featuring thousands of diverse games and top-tier customer service available 24/7.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://gioithieu.rr6958.com/?referralCode=vbx2083\"\u003eRegister now to claim your 100%% bonus\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"why-choose-rr6958\"\u003eWhy Choose RR6958?\u003c/h2\u003e\n\u003ch3 id=\"1-hundreds-of-exciting-casino-games\"\u003e1. \u003cstrong\u003eHundreds of Exciting Casino Games\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eRR6958 offers an unparalleled gaming experience with:\u003c/p\u003e","title":"Introducing RR6958 - The Ultimate Online Casino Experience"},{"content":"There are roughly four eras of Python web scraping. urllib and a regex. Then requests plus BeautifulSoup. Then Scrapy for anything serious. Then Playwright once half the web went JavaScript-only and sent the previous three tools into the cliff face.\nScrapling is one of the newer libraries trying to be the next layer on that stack — a single toolkit that covers the easy case, the heavy-JS case, and the anti-bot-protected case, without making you stitch together three different libraries.\nI\u0026rsquo;ve been reading through the project, the benchmarks, and the API. Here\u0026rsquo;s what\u0026rsquo;s actually interesting about it, what to be careful with, and when I\u0026rsquo;d reach for it instead of the obvious alternatives.\nWhat it is in one sentence Scrapling is a Python 3.10+ scraping framework that wraps three different fetching backends — plain HTTP with TLS fingerprint impersonation, a stealth-mode browser, and a full Playwright-driven browser — behind one consistent selector API. BSD-3-Clause licensed.\nThe tagline on the repo is \u0026ldquo;Effortless Web Scraping for the Modern Web,\u0026rdquo; which is the kind of thing every scraping library says. The more useful framing is: it\u0026rsquo;s trying to be Scrapy\u0026rsquo;s spider model + curl_cffi\u0026rsquo;s TLS fingerprinting + an undetected Playwright in one import.\nThe three-fetcher model This is the part of the design I find genuinely well thought out. Most scraping projects accumulate a hairball of requests for the fast pages, Selenium or Playwright for the JS-heavy ones, and some custom CDN-bypass for the protected ones. Scrapling separates those into three tiers with the same response shape:\nFetcher Backend When to use Fetcher Plain HTTP, with TLS fingerprint impersonation Static HTML; you don\u0026rsquo;t need a real browser; you want it fast StealthyFetcher Headless browser with anti-detection patches Cloudflare/JS-protected pages where a real browser is required DynamicFetcher Playwright/Chromium, full automation SPA with complex auth, click flows, or JS-rendered data In one Spider class you can mark different requests for different tiers. The README\u0026rsquo;s example:\nasync def parse(self, response: Response): for link in response.css(\u0026#39;a::attr(href)\u0026#39;).getall(): if \u0026#34;protected\u0026#34; in link: yield Request(link, sid=\u0026#34;stealth\u0026#34;) else: yield Request(link, sid=\u0026#34;fast\u0026#34;, callback=self.parse) The reason this matters: in a real crawl, only a fraction of pages need the heavy backend, but you usually end up paying browser overhead for everything because rewriting halfway through is painful. Letting a single spider mix tiers keeps the average page cheap.\nThe benchmarks, with a grain of salt The README publishes numbers for parsing 5,000 nested elements:\nLibrary Time Relative Scrapling 2.02 ms 1.0× Parsel / Scrapy 2.04 ms 1.01× Raw lxml 2.54 ms 1.26× BeautifulSoup4 + lxml 1584.31 ms ~784× Two honest reads of this:\nYes, BeautifulSoup is that much slower. That number is not a typo. BS4 is famously a usability-first library; for tight loops over many documents, lxml-based parsers (which Scrapling, Parsel, and raw lxml all are) are orders of magnitude faster. This is a known result, not a Scrapling-specific finding.\nNo, Scrapling isn\u0026rsquo;t really faster than Scrapy\u0026rsquo;s parser. Look at the table — Parsel (the engine Scrapy uses) is 2.04 ms vs Scrapling\u0026rsquo;s 2.02 ms. Within margin of error for a microbenchmark. The headline \u0026ldquo;orders of magnitude faster than BS4\u0026rdquo; is true; \u0026ldquo;faster than Scrapy\u0026rdquo; isn\u0026rsquo;t, at the parser layer.\nWhere Scrapling probably does win in real workloads is the network layer — TLS fingerprint impersonation in Fetcher lets you skip the \u0026ldquo;add Playwright just to get past basic JA3 fingerprinting\u0026rdquo; tax. That saves seconds per request, not microseconds.\nAdaptive selection — the interesting idea Most scraping pain isn\u0026rsquo;t writing the first selector, it\u0026rsquo;s the selector breaking three weeks later when the target site renames a class. Scrapling\u0026rsquo;s pitch on this is \u0026ldquo;smart element relocation after website changes using similarity algorithms\u0026rdquo; — i.e. the library can re-find an element when the original selector fails, by comparing structural similarity to what it captured before.\nI\u0026rsquo;m cautiously optimistic about this. It\u0026rsquo;s the right thing to want. In practice, similarity-based relocation will work great for cosmetic DOM rearrangements (class renames, wrapper divs added) and badly for semantic restructures (the data moved to a different page, or the section was re-templated). Treat it as a \u0026ldquo;save you from waking up at 3am for a one-class change\u0026rdquo; feature, not a \u0026ldquo;your scraper now maintains itself\u0026rdquo; feature.\nWhat\u0026rsquo;s the simplest thing that works? Lifted directly from the docs, the absolute minimum is:\nfrom scrapling.fetchers import Fetcher, FetcherSession with FetcherSession(impersonate=\u0026#39;chrome\u0026#39;) as session: page = session.get(\u0026#39;https://quotes.toscrape.com/\u0026#39;, stealthy_headers=True) quotes = page.css(\u0026#39;.quote .text::text\u0026#39;).getall() That\u0026rsquo;s it. Three lines for \u0026ldquo;fetch and parse with Chrome\u0026rsquo;s TLS fingerprint.\u0026rdquo; The impersonate='chrome' parameter is the curl_cffi-style fingerprint spoofing — useful when a target uses basic fingerprint-based bot detection.\nFor Cloudflare-protected pages:\nfrom scrapling.fetchers import StealthyFetcher page = StealthyFetcher.fetch(\u0026#39;https://nopecha.com/demo/cloudflare\u0026#39;) data = page.css(\u0026#39;#padded_content a\u0026#39;).getall() Note that StealthyFetcher requires a separate browser install:\npip install \u0026#34;scrapling[fetchers]\u0026#34; scrapling install The scrapling install step pulls down patched Chromium binaries. On a fresh server that\u0026rsquo;s a couple hundred MB of dependencies, which is worth knowing before you pip install it on a small VM.\nCompared to the obvious alternatives Need Reach for One-off script, simple HTML, learning requests + BeautifulSoup Big crawl, well-defined pipeline, mature ecosystem Scrapy Heavy JS app, complex auth flow, you control the browser Playwright directly TLS fingerprinting, no browser overhead curl_cffi Cloudflare/Turnstile-protected static-ish pages cloudscraper or Scrapling.StealthyFetcher You want all of the above in one library Scrapling The most honest thing I can say: if your project has a clear shape (\u0026ldquo;we\u0026rsquo;re crawling 10M product pages on a known site\u0026rdquo;), use the tool that\u0026rsquo;s specialized for that shape — Scrapy is still the right answer for big disciplined crawls, Playwright is still the right answer for serious browser automation. Scrapling\u0026rsquo;s value is in projects that don\u0026rsquo;t have one clear shape, where you\u0026rsquo;d otherwise end up maintaining three scrapers.\nWhere I\u0026rsquo;d be careful Anti-bot bypass is a moving target. The README is upfront about this: enterprise systems (Akamai, DataDome, Kasada, Incapsula) need third-party solutions and aren\u0026rsquo;t promised. Even for the systems Scrapling does target — Cloudflare Turnstile in particular — expect that what works today won\u0026rsquo;t necessarily work next quarter. If your business depends on it, plan for ongoing maintenance, not \u0026ldquo;set it and forget it.\u0026rdquo;\nrobots_txt_obey is opt-in, not default. This is a deliberate design choice (some users have legitimate reasons to bypass robots files — e.g. you own the site you\u0026rsquo;re crawling) but it means you have to consciously turn it on. Forgetting to do so on a third-party site is a thing you\u0026rsquo;ll regret in court before you regret it technically.\nStealth ≠ permission. The library has a LICENSE and a disclaimer that\u0026rsquo;s worth quoting:\n\u0026ldquo;This library is provided for educational and research purposes only. By using this library, you agree to comply with local and international data scraping and privacy laws.\u0026rdquo;\nAnti-detection capabilities are useful for legitimate cases — research, archival, accessibility scraping, monitoring sites where you have permission, scraping your own data out of services that won\u0026rsquo;t give you an export. They\u0026rsquo;re also exactly the capabilities used by ad fraud, content theft, and ToS violations. The library doesn\u0026rsquo;t care which you\u0026rsquo;re doing; courts and regulators do. Treat the \u0026ldquo;stealthy\u0026rdquo; features as power tools — useful, but you need to know when not to use them.\nWhen I\u0026rsquo;d actually reach for it Three concrete cases I think Scrapling is well-suited for:\nPersonal data export. A service holds your data and won\u0026rsquo;t provide a real export API. Scraping your own account with a real browser, slowly, with respect for their rate limits — Scrapling\u0026rsquo;s DynamicSession is good at this.\nA small commercial crawl that hits two or three different protection levels. You don\u0026rsquo;t want to architect a Scrapy + Playwright + curl_cffi pipeline for a 1-week project. Scrapling gets you to a working prototype faster.\nResearch and archival. Scraping public-record sites, datasets, government portals, news archives. The kind of work where you want fast HTTP for the bulk and a real browser for the handful of awkward pages.\nI would not reach for it as a first choice when the target is a single, well-understood site you\u0026rsquo;re going to scrape for years (Scrapy\u0026rsquo;s discipline pays off there) or when the site\u0026rsquo;s owner explicitly asks you not to (no library solves that problem).\nBottom line Scrapling is a real, well-designed library — not vapor, not hype. The benchmarks against BeautifulSoup are real and against Scrapy are honestly within noise; the genuine win is the unified surface across three fetcher tiers and the network-level fingerprinting that lets you avoid spinning up a browser for cases that don\u0026rsquo;t need one.\nIt doesn\u0026rsquo;t solve the actual hard problems of scraping — those are all on the legal/ethical side, where no library can help — but it solves a real engineering problem cleanly. If you\u0026rsquo;re starting a new scraping project today and you don\u0026rsquo;t know yet whether you\u0026rsquo;ll need a browser, this is a reasonable place to start.\nThe full source and docs are at github.com/D4Vinci/Scrapling.\n","permalink":"https://dibi8.com/posts/scrapling-reviewed-a-faster-stealthier-take-on-python-scraping/","summary":"Scrapling positions itself as a faster, stealthier successor to Scrapy and BeautifulSoup. After reading the docs and the benchmarks, here\u0026rsquo;s an honest look at what it actually delivers, where it fits, and where it doesn\u0026rsquo;t.","title":"Scrapling Reviewed: A Faster, Stealthier Take on Python Scraping"},{"content":"Most introductions to context managers in Python show one example — with open(\u0026quot;file.txt\u0026quot;) as f: — and call it a day. That\u0026rsquo;s enough to use them, but it doesn\u0026rsquo;t tell you when to write one.\nAfter a few years of writing Python services, I keep reaching for context managers in three specific situations. Each one solves a problem that try/finally can technically solve but tends to get wrong in practice.\nCase 1: Pairing acquire and release The classic case. You have something that must be released — a lock, a database connection, a temporary file, a network socket — and you want to make sure release happens even if the code in between raises.\nfrom contextlib import contextmanager import threading _lock = threading.Lock() @contextmanager def critical_section(): _lock.acquire() try: yield finally: _lock.release() with critical_section(): do_dangerous_thing() Why not just try/finally? You can — and at the call site, that\u0026rsquo;s all the context manager expands to. The win is that the try/finally lives in the helper, not the call site. Every caller gets it for free, and nobody can forget to write the finally block.\nWhen I see five copies of try: thing.acquire(); ...; finally: thing.release() in a codebase, I know there\u0026rsquo;s a context manager waiting to be extracted.\nCase 2: Temporarily changing global-ish state This one is less talked about, but it\u0026rsquo;s where context managers really earn their keep. You want to flip some setting for the duration of a block, and you want it back to what it was no matter how the block exits.\nimport os from contextlib import contextmanager @contextmanager def env(**overrides): \u0026#34;\u0026#34;\u0026#34;Temporarily set environment variables, restoring previous values on exit.\u0026#34;\u0026#34;\u0026#34; saved = {k: os.environ.get(k) for k in overrides} os.environ.update({k: str(v) for k, v in overrides.items()}) try: yield finally: for k, prev in saved.items(): if prev is None: os.environ.pop(k, None) else: os.environ[k] = prev with env(DEBUG=\u0026#34;1\u0026#34;, REGION=\u0026#34;us-east-1\u0026#34;): run_test_suite() # Environment is back to whatever it was here. The same pattern works for sys.path, logging levels, decimal contexts, mocked attributes — anything that follows the \u0026ldquo;save, change, restore\u0026rdquo; shape. Tests in particular benefit from this; the alternative is fixtures that leak when something raises mid-test.\nThe subtle part is restoring None correctly. A common bug is to do os.environ[k] = saved[k] without checking — that writes the literal string \u0026quot;None\u0026quot; into the variable when it didn\u0026rsquo;t exist before. Always restore \u0026ldquo;absent\u0026rdquo; as pop, not as a string.\nCase 3: Suppressing exceptions you genuinely want to ignore Sometimes you really do want to swallow a specific exception class and move on. Python ships contextlib.suppress for this:\nfrom contextlib import suppress with suppress(FileNotFoundError): os.unlink(\u0026#34;maybe-stale.lock\u0026#34;) This is dramatically clearer than the equivalent try/except: pass, because the limited surface forces you to be specific. You can\u0026rsquo;t accidentally suppress everything — you have to name the class. And you can\u0026rsquo;t accidentally suppress code below the cleanup; the with block\u0026rsquo;s scope is exactly what you wrote.\nI find this useful for cleanup in destructors and atexit handlers, where you really cannot afford the cleanup itself to raise.\nWhen not to write one Context managers are not free. Each with introduces a small amount of machinery, and stacking them affects readability fast. I avoid them when:\nThe \u0026ldquo;acquire\u0026rdquo; half doesn\u0026rsquo;t actually need a paired \u0026ldquo;release\u0026rdquo; — just call the function. The cleanup is best-effort and the scope is small enough that try/finally reads more clearly inline. The thing being managed is already managed by something else (e.g. don\u0026rsquo;t wrap a Session from a framework that already context-manages its own lifecycle). The test I use: \u0026ldquo;if I leave the cleanup out, will the next person silently leak resources?\u0026rdquo; If yes, write the context manager. If no, a plain function is fine.\nA note on async In async code, use @asynccontextmanager and async with. The shape is identical; the only thing to remember is that you can await inside the body, which makes the pattern even more useful for things like \u0026ldquo;acquire a connection from a pool, run a query, return it.\u0026rdquo;\nfrom contextlib import asynccontextmanager @asynccontextmanager async def borrowed(pool): conn = await pool.acquire() try: yield conn finally: await pool.release(conn) That\u0026rsquo;s it. Three patterns covers maybe 90% of the context managers I\u0026rsquo;ve written. The other 10% are weird and you\u0026rsquo;ll know one when you see it.\n","permalink":"https://dibi8.com/posts/python-context-managers-the-three-cases-you-actually-need/","summary":"Most tutorials show the trivial \u003ccode\u003ewith open(...)\u003c/code\u003e example and stop. Here are the three patterns I actually reach for in real code, and the failure modes each one prevents.","title":"Python Context Managers: The Three Cases You Actually Need"},{"content":"The first time you see Postgres EXPLAIN ANALYZE output, it looks like a Christmas tree of numbers. Most of them are noise for the question you actually have: why is this query slow?\nHere\u0026rsquo;s the order I read it in after a few hundred of these.\nA query to anchor on EXPLAIN (ANALYZE, BUFFERS) SELECT u.id, u.email, count(o.id) AS order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.signup_at \u0026gt; now() - interval \u0026#39;30 days\u0026#39; GROUP BY u.id; A typical output snippet:\nHashAggregate (cost=12345.67..23456.78 rows=10000 width=48) (actual time=412.331..480.219 rows=8742 loops=1) -\u0026gt; Hash Right Join (cost=2345.67..11234.56 rows=120000 width=44) (actual time=18.221..380.115 rows=98213 loops=1) ... Buffers: shared hit=18234 read=4521 That\u0026rsquo;s enough output to learn the technique on.\nStep 1: Look at the top line\u0026rsquo;s actual time The outermost node\u0026rsquo;s second actual time is the wall-clock cost of the whole query (in ms, for one execution of that node). In the example above: 480 ms. Everything else in the plan is breaking down where those 480 ms went.\nIf the top number is fine and you\u0026rsquo;re chasing a slow query report, double-check that you\u0026rsquo;re explaining the same query the application runs. Different parameter values produce different plans.\nStep 2: Compare rows= estimate vs actual Each line has two row counts:\nrows=N in the cost=… part — the planner\u0026rsquo;s estimate rows=N in the actual time=… part — what really happened When these disagree by 10× or more, the planner is operating on bad statistics, and every node above it has been chosen using a wrong assumption. That\u0026rsquo;s almost always your bug.\nIn the example above the planner expected the join to produce 120,000 rows and it produced 98,213. That\u0026rsquo;s fine, ~20% off. But if I saw something like estimate 100, actual 1,000,000 — full stop, that\u0026rsquo;s the problem. Common causes:\nStale statistics → run ANALYZE the_table and re-explain. Correlated columns → set CREATE STATISTICS on the column pair, or rewrite the predicate. Skewed data the planner can\u0026rsquo;t model → sometimes you need a hint via pg_hint_plan or query rewrite. Step 3: Find where the time actually went Each node\u0026rsquo;s actual time=A..B loops=L reads as: \u0026ldquo;first row produced at A ms after start, last row produced at B ms; this node ran L times.\u0026rdquo;\nTo get the time spent in just this node (excluding children), you have to subtract the children\u0026rsquo;s actual time ranges. For the common case of loops=1, the simple version is:\nSelf time ≈ this node\u0026rsquo;s B − sum of children\u0026rsquo;s B values\nI scan the plan top-down looking for the node with the biggest self time. That\u0026rsquo;s where the optimization budget should go.\nA node with loops=N where N is large (a Nested Loop inner side, for example) reports per-loop times. Multiply by loops to get total.\nStep 4: Use BUFFERS to tell I/O from CPU EXPLAIN (ANALYZE, BUFFERS) adds lines like:\nBuffers: shared hit=18234 read=4521 shared hit — pages already in Postgres\u0026rsquo; buffer cache. Cheap. shared read — pages fetched from the OS / disk. Expensive. temp written / read — sorts or hashes that didn\u0026rsquo;t fit in work_mem and spilled to disk. Also expensive. If read dominates, the plan is fine but the data isn\u0026rsquo;t in cache. Run the query twice — the second run is more representative of steady state. If both runs are slow with high read, the working set doesn\u0026rsquo;t fit and you need either more RAM, an index that touches fewer pages, or a smaller query.\nIf you see temp written appearing on a Sort or Hash node, bump work_mem for that session and re-explain. A spill to disk can easily multiply a node\u0026rsquo;s time by 10×.\nThe three patterns I see most often After all that, the actual bugs cluster into a few shapes:\n1. Sequential scan on a \u0026ldquo;should-be-indexed\u0026rdquo; column. Plan node says Seq Scan on big_table Filter: (...) and rows-removed-by-filter is huge. Add an index on the filter column. Don\u0026rsquo;t add it if the table is small or the filter is non-selective; the planner\u0026rsquo;s choice was correct.\n2. Nested Loop where Hash Join was expected. Inner side runs thousands of times. Almost always caused by row estimate being too low upstream (Step 2 problem). Fix the stats or rewrite the predicate, then the planner picks the right join.\n3. Aggregate over a join that should be filtered first. The plan joins everything and then filters. Push the filter into a subquery or CTE so it applies before the join, reducing the row count the join has to handle.\nA quick reading checklist When someone hands me an EXPLAIN ANALYZE, I do this in order:\nTotal time at the top — is it actually slow? Any node with rows estimate vs actual off by ≥10×? — that\u0026rsquo;s the bug. Which node has the biggest self-time? — that\u0026rsquo;s the budget. Any temp written or unusually high shared read? — I/O issue. Match the slow node to one of the three patterns above. That\u0026rsquo;s it. It\u0026rsquo;s not magic, but the order matters — chasing the biggest self-time before checking the row estimates leads you to optimize the symptom of a bad plan rather than fixing the plan itself.\n","permalink":"https://dibi8.com/posts/reading-explain-analyze-in-postgres-without-getting-lost/","summary":"EXPLAIN ANALYZE output looks intimidating until you know which three numbers actually matter. Here\u0026rsquo;s the order I read them in, and the patterns that point to specific bugs.","title":"Reading EXPLAIN ANALYZE in Postgres Without Getting Lost"},{"content":"This is a personal notebook of working write-ups on backend engineering, systems internals, and the parts of modern web development that I keep forgetting and have to look up again.\nPosts are written when I have something concrete to say — a debugging session that taught me something, a benchmark with a surprising result, or a feature I had to build from scratch and want to remember next time.\nIf anything you read here turns out to be wrong, please tell me. I\u0026rsquo;d rather correct it than leave bad information online.\n","permalink":"https://dibi8.com/about/","summary":"About this site","title":"About"}]