946 lines
47 KiB
Python
946 lines
47 KiB
Python
import sys
|
||
import time
|
||
import os
|
||
import json
|
||
import re
|
||
from datetime import datetime, timedelta
|
||
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.trading.paper_trader import PaperTrader
|
||
from src.utils.notifier import TelegramNotifier
|
||
|
||
|
||
def main():
|
||
# 1. 初始化配置与日志
|
||
config_data = load_config()
|
||
setup_logger(config_data.get("app", {}).get("log_level", "INFO"))
|
||
|
||
logger.info("🌟 PolyWeather 监控引擎启动中...")
|
||
|
||
# 2. 初始化核心组件
|
||
polymarket = PolymarketClient(config_data["polymarket"])
|
||
weather = WeatherDataCollector(config_data["weather"])
|
||
onchain = OnchainTracker(config_data["polymarket"], polymarket)
|
||
notifier = TelegramNotifier(config_data["telegram"])
|
||
|
||
# 3. 初始化分析与交易组件
|
||
predictor = TemperaturePredictor()
|
||
risk_manager = RiskManager(config_data.get("config", {}))
|
||
orderbook_analyzer = OrderbookAnalyzer(config_data.get("config", {}))
|
||
decision_engine = DecisionEngine(config_data.get("config", {}))
|
||
whale_tracker = WhaleTracker(config_data.get("config", {}), onchain)
|
||
paper_trader = PaperTrader()
|
||
|
||
# 发送启动通知
|
||
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 = {}
|
||
|
||
# 价格历史追踪(用于计算趋势)
|
||
PRICE_HISTORY_FILE = "data/price_history.json"
|
||
price_history = {}
|
||
if os.path.exists(PRICE_HISTORY_FILE):
|
||
try:
|
||
with open(PRICE_HISTORY_FILE, "r", encoding="utf-8") as f:
|
||
price_history = json.load(f)
|
||
except:
|
||
price_history = {}
|
||
|
||
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. 批量同步盘口价格 (优化:为每个档位获取其对应的真实 Token 价格)
|
||
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 = []
|
||
|
||
active_tid = m.get("active_token_id")
|
||
|
||
# 如果是多选一市场(比如 Dallas 76-77°F)
|
||
if len(ts) > 2 and active_tid:
|
||
# 获取该档位的买入价 (Ask)
|
||
price_requests.append({"token_id": active_tid, "side": "ask"})
|
||
# 获取该档位的买入“否”价所需的 Bid 价
|
||
price_requests.append({"token_id": active_tid, "side": "bid"})
|
||
# 如果是传统的 Yes/No 二选一市场
|
||
elif 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 = []
|
||
|
||
active_tid = m.get("active_token_id")
|
||
|
||
# 多选一市场逻辑
|
||
if len(ts) > 2 and active_tid:
|
||
m["buy_yes_live"] = token_price_map.get(f"{active_tid}:ask")
|
||
# 买入“否”的价格 = 1 - 该档位的 Bid
|
||
bid_val = token_price_map.get(f"{active_tid}:bid")
|
||
if bid_val:
|
||
m["buy_no_live"] = 1.0 - bid_val
|
||
# 二选一市场逻辑
|
||
elif len(ts) == 2:
|
||
m["buy_yes_live"] = token_price_map.get(f"{ts[0]}:ask")
|
||
m["buy_no_live"] = token_price_map.get(f"{ts[1]}:ask")
|
||
|
||
# 优先使用发现阶段已经识别出的城市名
|
||
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
|
||
|
||
temp_unit = weather_data.get("open-meteo", {}).get(
|
||
"unit", "celsius"
|
||
)
|
||
temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C"
|
||
logger.info(
|
||
f"☁️ {city} 当前气温: {consensus['average_temp']}{temp_symbol} (unit={temp_unit}) | 监控合约: {len(city_markets)}"
|
||
)
|
||
|
||
# --- 本城市汇总预警缓存 ---
|
||
city_alerts = []
|
||
city_local_time = None
|
||
city_total_vol = 0
|
||
city_pred_high = None
|
||
city_target_date = None
|
||
city_strategy_tips = []
|
||
|
||
# B. 遍历该城市所有合约
|
||
for market in city_markets:
|
||
market_id = market.get("condition_id")
|
||
question = market.get("question", "未知市场")
|
||
event_title = market.get("event_title", "")
|
||
|
||
# 累计城市总成交量
|
||
vol_raw = market.get("volume", 0)
|
||
if isinstance(vol_raw, str):
|
||
try:
|
||
vol_raw = float(
|
||
vol_raw.replace("$", "").replace(",", "")
|
||
)
|
||
except:
|
||
vol_raw = 0
|
||
city_total_vol += vol_raw
|
||
|
||
# 识别该合约的目标日期
|
||
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
|
||
|
||
# --- 价格获取逻辑 (增强版) ---
|
||
# 使用 token_price_map 获取实时数据
|
||
active_tid = market.get("active_token_id")
|
||
ts = market.get("tokens", [])
|
||
if isinstance(ts, str):
|
||
ts = json.loads(ts)
|
||
|
||
buy_yes_price = None
|
||
buy_no_price = None
|
||
bid_yes_price = None
|
||
|
||
if len(ts) == 2:
|
||
# 传统二选一市场 (Yes/No Token 独立)
|
||
buy_yes_price = token_price_map.get(f"{ts[0]}:ask")
|
||
buy_no_price = token_price_map.get(f"{ts[1]}:ask")
|
||
bid_yes_price = token_price_map.get(f"{ts[0]}:bid")
|
||
elif active_tid:
|
||
# 多选一市场 (单 Token 对应一个档位)
|
||
buy_yes_price = token_price_map.get(f"{active_tid}:ask")
|
||
bid_yes_price = token_price_map.get(f"{active_tid}:bid")
|
||
if bid_yes_price is not None:
|
||
buy_no_price = 1.0 - bid_yes_price
|
||
|
||
# 兜底概率计算
|
||
current_prob = (
|
||
(buy_yes_price + bid_yes_price) / 2
|
||
if (buy_yes_price and bid_yes_price)
|
||
else (buy_yes_price or 0.5)
|
||
)
|
||
if buy_no_price is None:
|
||
buy_no_price = 1.0 - current_prob
|
||
|
||
# 计算价格趋势
|
||
prev_data = price_history.get(market_id, {})
|
||
prev_prob = prev_data.get("price", current_prob)
|
||
prob_change = (current_prob - prev_prob) * 100
|
||
trend_str = (
|
||
f"▲{abs(prob_change):.0f}%"
|
||
if prob_change > 0.5
|
||
else (
|
||
f"▼{abs(prob_change):.0f}%"
|
||
if prob_change < -0.5
|
||
else ""
|
||
)
|
||
)
|
||
|
||
# 更新历史缓存
|
||
price_history[market_id] = {
|
||
"price": current_prob,
|
||
"timestamp": datetime.now().isoformat(),
|
||
}
|
||
|
||
# --- 预警收集 (自动推送逻辑) ---
|
||
# 严格触发条件: 价格必须处于 85-95¢ 区间 (真正的高概率信号)
|
||
yes_in_range = buy_yes_price and 0.85 <= buy_yes_price <= 0.95
|
||
no_in_range = buy_no_price and 0.85 <= buy_no_price <= 0.95
|
||
|
||
# 50¢ 保护:价格接近 50% 说明市场无明确方向,跳过
|
||
is_undecided = 0.45 <= current_prob <= 0.55
|
||
|
||
if (yes_in_range or no_in_range) and not is_undecided:
|
||
alert_key = f"alert_{market_id}_{int(current_prob * 100)}"
|
||
if alert_key not in pushed_signals:
|
||
# 深度分析订单簿
|
||
ob_data = (
|
||
polymarket.get_orderbook(active_tid)
|
||
if active_tid
|
||
else None
|
||
)
|
||
ob_analysis = (
|
||
orderbook_analyzer.analyze(ob_data)
|
||
if ob_data
|
||
else {
|
||
"tradeable": False,
|
||
"liquidity": "枯竭",
|
||
"spread": 0,
|
||
"mid_price": current_prob,
|
||
}
|
||
)
|
||
|
||
# 获取温度符号(在此处定义以便后续使用)
|
||
temp_unit = weather_data.get("open-meteo", {}).get("unit", "celsius")
|
||
temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C"
|
||
|
||
# 预测偏差分析
|
||
if ref_temp:
|
||
city_pred_high = ref_temp # 记录到城市概览
|
||
temp_match = re.search(
|
||
r"(\d+)(?:-(\d+))?°[FC]", question
|
||
)
|
||
if temp_match:
|
||
low_b = int(temp_match.group(1))
|
||
high_b = (
|
||
int(temp_match.group(2))
|
||
if temp_match.group(2)
|
||
else low_b
|
||
)
|
||
diff = ref_temp - ((low_b + high_b) / 2)
|
||
# 偏差信息将在后面构建 msg 时统一添加
|
||
|
||
# 生成策略建议:仅保留模型一致提示
|
||
if abs(diff) < 2 and current_prob > 0.7:
|
||
city_strategy_tips.append(
|
||
f"预测温度{ref_temp}{temp_symbol}落在{question}区间,市场与模型一致"
|
||
)
|
||
|
||
|
||
# 模拟下单 - 使用 Ask 价格(实际可成交价格)
|
||
if buy_yes_price and buy_yes_price > 0.5:
|
||
trigger_side = "Buy Yes"
|
||
trigger_price = int(buy_yes_price * 100)
|
||
else:
|
||
trigger_side = "Buy No"
|
||
trigger_price = (
|
||
int(buy_no_price * 100)
|
||
if buy_no_price
|
||
else int((1 - current_prob) * 100)
|
||
)
|
||
|
||
# 构建预测文本
|
||
forecast_text = f"{ref_temp}{temp_symbol}" if ref_temp else "N/A"
|
||
|
||
# 构建简约版消息
|
||
side_display = "Buy No" if trigger_side == "Buy No" else "Buy Yes"
|
||
msg = f"⚡ {question} ({target_date}): {side_display} {trigger_price}¢ | 预测:{forecast_text}"
|
||
|
||
success = paper_trader.open_position(
|
||
market_id=market_id,
|
||
city=city,
|
||
option=question,
|
||
price=trigger_price,
|
||
side="YES" if trigger_side == "Buy Yes" else "NO",
|
||
amount_usd=5.0,
|
||
target_date=target_date,
|
||
predicted_temp=ref_temp,
|
||
)
|
||
|
||
# 添加模拟交易标签
|
||
if success:
|
||
msg += " [🛒 $5.0 💡试探]"
|
||
|
||
city_alerts.append(
|
||
{
|
||
"market": target_date or "今日",
|
||
"msg": msg,
|
||
"bought": success,
|
||
"amount": 5.0,
|
||
"confidence": "💡试探",
|
||
}
|
||
)
|
||
pushed_signals[alert_key] = time.time()
|
||
if target_date:
|
||
city_target_date = target_date
|
||
|
||
# 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")
|
||
)
|
||
|
||
current_price = buy_yes_price if buy_yes_price else 0.5
|
||
|
||
# 计算价格趋势
|
||
prev_data = price_history.get(market_id, {})
|
||
prev_price = prev_data.get("price", current_price)
|
||
price_change_pct = (
|
||
((current_price - prev_price) / prev_price * 100)
|
||
if prev_price > 0
|
||
else 0
|
||
)
|
||
|
||
# 更新价格历史缓存
|
||
price_history[market_id] = {
|
||
"price": current_price,
|
||
"timestamp": datetime.now().isoformat(),
|
||
}
|
||
|
||
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) if buy_yes_price else 0,
|
||
"buy_no": int(buy_no_price * 100) if buy_no_price else 0,
|
||
"url": f"https://polymarket.com/event/{market.get('slug')}",
|
||
"local_time": city_local_time,
|
||
"target_date": target_date,
|
||
"score": 0,
|
||
"rationale": "ACTIVE",
|
||
"trend": round(price_change_pct, 1),
|
||
}
|
||
|
||
# --- 最终过滤器 (拦截垃圾信号) ---
|
||
|
||
# 1. 过滤已锁定价格 (>= 98.5c)
|
||
if (buy_yes_price and buy_yes_price >= 0.985) or (
|
||
buy_no_price and buy_no_price >= 0.985
|
||
):
|
||
cache_entry["rationale"] = "ENDED"
|
||
all_markets_cache[market_id] = cache_entry
|
||
continue
|
||
|
||
# 2. 过滤已过期日期 (动态获取当前日期)
|
||
current_today = datetime.now().strftime("%Y-%m-%d")
|
||
if target_date and target_date < current_today:
|
||
cache_entry["rationale"] = "EXPIRED"
|
||
all_markets_cache[market_id] = cache_entry
|
||
continue
|
||
|
||
# 3. 评分计算
|
||
try:
|
||
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=None,
|
||
)
|
||
cache_entry["score"] = signal.get("final_score", 0)
|
||
cache_entry["rationale"] = signal.get(
|
||
"recommendation", "ACTIVE"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"计算信号失败 [{market_id}]: {e}")
|
||
cache_entry["score"] = 0
|
||
cache_entry["rationale"] = "ERROR"
|
||
|
||
all_markets_cache[market_id] = cache_entry
|
||
|
||
# --- 预警收集 (自动推送逻辑) ---
|
||
if (buy_yes_price and 0.85 <= buy_yes_price <= 0.95) or (
|
||
buy_no_price and 0.85 <= buy_no_price <= 0.95
|
||
):
|
||
alert_key = f"alert_{market_id}_range_85_95"
|
||
if alert_key not in pushed_signals:
|
||
# --- 基础参数识别 ---
|
||
is_categorical = len(ts) > 2 and active_tid
|
||
if is_categorical:
|
||
# 语义转换逻辑保持一致
|
||
if buy_no_price and buy_no_price >= 0.85:
|
||
trigger_side = "Sell Yes"
|
||
trigger_price = int(
|
||
buy_no_price * 100
|
||
) # 预估价
|
||
else:
|
||
trigger_side = "Buy Yes"
|
||
trigger_price = int(buy_yes_price * 100)
|
||
else:
|
||
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)
|
||
)
|
||
|
||
# --- 深度流动性与 Spread 检查 ---
|
||
target_tid = (
|
||
active_tid
|
||
if is_categorical
|
||
else (ts[0] if trigger_side == "Buy Yes" else ts[1])
|
||
)
|
||
ob_data = (
|
||
polymarket.get_orderbook(target_tid)
|
||
if target_tid
|
||
else None
|
||
)
|
||
|
||
ob_analysis = {
|
||
"tradeable": True,
|
||
"liquidity": "未知",
|
||
"spread": 0,
|
||
"mid_price": trigger_price / 100,
|
||
}
|
||
if ob_data:
|
||
ob_analysis = orderbook_analyzer.analyze(ob_data)
|
||
|
||
if not ob_analysis.get("tradeable", True):
|
||
confidence_tag = (
|
||
f"🔴不可交易 ({ob_analysis.get('liquidity')})"
|
||
)
|
||
if not is_categorical:
|
||
logger.warning(
|
||
f"跳过不可交易信号 (Spread {ob_analysis.get('spread')}): {city} {question}"
|
||
)
|
||
continue
|
||
|
||
# 更新实时数据显示
|
||
mid_c = round(ob_analysis.get("mid_price", 0) * 100, 1)
|
||
spr_c = round(ob_analysis.get("spread", 0) * 100, 1)
|
||
depth = ob_analysis.get(
|
||
"ask_depth"
|
||
if trigger_side.startswith("Buy")
|
||
else "bid_depth",
|
||
0,
|
||
)
|
||
|
||
# 流动性图标
|
||
liq_map = {
|
||
"充裕": "✅ 充裕",
|
||
"正常": "🟡 正常",
|
||
"稀薄": "🟠 稀薄",
|
||
"枯竭": "🔴 枯竭",
|
||
}
|
||
liq_status = liq_map.get(
|
||
ob_analysis.get("liquidity", "未知"), "❓ 未知"
|
||
)
|
||
|
||
if is_categorical:
|
||
ask_str = (
|
||
"--"
|
||
if trigger_side == "Sell Yes"
|
||
else f"{trigger_price}¢"
|
||
)
|
||
bid_str = (
|
||
f"{trigger_price}¢"
|
||
if trigger_side == "Sell Yes"
|
||
else "--"
|
||
)
|
||
|
||
display_side = (
|
||
f"📊 <b>{question}</b>\n"
|
||
f"Ask: {ask_str} | Bid: {bid_str} | Mid: {mid_c}¢\n"
|
||
f"Spread: {spr_c}¢ | 深度: ${depth}\n"
|
||
f"流动性: {liq_status}"
|
||
)
|
||
else:
|
||
display_side = (
|
||
f"📊 <b>{question}</b>\n"
|
||
f"报价: {trigger_side} {trigger_price}¢ | Mid: {mid_c}¢\n"
|
||
f"Spread: {spr_c}¢ | 深度: ${depth}\n"
|
||
f"流动性: {liq_status}"
|
||
)
|
||
|
||
# --- 智能动态仓位计算 ---
|
||
# 1. 获取 Open-Meteo 对目标日期的最高温预测
|
||
predicted_high = None
|
||
weather_supports = False
|
||
daily_data = weather_data.get("open-meteo", {}).get(
|
||
"daily", {}
|
||
)
|
||
if daily_data and target_date:
|
||
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 and idx < len(
|
||
max_temps
|
||
):
|
||
predicted_high = max_temps[idx]
|
||
break
|
||
|
||
# 2. 判断天气预测是否支持当前方向
|
||
if predicted_high is not None:
|
||
# 解析选项的温度范围 (例如 "40-41°F" 或 "32°F or below")
|
||
temp_match = re.search(
|
||
r"(\d+)(?:-(\d+))?°[FC]", question
|
||
)
|
||
if temp_match:
|
||
low_bound = int(temp_match.group(1))
|
||
high_bound = (
|
||
int(temp_match.group(2))
|
||
if temp_match.group(2)
|
||
else low_bound
|
||
)
|
||
|
||
# 如果买 NO,天气预测应该在这个区间之外
|
||
if trigger_side == "Buy No":
|
||
weather_supports = (
|
||
predicted_high < low_bound - 2
|
||
) or (predicted_high > high_bound + 2)
|
||
else: # 买 YES
|
||
weather_supports = (
|
||
low_bound - 2
|
||
<= predicted_high
|
||
<= high_bound + 2
|
||
)
|
||
|
||
# 3. 获取成交量信息
|
||
market_volume = market.get("volume", 0)
|
||
if isinstance(market_volume, str):
|
||
try:
|
||
market_volume = float(
|
||
market_volume.replace("$", "").replace(
|
||
",", ""
|
||
)
|
||
)
|
||
except:
|
||
market_volume = 0
|
||
high_volume = market_volume >= 5000 # $5000+ 算高成交量
|
||
|
||
# --- Pro 级仓位决策系统 ---
|
||
# 1. 计算离结算剩余小时数 (假设气温市场在目标日期晚上 23:59 结算)
|
||
hours_to_settle = 24.0
|
||
if target_date:
|
||
try:
|
||
settle_dt = datetime.strptime(
|
||
f"{target_date} 23:59:59",
|
||
"%Y-%m-%d %H:%M:%S",
|
||
)
|
||
now_utc = datetime.utcnow()
|
||
diff = settle_dt - now_utc
|
||
hours_to_settle = diff.total_seconds() / 3600.0
|
||
except:
|
||
pass
|
||
|
||
# 2. 计算相对成交量比例
|
||
total_daily_vol = sum(
|
||
[
|
||
float(
|
||
str(m.get("volume", 0))
|
||
.replace("$", "")
|
||
.replace(",", "")
|
||
)
|
||
for m in city_markets
|
||
if (
|
||
weather.extract_date_from_title(
|
||
m.get("event_title", "")
|
||
)
|
||
or weather.extract_date_from_title(
|
||
m.get("question", "")
|
||
)
|
||
)
|
||
== target_date
|
||
]
|
||
)
|
||
market_vol = float(
|
||
str(market.get("volume", 0))
|
||
.replace("$", "")
|
||
.replace(",", "")
|
||
)
|
||
is_rel_high_vol = (
|
||
(market_vol / total_daily_vol > 0.3)
|
||
if total_daily_vol > 0
|
||
else False
|
||
)
|
||
|
||
# 3. 基础意向仓位 (基于置信度)
|
||
base_pos = 3.0 # 默认探路
|
||
confidence_tag = "💡试探"
|
||
if (
|
||
trigger_price >= 90
|
||
and weather_supports
|
||
and high_volume
|
||
):
|
||
base_pos, confidence_tag = 10.0, "🔥高置信"
|
||
elif trigger_price >= 90 and weather_supports:
|
||
base_pos, confidence_tag = 7.0, "⭐中置信"
|
||
elif trigger_price >= 92:
|
||
base_pos, confidence_tag = 5.0, "📌价格锁定"
|
||
|
||
# 4. 四层过滤决策
|
||
amount_usd, risk_reason = (
|
||
risk_manager.calculate_position_size(
|
||
base_confidence_usd=base_pos,
|
||
depth=depth,
|
||
hours_to_settle=hours_to_settle,
|
||
is_high_relative_volume=is_rel_high_vol,
|
||
)
|
||
)
|
||
|
||
logger.info(
|
||
f"【Pro仓位】{city} {question} | "
|
||
f"基础:{base_pos}$ -> 最终:{amount_usd}$ | 原因:{risk_reason} | "
|
||
f"深度:${depth} | 剩:{hours_to_settle:.1f}h"
|
||
)
|
||
|
||
# --- 模拟交易触发逻辑 ---
|
||
if amount_usd > 0:
|
||
side = "YES" if trigger_side == "Buy Yes" else "NO"
|
||
success = paper_trader.open_position(
|
||
market_id=market_id,
|
||
city=city,
|
||
option=question,
|
||
price=trigger_price,
|
||
side=side,
|
||
amount_usd=amount_usd,
|
||
target_date=target_date,
|
||
predicted_temp=predicted_high,
|
||
)
|
||
if success:
|
||
risk_manager.record_trade(amount_usd)
|
||
else:
|
||
# 如果被风控拦截(金额为0),则不进行任何推送,避免刷屏
|
||
success = False
|
||
logger.info(
|
||
f"Skipping alert for {question}: {risk_reason}"
|
||
)
|
||
continue
|
||
|
||
# 构建预测温度显示文本
|
||
temp_unit = weather_data.get("open-meteo", {}).get(
|
||
"unit", "celsius"
|
||
)
|
||
temp_symbol = (
|
||
"°F" if temp_unit == "fahrenheit" else "°C"
|
||
)
|
||
forecast_text = (
|
||
f"{predicted_high}{temp_symbol}"
|
||
if predicted_high
|
||
else "N/A"
|
||
)
|
||
|
||
# 构建简约版消息: ⚡ {question} ({date}): {side} {price}¢ | 预测:{forecast} [🛒 ${amount} {tag}]
|
||
side_display = (
|
||
"Buy No" if trigger_side == "Buy No" else "Buy Yes"
|
||
)
|
||
msg = (
|
||
f"⚡ {question} ({target_date}): {side_display} {trigger_price}¢ | "
|
||
f"预测:{forecast_text} [🛒 ${amount_usd} {confidence_tag}]"
|
||
)
|
||
|
||
city_alerts.append(
|
||
{
|
||
"type": "price",
|
||
"market": f"{target_date or '今日'}",
|
||
"msg": msg,
|
||
"bought": success,
|
||
"amount": amount_usd,
|
||
"confidence": confidence_tag,
|
||
}
|
||
)
|
||
pushed_signals[alert_key] = time.time()
|
||
|
||
# 3. 信号暂存
|
||
cached_signals[market_id] = cache_entry
|
||
|
||
# E. 统一发送城市汇总通知 (使用新 Pro 模板)
|
||
if city_alerts:
|
||
# 去重策略建议
|
||
unique_tips = list(dict.fromkeys(city_strategy_tips))
|
||
notifier.send_combined_alert(
|
||
city=city,
|
||
alerts=city_alerts,
|
||
local_time=city_local_time,
|
||
forecast_temp=f"{city_pred_high}{temp_symbol}"
|
||
if city_pred_high
|
||
else "N/A",
|
||
total_volume=city_total_vol,
|
||
brackets_count=len(city_markets),
|
||
strategy_tips=unique_tips,
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析城市 {city} 时出错: {e}")
|
||
# --- 每处理完一个城市,立即更新 JSON 文件 ---
|
||
try:
|
||
# --- 周期性结算:保存高价值信号 ---
|
||
active_signals = []
|
||
for mid, entry in all_markets_cache.items():
|
||
# 核心过滤:只有 ACTIVE 且 价格未锁定、日期未过期的才进入 signals 列表
|
||
if entry.get("rationale") not in ["ENDED", "EXPIRED", "ERROR"]:
|
||
# 再次双重检查日期 (硬核拦截 2026-02-06)
|
||
target_dt = entry.get("target_date")
|
||
if target_dt and target_dt < "2026-02-06":
|
||
continue
|
||
active_signals.append(entry)
|
||
|
||
# 按分数排序
|
||
active_signals.sort(key=lambda x: x.get("score", 0), reverse=True)
|
||
|
||
with open("data/active_signals.json", "w", encoding="utf-8") as f:
|
||
json.dump(active_signals, f, ensure_ascii=False, indent=4)
|
||
|
||
logger.info(
|
||
f"已更新活跃信号库,包含 {len(active_signals)} 个有效信号。"
|
||
)
|
||
|
||
# 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)
|
||
|
||
# 3.5 保存价格历史(用于趋势计算)
|
||
with open(PRICE_HISTORY_FILE, "w", encoding="utf-8") as f:
|
||
json.dump(price_history, f, ensure_ascii=False)
|
||
|
||
# --- 4. 更新模拟仓位盈亏 ---
|
||
price_snapshot = {}
|
||
for mid, entry in all_markets_cache.items():
|
||
price_snapshot[mid] = {"price": entry["price"]}
|
||
paper_trader.update_pnl(price_snapshot)
|
||
|
||
# --- 5. 每日收益总结推送 (北京时间 23:55 - 00:05 之间发送) ---
|
||
now_bj = datetime.utcnow() + timedelta(hours=8)
|
||
if now_bj.hour == 23 and now_bj.minute >= 50:
|
||
summary_key = f"daily_pnl_{now_bj.strftime('%Y%m%d')}"
|
||
if summary_key not in pushed_signals:
|
||
# 构造总结消息
|
||
total_cost = 0
|
||
total_pnl = 0
|
||
data = paper_trader._load_data()
|
||
pos_list = data.get("positions", {})
|
||
|
||
if pos_list:
|
||
report = [
|
||
f"📊 <b>每日模拟仓结算总结 ({now_bj.strftime('%Y-%m-%d')})</b>\n"
|
||
+ "═" * 15
|
||
]
|
||
for p in pos_list.values():
|
||
if p["status"] == "OPEN":
|
||
total_cost += p["cost_usd"]
|
||
total_pnl += p.get("pnl_usd", 0)
|
||
|
||
report.append(
|
||
f"💳 可用余额: <b>${data.get('balance', 0):.2f}</b>"
|
||
)
|
||
report.append(
|
||
f"💰 今日累计投入: <b>${total_cost:.2f}</b>"
|
||
)
|
||
report.append(
|
||
f"📈 累计浮动盈亏: <b>{total_pnl:+.2f}$</b>"
|
||
)
|
||
notifier._send_message("\n".join(report))
|
||
pushed_signals[summary_key] = time.time()
|
||
|
||
except Exception as e:
|
||
logger.error(f"即时保存数据失败: {e}")
|
||
|
||
logger.info("本轮扫描结束。等待 5 分钟...")
|
||
time.sleep(300)
|
||
|
||
except KeyboardInterrupt:
|
||
logger.info("收到关机指令,正在退出...")
|
||
except Exception as e:
|
||
logger.exception(f"系统运行出错: {e}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|