[{"content":"WiFi 공격을 전통적인 방식으로 배워본 적이 있다면, 흐름은 대개 이렇습니다: 모니터 모드와 패킷 인젝션을 지원하는 USB 무선 어댑터를 주문하고, 한나절 동안 Linux 드라이버와 씨름하고, 본인 소유의 테스트 AP를 세팅하고, 그제서야 진짜로 배우려던 공격 연습을 시작합니다. WiFi-Forge는 이 모든 사전 준비를 통째로 건너뛰게 해줍니다.\nWiFi-Forge는 Black Hills InfoSec에서 공개한 오픈소스 프로젝트로, 무선 공격을 연습할 안전하고 합법적인 환경을 제공합니다. 하드웨어를 살 필요가 없고, 남의 네트워크를 건드릴 위험도 없으며, 드라이버를 망가뜨릴 일도 없습니다. 가상 랩을 띄우면 곧바로 해킹 연습을 시작할 수 있습니다.\n왜 필요한가 —— WiFi 학습의 세 가지 함정 기존 WiFi 실습 학습이 사람을 떨어뜨리는 세 가지 이유:\n하드웨어 복불복. 모든 USB 어댑터가 모니터 + 인젝션을 깔끔하게 지원하지는 않습니다. 검증된 모델(Alfa AWUS036, Panda PAU09 등)은 30~60 달러이며, 한 번에 한 개만 사용할 수 있습니다. 법적 회색지대. 대부분 국가에서 본인 소유가 아닌 네트워크를 건드리는 것은 —— 단순히 패시브 스니핑조차 —— 모두 위법입니다. \u0026ldquo;그냥 보기만 했다\u0026rdquo; 는 변명이 되지 않습니다. 롤백 비용. 실제 하드웨어는 한 번의 명령으로 초기화되지 않습니다. 망가진 설정을 git checkout으로 되돌릴 수 없습니다. WiFi-Forge는 이 세 가지를 노트북 위의 단일 샌드박스로 통합합니다.\n내부 구조 WiFi-Forge는 mininet-wifi 위에 구축되었습니다 —— 802.11 네트워크 에뮬레이터로, Linux 네트워크 네임스페이스 내부에 가상 AP, 스테이션, \u0026ldquo;전파\u0026quot;를 만들어냅니다. 모든 AP와 클라이언트는 실제 Linux 프로세스이므로 iwconfig, airodump-ng, tcpdump는 물론 Reaver, Hashcat까지 시뮬레이션된 트래픽에 그대로 적용 가능하며, 표준 도구들이 실제 전파를 다루는 것처럼 동작합니다.\nWiFi-Forge가 그 위에 더하는 것: 미리 만들어진 토폴로지, 바로 실행되는 공격 시나리오, 그리고 매번 네트워크를 처음부터 설계할 필요가 없는 가이드 구조.\n무엇을 연습할 수 있나 번들된 랩은 일반적인 WiFi 공격 카테고리를 망라합니다:\nWPA/WPA2 핸드셰이크 캡처 —— 클라이언트를 deauth하고, 4-way handshake를 캡처하고, hashcat이나 aircrack-ng로 오프라인 크래킹 WPS 공격 —— Reaver PIN 무차별 대입, Pixie-Dust 공격 Evil-twin / Karma —— 타깃 SSID를 흉내낸 악성 AP를 띄우고 클라이언트가 자동 연결되는 모습 관찰 Deauth 플러드 —— 합법 AP에서 클라이언트를 떨어뜨리기 Beacon 플러딩 —— 가짜 AP 수천 개를 살포해 스캐너 혼란시키기 MAC 무작위화 분석 —— 최신 기기들이 신원을 어떻게 숨기는지 (그리고 어디서 노출되는지) 관찰 PMKID 공격 —— 클라이언트가 연결되어 있지 않아도 핸드셰이크 캡처 각 랩은 특정 토폴로지를 부팅하고 셸을 제공한 뒤, 작은 CTF 스타일의 목표를 부여합니다.\n시작하기 git clone https://github.com/blackhillsinfosec/WifiForge cd WifiForge sudo ./install.sh sudo python3 wififorge.py Linux(Ubuntu 또는 Debian 권장), Python 3, root 권한이 필요합니다(mininet-wifi가 커널 기능을 사용함). 설치 스크립트가 의존성을 자동으로 처리합니다 —— mininet-wifi, aircrack-ng, hashcat, reaver 등.\n누구에게 적합한가 OSCP / OSWP 수험생 —— 시험 랩과 유사한 시나리오를 하드웨어 없이 연습 CTF 출제자 —— 실제 무전기 없이 무선 챌린지를 빠르게 구성 보안 교육자 —— 모든 학생에게 몇 초 만에 초기화되는 독립적인 랩 제공 호기심 있는 개발자 —— 4-way handshake가 실제로 어떻게 작동하는지 디버거 붙여 한 단계씩 추적 ⚠️ WiFi-Forge로 배울 수 없는 것: 물리 계층 —— RF, 안테나 선택, 실제 신호 감쇠. 이것은 결국 진짜 카드가 필요합니다. 그러나 실전 공격의 90%가 발생하는 프로토콜 계층에서는 시뮬레이션과 실제 트래픽이 사실상 구별 불가능합니다.\n합법성과 윤리에 관한 한마디 이런 프로젝트는 직접적으로 말하는 것이 중요합니다: 본인이 소유하거나 명시적으로 서면 허가를 받은 네트워크에서만 이 기술을 사용하세요. WiFi-Forge가 존재하는 이유는, 시뮬레이션 랩이 있기에 옆 카페 WiFi에 \u0026ldquo;한 번 해볼까\u0026quot;라는 유혹이 사라지기 때문입니다. 안전하게 배우는 것 —— 그것이 핵심입니다.\n저장소: github.com/blackhillsinfosec/WifiForge 기반: mininet-wifi 관리: Black Hills InfoSec ","permalink":"https://dibi8.com/ko/posts/wifi-forge-wifi-%ED%95%B4%ED%82%B9%EC%9D%84-%EC%95%88%EC%A0%84%ED%95%98%EA%B3%A0-%ED%95%A9%EB%B2%95%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EC%83%8C%EB%93%9C%EB%B0%95%EC%8A%A4/","summary":"\u003cp\u003eWiFi 공격을 전통적인 방식으로 배워본 적이 있다면, 흐름은 대개 이렇습니다: 모니터 모드와 패킷 인젝션을 지원하는 USB 무선 어댑터를 주문하고, 한나절 동안 Linux 드라이버와 씨름하고, 본인 소유의 테스트 AP를 세팅하고, \u003cem\u003e그제서야\u003c/em\u003e 진짜로 배우려던 공격 연습을 시작합니다. \u003cstrong\u003eWiFi-Forge\u003c/strong\u003e는 이 모든 사전 준비를 통째로 건너뛰게 해줍니다.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"WiFi-Forge 배너\" loading=\"lazy\" src=\"https://github.com/her3ticAVI/MiniNet-framework/raw/main/images/WifiForgeVersion2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/blackhillsinfosec/WifiForge\"\u003eWiFi-Forge\u003c/a\u003e는 \u003ca href=\"https://www.blackhillsinfosec.com/\"\u003eBlack Hills InfoSec\u003c/a\u003e에서 공개한 오픈소스 프로젝트로, 무선 공격을 연습할 \u003cstrong\u003e안전하고 합법적인\u003c/strong\u003e 환경을 제공합니다. 하드웨어를 살 필요가 없고, 남의 네트워크를 건드릴 위험도 없으며, 드라이버를 망가뜨릴 일도 없습니다. 가상 랩을 띄우면 곧바로 해킹 연습을 시작할 수 있습니다.\u003c/p\u003e","title":"WiFi-Forge — WiFi 해킹을 안전하고 합법적으로 배우는 샌드박스"},{"content":"본 글은 Code Vault에 현재 공개된 7개 코드 스니펫을 한 번에 정리한 것입니다 — 암호화폐 트레이딩 레이더, 자율 트레이딩 시스템, 보안 도구를 다루는 개인 코드 저장소이며, 모두 순수 Python으로 작성되었고 API 비용은 0 또는 거의 0입니다. 각 스니펫 아래에 전체 소스 코드가 포함되어 있어 그대로 읽고 Fork하여 로컬에서 실행할 수 있습니다. 오른쪽 목차에서 원하는 도구로 바로 이동할 수 있습니다.\n⚠️ 위험 경고 — 이 도구들은 실시간 시세와 온체인 데이터를 다루며, 일부는 Telegram으로 실시간 알림을 보냅니다. 그 중 \u0026ldquo;AI 자율 트레이딩\u0026quot;은 바이낸스 선물에서 가상 포지션을 엽니다. 각 도구의 설명을 반드시 자세히 읽으세요. 사용에 따른 위험은 본인 책임이며, 저자는 트레이딩 결과에 대해 어떠한 보증도 하지 않습니다.\n트레이딩 레이더 Vitalik 매도 레이더 게시일: 2026.05.02　태그: Python · WebSocket · Ethereum · Telegram · Event-Driven\nGitHub: vitalik-sell-radar\nWebSocket 이벤트 기반 · Vitalik 지갑 매도 감지 · Telegram 초단위 푸시\nWebSocket 이벤트 구독을 통해 Vitalik Buterin의 지갑(vitalik.eth)에서 발생하는 ERC-20 토큰 매도를 실시간 감지 — 폴링 없음, 1초 미만 지연. 수신자를 자동으로 분류합니다: DEX Router(Uniswap, 1inch, SushiSwap), CEX 핫월렛(Binance, Coinbase, Kraken), LP 풀. DexScreener에서 실시간 토큰 가격 조회. 다중 RPC 페일오버 + 자동 재연결. 순수 Python, 비용 0 — 무료 공개 RPC 노드 사용.\n전체 소스 코드 #!/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()) 온체인 내러티브 레이더 게시일: 2026.04.28　태그: Python · GMGN · DEXScreener · Telegram\n모멘텀 기반 · 온체인 신규 토큰 발견 · ETH/SOL/BSC/Base 커버\n모멘텀이 유일한 푸시 엔진이고, 내러티브는 단지 분류 라벨입니다. 4개 체인을 30초마다 스캔합니다. 토큰은 시가총액이 3회 연속 상승하고 누적 ≥5% 증가해야만 알림이 발동됩니다. 내러티브(머스크/트럼프, 바이낸스/CZ, 셀럽)는 ★★★/★★/★ 라벨로 분류되지만 단독으로 푸시를 트리거하지는 않습니다. 안전 점검은 SOL은 RugCheck, EVM은 GoPlus를 사용. 순수 Python, AI 비용 0.\n전체 소스 코드 #!/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 + 펀딩비 스캐너 게시일: 2026.04.25　태그: Python · Binance Futures · Telegram\n펀딩비 양→음 전환 감지 + OI 급증\n스냅샷 기반 스캐너: 펀딩비가 양에서 음으로 뒤집히면서 동시에 OI가 상승 중인 종목을 감지합니다. 5분마다 실행.\n전체 소스 코드 #!/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() 축적 레이더 게시일: 2026.04.25　태그: Python · Binance · CoinGlass · Telegram\n모멘텀 + OI 이상 + 스마트 알림\n시간 단위 스캔: 상승률 상위 종목 모멘텀 추적, OI 이상 감지, Telegram 푸시. 순수 Python, AI 비용 0.\n전체 소스 코드 #!/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() 바이낸스 Alpha 모니터 게시일: 2026.04.23　태그: Python · Binance · Claude AI · Telegram\nWebSocket 실시간 + AI 분석 + Telegram 푸시\n바이낸스 Alpha 신규 상장을 자동 감지하고 Claude AI로 토큰 품질을 분석한 다음 Telegram으로 알림을 보냅니다.\n전체 소스 코드 #!/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 자율 트레이딩 선물 + Alpha 자율 트레이딩 v1 게시일: 2026.04.29　태그: Python · Binance Futures · Autonomous · Telegram\nAI가 시장 스캔 → 분석 → 가상 매매 → 모니터 → 복기 — 완전 자율\n⚠️ 위험 경고: 이 코드는 가상/페이퍼 트레이딩에서만 테스트되었습니다. 실거래 경험이 전혀 없으며 실거래 진입의 근거로 사용해서는 안 됩니다. 실자금 검증을 거치지 않았습니다 — 사용에 따른 위험은 본인 책임입니다.\n30초마다 바이낸스 선물 시장 전체를 자율적으로 스캔하고 이상치를 감지하여 가상 거래를 진행하는 AI. 4가지 시그널 감지 전략(극단적 음수 펀딩비 → 롱 스퀴즈, 극단적 양수 펀딩 → 숏, 급등 후 숏, 급락 반등). 포지션을 열기 전에 다차원 환경 점검(BTC 추세 + Fear\u0026amp;Greed + OI 관심도 + 거래량) 수행 — 7점 만점에 3점 이상이어야 진행. 30초마다 자동 손절/익절 모니터링.\n현재 성적(정직 공개): 청산 4건, 승률 75%, +13.94U. 그러나 수익은 한 거래에 집중(IR 숏 +45%), 나머지는 본전 수준. 포지션 분산 부족 — 같은 방향 같은 논리로 쌓는 경향. 여전히 규칙 기반 점수 매기기이며, 진정한 독립 사고가 아닙니다.\n학습 경로: 스캔 → 진입 → 청산 → 복기 → 문제 발견 → 전략 개선 → 반복. 목표: 규칙 실행기에서 독립적으로 사고하는 트레이더로 진화.\n전체 소스 코드 #!/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() 유틸리티 도구 VoiceKey — 화자 인증 게시일: 2026.04.27　태그: Python · Security · Telegram · Speaker Verification\n성문 인증으로 AI 에이전트를 보호하세요\nTelegram 봇 보안을 위한 화자 검증. 화자 임베딩 추출에 resemblyzer(GE2E 모델)를 사용하고 코사인 유사도로 일치 여부를 판단합니다. 비밀번호 없이 음성으로 인증 — 알려진 화자만 로그인 가능. 가벼운 의존성, 순수 Python.\n전체 소스 코드 #!/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() 마무리 위 7개 스니펫은 모두 connectfarm1.com에서 가져왔으며 수집 시점은 2026-05-04입니다. 세 가지 철학을 공유합니다: (1) 거의 0에 가까운 AI 비용 — 대부분 LLM 대신 규칙 엔진과 무료 공개 API를 사용; (2) 오직 Python, 저렴한 VPS에서 crontab 또는 단순한 while True로 충분히 실행 가능; (3) Telegram을 단일 출력 채널로 — 대시보드도 프론트엔드도 만들지 않고, 실제로 보는 곳으로 바로 푸시. 여러 레이더를 조합해 신호를 교차 검증하고, \u0026ldquo;자율 트레이딩\u0026quot;은 인쇄기가 아니라 연구용 샌드박스로 다루세요.\n","permalink":"https://dibi8.com/ko/posts/code-vault-7%EA%B0%9C%EC%9D%98-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90-%EB%A0%88%EC%9D%B4%EB%8D%94-%EB%B0%8F-%ED%8A%B8%EB%A0%88%EC%9D%B4%EB%94%A9-%EB%8F%84%EA%B5%AC/","summary":"\u003cp\u003e본 글은 \u003ca href=\"https://connectfarm1.com/\"\u003eCode Vault\u003c/a\u003e에 현재 공개된 7개 코드 스니펫을 한 번에 정리한 것입니다 — 암호화폐 트레이딩 레이더, 자율 트레이딩 시스템, 보안 도구를 다루는 개인 코드 저장소이며, 모두 순수 Python으로 작성되었고 API 비용은 0 또는 거의 0입니다. 각 스니펫 아래에 \u003cstrong\u003e전체 소스 코드\u003c/strong\u003e가 포함되어 있어 그대로 읽고 Fork하여 로컬에서 실행할 수 있습니다. 오른쪽 목차에서 원하는 도구로 바로 이동할 수 있습니다.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e⚠️ \u003cstrong\u003e위험 경고\u003c/strong\u003e — 이 도구들은 실시간 시세와 온체인 데이터를 다루며, 일부는 Telegram으로 실시간 알림을 보냅니다. 그 중 \u0026ldquo;AI 자율 트레이딩\u0026quot;은 바이낸스 선물에서 \u003cstrong\u003e가상 포지션\u003c/strong\u003e을 엽니다. 각 도구의 설명을 반드시 자세히 읽으세요. \u003cstrong\u003e사용에 따른 위험은 본인 책임이며, 저자는 트레이딩 결과에 대해 어떠한 보증도 하지 않습니다.\u003c/strong\u003e\u003c/p\u003e","title":"Code Vault — 7개의 오픈소스 암호화폐 레이더 및 트레이딩 도구"},{"content":"TikCoin 발견: 우리 수익을 함께 늘리는 혁신적인 방법! 방금 우리 수익을 함께 늘릴 수 있는 놀라운 방법을 발견했습니다! TikCoin에 참여하면 우리는 모두 이익을 얻습니다. 활성 파트너가 많을수록 우리의 보상이 높아집니다! 💰✨\nTikCoin은 채굴, 커뮤니티 보상 및 파트너십 인센티브를 결합하여 모든 참여자를 위한 윈윈 생태계를 만드는 최첨단 암호화폐 플랫폼입니다.\n지금 등록하고 나와 함께 채굴 시작: GRP-NBUC3PAH\nTikCoin 채굴이 다른 이유 커뮤니티 기반 보상 혼자 일하는 전통적인 채굴과 달리 TikCoin 보상은 커뮤니티 참여에 따라 증가합니다. 내 초대 코드로 참여하면 우리 둘 다 성장하는 네트워크에서 이익을 얻습니다.\n수동 소득 생성 TikCoin의 혁신적인 채굴 시스템을 통해 암호화폐 보상을 시작하세요. 우리 네트워크에 참여하는 사람이 많을수록 모든 사람의 보상이 높아집니다.\n쉽게 시작 값비싼 하드웨어가 필요하지 않습니다. 초대 링크로 등록하기만 하면 즉시 채굴을 시작할 수 있습니다.\n추천 보너스 친구와 가족을 TikCoin에 초대하여 추가 보상을 얻으세요. 우리 커뮤니티가 커질수록 우리는 모두 더 많이 벌게 됩니다.\nTikCoin 채굴 작동 방식 1단계: 초대 코드로 등록 내 독점 초대 코드 GRP-NBUC3PAH를 사용하여 TikCoin 네트워크에 참여하세요.\n2단계: 채굴 시작 플랫폼의 채굴 시스템을 통해 TikCoin 토큰을 획득하세요.\n3단계: 파트너 초대 초대 코드를 공유하여 커뮤니티를 성장시키고 보상을 늘리세요.\n4단계: 함께 수익 창출 새로운 활성 멤버가 참여할 때마다 우리의 집단 보상이 증가하는 것을 지켜보세요.\n보상 분배 활성 참여를 위한 기본 채굴 보상 네트워크 성장에 기반한 커뮤니티 보너스 새로운 멤버를 유치하는 추천 보상 특별 이벤트 보너스 및 경품 TikCoin 참여 혜택 재정적 장점 수동으로 암호화폐 획득 상당한 수익 가능성 다중 수입원 커뮤니티 혜택 성장하는 채굴자 네트워크 참여 독점 커뮤니티 이벤트 액세스 숙련된 멤버의 지원 보안 기능 안전한 지갑 통합 보호된 거래 처리 정기 플랫폼 감사 TikCoin 채굴 플랜 스타터 플랜 낮은 진입 장벽 기본 채굴 보상 커뮤니티 액세스 프로 플랜 향상된 채굴 기능 더 높은 보상률 우선 지원 엔터프라이즈 플랜 최대 채굴 성능 최고 보상 배율 VIP 커뮤니티 액세스 TikCoin 사용자 성공 사례 뉴욕의 Alex: \u0026ldquo;친구 코드로 참여한 이후 수익이 3배로 늘었습니다! 커뮤니티 측면이 훨씬 더 보람 있게 만듭니다.\u0026rdquo;\n런던의 Sarah: \u0026ldquo;TikCoin은 암호 채굴에 대한 생각을 바꿔놓았습니다. 돈을 벌는 것뿐만 아니라 함께 무언가를 만드는 것입니다.\u0026rdquo;\n도쿄의 Mike: \u0026ldquo;추천 보너스가 믿을 수 없습니다. 내가 유치하는 모든 새로운 멤버가 모든 사람의 보상을 늘립니다.\u0026rdquo;\nTikCoin 시작하기 혁명에 참여할 준비가 되셨나요? 방법은 다음과 같습니다:\n등록 링크 클릭: 지금 TikCoin 참여 초대 코드 입력: GRP-NBUC3PAH 등록 완료: 계정 설정 채굴 시작: 즉시 수익 창출 시작 다른 사람 초대: 성공 공유 내 초대 코드를 선택하는 이유? GRP-NBUC3PAH를 사용할 때 활성 채굴자의 입증된 네트워크에 참여하게 됩니다. 이는 다음을 의미합니다:\n더 높은 초기 보상 더 빠른 커뮤니티 성장 보너스 새 기능 우선 액세스 숙련된 커뮤니티 멤버의 지원 TikCoin vs 전통 채굴 기능 전통 채굴 TikCoin 채굴 하드웨어 필요 값비싼 GPU/ASIC 없음 커뮤니티 혜택 없음 높음 보상 규모 조정 고정 네트워크 증가 진입 용이성 복잡 간단 수동 소득 제한적 높음 보안 및 투명성 TikCoin은 다음을 통해 사용자 보안을 우선시합니다:\n고급 암호화 투명한 보상 계산 안전한 지갑 연결 정기 보안 업데이트 커뮤니티 채굴의 미래 TikCoin은 개인 성공이 집단 번영에 기여하는 암호화폐 채굴의 미래를 대표합니다. 커뮤니티가 성장함에 따라 모든 사람의 보상도 증가합니다.\n혁신적인 것의 일부가 될 이 기회를 놓치지 마세요!\n코드 GRP-NBUC3PAH로 오늘 등록\n자주 묻는 질문 TikCoin이란 무엇인가? TikCoin은 채굴 및 네트워크 참여에 대해 사용자에게 보상하는 커뮤니티 기반 암호화폐 플랫폼입니다.\n보상이 어떻게 작동하나요? 보상은 커뮤니티 참여 및 네트워크 성장에 따라 증가합니다.\nTikCoin이 안전한가요? 예, 은행급 보안 및 정기 감사로 안전합니다.\n수익을 인출할 수 있나요? 예, 수익은 언제든지 지원되는 지갑으로 인출할 수 있습니다.\n추천이 어떻게 작동하나요? 초대 코드를 공유하여 다른 사람이 참여할 때 보너스 보상을 얻으세요.\n지금 TikCoin 커뮤니티 참여\nTikCoin 참여 - 함께 더 많이 벌자! 커뮤니티 채굴의 힘이 여기 있습니다. 오늘 TikCoin에 참여하여 수익을 함께 늘리자!\nTikCoin으로 채굴 시작\n면책 조항: 암호화폐 채굴에는 위험이 따릅니다. 항상 참여하기 전에 철저한 조사를 수행하세요.\n","permalink":"https://dibi8.com/ko/posts/tikcoin-%EC%B1%84%EA%B5%B4%EC%97%90-%EC%B0%B8%EC%97%AC%ED%95%98%EC%84%B8%EC%9A%94-%EC%9A%B0%EB%A6%AC-%EC%88%98%EC%9D%B5%EC%9D%84-%ED%95%A8%EA%BB%98-%EB%8A%98%EB%A6%AC%EC%9E%90/","summary":"\u003ch2 id=\"tikcoin-발견-우리-수익을-함께-늘리는-혁신적인-방법\"\u003eTikCoin 발견: 우리 수익을 함께 늘리는 혁신적인 방법!\u003c/h2\u003e\n\u003cp\u003e방금 우리 수익을 함께 늘릴 수 있는 놀라운 방법을 발견했습니다! TikCoin에 참여하면 우리는 모두 이익을 얻습니다. 활성 파트너가 많을수록 우리의 보상이 높아집니다! 💰✨\u003c/p\u003e\n\u003cp\u003eTikCoin은 채굴, 커뮤니티 보상 및 파트너십 인센티브를 결합하여 모든 참여자를 위한 윈윈 생태계를 만드는 최첨단 암호화폐 플랫폼입니다.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tikcoin.info/?invitationCode=GRP-NBUC3PAH\"\u003e지금 등록하고 나와 함께 채굴 시작: GRP-NBUC3PAH\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"tikcoin-채굴이-다른-이유\"\u003eTikCoin 채굴이 다른 이유\u003c/h2\u003e\n\u003ch3 id=\"커뮤니티-기반-보상\"\u003e\u003cstrong\u003e커뮤니티 기반 보상\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e혼자 일하는 전통적인 채굴과 달리 TikCoin 보상은 커뮤니티 참여에 따라 증가합니다. 내 초대 코드로 참여하면 우리 둘 다 성장하는 네트워크에서 이익을 얻습니다.\u003c/p\u003e","title":"TikCoin 채굴에 참여하세요 - 우리 수익을 함께 늘리자!"},{"content":"TikChain 소개 - 블록체인으로 소셜 미디어를 재정의 소셜 미디어와 암호화폐가 역동적으로 발전하는 환경에서 TikChain은 콘텐츠 크리에이터, 시청자 및 블록체인 기술 사이의 격차를 메우는 획기적인 플랫폼으로 등장합니다. TikTok 애호가, 콘텐츠 크리에이터 또는 암호 투자자이든, TikChain은 현대 디지털 경제를 위한 혁신 솔루션을 제공합니다.\n오늘 TikChain에 참여하세요\nTikChain이란 무엇인가? TikChain은 소셜 미디어 통합을 위해 특별히 설계된 첨단 블록체인 플랫폼입니다. 고급 블록체인 기술을 활용하여 사용자가 토큰 보상과 독점 기능을 통해 소셜 미디어 존재감을 통화화할 수 있는 탈중앙화 생태계를 만듭니다.\n주요 기능: 소셜 미디어 통합: 주요 소셜 플랫폼과의 원활한 연결 토큰 보상: 콘텐츠 생성 및 참여로 토큰 획득 탈중앙화 거버넌스: 커뮤니티 기반 플랫폼 개발 보안 거래: 블록체인 기반 결제 시스템 크로스 플랫폼 호환성: 여러 소셜 미디어 네트워크에서 작동 TikChain을 선택하는 이유? 1. 콘텐츠 크리에이터 역량 강화 소셜 미디어 영향력을 유형 가치로 전환하세요. TikChain은 크리에이터가 토큰 보상과 독점 기능을 통해 콘텐츠를 통화화할 수 있게 합니다.\n2. 커뮤니티 기반 경제 참여가 플랫폼 성장과 거버넌스에 직접 기여하는 탈중앙화 경제에 참여하세요.\n3. 안전하고 투명 강력한 블록체인 기술 기반 구축으로 투명성, 보안성 및 모든 거래의 불변성을 보장합니다.\n4. 다중 플랫폼 지원 TikTok, Instagram, Twitter 및 기타 소셜 미디어 플랫폼과 원활하게 통합됩니다.\n5. 미래 지향적 기술 블록체인 혁신과 소셜 미디어 트렌드의 최첨단을 유지하세요.\nTikChain 작동 방식 단계별 가이드: 가입: 추천 링크로 계정 생성 플랫폼 연결: 소셜 미디어 계정 연결 수익 창출 시작: 참여를 통해 토큰 획득 시작 참여: 커뮤니티 이벤트 및 거버넌스 참여 수익 인출: 언제든지 수익을 지갑으로 전송 토큰 경제: 네이티브 토큰: TikChain의 플랫폼 기능 토큰 보상 시스템: 좋아요, 공유 및 콘텐츠 생성으로 토큰 획득 스테이킹 옵션: 추가 이익을 위해 토큰 잠금 추천 프로그램: 다른 사람 초대하여 보너스 보상 획득 TikChain 참여 혜택 콘텐츠 크리에이터 소셜 미디어 존재감 통화화 충성스러운 커뮤니티 구축 독점 크리에이터 도구 액세스 플랫폼 거버넌스 참여 사용자 참여로 보상 획득 프리미엄 콘텐츠 액세스 커뮤니티 이벤트 참여 안전한 디지털 자산 관리 투자자 혁신 플랫폼 초기 액세스 잠재적 토큰 가치 상승 거버넌스 참여 다각화된 암호 포트폴리오 TikChain 생태계 TikChain은 다음을 포함한 포괄적인 생태계를 만듭니다:\n콘텐츠 마켓플레이스: 디지털 콘텐츠 매매 NFT 통합: 소셜 미디어 NFT 생성 및 거래 DeFi 기능: 대출, 차입 및 yield farming 게임 요소: 게임화된 소셜 미디어 경험 TikChain 시작하기 소셜 미디어 블록체인의 미래를 탐구할 준비가 되셨나요? 다음 단계에 따라 진행하세요:\n플랫폼 방문: TikChain 등록 등록 완료: 프로필 설정 계정 연결: 소셜 미디어 프로필 연결 탐색 시작: 기능 발견 및 보상 획득 커뮤니티 참여: 토론 및 이벤트 참여 보안 및 준수 TikChain은 사용자 보안을 우선시합니다:\n종단 간 암호화 안전한 지갑 통합 정기 보안 감사 글로벌 규정 준수 소셜 미디어의 미래 소셜 미디어가 계속 진화함에 따라 TikChain과 같은 플랫폼은 차세대 디지털 상호 작용을 대표합니다. 소셜 미디어와 블록체인 기술의 최고를 결합하여 TikChain은 크리에이터, 사용자 및 투자자를 위한 새로운 기회를 창출합니다.\n오늘 TikChain의 일부가 되세요\n커뮤니티 기능 증가하는 TikChain 커뮤니티에 참여하세요:\n개발자 포럼 및 토론 팀과의 정기 AMA 커뮤니티 이벤트 및 경품 교육 리소스 및 튜토리얼 자주 묻는 질문 TikChain이 독특한 점은 무엇인가? TikChain은 소셜 미디어 참여와 블록체인 보상을 독특하게 결합하여 원활한 콘텐츠 통화화 생태계를 만듭니다.\nTikChain에서 토큰을 어떻게 얻나요? 콘텐츠 생성, 소셜 미디어 참여, 커뮤니티 참여 및 추천 프로그램을 통해 토큰을 얻습니다.\nTikChain이 안전한가? 예, 블록체인 보안, 암호화 및 정기 감사로 안전합니다.\n어떤 소셜 미디어 플랫폼이 지원되나요? TikChain은 TikTok, Instagram, Twitter, YouTube 등과의 통합을 지원합니다.\n수익을 어떻게 인출하나요? 지원되는 암호 지갑으로 인출하거나 다른 자산으로 교환할 수 있습니다.\nTikChain 여정 시작\nTikChain 혁명에 참여하세요 소셜 미디어와 블록체인을 결합한 이 혁명적인 플랫폼을 놓치지 마세요. 크리에이터, 사용자 또는 투자자이든, TikChain은 모두에게 무언가를 제공합니다.\n추천 링크로 지금 등록\n면책 조항: 암호화폐 및 블록체인 투자에는 위험이 따릅니다. 항상 참여하기 전에 철저한 조사를 수행하세요.\n","permalink":"https://dibi8.com/ko/posts/tikchain-%EB%B0%9C%EA%B2%AC-%EA%B7%80%ED%95%98%EC%9D%98-%EC%86%8C%EC%85%9C-%EB%AF%B8%EB%94%94%EC%96%B4-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%EA%B2%8C%EC%9D%B4%ED%8A%B8%EC%9B%A8%EC%9D%B4/","summary":"\u003ch2 id=\"tikchain-소개---블록체인으로-소셜-미디어를-재정의\"\u003eTikChain 소개 - 블록체인으로 소셜 미디어를 재정의\u003c/h2\u003e\n\u003cp\u003e소셜 미디어와 암호화폐가 역동적으로 발전하는 환경에서 TikChain은 콘텐츠 크리에이터, 시청자 및 블록체인 기술 사이의 격차를 메우는 획기적인 플랫폼으로 등장합니다. TikTok 애호가, 콘텐츠 크리에이터 또는 암호 투자자이든, TikChain은 현대 디지털 경제를 위한 혁신 솔루션을 제공합니다.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tikchain.network/user/luckybbjason\"\u003e오늘 TikChain에 참여하세요\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"tikchain이란-무엇인가\"\u003eTikChain이란 무엇인가?\u003c/h2\u003e\n\u003cp\u003eTikChain은 소셜 미디어 통합을 위해 특별히 설계된 첨단 블록체인 플랫폼입니다. 고급 블록체인 기술을 활용하여 사용자가 토큰 보상과 독점 기능을 통해 소셜 미디어 존재감을 통화화할 수 있는 탈중앙화 생태계를 만듭니다.\u003c/p\u003e","title":"TikChain 발견 - 귀하의 소셜 미디어 블록체인 게이트웨이"},{"content":"Billions 지갑 소개 - 암호화폐 관리 재정의 급속히 발전하는 암호화폐 세계에서 신뢰할 수 있고 기능이 풍부한 지갑을 갖는 것이 필수적입니다. Billions 지갑은 모든 디지털 자산 관리 요구를 충족하도록 설계된 포괄적인 솔루션으로 등장합니다. 숙련된 트레이더이든 암호화폐 여정을 막 시작했든, Billions 지갑은 동급 없는 보안, 기능 및 사용자 경험을 제공합니다.\n오늘 Billions 지갑 사용 시작\nBillions 지갑을 선택하는 이유? 1. 비할 데 없는 보안 기능 Billions 지갑은 고급 암호화, 다중 서명 지원 및 생체 인식 인증을 통해 자산 안전을 우선시합니다. 귀하의 암호화폐는 은행급 보안 프로토콜로 보호됩니다.\n2. 다중 자산 지원 비트코인, 이더리움, 스테이블코인 및 기타 알트코인을 포함한 다양한 암호화폐를 관리하세요. Billions 지갑은 여러 블록체인에 걸쳐 1000개 이상의 디지털 자산을 지원합니다.\n3. 사용자 친화적 인터페이스 단순성을 염두에 두고 설계된 직관적인 인터페이스는 초보자와 전문가 모두가 포트폴리오를 쉽게 탐색하고 관리할 수 있게 합니다.\n4. 고급 거래 도구 지갑 내에서 직접 암호화폐를 거래할 수 있는 내장 교환 기능으로 경쟁력 있는 요금과 빠른 실행을 제공합니다.\n5. 스테이킹 및 수익 기회 자산을 스테이킹하거나 yield farming 프로그램에 참여하여 지갑에서 수동 수입을 얻으세요.\nBillions 지갑의 주요 기능 보안 저장 하드웨어 지갑 통합 콜드 스토리지 옵션 암호화된 개인 키 복구 문구 백업 DeFi 통합 탈중앙화 거래소 액세스 유동성 풀 참여 yield farming 기회 NFT 마켓플레이스 분석 및 인사이트 실시간 포트폴리오 추적 가격 알림 및 알림 성능 분석 세금 보고 도구 Billions 지갑 시작하기 단계 1: 다운로드 및 설치 공식 웹사이트를 방문하여 선호하는 플랫폼용 Billions 지갑을 다운로드하세요 - iOS, Android, Desktop 또는 Web.\n단계 2: 계정 생성 추천 링크로 등록하여 독점 보너스와 향상된 기능을 받으세요.\nBillions 지갑 계정 생성\n단계 3: 지갑 보호 생체 인식 인증을 설정하고 안전한 위치에 복구 문구를 백업하세요.\n단계 4: 자산 추가 암호화폐를 입금하거나 통합 교환을 통해 직접 구매하세요.\n단계 5: 관리 및 거래 시작 모든 기능을 탐색하고 암호화폐 포트폴리오 최적화를 시작하세요.\nBillions 지갑 vs 전통 지갑 기능 Billions 지갑 전통 지갑 다중 자산 지원 1000+ 자산 제한적 DeFi 통합 완전 지원 없음 내장 교환 예 아니오 스테이킹 보상 고수익 낮거나 없음 보안 군사급 기본 사용자 경험 직관적 복잡 커뮤니티 및 지원 번창하는 Billions 지갑 커뮤니티에 참여하세요:\n활성 Discord 및 Telegram 그룹 포괄적인 지식 베이스 24/7 고객 지원 정기 업데이트 및 신기능 Billions 사용자 성공 사례 Alex Chen, 싱가포르: \u0026ldquo;Billions 지갑은 제 암호화폐 경험을 바꿔놓았습니다. 스테이킹 보상이 믿을 수 없어요!\u0026rdquo;\nMaria Rodriguez, 스페인: \u0026ldquo;마침내 제가 가장 좋아하는 알트코인을 지원하는 지갑을 찾았습니다. 강력 추천합니다!\u0026rdquo;\nDavid Kim, 한국: \u0026ldquo;보안 기능이 저를 안심시킵니다. 제가 사용한 최고의 지갑입니다.\u0026rdquo;\n미래 로드맵 Billions 지갑은 지속적으로 발전하며 계획된 기능은 다음과 같습니다:\n크로스 체인 상호 운용성 향상된 NFT 지원 기관급 도구 AI 기반 거래 어시스턴트 자주 묻는 질문 Billions 지갑이 안전한가요? 예, 다중 보안 레이어, 암호화 및 정기 감사로 안전합니다.\n어떤 암호화폐가 지원되나요? BTC, ETH, USDT 등을 포함한 1000개 이상의 자산.\nBillions에서 토큰을 어떻게 얻나요? 스테이킹, yield farming 및 추천 프로그램을 통해.\n모바일 앱이 있나요? 예, CH Play 및 App Store에서 다운로드 가능합니다.\n지금 Billions 지갑 다운로드\n암호화폐 지갑의 미래가 여기 있습니다 Billions 지갑은 포괄적인 기능 세트, 최고 수준의 보안 및 사용자 중심 설계로 암호화폐 애호가의 최선호 지갑이 될 것입니다.\n암호화폐 경험을 업그레이드할 기회를 놓치지 마세요. 이미 Billions 지갑의 힘을 발견한 수백만 사용자와 함께하세요.\n코드 33E57NWH45로 지금 등록\n면책 조항: 암호화폐 투자에는 위험이 따릅니다. 항상 철저한 조사를 수행한 후 투자하세요.\n","permalink":"https://dibi8.com/ko/posts/billions-%EC%A7%80%EA%B0%91-%EB%B0%9C%EA%B2%AC-%EA%B7%80%ED%95%98%EC%9D%98-%EA%B6%81%EA%B7%B9%EC%A0%81%EC%9D%B8-%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90-%EB%8F%99%EB%B0%98%EC%9E%90/","summary":"\u003ch2 id=\"billions-지갑-소개---암호화폐-관리-재정의\"\u003eBillions 지갑 소개 - 암호화폐 관리 재정의\u003c/h2\u003e\n\u003cp\u003e급속히 발전하는 암호화폐 세계에서 신뢰할 수 있고 기능이 풍부한 지갑을 갖는 것이 필수적입니다. Billions 지갑은 모든 디지털 자산 관리 요구를 충족하도록 설계된 포괄적인 솔루션으로 등장합니다. 숙련된 트레이더이든 암호화폐 여정을 막 시작했든, Billions 지갑은 동급 없는 보안, 기능 및 사용자 경험을 제공합니다.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://official.app/rc/33E57NWH45\"\u003e오늘 Billions 지갑 사용 시작\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"billions-지갑을-선택하는-이유\"\u003eBillions 지갑을 선택하는 이유?\u003c/h2\u003e\n\u003ch3 id=\"1-비할-데-없는-보안-기능\"\u003e1. \u003cstrong\u003e비할 데 없는 보안 기능\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eBillions 지갑은 고급 암호화, 다중 서명 지원 및 생체 인식 인증을 통해 자산 안전을 우선시합니다. 귀하의 암호화폐는 은행급 보안 프로토콜로 보호됩니다.\u003c/p\u003e","title":"Billions 지갑 발견 - 귀하의 궁극적인 암호화폐 동반자"},{"content":"Access Network으로 디지털 자산 처리의 미래를 발견하세요 커뮤니티의 관련 이미지와 비디오를 확인하세요:\n커뮤니티 비디오 보기\n블록체인과 암호화폐가 진화하는 환경에서 Access Network는 혁신적인 채굴 기술을 통해 사용자가 디지털 자산 처리에 참여할 수 있게 하는 혁명적인 플랫폼으로 등장합니다. 암호화폐 초보자이든 숙련된 채굴자이든 Access Network는 네트워크에 기여하고 상당한 보상을 얻을 수 있는 원활한 방법을 제공합니다.\n오늘 Access Network에 참여하세요\nAccess Network 채굴이란 무엇인가? Access Network는 고급 처리 능력을 활용하여 거래를 검증하고, 네트워크를 보호하며, 디지털 자산을 분배하는 첨단 블록체인 플랫폼입니다. 값비싼 하드웨어가 필요한 전통적인 채굴과 달리 Access Network는 클라우드 기반 처리를 사용하여 채굴을 모든 사람에게 접근 가능하게 만듭니다.\n주요 기능: 클라우드 기반 채굴: 값비싼 GPU 또는 ASIC 불필요 즉시 보상: 처리 즉시 토큰 획득 낮은 에너지 소비: 친환경 채굴 솔루션 글로벌 접근성: 전 세계 어디에서나 참여 다중 자산 지원: 다양한 디지털 자산 동시 처리 Access Network 채굴을 선택하는 이유? 1. 수동 소득 생성 지속적인 모니터링 없이 디지털 보상을 시작하세요. Access Network의 자동화 시스템이 처리를 처리하는 동안 귀하는 수익을 수집합니다.\n2. 낮은 진입 장벽 수천 달러가 드는 전통적인 채굴 장비와 달리 Access Network 채굴은 최소 투자와 기술 지식이 필요합니다.\n3. 지속 가능한 채굴 전통적인 채굴 방법보다 에너지 소비가 현저히 낮은 환경 친화적 접근 방식.\n4. 커뮤니티 주도 성장 번창하는 채굴자 커뮤니티에 참여하고 더 많은 사용자가 참여함에 따라 네트워크 효과의 혜택을 누리세요.\n5. 미래 지향적 기술 시장 변화와 기술 발전에 적응하는 차세대 블록체인 기술 기반 구축.\nAccess Network 채굴 작동 방식 단계별 과정: 가입: 초대 링크로 계정 생성 플랜 선택: 다양한 채굴 패키지 선택 처리 시작: 즉시 보상 획득 시작 수익 인출: 언제든지 보상을 지갑으로 전송 채굴 메커니즘: 처리 능력 할당: 플랜에 따라 시스템이 처리 작업 할당 보상 분배: 실시간으로 수익 계산 스테이킹 옵션: 추가 보상을 위해 토큰 잠금 추천 프로그램: 다른 사람 초대하여 보너스 보상 획득 Access Network 참여 혜택 재정적 장점 경쟁력 있는 보상률 일일 지급 복리 옵션 다중 인출 방법 기술적 혜택 사용자 친화적 인터페이스 24/7 시스템 가용성 안전한 거래 처리 고급 암호화 커뮤니티 기능 토론 포럼 교육 리소스 정기 업데이트 및 뉴스 숙련된 멤버의 지원 Access Network 채굴 시작하기 채굴 여정을 시작할 준비가 되셨나요? 다음 간단한 단계に従ってください:\n플랫폼 방문: 초대로 참여 등록 완료: 기본 정보 제공 계정 확인: 2FA로 계정 보호 계정 자금: 초기 투자 추가 채굴 시작: 처리 및 수익 시작 Access Network 채굴 플랜 스타터 플랜 최소 투자: $50 일일 보상: 1-2%% 처리 능력: 100 GH/s 프로페셔널 플랜 최소 투자: $500 일일 보상: 2-3%% 처리 능력: 1000 GH/s 엔터프라이즈 플랜 최소 투자: $5000 일일 보상: 3-5%% 처리 능력: 10000 GH/s 보안 및 투명성 Access Network는 다음과 같은 보안을 우선시합니다:\n군사급 암호화 정기 보안 감사 투명한 보상 계산 안전한 지갑 통합 채굴의 미래 전통적인 채굴이 점점 더 어렵고 비싸짐에 따라 Access Network는 디지털 자산 처리의 미래를 대표합니다. 클라우드 기술과 혁신적인 알고리즘을 활용하여 Access Network는 채굴을 대중화하면서 수익성을 유지합니다.\n자주 묻는 질문 Access Network가 다른 점은 무엇인가? Access Network는 클라우드 채굴과 블록체인 처리를 결합하여 하드웨어 요구 사항을 제거하면서 수익성을 유지합니다.\n보상이 얼마나 자주 지급되나요? 보상은 매일 계산 및 지급되며 즉시 인출이 가능합니다.\nAccess Network가 안전한가요? 예, 은행급 보안, 암호화 및 정기 감사로 안전합니다.\n언제든지 인출할 수 있나요? 예, 요청 시 인출이 즉시 처리됩니다.\n지금 채굴 여정 시작\nAccess Network 혁명에 참여하세요 디지털 자산 처리 혁명의 일부가 될 이 기회를 놓치지 마세요. Access Network로 채굴은 그 어느 때보다 쉽고 보람 있습니다.\n오늘 코드 YCM9D7로 가입하세요\n면책 조항: 디지털 자산 처리에는 위험이 따릅니다. 항상 책임감 있게 투자하고 철저한 조사를 수행하세요.\n","permalink":"https://dibi8.com/ko/posts/access-network-%EC%B1%84%EA%B5%B4%EC%97%90-%EC%B0%B8%EC%97%AC%ED%95%98%EC%84%B8%EC%9A%94-%EB%94%94%EC%A7%80%ED%84%B8-%EC%9E%90%EC%82%B0-%EC%B2%98%EB%A6%AC-%EB%B0%8F-%EB%B3%B4%EC%83%81-%ED%9A%8D%EB%93%9D/","summary":"\u003ch2 id=\"access-network으로-디지털-자산-처리의-미래를-발견하세요\"\u003eAccess Network으로 디지털 자산 처리의 미래를 발견하세요\u003c/h2\u003e\n\u003cp\u003e커뮤니티의 관련 이미지와 비디오를 확인하세요:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"채굴 이미지 1\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HHYvLD_bAAEtLCi?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"채굴 이미지 2\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HHZ2puFWUAEXRBs?format=jpg\u0026name=medium\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"채굴 이미지 3\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HGq5w8bW4AA6ShR?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"채굴 이미지 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\"\u003e커뮤니티 비디오 보기\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e블록체인과 암호화폐가 진화하는 환경에서 Access Network는 혁신적인 채굴 기술을 통해 사용자가 디지털 자산 처리에 참여할 수 있게 하는 혁명적인 플랫폼으로 등장합니다. 암호화폐 초보자이든 숙련된 채굴자이든 Access Network는 네트워크에 기여하고 상당한 보상을 얻을 수 있는 원활한 방법을 제공합니다.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://accesschain.org/?invite=YCM9D7\"\u003e오늘 Access Network에 참여하세요\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"access-network-채굴이란-무엇인가\"\u003eAccess Network 채굴이란 무엇인가?\u003c/h2\u003e\n\u003cp\u003eAccess Network는 고급 처리 능력을 활용하여 거래를 검증하고, 네트워크를 보호하며, 디지털 자산을 분배하는 첨단 블록체인 플랫폼입니다. 값비싼 하드웨어가 필요한 전통적인 채굴과 달리 Access Network는 클라우드 기반 처리를 사용하여 채굴을 모든 사람에게 접근 가능하게 만듭니다.\u003c/p\u003e","title":"Access Network 채굴에 참여하세요 - 디지털 자산 처리 및 보상 획득"},{"content":"이 흥미로운 Twitter 게시물이 제 관심을 끌었습니다:\n여기에서 전체 Twitter 게시물 보기\n이 게시물에 대해 어떻게 생각하시나요? 댓글에 의견을 공유해주세요!\n","permalink":"https://dibi8.com/ko/posts/%ED%9D%A5%EB%AF%B8%EB%A1%9C%EC%9A%B4-twitter-%EA%B2%8C%EC%8B%9C%EB%AC%BC-%ED%99%95%EC%9D%B8%ED%95%B4%EB%B3%B4%EC%84%B8%EC%9A%94/","summary":"\u003cp\u003e이 흥미로운 Twitter 게시물이 제 관심을 끌었습니다:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Twitter 이미지 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 이미지 2\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HHZ2puFWUAEXRBs?format=jpg\u0026name=medium\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Twitter 이미지 3\" loading=\"lazy\" src=\"https://pbs.twimg.com/media/HGq5w8bW4AA6ShR?format=jpg\u0026name=large\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Twitter 이미지 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\"\u003e여기에서 전체 Twitter 게시물 보기\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e이 게시물에 대해 어떻게 생각하시나요? 댓글에 의견을 공유해주세요!\u003c/p\u003e","title":"흥미로운 Twitter 게시물 - 확인해보세요"},{"content":"오늘날의 상호 연결된 글로벌 경제에서 기업은 전 세계 고객으로부터 결제를 받는 도전에 직면합니다. 전통적인 결제 방법은 종종 판매자를 특정 통화나 지역으로 제한하지만, NowPayments는 모든 통화로 결제 수락을 가능하게 함으로써 이를 혁신합니다. 고객이 미국 달러, 유로 또는 엔화와 같은 법정 화폐를 사용하든, 비트코인, 이더리움 또는 스테이블코인과 같은 암호화폐를 사용하든, NowPayments는 원활한 거래를 보장합니다.\nNowPayments란 무엇인가? NowPayments는 전통 금융과 디지털 자산 사이의 격차를 메우는 선도적인 암호화폐 결제 게이트웨이입니다. 암호화폐 결제를 모든 사람에게 접근하기 쉽게 만드는 비전을 가지고 설립된 NowPayments는 판매자에게 간단하고 안전하며 효율적인 글로벌 결제 방법을 제공합니다.\n주요 기능은 다음과 같습니다:\n범용 통화 지원: 100개 이상의 암호화폐와 주요 법정 화폐로 결제 처리 즉시 정산: 중개자 없이 지갑으로 직접 자금 수령 낮은 거래 수수료: 거래당 0.5%부터 시작하는 경쟁력 있는 요금 고급 보안: PCI DSS 준수, 다중 서명 지갑 및 암호화 쉬운 통합: WooCommerce, Shopify 등 인기 전자상거래 플랫폼용 API 및 플러그인 글로벌 결제를 위해 NowPayments를 선택하는 이유? 1. 통화 장벽 허물기 전통적인 결제 프로세서는 종종 판매자를 현지 통화나 소수의 주요 통화로 제한합니다. NowPayments는 이러한 장벽을 제거하여 통화 변환 번거로움 없이 전 세계 어디에서나 결제를 받을 수 있게 합니다.\n2. 암호화폐 채택 수용 디지털 통화가 주류 인정을 얻음에 따라 NowPayments는 귀사의 비즈니스를 이 트렌드의 최전선에 위치시킵니다. 전통 결제와 함께 비트코인, 이더리움, USDT 및 기타 인기 암호화폐를 수락하세요.\n3. 거래 비용 절감 수수료가 0.5%로 낮은 NowPayments는 2-3% 이상을 청구하는 전통 결제 프로세서에 비해 상당한 절감을 제공합니다. 또한 즉시 정산은 자금에 대한 더 빠른 접근을 의미합니다.\n4. 고객 경험 향상 고객은 선호하는 결제 방법을 선택할 수 있는 유연성을 높이 평가합니다. 신용카드, 은행 송금 또는 암호화폐를 선호하든, NowPayments는 다양한 선호도를 충족합니다.\n5. 비즈니스 미래 보장 세계가 디지털 금융으로 이동함에 따라 NowPayments 통합은 귀사의 비즈니스가 경쟁력을 유지하고 새로운 결제 트렌드에 대비할 수 있도록 보장합니다.\nNowPayments 시작하기 시작하는 것은 간단합니다:\n가입: NowPayments에서 무료 계정 생성 플랜 선택: Starter, Business 또는 Enterprise 플랜 중 선택 통합: WooCommerce, Shopify 등용 API 또는 플러그인 사용 결제 수락 시작: 즉시 결제 수령 시작 실제 적용 사례 NowPayments는 다음과 같은 결제를 지원합니다:\n전자상거래 상점: 글로벌 주문을 받는 온라인 소매업체 프리랜서: 국제 고객으로부터 결제를 받는 전문가 게임 회사: 게임 내 구매 및 구독 비영리 단체: 전 세계 지지자로부터 기부 소프트웨어 회사: SaaS 구독 및 라이선스 수수료 보안 및 준수 NowPayments는 다음과 같은 보안을 우선시합니다:\n종단 간 암호화 자금 냉장 보관 정기 보안 감사 국제 규정 준수 결제의 미래는 여기 있습니다 암호화폐 채택이 증가함에 따라 NowPayments와 같은 결제 게이트웨이는 디지털 시대에 번영하고자 하는 기업에 필수적입니다. 오늘 뒤처지지 마세요 – 결제의 미래를 수용하세요.\n모든 통화로 결제를 수락할 준비가 되셨나요? 지금 NowPayments 계정을 생성하고 글로벌 결제를 즉시 받기 시작하세요.\n계정 생성\n원활하고 국경 없는 결제의 혜택을 이미 누리고 있는 수천 명의 판매자와 함께하세요.\n","permalink":"https://dibi8.com/ko/posts/%EB%AA%A8%EB%93%A0-%ED%86%B5%ED%99%94%EB%A1%9C-%EA%B2%B0%EC%A0%9C-%EC%88%98%EB%9D%BD-nowpayments/","summary":"\u003cp\u003e오늘날의 상호 연결된 글로벌 경제에서 기업은 전 세계 고객으로부터 결제를 받는 도전에 직면합니다. 전통적인 결제 방법은 종종 판매자를 특정 통화나 지역으로 제한하지만, NowPayments는 \u003cstrong\u003e모든 통화로 결제 수락\u003c/strong\u003e을 가능하게 함으로써 이를 혁신합니다. 고객이 미국 달러, 유로 또는 엔화와 같은 법정 화폐를 사용하든, 비트코인, 이더리움 또는 스테이블코인과 같은 암호화폐를 사용하든, NowPayments는 원활한 거래를 보장합니다.\u003c/p\u003e\n\u003ch2 id=\"nowpayments란-무엇인가\"\u003eNowPayments란 무엇인가?\u003c/h2\u003e\n\u003cp\u003eNowPayments는 전통 금융과 디지털 자산 사이의 격차를 메우는 선도적인 암호화폐 결제 게이트웨이입니다. 암호화폐 결제를 모든 사람에게 접근하기 쉽게 만드는 비전을 가지고 설립된 NowPayments는 판매자에게 간단하고 안전하며 효율적인 글로벌 결제 방법을 제공합니다.\u003c/p\u003e","title":"모든 통화로 결제 수락 - NowPayments"},{"content":"RR6958에 오신 것을 환영합니다 - 온라인 엔터테인먼트의 정점 2026년, 온라인 카지노 산업이 계속 번창함에 따라 RR6958은 세계적 수준의 게임 경험을 제공하는 최고의 목적지로 두드러집니다. 10년 이상의 전문성으로 RR6958은 전 세계 수백만 플레이어를 위해 공정하고 투명하며 고도로 재미있는 플랫폼을 제공하며 수천 가지 다양한 게임과 24/7 최고 수준의 고객 서비스를 제공합니다.\n지금 등록하여 100%% 보너스 청구\nRR6958을 선택하는 이유? 1. 수백 가지 흥미진진한 카지노 게임 RR6958은 비교할 수 없는 게임 경험을 제공합니다:\n매력적인 딜러와 라이브 바카라 모든 선호도에 맞는 룰렛 변형 간단한 규칙과 높은 승리로 블랙잭 거대한 잭팟이 있는 슬롯 머신 포커, 시크보, 드래곤 타이거 등 2. 관대한 보너스와 프로모션 첫 입금 보너스: 초기 입금에 최대 200%% 주간 캐시백: 레벨에 따라 5-15%% 충성도 프로그램: 실제 돈으로 변환되는 포인트 특별 이벤트: 휴일 동안 이중 보너스 3. 절대적인 보안과 공정성 모든 데이터를 보호하는 SSL 256비트 암호화 국제 기관에서 인증한 RNG 1-5분 내 초고속 지급 전문 팀의 24/7 지원 4. 사용자 친화적 인터페이스 모바일 플레이를 위한 반응형 디자인 영어를 포함한 다국어 지원 라이브 채팅 지원 iOS/Android 편리한 모바일 앱 RR6958에 등록하는 방법 시작하는 것은 간단합니다 - 단 3단계:\n등록 페이지 방문: RR6958에서 등록 세부 정보 입력: 이름, 이메일, 전화번호 확인 및 입금: 즉시 보너스 받기 RR6958의 다양한 결제 방법 RR6958은 베트남의 모든 인기 결제 옵션을 지원합니다:\n베트남 은행 송금 (BIDV, Vietcombank, Techcombank) 전자 지갑 (Momo, ZaloPay, ViettelPay) 전화 카드 (Vinaphone, Mobifone, Viettel) 암호화폐 (비트코인, 이더리움, USDT) 대면 결제를 위한 현지 에이전트 RR6958에서 플레이하는 독점 혜택 프리미엄 게임 경험 번개 같은 속도를 위한 베트남 서버 4K 그래픽과 몰입형 사운드 필리핀 출신 실제 딜러 중단 없는 원활한 게임 플레이 지속적인 프로모션 프로그램 모든 회원을 위한 일일 보너스 주말 3배 보너스 적극적인 플레이어를 위한 월간 보상 독점 이벤트와 토너먼트 5성급 고객 서비스 핫라인: 1900 XXX XXX (무료 통화) 이메일: support@rr6958.com 24/7 라이브 채팅 베트남어를 구사하는 지원 직원 RR6958 - 베트남 플레이어의 넘버 1 선택 10년 이상의 개발 끝에 RR6958은 전 세계 수백만 플레이어를 자랑스럽게 서비스하며 다음과 같습니다:\n전 세계 1000만+ 회원 모든 카테고리 1000+ 게임 월별 지급 수십억 잭팟 99.9%% 지급 성공률 오늘 RR6958 커뮤니티에 참여할 기회를 놓치지 마세요!\n무료 등록 및 200%% 보너스 청구\nRR6958 2026 인기 게임 VIP 바카라 프로페셔널 플레이어를 위한 프리미엄 게임, 베팅의 최대 9배 지급.\n유럽 룰렛 높은 승률과 매력적인 보너스가 있는 클래식 운명의 바퀴.\n잭팟 슬롯 수백 대의 슬롯 머신이 백만 달러 잭팟을 치기를 기다리고 있습니다.\nRR6958에서 안전한 도박 팁 항상 책임감 있게 플레이하고 예산을 초과하지 마세요 일일 프로모션 확인 전략을 배우기 위해 회원 그룹 참여 추가 보너스를 위한 추천 코드 사용 추천 코드 vbx2083으로 등록\n자주 묻는 질문 RR6958이 베트남에서 합법적인가요? RR6958은 국제 라이선스 하에 운영되며 베트남 데이터 보호법을 준수합니다.\n출금에 얼마나 걸리나요? 출금은 1-5분 내에 처리됩니다.\n모바일 앱이 있나요? 예, CH Play와 App Store에서 다운로드 가능합니다.\n첫 입금 보너스는 무엇인가요? 첫 입금에 200%% 보너스, 즉시 적용.\n200%% 보너스로 지금 등록\n결론 - RR6958은 온라인 카지노의 미래입니다 2026년 디지털 시대에 RR6958은 첨단 기술과 탁월한 서비스로 계속 선두를 달리고 있습니다. 지금 참여하여 차이를 경험하세요!\nRR6958 참여 - 베트남 최고의 카지노\n참고: 책임감 있게 플레이하세요. RR6958은 건강한 게임 습관을 장려합니다.\n","permalink":"https://dibi8.com/ko/posts/rr6958-%EC%86%8C%EA%B0%9C-%EC%B5%9C%EA%B3%A0%EC%9D%98-%EC%98%A8%EB%9D%BC%EC%9D%B8-%EC%B9%B4%EC%A7%80%EB%85%B8-%EA%B2%BD%ED%97%98/","summary":"\u003ch2 id=\"rr6958에-오신-것을-환영합니다---온라인-엔터테인먼트의-정점\"\u003eRR6958에 오신 것을 환영합니다 - 온라인 엔터테인먼트의 정점\u003c/h2\u003e\n\u003cp\u003e2026년, 온라인 카지노 산업이 계속 번창함에 따라 RR6958은 세계적 수준의 게임 경험을 제공하는 최고의 목적지로 두드러집니다. 10년 이상의 전문성으로 RR6958은 전 세계 수백만 플레이어를 위해 공정하고 투명하며 고도로 재미있는 플랫폼을 제공하며 수천 가지 다양한 게임과 24/7 최고 수준의 고객 서비스를 제공합니다.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://gioithieu.rr6958.com/?referralCode=vbx2083\"\u003e지금 등록하여 100%% 보너스 청구\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"rr6958을-선택하는-이유\"\u003eRR6958을 선택하는 이유?\u003c/h2\u003e\n\u003ch3 id=\"1-수백-가지-흥미진진한-카지노-게임\"\u003e1. \u003cstrong\u003e수백 가지 흥미진진한 카지노 게임\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eRR6958은 비교할 수 없는 게임 경험을 제공합니다:\u003c/p\u003e","title":"RR6958 소개 - 최고의 온라인 카지노 경험"},{"content":"Python 웹 스크래핑은 대략 네 시대를 거쳐 왔습니다. urllib과 정규식. 그다음 requests + BeautifulSoup. 그다음 진지한 작업은 전부 Scrapy. 그리고 웹의 절반이 JavaScript-only로 넘어가면서 앞의 셋이 절벽 아래로 떨어지고 Playwright가 그 자리를 차지했습니다.\nScrapling은 이 스택 위에 또 한 층을 쌓으려는 비교적 새로운 라이브러리 중 하나입니다. 간단한 페이지, JS가 무거운 페이지, 그리고 봇 차단으로 보호된 페이지까지 — 세 가지 케이스를 한 라이브러리에서 다루고, 세 가지 다른 라이브러리를 꿰매 붙이지 않아도 되게 만드는 것을 목표로 합니다.\n저는 이 프로젝트와 벤치마크, API를 한 차례 훑어봤습니다. 아래는 무엇이 진짜 흥미로운지, 무엇을 조심해야 하는지, 그리고 명백한 대안 대신 이걸 선택할 만한 상황에 대한 정리입니다.\n한 문장 정의 Scrapling은 Python 3.10+ 스크래핑 프레임워크로, 세 가지 다른 페치 백엔드 — TLS 핑거프린트 위장이 들어간 일반 HTTP, 안티 디텍션이 적용된 stealth 모드 브라우저, 그리고 풀 Playwright 브라우저 — 를 동일한 셀렉터 API 뒤에 감싸 놓은 것입니다. 라이선스는 BSD-3-Clause.\n저장소의 태그라인은 \u0026ldquo;Effortless Web Scraping for the Modern Web\u0026rdquo; 인데, 이런 류의 문구는 모든 스크래핑 라이브러리가 합니다. 더 유용한 표현은 이쪽입니다: Scrapy의 spider 모델 + curl_cffi 의 TLS 핑거프린팅 + 디텍션을 피하는 Playwright를 한 번의 import 안에 합치려고 한 것.\n세 단계 페처 모델 이 부분이 디자인에서 진짜 잘 만들어졌다고 느낀 곳입니다. 대부분 의 스크래핑 프로젝트는 결국 누더기가 됩니다 — 빠른 페이지엔 requests, JS가 무거운 페이지엔 Selenium이나 Playwright, 보호된 페이지엔 직접 짠 CDN 우회 코드. Scrapling은 이걸 세 계층으로 분리하면서도 응답 객체 모양은 통일해 둡니다:\nFetcher 백엔드 사용 시점 Fetcher TLS 핑거프린트 위장이 적용된 일반 HTTP 정적 HTML, 진짜 브라우저 불필요, 빠르게 StealthyFetcher 안티 디텍션 패치가 들어간 헤드리스 브라우저 Cloudflare/JS 보호 페이지, 진짜 브라우저가 필요할 때 DynamicFetcher Playwright/Chromium 풀 자동화 SPA, 복잡한 인증 플로우, JS로 렌더링되는 데이터 같은 Spider 클래스 안에서 요청마다 다른 계층을 지정할 수 있습니다. README의 예제를 보면:\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) 이게 왜 중요한지: 실제 크롤에서 무거운 백엔드가 정말 필요한 페이지는 일부에 불과합니다. 하지만 도중에 다시 짜기 귀찮아서 보통은 모든 페이지에 브라우저 비용을 그대로 지불하게 됩니다. 하나의 spider가 계층을 섞어 쓸 수 있다는 건, 평균 페이지 비용을 낮춰준다는 뜻입니다.\n벤치마크, 약간의 의심과 함께 README는 5,000개의 중첩 요소를 파싱하는 숫자를 공개합니다:\n라이브러리 시간 상대값 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× 여기서 정직한 두 가지 해석:\n예, BeautifulSoup은 실제로 그만큼 느립니다. 오타가 아닙니다. BS4는 사용성 우선 라이브러리이고, 많은 문서를 빡빡한 루프로 도는 작업에서는 lxml 기반 파서(Scrapling, Parsel, raw lxml 모두) 가 몇 자릿수 빠릅니다. 이건 알려진 결과지, Scrapling의 고유한 발견은 아닙니다.\n아니요, 파서 레이어에서 Scrapling이 실제로 Scrapy보다 빠르진 않습니다. 표를 보세요. Scrapy가 쓰는 Parsel은 2.04 ms, Scrapling은 2.02 ms. 마이크로벤치마크의 오차 범위 안입니다. \u0026ldquo;BS4보다 몇 자릿수 빠름\u0026quot;은 사실이지만 \u0026ldquo;Scrapy보다 빠름\u0026quot;은 파싱 레이어에서는 사실이 아닙니다.\nScrapling이 실제 워크로드에서 분명히 이기는 곳은 네트워크 레이어입니다 — Fetcher의 TLS 핑거프린트 위장 덕분에 \u0026ldquo;기본 JA3 핑거프린팅을 통과하기 위해 Playwright를 끼워 넣는\u0026rdquo; 비용을 건너뛸 수 있습니다. 절약 단위가 마이크로초가 아니라 초입니다.\n적응형 셀렉션 — 흥미로운 아이디어 스크래핑의 진짜 고통은 첫 셀렉터를 짜는 게 아니라, 3주 뒤 대상 사이트가 클래스 이름을 바꿔서 셀렉터가 망가지는 일입니다. Scrapling이 이 부분에서 내세우는 건 \u0026ldquo;smart element relocation after website changes using similarity algorithms\u0026rdquo;(유사도 알고리즘으로 사이트 변경 후 요소를 재배치) — 즉 원래 셀렉터가 실패했을 때, 라이브러리가 이전에 캡처한 구조와의 구조적 유사도를 비교해 요소를 다시 찾으려 시도한다는 뜻입니다.\n이 부분은 조심스럽게 낙관적으로 봅니다. 누구나 원하는 기능입니다. 실무에서는 외형적 변경(클래스 이름 변경, 래퍼 div 추가) 에는 유사도 기반 재배치가 잘 동작할 거고, 의미적 재구성 (데이터가 다른 페이지로 이동, 섹션 자체가 새로 템플릿화됨)에는 실패할 겁니다. \u0026ldquo;클래스 이름 하나 바뀐 것 때문에 새벽 세 시에 일어나는 걸 막아주는\u0026rdquo; 기능으로 받아들이고, \u0026ldquo;스크래퍼가 이제 스스로 유지보수된다\u0026quot;라는 기능으로 오해하지 마세요.\n가장 단순하게 동작하는 코드 문서에서 그대로 가져온, 가장 작은 예제는 이렇습니다:\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() 끝입니다. \u0026ldquo;Chrome의 TLS 핑거프린트로 페치하고 파싱하기\u0026quot;가 세 줄. impersonate='chrome' 파라미터가 curl_cffi 스타일의 핑거 프린트 위장입니다. 대상 사이트가 기본 핑거프린트 기반의 봇 디텍션을 쓸 때 유용합니다.\nCloudflare로 보호된 페이지의 경우:\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() StealthyFetcher는 별도 브라우저 설치가 필요합니다:\npip install \u0026#34;scrapling[fetchers]\u0026#34; scrapling install scrapling install 단계에서 패치된 Chromium 바이너리를 받아 옵니다. 새 서버 기준 수백 MB 의존성이라서, 작은 VM에 pip install 하기 전에 알아두는 게 좋습니다.\n명백한 대안과의 비교 필요한 것 손이 가는 도구 일회성 스크립트, 단순한 HTML, 학습 목적 requests + BeautifulSoup 큰 크롤, 잘 정의된 파이프라인, 성숙한 생태계 Scrapy 무거운 JS 앱, 복잡한 인증 플로우, 브라우저를 직접 제어 Playwright 직접 사용 TLS 핑거프린팅, 브라우저 오버헤드 없이 curl_cffi Cloudflare/Turnstile 보호된 정적-ish 페이지 cloudscraper 또는 Scrapling.StealthyFetcher 위 전부를 한 라이브러리에서 원할 때 Scrapling 가장 정직하게 말하자면: 프로젝트의 형태가 명확할 때(예: \u0026ldquo;이미 알고 있는 사이트의 1천만 개 상품 페이지를 크롤링한다\u0026rdquo;) 그 형태에 특화된 도구를 쓰세요. 큰 규모의 규율 있는 크롤에는 Scrapy가 여전히 정답이고, 진지한 브라우저 자동화에는 Playwright가 여전히 정답입니다. Scrapling의 가치는 형태가 명확하지 않은 프로젝트, 그러니까 그렇지 않으면 세 개의 스크래퍼를 동시에 유지해야 하는 프로젝트에 있습니다.\n조심해야 할 것 안티 봇 우회는 움직이는 표적입니다. README도 이 점을 솔직히 인정합니다 — 엔터프라이즈급 시스템(Akamai, DataDome, Kasada, Incapsula)은 별도 솔루션이 필요하고, 이쪽은 약속하지 않습니다. Scrapling이 실제로 타깃하는 시스템 — 특히 Cloudflare Turnstile — 에 대해서도 오늘 되는 게 다음 분기에도 된다는 보장이 없다고 생각하세요. 비즈니스가 여기에 의존한다면 \u0026ldquo;한 번 설정하고 잊자\u0026rdquo; 가 아니라 지속적인 유지보수를 예산에 넣어야 합니다.\nrobots_txt_obey는 기본값이 아니라 opt-in입니다. 의도된 설계 결정입니다(어떤 사용자는 robots 파일을 무시할 합당한 이유가 있죠 — 예: 자기 사이트를 크롤하는 경우). 하지만 이는 의식적으로 켜야 한다는 뜻이기도 합니다. 제3자 사이트에서 켜는 걸 잊으면, 기술적으로 후회하기 전에 법정에서 먼저 후회하게 됩니다.\n은밀함 ≠ 허락. 라이브러리에는 인용할 만한 면책 조항이 있습니다:\n\u0026ldquo;이 라이브러리는 교육 및 연구 목적으로만 제공됩니다. 사용함으 로써 귀하는 현지 및 국제 데이터 스크래핑 및 개인정보 보호 법률을 준수하는 데 동의합니다.\u0026rdquo;\n안티 디텍션 능력은 합법적인 케이스에 유용합니다 — 연구, 아카 이빙, 접근성 스크래핑, 권한 있는 사이트 모니터링, 데이터 내보 내기를 막는 서비스에서 자기 데이터를 빼내기. 동시에 정확히 같은 능력이 광고 사기, 콘텐츠 도용, ToS 위반에 쓰입니다. 라이브러리는 당신이 어느 쪽을 하는지 신경 쓰지 않습니다. 법원과 규제기관은 신경 씁니다. \u0026ldquo;stealthy\u0026rdquo; 기능은 전동 공구처럼 다루세요 — 유용하지만, 언제 쓰지 말아야 하는지를 알아야 합니다.\n제가 실제로 손이 갈 만한 케이스 Scrapling이 잘 어울린다고 생각하는 세 가지 구체적인 상황:\n개인 데이터 내보내기. 어떤 서비스가 당신 데이터를 가지고 있는데 진짜 export API를 안 줍니다. 진짜 브라우저로, 천천히, 상대 사이트의 레이트 리밋을 존중하면서 자기 계정을 스크랩 하기 — Scrapling의 DynamicSession이 이 일에 잘 맞습니다.\n두세 가지 보호 수준을 모두 겪는 작은 상업용 크롤. 일주일 짜리 프로젝트를 위해 Scrapy + Playwright + curl_cffi 파이프 라인을 설계하고 싶지는 않을 겁니다. Scrapling이 작동하는 프로토타입까지 더 빨리 데려다줍니다.\n연구와 아카이빙. 공개 기록 사이트, 데이터셋, 정부 포털, 뉴스 아카이브 스크랩. 대부분은 빠른 HTTP로, 몇몇 까다로운 페이지는 진짜 브라우저로 처리하고 싶은 작업.\n처음부터 손이 안 갈 상황: 한 사이트를 깊이 이해하고 몇 년을 크롤 할 거면(이런 곳에선 Scrapy의 규율이 보상해 줍니다), 또는 사이트 소유자가 명시적으로 크롤하지 말라고 하는 경우(이 문제는 어떤 라이브러리도 풀 수 없습니다).\n결론 Scrapling은 진짜로 존재하는, 잘 설계된 라이브러리입니다 — 헛바람도 아니고 과대 광고도 아닙니다. BeautifulSoup에 대한 벤치마크는 사실이고, Scrapy에 대한 벤치마크는 솔직히 말해 잡음 범위 안입니다. 진짜 승부수는 세 단계 페처를 가로지르는 통일된 인터페이스와, 브라우저가 필요 없는 케이스에는 브라우저를 띄우지 않게 해 주는 네트워크 레벨의 핑거프린팅입니다.\n스크래핑의 진짜 어려운 문제는 풀어주지 않습니다 — 그것들은 전부 법적/윤리적 측면에 있고, 어떤 라이브러리도 거기엔 도움이 안 됩니다 — 하지만 진짜 엔지니어링 문제 하나는 깔끔하게 풀어줍니다. 지금 새로운 스크래핑 프로젝트를 시작하는데 브라우저가 필요할지 아직 모르겠다면, 출발점으로 합리적인 선택입니다.\n전체 소스와 문서는 github.com/D4Vinci/Scrapling 에서 볼 수 있습니다.\n","permalink":"https://dibi8.com/ko/posts/scrapling-review/","summary":"Scrapling은 자신을 Scrapy와 BeautifulSoup의 더 빠르고 은밀한 후속작으로 포지셔닝합니다. 문서와 벤치마크를 다 본 뒤 이 라이브러리가 실제로 무엇을 제공하는지, 어디에 어울리고 어디에 어울리지 않는지를 정직하게 정리했습니다.","title":"Scrapling 리뷰: 더 빠르고 더 은밀한 Python 스크래핑"},{"content":"대부분의 튜토리얼은 평범한 with open(...) 예제를 보여주고 멈춥니다. 여기 실제 코드에서 실제로 사용하는 세 가지 패턴과 각 패턴이 방지하는 실패 모드가 있습니다.\n경우 1: 리소스 정리 및 오류 처리 무엇이든 발생하더라도 정리해야 하는 리소스가 있을 때:\nclass DatabaseConnection: def __init__(self, config): self.config = config self.connection = None def __enter__(self): self.connection = create_connection(self.config) return self.connection def __exit__(self, exc_type, exc_val, exc_tb): if self.connection: self.connection.close() 왜 이것이 중요한가 예외 상황에서도 리소스 정리 리소스 누수 방지 코드 더 신뢰할 수 있게 만들기 경우 2: 임시 상태 관리 일부 상태를 임시로 변경한 다음 복원해야 할 때:\nclass TempDir: def __init__(self, base_path=\u0026#34;/tmp\u0026#34;): self.base_path = base_path self.temp_path = None def __enter__(self): import tempfile self.temp_path = tempfile.mkdtemp(dir=self.base_path) return self.temp_path def __exit__(self, exc_type, exc_val, exc_tb): import shutil if self.temp_path and os.path.exists(self.temp_path): shutil.rmtree(self.temp_path) 왜 이것이 중요한가 임시 파일 자동 정리 디스크 공간 낭비 방지 오류 처리 간소화 경우 3: 성능 모니터링 및 타이밍 코드 실행 시간을 측정해야 할 때:\nimport time class Timer: def __enter__(self): self.start = time.time() return self def __exit__(self, exc_type, exc_val, exc_tb): self.end = time.time() print(f\u0026#34;실행 시간: {self.end - self.start:.2f} 초\u0026#34;) 왜 이것이 중요한가 코드 블록 자동 타이밍 예외 상황에서도 시간 보고 성능 모니터링 용이 주요 포인트 컨텍스트 관리자는 파일뿐만이 아닙니다 - 설정/정리가 필요한 모든 리소스에 적용 예외 안전 - __exit__ 메소드는 예외 상황에서도 항상 호출됨 조합 가능 - 여러 컨텍스트 관리자 중첩 가능 재사용 가능 - 한 번 생성, 여러 번 사용 실제 적용 이러한 패턴은 실제 코드에서 매우 유용하며, 특히 데이터베이스 연결, 임시 파일, 네트워크 연결 또는 정확한 타이밍이 필요한 작업을 처리할 때 그렇습니다.\n기억하세요: 컨텍스트 관리자는 Python에서 리소스 관리를 위한 스위스 아미 나이프입니다. 이를 사용하는 법을 배우면 코드가 더 견고하고 유지 관리하기 쉬워집니다.\n","permalink":"https://dibi8.com/ko/posts/python-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EA%B4%80%EB%A6%AC%EC%9E%90-%EC%8B%A4%EC%A0%9C%EB%A1%9C-%ED%95%84%EC%9A%94%ED%95%9C-%EC%84%B8-%EA%B0%80%EC%A7%80-%EA%B2%BD%EC%9A%B0/","summary":"\u003cp\u003e대부분의 튜토리얼은 평범한 \u003ccode\u003ewith open(...)\u003c/code\u003e 예제를 보여주고 멈춥니다. 여기 실제 코드에서 실제로 사용하는 세 가지 패턴과 각 패턴이 방지하는 실패 모드가 있습니다.\u003c/p\u003e\n\u003ch2 id=\"경우-1-리소스-정리-및-오류-처리\"\u003e경우 1: 리소스 정리 및 오류 처리\u003c/h2\u003e\n\u003cp\u003e무엇이든 발생하더라도 정리해야 하는 리소스가 있을 때:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDatabaseConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"fm\"\u003e__init__\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econnection\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"fm\"\u003e__enter__\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econnection\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecreate_connection\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econnection\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"fm\"\u003e__exit__\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eexc_type\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eexc_val\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eexc_tb\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econnection\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econnection\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eclose\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"왜-이것이-중요한가\"\u003e왜 이것이 중요한가\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e예외 상황에서도 리소스 정리\u003c/li\u003e\n\u003cli\u003e리소스 누수 방지\u003c/li\u003e\n\u003cli\u003e코드 더 신뢰할 수 있게 만들기\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"경우-2-임시-상태-관리\"\u003e경우 2: 임시 상태 관리\u003c/h2\u003e\n\u003cp\u003e일부 상태를 임시로 변경한 다음 복원해야 할 때:\u003c/p\u003e","title":"Python 컨텍스트 관리자: 실제로 필요한 세 가지 경우"},{"content":"EXPLAIN ANALYZE 출력은 실제로 중요한 세 숫자를 알기 전까지는 위협적으로 보입니다. 여기 내가 그것들을 읽는 순서와 특정 버그를 가리키는 패턴이 있습니다.\n실제로 중요한 세 숫자 1. 총 실행 시간 (Total Execution Time) Total runtime: 1234.567 ms 가장 중요한 지표입니다. 쿼리가 느리면 여기에서 알려줍니다.\n2. 실제 행 수 vs 예상 행 수 (Actual vs Estimated Rows) Seq Scan on users (cost=0.00..123.45 rows=1000 width=32) (actual time=1.234..567.890 rows=50000 loops=1) 거대한 차이는 플래너가 잘못된 가정을 했다는 것을 나타냅니다.\n3. 버퍼 히트 비율 (Buffer Hit Ratio) Buffers: shared hit=1000 read=50 높은 히트율 = 좋은 캐시 사용, 낮은 히트율 = 디스크 I/O 문제.\n읽는 순서 아래에서 시작 - 총 실행 시간 위쪽으로 작업 - 가장 비용이 많이 드는 노드 찾기 행 수 추정 확인 - 실제 vs 추정 버퍼 통계 보기 - I/O 패턴 일반적인 문제 패턴 인덱스 스캔이어야 할 때 순차 스캔 Seq Scan on large_table (cost=1000.00..2000.00 rows=100000 width=32) 해결책: 적절한 인덱스 추가\n해시 조인이어야 할 때 중첩 루프 Nested Loop (cost=1000.00..100000.00 rows=1000 width=64) -\u0026gt; Seq Scan on users -\u0026gt; Index Scan on orders 해결책: work_mem 증가 또는 쿼리 재작성\n너무 많은 버퍼 미스 Buffers: shared hit=10 read=1000 해결책: shared_buffers 증가 또는 쿼리 개선\n디스크로 정렬 오버플로 Sort Method: external merge Disk: 16384kB 해결책: work_mem 증가\n실제 적용 이러한 통찰은 내가 식별하고 수정하는 데 도움이 되었습니다:\n누락된 인덱스 비효율적인 조인 순서 메모리 부족 문제 캐시 미스 기억하세요: EXPLAIN ANALYZE는 쿼리 성능 디버거입니다. 그것을 읽는 법을 배우면 추측에 수많은 시간을 절약할 수 있습니다.\n","permalink":"https://dibi8.com/ko/posts/postgresql%EC%97%90%EC%84%9C-explain-analyze-%EC%B6%9C%EB%A0%A5-%EC%9D%BD%EA%B8%B0-%EA%B8%B8%EC%9D%84-%EC%9E%83%EC%A7%80-%EC%95%8A%EA%B2%8C/","summary":"\u003cp\u003eEXPLAIN ANALYZE 출력은 실제로 중요한 세 숫자를 알기 전까지는 위협적으로 보입니다. 여기 내가 그것들을 읽는 순서와 특정 버그를 가리키는 패턴이 있습니다.\u003c/p\u003e\n\u003ch2 id=\"실제로-중요한-세-숫자\"\u003e실제로 중요한 세 숫자\u003c/h2\u003e\n\u003ch3 id=\"1-총-실행-시간-total-execution-time\"\u003e1. \u003cstrong\u003e총 실행 시간 (Total Execution Time)\u003c/strong\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eTotal runtime: 1234.567 ms\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e가장 중요한 지표입니다. 쿼리가 느리면 여기에서 알려줍니다.\u003c/p\u003e\n\u003ch3 id=\"2-실제-행-수-vs-예상-행-수-actual-vs-estimated-rows\"\u003e2. \u003cstrong\u003e실제 행 수 vs 예상 행 수 (Actual vs Estimated Rows)\u003c/strong\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eSeq Scan on users  (cost=0.00..123.45 rows=1000 width=32) (actual time=1.234..567.890 rows=50000 loops=1)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e거대한 차이는 플래너가 잘못된 가정을 했다는 것을 나타냅니다.\u003c/p\u003e","title":"PostgreSQL에서 EXPLAIN ANALYZE 출력 읽기 - 길을 잃지 않게"},{"content":"백엔드 엔지니어링, 시스템 내부 동작, 그리고 자꾸 잊어버려서 다시 찾아보게 되는 현대 웹 개발의 자잘한 부분들에 대해 적어 두는 개인 노트입니다.\n쓸 만한 게 실제로 손에 있을 때만 글을 씁니다 —— 무언가를 배우게 된 디버깅 세션, 예상 밖의 결과가 나온 벤치마크, 혹은 처음부터 직접 만들어 보고 다음에 또 필요할 때 자기 자신에게 남기는 메모 같은 것들이요.\n여기 적힌 내용 중 잘못된 부분이 있다면 알려주세요. 잘못된 정보를 인터넷에 그대로 두는 것보다 고치는 쪽이 낫습니다.\n","permalink":"https://dibi8.com/ko/about/","summary":"이 사이트에 대하여","title":"소개"}]