435 lines
19 KiB
Python
435 lines
19 KiB
Python
import sys
|
|
import time
|
|
import os
|
|
import json
|
|
from datetime import datetime
|
|
from loguru import logger
|
|
|
|
from src.utils.config_loader import load_config
|
|
from src.utils.logger import setup_logger
|
|
from src.data_collection.polymarket_api import PolymarketClient
|
|
from src.data_collection.weather_sources import WeatherDataCollector
|
|
from src.data_collection.onchain_tracker import OnchainTracker
|
|
from src.models.statistical_model import TemperaturePredictor
|
|
from src.analysis.volume_analyzer import VolumeAnalyzer
|
|
from src.analysis.orderbook_analyzer import OrderbookAnalyzer
|
|
from src.analysis.technical_indicators import TechnicalIndicators
|
|
from src.analysis.whale_tracker import WhaleTracker
|
|
from src.strategy.decision_engine import DecisionEngine
|
|
from src.strategy.risk_manager import RiskManager
|
|
from src.utils.notifier import TelegramNotifier
|
|
|
|
|
|
def main():
|
|
"""
|
|
Polymarket 交易系统主循环 - 监控与推送模式
|
|
"""
|
|
# 1. 设置日志
|
|
setup_logger()
|
|
logger.info("正在启动 Polymarket 天气交易信号监控系统...")
|
|
|
|
# 2. 加载配置
|
|
try:
|
|
config_data = load_config()
|
|
logger.info("配置加载成功。")
|
|
except Exception as e:
|
|
logger.error(f"配置加载失败: {e}")
|
|
sys.exit(1)
|
|
|
|
# 3. 初始化组件
|
|
polymarket = PolymarketClient(config_data["polymarket"])
|
|
weather = WeatherDataCollector(config_data["weather"])
|
|
onchain = OnchainTracker(config_data["polymarket"], polymarket)
|
|
notifier = TelegramNotifier(config_data["telegram"])
|
|
|
|
predictor = TemperaturePredictor()
|
|
volume_analyzer = VolumeAnalyzer()
|
|
orderbook_analyzer = OrderbookAnalyzer()
|
|
tech_indicators = TechnicalIndicators()
|
|
whale_tracker = WhaleTracker(config_data, onchain)
|
|
|
|
decision_engine = DecisionEngine(config_data)
|
|
risk_manager = RiskManager(config_data)
|
|
|
|
# 发送启动通知
|
|
notifier._send_message(
|
|
"🚀 <b>Polymarket 天气监控系统启动成功</b>\n正在扫描 12 个核心城市的最高温市场..."
|
|
)
|
|
|
|
# 信号记忆(持久化到文件)
|
|
pushed_signals = {}
|
|
SIGNALS_FILE = "data/pushed_signals.json"
|
|
if os.path.exists(SIGNALS_FILE):
|
|
try:
|
|
with open(SIGNALS_FILE, "r", encoding="utf-8") as f:
|
|
pushed_signals = json.load(f)
|
|
logger.info(f"已加载历史推送记录,共 {len(pushed_signals)} 条")
|
|
except:
|
|
pushed_signals = {}
|
|
|
|
# 确保data目录存在
|
|
if not os.path.exists("data"):
|
|
os.makedirs("data")
|
|
|
|
location_cache = {}
|
|
|
|
try:
|
|
while True:
|
|
logger.info("--- 开启新一轮全量动态监控 (自动搜寻所有天气市场) ---")
|
|
cached_signals = {}
|
|
all_markets_cache = {}
|
|
|
|
# 1. 直接从 Polymarket 获取所有天气合约
|
|
all_weather_markets = polymarket.get_weather_markets()
|
|
|
|
# 1.5 尝试通过slug获取可能遗漏的市场(如部分结算的市场)
|
|
special_slugs = []
|
|
|
|
for slug in special_slugs:
|
|
event = polymarket.get_event_by_slug(slug)
|
|
if event:
|
|
title = event.get("title", "")
|
|
logger.info(f"通过slug找到特殊事件: {title}")
|
|
|
|
# 提取城市名
|
|
city = weather.extract_city_from_question(title)
|
|
if not city:
|
|
city = "Unknown"
|
|
|
|
# 将该事件的所有市场添加到列表
|
|
for m in event.get("markets", []):
|
|
# 检查是否已存在
|
|
c_id = m.get("conditionId")
|
|
if not any(
|
|
existing.get("condition_id") == c_id
|
|
for existing in all_weather_markets
|
|
):
|
|
all_weather_markets.append(
|
|
{
|
|
"condition_id": c_id,
|
|
"question": m.get("groupItemTitle")
|
|
or m.get("question"),
|
|
"active_token_id": m.get("activeTokenId"),
|
|
"tokens": m.get("clobTokenIds"),
|
|
"prices": m.get("outcomePrices"),
|
|
"event_title": title,
|
|
"slug": slug,
|
|
"city": city, # 提前标记城市
|
|
}
|
|
)
|
|
logger.debug(f"添加特殊市场: {m.get('groupItemTitle')}")
|
|
|
|
if not all_weather_markets:
|
|
logger.warning("当前 Polymarket 似乎没有任何活跃的天气市场,等待中...")
|
|
time.sleep(300)
|
|
continue
|
|
|
|
# 2. 批量同步盘口价格 (优化:一次请求获取所有市场的 Buy Yes/No)
|
|
token_price_map = {}
|
|
price_requests = []
|
|
for m in all_weather_markets:
|
|
ts = m.get("tokens", [])
|
|
if isinstance(ts, str):
|
|
try:
|
|
ts = json.loads(ts)
|
|
except:
|
|
ts = []
|
|
if ts and len(ts) >= 2:
|
|
price_requests.append({"token_id": ts[0], "side": "ask"}) # Buy Yes
|
|
price_requests.append({"token_id": ts[1], "side": "ask"}) # Buy No
|
|
|
|
if price_requests:
|
|
logger.info(f"正在同步 {len(price_requests)} 个档位的盘口价格...")
|
|
token_price_map = polymarket.get_multiple_prices(price_requests)
|
|
logger.info(f"价格同步完成,成功获取 {len(token_price_map)} 个实时报价")
|
|
|
|
# 3. 按城市分组(按condition_id去重)
|
|
markets_by_city = {}
|
|
seen_condition_ids = set()
|
|
|
|
for i, m in enumerate(all_weather_markets):
|
|
c_id = m.get("condition_id")
|
|
if c_id in seen_condition_ids:
|
|
continue # 跳过重复
|
|
seen_condition_ids.add(c_id)
|
|
|
|
# 注入实时批量价格
|
|
ts = m.get("tokens", [])
|
|
if isinstance(ts, str):
|
|
try:
|
|
ts = json.loads(ts)
|
|
except:
|
|
ts = []
|
|
if ts and len(ts) >= 2:
|
|
m["buy_yes_live"] = token_price_map.get(ts[0])
|
|
m["buy_no_live"] = token_price_map.get(ts[1])
|
|
|
|
# 优先使用发现阶段已经识别出的城市名
|
|
city = m.get("city")
|
|
|
|
# 如果发现阶段没识别出,再尝试从问题文本提取
|
|
if not city or city == "Unknown":
|
|
full_context = f"{m.get('event_title', '')} {m.get('question', '')}"
|
|
city = weather.extract_city_from_question(full_context)
|
|
|
|
if i < 5:
|
|
logger.debug(
|
|
f"分析合约 {i}: City='{city}' | Title='{m.get('event_title')}"
|
|
)
|
|
|
|
if not city:
|
|
continue
|
|
|
|
if city not in markets_by_city:
|
|
markets_by_city[city] = []
|
|
markets_by_city[city].append(m)
|
|
|
|
logger.info(
|
|
f"动态发现 {len(markets_by_city)} 个受监控城市,共 {len(all_weather_markets)} 个合约"
|
|
)
|
|
|
|
# 3. 逐个城市分析
|
|
for city, city_markets in markets_by_city.items():
|
|
try:
|
|
# 获取/缓存坐标
|
|
if city not in location_cache:
|
|
coords = weather.get_coordinates(city)
|
|
if not coords:
|
|
continue
|
|
location_cache[city] = coords
|
|
logger.info(
|
|
f"📍 城市定位成功: {city} -> ({coords['lat']}, {coords['lon']})"
|
|
)
|
|
|
|
loc = location_cache[city]
|
|
|
|
# A. 获取实时天气共识
|
|
weather_data = weather.fetch_all_sources(
|
|
city, lat=loc["lat"], lon=loc["lon"]
|
|
)
|
|
consensus = weather.check_consensus(weather_data)
|
|
|
|
if not consensus.get("consensus"):
|
|
continue
|
|
|
|
logger.info(
|
|
f"☁️ {city} 当前气温: {consensus['average_temp']}°C | 监控合约: {len(city_markets)}"
|
|
)
|
|
|
|
# --- 本城市汇总预警缓存 ---
|
|
city_alerts = []
|
|
city_local_time = None
|
|
|
|
# B. 遍历该城市所有合约
|
|
for market in city_markets:
|
|
market_id = market.get("condition_id")
|
|
question = market.get("question", "未知市场")
|
|
event_title = market.get("event_title", "")
|
|
|
|
# (日期处理逻辑保持不变...)
|
|
target_date = weather.extract_date_from_title(
|
|
event_title
|
|
) or weather.extract_date_from_title(question)
|
|
ref_temp = consensus["average_temp"]
|
|
if target_date:
|
|
daily_data = weather_data.get("open-meteo", {}).get(
|
|
"daily", {}
|
|
)
|
|
if daily_data:
|
|
dates = daily_data.get("time", [])
|
|
max_temps = daily_data.get("temperature_2m_max", [])
|
|
for idx, d_str in enumerate(dates):
|
|
if target_date == d_str:
|
|
ref_temp = max_temps[idx]
|
|
break
|
|
|
|
# --- 价格获取逻辑 ---
|
|
buy_yes_price = market.get("buy_yes_live")
|
|
buy_no_price = market.get("buy_no_live")
|
|
current_price = 0.5
|
|
gamma_prices = market.get("prices", [])
|
|
if isinstance(gamma_prices, str):
|
|
try:
|
|
gamma_prices = json.loads(gamma_prices)
|
|
except:
|
|
gamma_prices = []
|
|
if gamma_prices and len(gamma_prices) > 0:
|
|
current_price = float(gamma_prices[0])
|
|
|
|
if buy_yes_price is None:
|
|
buy_yes_price = current_price
|
|
if buy_no_price is None:
|
|
buy_no_price = 1.0 - current_price
|
|
|
|
# C. 准备缓存
|
|
temp_unit = weather_data.get("open-meteo", {}).get(
|
|
"unit", "celsius"
|
|
)
|
|
temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C"
|
|
city_local_time = (
|
|
weather_data.get("open-meteo", {})
|
|
.get("current", {})
|
|
.get("local_time")
|
|
)
|
|
|
|
cache_entry = {
|
|
"city": city,
|
|
"full_title": event_title,
|
|
"option": question,
|
|
"prediction": f"{ref_temp}{temp_symbol}",
|
|
"price": int(current_price * 100),
|
|
"buy_yes": int(buy_yes_price * 100),
|
|
"buy_no": int(buy_no_price * 100),
|
|
"url": f"https://polymarket.com/event/{market.get('slug')}",
|
|
"local_time": city_local_time,
|
|
"target_date": target_date,
|
|
"score": 0,
|
|
"rationale": "ACTIVE",
|
|
}
|
|
|
|
if buy_yes_price <= 0.01 or buy_yes_price >= 0.99:
|
|
cache_entry["rationale"] = "ENDED"
|
|
all_markets_cache[market_id] = cache_entry
|
|
continue
|
|
|
|
# D. 评分
|
|
signal = decision_engine.calculate_signal(
|
|
model_prediction=predictor.predict_ensemble([ref_temp]),
|
|
market_data={
|
|
"orderbook": {},
|
|
"price_history": [current_price],
|
|
"transactions": [],
|
|
},
|
|
weather_consensus={"average_temp": ref_temp},
|
|
whale_activity=whale_tracker.analyze_market_whales(
|
|
market_id
|
|
),
|
|
)
|
|
cache_entry["score"] = signal["final_score"]
|
|
cache_entry["rationale"] = signal.get("recommendation", "N/A")
|
|
all_markets_cache[market_id] = cache_entry
|
|
|
|
# --- 预警收集 ---
|
|
# 1. 价格预警
|
|
if (0.85 <= buy_yes_price <= 0.95) or (
|
|
0.85 <= buy_no_price <= 0.95
|
|
):
|
|
alert_key = f"alert_{market_id}_range_85_95"
|
|
if alert_key not in pushed_signals:
|
|
trigger_side = (
|
|
"Buy Yes" if buy_yes_price >= 0.85 else "Buy No"
|
|
)
|
|
trigger_price = (
|
|
int(buy_yes_price * 100)
|
|
if trigger_side == "Buy Yes"
|
|
else int(buy_no_price * 100)
|
|
)
|
|
city_alerts.append(
|
|
{
|
|
"type": "price",
|
|
"market": f"{question} ({target_date or '今日'})",
|
|
"msg": f"{trigger_side}进入锁定区间 {trigger_price}¢",
|
|
}
|
|
)
|
|
pushed_signals[alert_key] = time.time()
|
|
|
|
# 2. 市场异常
|
|
whale_sig = signal["factor_details"].get("whale", {})
|
|
volume_sig = signal["factor_details"].get("volume", {})
|
|
if (
|
|
whale_sig.get("signal")
|
|
in ["STRONG_ACCUMULATION", "STRONG_DISTRIBUTION"]
|
|
or volume_sig.get("volume_signal", {}).get("signal")
|
|
== "VOLUME_SPIKE"
|
|
):
|
|
anomaly_key = f"anomaly_{market_id}"
|
|
if anomaly_key not in pushed_signals:
|
|
msg = (
|
|
"检测到异常交易流"
|
|
if volume_sig.get("score", 0) > 0.7
|
|
else "大户入场"
|
|
)
|
|
city_alerts.append(
|
|
{
|
|
"type": "anomaly",
|
|
"market": f"{question} ({target_date or '今日'})",
|
|
"msg": f"{msg} (当前 {int(buy_yes_price * 100)}¢)",
|
|
}
|
|
)
|
|
pushed_signals[anomaly_key] = time.time()
|
|
|
|
# 3. 信号暂存
|
|
cached_signals[market_id] = cache_entry
|
|
|
|
# --- 循环结束后统一推送本城市汇总 ---
|
|
if city_alerts:
|
|
notifier.send_combined_alert(
|
|
city, city_alerts, local_time=city_local_time
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"分析城市 {city} 时出错: {e}")
|
|
continue
|
|
except Exception as e:
|
|
logger.error(f"分析城市 {city} 时出错: {e}")
|
|
continue
|
|
|
|
# --- 每处理完一个城市,立即更新 JSON 文件 ---
|
|
try:
|
|
# 1. 更新活跃信号缓存 (合并旧数据避免扫描中途变空)
|
|
final_signals = {}
|
|
if os.path.exists("data/active_signals.json"):
|
|
try:
|
|
with open(
|
|
"data/active_signals.json", "r", encoding="utf-8"
|
|
) as f:
|
|
final_signals = json.load(f)
|
|
except:
|
|
pass
|
|
|
|
final_signals.update(cached_signals)
|
|
|
|
with open("data/active_signals.json", "w", encoding="utf-8") as f:
|
|
json.dump(final_signals, f, ensure_ascii=False, indent=2)
|
|
|
|
# 2. 更新全量市场缓存
|
|
try:
|
|
with open("data/all_markets.json", "r", encoding="utf-8") as f:
|
|
existing_markets = json.load(f)
|
|
except:
|
|
existing_markets = {}
|
|
|
|
existing_markets.update(all_markets_cache)
|
|
|
|
# 清理过期日期
|
|
today_str = datetime.now().strftime("%Y-%m-%d")
|
|
cleaned_markets = {}
|
|
for k, v in existing_markets.items():
|
|
t_date = v.get("target_date")
|
|
if not t_date or t_date >= today_str:
|
|
cleaned_markets[k] = v
|
|
|
|
with open("data/all_markets.json", "w", encoding="utf-8") as f:
|
|
json.dump(cleaned_markets, f, ensure_ascii=False, indent=2)
|
|
|
|
# 3. 保存推送记录
|
|
with open("data/pushed_signals.json", "w", encoding="utf-8") as f:
|
|
json.dump(pushed_signals, f, ensure_ascii=False)
|
|
|
|
except Exception as e:
|
|
logger.error(f"即时保存数据失败: {e}")
|
|
|
|
# 4. 每日概览已移除
|
|
|
|
logger.info("本轮扫描结束。等待 5 分钟...")
|
|
time.sleep(300)
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("收到关机指令,正在退出...")
|
|
except Exception as e:
|
|
logger.exception(f"系统运行出错: {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|