#!/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()