#!/usr/bin/env python3 """ Polymarket Sports Liquidity Provider v6 策略:当实时比赛比分悬殊时,在 Polymarket 挂 BUY 限价单(买在卖一价), 给想早点拿到结算的人提供流动性,赚取 (1.0 - buy_price) 的差价。 v6 关键修复: 1. 模糊匹配:加入 containment 算法 —— "Canucks" 能正确匹配 "Vancouver Canucks" 2. 买在 ask 价:改用 /price?side=BUY 获取卖一价,不再挂在 midpoint(立即成交) 3. 支持 ts=3(刚结束)的比赛 —— 此时结果确定、市场未结算,价格仍 ~0.99 4. 更详细的日志:每轮打印各运动的扫描情况 触发条件: 1. 仅处理进行中 (ts=1) 或刚结束 (ts=3) 的比赛 2. 比分差 >= 各运动阈值(冰球3 / 篮球20 / 棒球5 / 美橄17 / 足球3) 3. Polymarket winning token 卖一价在 [MIN_POLY_PRICE, MAX_POLY_PRICE] 区间 """ import os import sys import time import json import threading import requests from datetime import datetime, timezone, timedelta # ─── 配置 ─────────────────────────────────────────────────────────────────── BETSAPI_KEY = os.environ.get("BETSAPI_KEY", "57608-mTenPFNVfYeXhR") TG_BOT_TOKEN = os.environ.get("TG_BOT_TOKEN", "") TG_CHAT_ID = os.environ.get("TG_CHAT_ID", "") PRIVATE_KEY = os.environ.get("POLY_KEY", "") PROXY_ADDRESS = os.environ.get("POLY_PROXY", "") # 单次买入金额 (USDC) BET_AMOUNT = 10.0 # 进行中比赛的价格区间 MIN_POLY_PRICE = 0.92 # 低于此值 → 结果仍不确定,跳过 MAX_POLY_PRICE = 0.998 # 高于此值 → 利润 < 0.2¢,跳过 # 刚结束比赛的价格区间(结果已定,市场未结算) MIN_POLY_PRICE_ENDED = 0.96 MAX_POLY_PRICE_ENDED = 0.9990 # 挂单超时:30 分钟 ORDER_TIMEOUT = 1800 # 市场最低交易量 (USDC) MIN_MARKET_VOLUME = 1000.0 # 只匹配 N 小时内结束的市场(防止匹配到未来场次) MAX_MARKET_HOURS_AHEAD = 36 # Polymarket APIs GAMMA_API = "https://gamma-api.polymarket.com" CLOB_HOST = "https://clob.polymarket.com" # 模糊匹配:每支球队的最低匹配分 FUZZY_MIN = 0.60 MIN_EACH = 0.55 # 降低阈值以支持短队名("Canucks" 匹配 "Vancouver Canucks") # ─── 各运动"比赛基本定局"的最小分差阈值 ────────────────────────────────────── MIN_MARGIN_BY_SPORT = { 18: 20, # 篮球 (NBA/NCAA): 领先 20 分 16: 5, # 棒球 (MLB): 领先 5 分 12: 17, # 美式橄榄球 (NFL): 领先 17 分 17: 3, # 冰球 (NHL): 领先 3 球 1: 3, # 足球 (Soccer): 领先 3 球 91: 5, # 排球 78: 7, # 手球 13: 99, # 网球: 不适用 6: 99, # 板球: 不适用 } # 重复下单冷却(同一 token_id 在此时间窗口内不重复下单) REBET_COOLDOWN = 7200 # 2 小时 recently_bet: dict = {} # ─── 跳过的联赛 ────────────────────────────────────────────────────────────── SKIP_LEAGUES = [ "esoccer", "e-soccer", "esport", "e-sport", "virtual", "cyber", "simulated", "volta", "gt league", "battle", "mins play", "6 mins", "12 mins", "e liga", "ebasketball", "h2h gg", "gg league", "spring training", "preseason", "pre-season", "friendly", "exhibition", ] # ─── BetsAPI 目标运动 ───────────────────────────────────────────────────────── SPORT_IDS = { 1: "Soccer", 18: "Basketball", 13: "Tennis", 91: "Volleyball", 78: "Handball", 17: "Ice Hockey", 16: "Baseball", 12: "American Football", 6: "Cricket", } # ──────────────────────────────────────────────────────────────────────────── # Telegram 通知 # ──────────────────────────────────────────────────────────────────────────── def tg(text: str): if not TG_BOT_TOKEN or not TG_CHAT_ID: return try: requests.post( f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage", json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "HTML"}, timeout=10, ) except Exception as e: print(f" ⚠️ TG 发送失败: {e}") # ──────────────────────────────────────────────────────────────────────────── # Polymarket 市场缓存 # ──────────────────────────────────────────────────────────────────────────── market_cache: list = [] cache_lock = threading.Lock() def refresh_market_cache(): global market_cache sports_keywords = [ " vs ", " vs.", "nba", "nfl", "mlb", "nhl", "ufc", "premier league", "la liga", "champions league", "world cup", "super bowl", "mavericks","celtics","knicks","nuggets","heat","hornets","clippers","spurs", "rockets","pelicans","suns","pacers","lakers","warriors","thunder","grizzlies", "nets","pistons","bulls","bucks","jazz","trail blazers","raptors","cavaliers", "magic","wizards","hawks","76ers","timberwolves","kings","oklahoma", # NHL teams (short names used in Polymarket titles) "avalanche","stars","panthers","oilers","maple leafs","bruins","rangers", "penguins","capitals","lightning","hurricanes","golden knights", "canucks","flames","senators","predators","jets","wild","coyotes", "blue jackets","sharks","ducks","kings","islanders","canadiens","sabres", "red wings","devils","flyers","blues","blackhawks","kraken", # Soccer / other "cardinals","dodgers","yankees","red sox","cubs","mets","braves","astros", "giants","phillies","padres","brewers","tigers","twins","athletics","mariners", "chiefs","eagles","cowboys","patriots","packers","rams","49ers","ravens", "broncos","bills","dolphins","chargers","raiders","colts","texans","falcons", "buccaneers","saints","bears","lions","vikings","seahawks","jaguars", "tennis","atp","wta", "series win","game 1","game 2","game 3","game 4","game 5","game 6","game 7", "will win on","win the match","oliveira","holloway","ufc 3", ] entries = [] total_scanned = 0 offset = 0 batch_size = 500 try: while True: resp = requests.get( f"{GAMMA_API}/markets", params={ "active": "true", "closed": "false", "limit": batch_size, "offset": offset, }, timeout=30, ) resp.raise_for_status() batch = resp.json() if not batch: break total_scanned += len(batch) for m in batch: title = (m.get("question") or m.get("title") or "").strip() if not title: continue title_lower = title.lower() if not any(kw in title_lower for kw in sports_keywords): continue outcomes_raw = m.get("outcomes", "[]") if isinstance(outcomes_raw, str): try: outcomes = json.loads(outcomes_raw) except Exception: outcomes = [] else: outcomes = outcomes_raw tokens_raw = m.get("clobTokenIds", "[]") if isinstance(tokens_raw, str): try: token_ids = json.loads(tokens_raw) except Exception: token_ids = [] else: token_ids = tokens_raw or [] if len(outcomes) < 2 or len(token_ids) < 1: continue volume = float(m.get("volume", 0) or m.get("volumeNum", 0) or 0) end_date_str = m.get("endDate") or m.get("end_date_iso") or "" entries.append({ "title": title, "outcomes": outcomes, "token_id": token_ids[0], "token_ids": token_ids, "slug": m.get("slug", ""), "volume": volume, "end_date": end_date_str, }) if len(batch) < batch_size: break offset += batch_size with cache_lock: market_cache = entries print( f"[{now()}] 🔄 市场缓存已更新: {len(entries)} 个体育市场" f"(扫描 {total_scanned} 个总市场)" ) except Exception as e: print(f"[{now()}] ❌ 刷新市场缓存失败 (offset={offset}): {e}") def cache_refresh_loop(): while True: refresh_market_cache() time.sleep(900) # ──────────────────────────────────────────────────────────────────────────── # 球队名称模糊匹配(v6: 加入 containment 算法) # ──────────────────────────────────────────────────────────────────────────── def normalize(s: str) -> str: import re s = s.lower() s = re.sub(r"[^a-z0-9 ]", " ", s) s = re.sub(r"\s+", " ", s).strip() # 去掉常见后缀/前缀词(含"vs"以减少干扰) noise = {"fc", "sc", "ac", "cf", "united", "city", "town", "athletic", "athletics", "club", "the", "vs"} tokens = [t for t in s.split() if t not in noise] return " ".join(tokens) def token_overlap(a: str, b: str) -> float: """ v6 改进:同时计算 Jaccard 和 containment,取较大者(乘以折扣系数)。 解决 "Vancouver Canucks" vs "Canucks" 匹配失败的问题: - Jaccard: {"canucks"} / max(2, 1) = 0.5 - Containment: {"canucks"} / min(2, 1) = 1.0 → 结果 = max(0.5, 1.0*0.8) = 0.80 ✅ """ ta = set(normalize(a).split()) tb = set(normalize(b).split()) if not ta or not tb: return 0.0 intersection_size = len(ta & tb) if intersection_size == 0: return 0.0 jaccard = intersection_size / max(len(ta), len(tb)) containment = intersection_size / min(len(ta), len(tb)) # containment 打 80% 折扣(防止单字符短词误匹配) return max(jaccard, containment * 0.8) def team_match_score(team: str, title: str, outcomes: list) -> float: best = 0.0 for candidate in [title] + (outcomes or []): s = token_overlap(team, str(candidate)) if s > best: best = s return best def find_in_cache(home: str, away: str): """ 在缓存中找最佳匹配市场。 过滤:volume >= MIN_MARKET_VOLUME,endDate 在 MAX_MARKET_HOURS_AHEAD 小时内。 返回 (entry, score, home_token_idx) 或 None。 """ best_entry = None best_score = 0.0 best_home_idx = 0 now_utc = datetime.now(timezone.utc) deadline = now_utc + timedelta(hours=MAX_MARKET_HOURS_AHEAD) with cache_lock: for entry in market_cache: if entry.get("volume", 0) < MIN_MARKET_VOLUME: continue end_date_str = entry.get("end_date", "") if end_date_str: try: end_dt = datetime.fromisoformat( end_date_str.replace("Z", "+00:00") ) if end_dt > deadline: continue except Exception: pass title = entry["title"] outcomes = entry["outcomes"] score_home = team_match_score(home, title, outcomes) score_away = team_match_score(away, title, outcomes) if score_home < MIN_EACH or score_away < MIN_EACH: continue combined = (score_home + score_away) / 2 if combined > best_score: best_score = combined best_entry = entry home_scores = [team_match_score(home, str(o), []) for o in outcomes] best_home_idx = int(home_scores[1] > home_scores[0]) if best_score >= FUZZY_MIN and best_entry: return best_entry, best_score, best_home_idx return None # ──────────────────────────────────────────────────────────────────────────── # CLOB 客户端(单例) # ──────────────────────────────────────────────────────────────────────────── _clob_client = None _clob_lock = threading.Lock() def get_clob_client(): global _clob_client if not PRIVATE_KEY: return None with _clob_lock: if _clob_client is None: try: from py_clob_client.client import ClobClient if PROXY_ADDRESS: c = ClobClient( host = CLOB_HOST, key = PRIVATE_KEY, chain_id = 137, signature_type = 1, funder = PROXY_ADDRESS, ) print(f"[{now()}] ✅ ClobClient 初始化完成(Proxy 模式: {PROXY_ADDRESS[:10]}...)") else: c = ClobClient(host=CLOB_HOST, key=PRIVATE_KEY, chain_id=137) print(f"[{now()}] ✅ ClobClient 初始化完成(EOA 模式)") c.set_api_creds(c.create_or_derive_api_creds()) _clob_client = c except Exception as e: print(f"[{now()}] ❌ ClobClient 初始化失败: {e}") return _clob_client # ──────────────────────────────────────────────────────────────────────────── # 价格查询 # ──────────────────────────────────────────────────────────────────────────── def get_clob_price(token_id: str) -> float: """获取当前 midpoint(用于判断市场确定性)""" try: resp = requests.get( f"{CLOB_HOST}/midpoint", params={"token_id": token_id}, timeout=10, ) data = resp.json() return float(data.get("mid", 0) or 0) except Exception: return 0.0 def get_best_ask(token_id: str) -> float: """ v6 新增:获取当前最优卖价(ask price)= 我们 BUY 需支付的价格。 买在 ask 价可立即成交,不会像挂在 mid 那样无人理会。 """ try: resp = requests.get( f"{CLOB_HOST}/price", params={"token_id": token_id, "side": "BUY"}, timeout=10, ) data = resp.json() return float(data.get("price", 0) or 0) except Exception: return 0.0 # ──────────────────────────────────────────────────────────────────────────── # 挂单跟踪 # ──────────────────────────────────────────────────────────────────────────── pending_orders: dict = {} pending_lock = threading.Lock() def check_pending_orders(): if not pending_orders: return client = get_clob_client() if client is None: return with pending_lock: order_ids = list(pending_orders.keys()) for order_id in order_ids: with pending_lock: if order_id not in pending_orders: continue info = pending_orders[order_id] try: order = client.get_order(order_id) status = (order or {}).get("status", "") size_matched = float((order or {}).get("sizeMatched", 0) or 0) size_amount = float((order or {}).get("size", 1) or 1) filled = ( order is None or status in ("MATCHED", "FILLED") or (status == "PARTIALLY_FILLED" and size_matched >= size_amount * 0.99) ) cancelled = status in ("CANCELLED", "CANCELED") if filled: recently_bet[info["token_id"]] = time.time() tg( f"✅ 下单成交!\n" f"市场: {info['market_title']}\n" f"押注: {info['winner']}\n" f"金额: ${info['amount']:.2f} @ {info['limit_price']:.4f}\n" f"利润空间: {(1.0 - info['limit_price'])*100:.2f}¢/token\n" f"订单ID: {order_id[:16]}..." ) print(f"[{now()}] ✅ 订单成交: {order_id[:16]}...") with pending_lock: pending_orders.pop(order_id, None) elif cancelled: recently_bet[info["token_id"]] = time.time() print(f"[{now()}] ❌ 订单被取消: {order_id[:16]}...") with pending_lock: pending_orders.pop(order_id, None) elif time.time() - info["placed_at"] > ORDER_TIMEOUT: try: client.cancel(order_id) except Exception: pass recently_bet[info["token_id"]] = time.time() tg( f"⏰ 挂单超时取消\n" f"市场: {info['market_title']}\n" f"押注: {info['winner']}\n" f"挂单价: {info['limit_price']:.4f}(30分钟未成交)" ) print(f"[{now()}] ⏰ 订单超时取消: {order_id[:16]}...") with pending_lock: pending_orders.pop(order_id, None) except Exception as e: print(f"[{now()}] ⚠️ 检查订单 {order_id[:16]} 失败: {e}") # ──────────────────────────────────────────────────────────────────────────── # 下单(GTC 限价单,买在 ask 价) # ──────────────────────────────────────────────────────────────────────────── def place_arb_bet(token_id: str, amount: float, market_title: str, winner: str, price: float): """ v6: price = best_ask(卖一价),直接挂在 ask 以立即成交。 """ if not PRIVATE_KEY: print(" ⚠️ PRIVATE_KEY 未设置,纯预警模式") return None if not token_id: print(" ⚠️ token_id 为空,跳过") return None now_ts = time.time() if token_id in recently_bet: elapsed = now_ts - recently_bet[token_id] if elapsed < REBET_COOLDOWN: print(f" ⏳ 冷却中({elapsed/60:.0f}分钟前已投注该市场),跳过") return None with pending_lock: for info in pending_orders.values(): if info.get("token_id") == token_id: print(f" ⏳ 已有该市场挂单,跳过重复下单") return None client = get_clob_client() if client is None: print(" ❌ ClobClient 不可用") return None try: from py_clob_client.clob_types import OrderArgs, OrderType limit_price = round(price, 4) size = round(amount / limit_price, 4) order_args = OrderArgs( token_id = token_id, price = limit_price, size = size, side = "BUY", ) signed_order = client.create_order(order_args) resp = client.post_order(signed_order, OrderType.GTC) print(f" 💸 CLOB 挂单响应: {resp}") order_id = resp.get("orderID") or "" success = bool(resp.get("success") or order_id) if success and order_id: recently_bet[token_id] = time.time() with pending_lock: pending_orders[order_id] = { "placed_at": time.time(), "market_title": market_title, "winner": winner, "limit_price": limit_price, "amount": amount, "token_id": token_id, } tg( f"🔔 已挂单(等待成交)\n" f"市场: {market_title}\n" f"押注: {winner}\n" f"金额: ${amount:.2f} @ {limit_price:.4f}\n" f"利润空间: {(1.0-limit_price)*100:.2f}¢/token\n" f"订单ID: {order_id[:16]}...\n" f"成交后将发送通知" ) print(f" ✅ 挂单成功: {order_id[:16]}...") else: err_msg = resp.get("errorMsg", str(resp)) tg( f"⚠️ 挂单失败\n" f"市场: {market_title}\n" f"错误: {err_msg[:200]}" ) print(f" ⚠️ 挂单失败: {err_msg}") return resp except Exception as e: err = str(e) print(f" ❌ 下单异常: {err}") tg(f"❌ 下单异常\n{market_title}\n{err[:300]}") return None # ──────────────────────────────────────────────────────────────────────────── # BetsAPI # ──────────────────────────────────────────────────────────────────────────── BETSAPI_BASE = "https://api.betsapi.com/v1" def betsapi_get(endpoint: str, params: dict) -> dict: params["token"] = BETSAPI_KEY try: resp = requests.get( f"{BETSAPI_BASE}/{endpoint}", params=params, timeout=10, headers={"User-Agent": "Mozilla/5.0"}, ) resp.raise_for_status() return resp.json() except Exception as e: print(f" ⚠️ BetsAPI 请求失败 ({endpoint}): {e}") return {} def should_skip_league(league_name: str) -> bool: ln = league_name.lower() return any(kw in ln for kw in SKIP_LEAGUES) def get_inplay_events(sport_id: int) -> list: data = betsapi_get("bet365/inplay_filter", {"sport_id": sport_id}) return (data.get("results") or []) # ──────────────────────────────────────────────────────────────────────────── # 核心策略处理(v6) # ──────────────────────────────────────────────────────────────────────────── def process_event(event: dict, sport_id: int, game_ended: bool = False): """ 流动性提供策略: - game_ended=False (ts=1): 比赛进行中,比分悬殊,挂 BUY 单在 ask 价 - game_ended=True (ts=3): 比赛刚结束,结果已定,市场未结算,挂单在 ask(~0.99) 触发条件: 1. 比分差 >= MIN_MARGIN_BY_SPORT[sport_id] 2. Polymarket ask 价在对应价格区间内 """ try: league_name = event.get("league", {}).get("name", "") if should_skip_league(league_name): return home_name = event.get("home", {}).get("name", "") away_name = event.get("away", {}).get("name", "") if not home_name or not away_name: return ss = event.get("ss", "") or "" if not ss or "-" not in ss: return parts = ss.split("-") try: home_score = int(parts[0].strip()) away_score = int(parts[1].strip()) except (ValueError, IndexError): return if home_score == away_score: return home_winning = home_score > away_score winner_name = home_name if home_winning else away_name score_str = f"{home_score}-{away_score}" margin = abs(home_score - away_score) min_margin = MIN_MARGIN_BY_SPORT.get(sport_id, 999) if margin < min_margin: return # 在 Polymarket 缓存中匹配 result = find_in_cache(home_name, away_name) if result is None: return entry, match_score, home_token_idx = result title = entry["title"] outcomes = entry["outcomes"] token_ids = entry.get("token_ids", [entry["token_id"]]) volume = entry.get("volume", 0) winner_token_idx = home_token_idx if home_winning else (1 - home_token_idx) token_id = token_ids[winner_token_idx] if winner_token_idx < len(token_ids) else token_ids[0] winner_outcome = outcomes[winner_token_idx] if winner_token_idx < len(outcomes) else winner_name # ── v6: 获取 midpoint 和 best ask ─────────────────────────────────── mid_price = get_clob_price(token_id) ask_price = get_best_ask(token_id) # 如果无法获取 ask,退回到 mid if ask_price <= 0: ask_price = mid_price if mid_price <= 0: return # 确定价格区间 if game_ended: min_price = MIN_POLY_PRICE_ENDED # 0.96 max_price = MAX_POLY_PRICE_ENDED # 0.999 status_label = "已结束" else: min_price = MIN_POLY_PRICE # 0.92 max_price = MAX_POLY_PRICE # 0.998 status_label = "进行中" profit_potential = round(1.0 - ask_price, 4) print( f"[{now()}] 📊 [{status_label}] {home_name} {score_str} {away_name} " f"({league_name}) | +{margin} | " f"mid={mid_price:.4f} ask={ask_price:.4f} 利润={profit_potential:.4f} | " f"匹配: {match_score:.2f} → {title[:40]}" ) # 用 midpoint 检查市场确定性,用 ask 检查可执行价位 if mid_price < min_price: print(f" ⏩ mid {mid_price:.4f} < {min_price},市场不确定,跳过") return if ask_price > max_price: print(f" ⏩ ask {ask_price:.4f} > {max_price},差价太薄,跳过") return # ── 触发!以 ask 价挂 BUY 单 ──────────────────────────────────────── print(f" ✅ 触发!买在 ask {ask_price:.4f},利润空间 {profit_potential*100:.2f}¢/token") tg( f"🎯 流动性挂单机会 [{status_label}]\n" f"赛事: {home_name} {score_str} {away_name}\n" f"联赛: {league_name}\n" f"领先: {winner_name} +{margin}\n" f"市场: {title}\n" f"买入: {winner_outcome} @ {ask_price:.4f} (mid={mid_price:.4f})\n" f"结算值: 1.000 | 利润空间: {profit_potential*100:.2f}¢\n" f"成交量: ${volume:.0f} | 匹配度: {match_score:.2f}" ) place_arb_bet( token_id = token_id, amount = BET_AMOUNT, market_title = title, winner = f"{winner_name} ({winner_outcome})", price = ask_price, ) except Exception as e: print(f" ⚠️ process_event 异常: {e}") # ──────────────────────────────────────────────────────────────────────────── # 工具函数 # ──────────────────────────────────────────────────────────────────────────── def now() -> str: return datetime.now().strftime("%Y-%m-%d %H:%M:%S") # ──────────────────────────────────────────────────────────────────────────── # 主循环 # ──────────────────────────────────────────────────────────────────────────── def main(): print(f"[{now()}] 🚀 Polymarket 流动性提供监控 v6 启动") print(f" BetsAPI key: {BETSAPI_KEY[:12]}...") print(f" TG bot: {'已配置' if TG_BOT_TOKEN else '未配置'}") print(f" POLY_KEY: {'已配置' if PRIVATE_KEY else '未配置(纯预警模式)'}") print(f" 进行中价格区间: [{MIN_POLY_PRICE}, {MAX_POLY_PRICE}]") print(f" 已结束价格区间: [{MIN_POLY_PRICE_ENDED}, {MAX_POLY_PRICE_ENDED}]") print(f" 下单额: ${BET_AMOUNT} | 超时: {ORDER_TIMEOUT//60}分钟") print(f" 分差阈值: 冰球≥{MIN_MARGIN_BY_SPORT[17]} 篮球≥{MIN_MARGIN_BY_SPORT[18]} 足球≥{MIN_MARGIN_BY_SPORT[1]} 棒球≥{MIN_MARGIN_BY_SPORT[16]}") print(f" 模糊匹配: FUZZY_MIN={FUZZY_MIN} MIN_EACH={MIN_EACH}") print(f" [v6新增] 买在 ask 价 + 支持 ts=3(已结束)比赛 + containment 匹配") get_clob_client() t = threading.Thread(target=cache_refresh_loop, daemon=True) t.start() time.sleep(15) tg( f"🚀 流动性提供监控 v6 已启动\n" f"[修复] 买在 ask 价(立即成交)\n" f"[修复] 支持已结束比赛(ts=3)\n" f"[修复] NHL 球队名称匹配(Canucks 等)\n" f"进行中区间: [{MIN_POLY_PRICE}, {MAX_POLY_PRICE}] | 已结束: [{MIN_POLY_PRICE_ENDED}, {MAX_POLY_PRICE_ENDED}]\n" f"冰球≥{MIN_MARGIN_BY_SPORT[17]}球 / 篮球≥{MIN_MARGIN_BY_SPORT[18]}分 / 足球≥{MIN_MARGIN_BY_SPORT[1]}球" ) scan_count = 0 while True: scan_count += 1 try: check_pending_orders() total_live = 0 total_ended = 0 total_qualify = 0 for sport_id, sport_name in SPORT_IDS.items(): try: events = get_inplay_events(sport_id) live_count = 0 ended_count = 0 for ev in events: ts = str(ev.get("time_status", "")) if ts == "1": live_count += 1 process_event(ev, sport_id, game_ended=False) elif ts == "3": ended_count += 1 process_event(ev, sport_id, game_ended=True) total_live += live_count total_ended += ended_count # 每轮都记录有进行中/已结束比赛的运动 if live_count > 0 or ended_count > 0: print(f" {sport_name}: {live_count}场进行中, {ended_count}场已结束") except Exception as e: print(f"[{now()}] ⚠️ {sport_name} 扫描失败: {e}") # 每 20 轮打印摘要 if scan_count % 20 == 0: pending_count = len(pending_orders) print( f"[{now()}] 💤 第{scan_count}轮 | " f"进行中={total_live} 已结束={total_ended} | " f"挂单中={pending_count}" ) except KeyboardInterrupt: print(f"\n[{now()}] 👋 手动停止") break except Exception as e: print(f"[{now()}] ❌ 主循环异常: {e}") time.sleep(30) if __name__ == "__main__": main()