🌡️ Polymarket 香港天气做市系统 — 完整实现文档
作者: Spark ⚡ | 创建: 2026-04-26 | 基于截至 2026-04-26 16:36 的实际代码
目录
1. [系统总览](#一系统总览)
2. [数据管线](#二数据管线)
3. [预测模型](#三预测模型)
4. [做市引擎](#四做市引擎)
5. [下单系统](#五下单系统)
6. [Web Dashboard](#六web-dashboard)
7. [钱包架构](#七钱包架构)
8. [风控系统](#八风控系统)
9. [部署与调度](#九部署与调度)
10. [附录: 踩坑记录与注意事项](#附录-踩坑记录与注意事项)
一、系统总览
1.1 核心流程
HKO 天文台 API ──→ hko_data_fetcher.py (每10分钟) ──→ hko_latest.json
│
┌─────────────────────────────────────────┘
▼
hko_predict.py (每10分钟)
21信号集成 → prob_distribution
│
▼
hko_prediction.json
│
┌───────────────┼───────────────┐
▼ ▼
weather_mm_runner.py Web Dashboard
(每5分钟扫描+报价) /mm/weather
│ │
▼ ▼
weather_quotes.py /weather/hk
非对称报价生成 预测展示页面
│
▼
CLOB API → Polymarket 挂单
1.2 代码结构
polymarket/
├── scripts/
│ ├── hko_data_fetcher.py # 🔵 HKO 数据采集 (1037行)
│ ├── hko_predict.py # 🟢 核心预测模型 (1633行)
│ ├── weather_arb_scanner.py # 🟡 全城市套利扫描 (2675行)
│ ├── hko_backfill.py # 历史数据回填
│ ├── hko_backtest.py # 回测框架
│ └── hko_llm_reasoning.py # LLM 推理分析
│
├── src/mm/
│ ├── weather_signal.py # 🔵 预测信号读取 (273行)
│ ├── weather_quotes.py # 🟢 报价引擎 (445行)
│ ├── weather_mm_runner.py # 🟢 做市主循环 (1209行)
│ ├── clob_client.py # 🔵 CLOB API 客户端 (769行)
│ ├── paper_trader.py # Paper trading
│ ├── quote_engine.py # 通用报价框架
│ └── signals.py # 通用信号框架
│
├── src/web/
│ ├── app.py # 🟢 Flask 应用 (~10K行)
│ └── templates/
│ ├── mm_weather.html # MM Dashboard 前端 (~3K行)
│ └── weather_hk.html # HK 天气预测页面
│
├── data/
│ ├── hko/
│ │ ├── hko_latest.json # 实时 HKO 数据
│ │ ├── hko_prediction.json # 最新预测结果
│ │ └── hko_curve_YYYYMMDD.json # 逐时曲线
│ └── mm/
│ ├── weather_state.json # MM 运行时状态
│ ├── weather_quotes.jsonl # 报价历史
│ ├── weather_fills.jsonl # 成交/挂单记录
│ ├── weather_positions.json # 持仓
│ └── auto_trading.json # 自动交易开关
│
└── docs/
├── weather-prediction-mm-strategy.md # 策略设计文档
└── weather-mm-system-complete.md # 📄 本文档
1.3 部署位置
https://polymkt.lt.sopher.cool:4433 (本机 macOS VM, 端口 8899)/Volumes/data128G/polymarket-data/polymarket_analytics.duckdb) + SQLite二、数据管线
2.1 数据采集: hko_data_fetcher.py
运行频率: 每 10 分钟 (cron)
采集的数据源:
| 数据 | HKO API | 频率 | 字段 |
|---|---|---|---|
| 实时天气 | rhrread | 每小时 | 温度/湿度/风向/风速/气压/辐射 (27站) |
| 1分钟温度 | CSV | 每10分钟 | 40+ 站逐分钟温度 |
| 午夜以来最高/最低 | CSV | 每10分钟 | 各站 daily max/min |
| 9天预报 | fnd | 每天2次 | 预报最高/最低/天气描述/Icon |
| 本地预报 | flw | 每小时 | 天气文字描述/警告 |
| 1分钟湿度 | CSV | 每10分钟 | 30+ 站湿度 |
| 10分钟风 | CSV | 每10分钟 | 风向/风速/阵风 |
| 1分钟太阳辐射 | CSV | 每10分钟 | W/m² |
| 1分钟气压 | CSV | 每10分钟 | 海平面气压 |
| 每日报告 | RYES | 每天01:30 | 昨日官方最高/最低 |
输出:
data/hko/hko_latest.json — 实时快照data/hko/hko_history_YYYYMMDD.jsonl — 追加日志data/hko/hko_curve_YYYYMMDD.json — 每日逐时曲线2.2 外部数据源 (Open-Meteo)
| 数据 | 频率 | 缓存 |
|---|---|---|
| 逐时云量预报 (9-17h) | 30分钟 | om_hourly_cache.json |
| 逐时短波辐射预报 | 30分钟 | 同上 |
| 逐时降雨概率 | 30分钟 | 同上 |
| CAPE 对流指数 | 30分钟 | 同上 |
| 逐时湿度预报 | 30分钟 | 同上 |
| DLWP AI 模型 | 6小时 | dlwp_hko_latest.json |
| NWP 6模型集成 | 每次预测 | 内存 |
| 深圳实时温度 | 每次预测 | 内存 |
三、预测模型
3.1 核心算法: hko_predict.py
21 信号加权集成模型 + LLM 推理,运行频率每 10 分钟。
公式:
predicted_max = Σ(signal_i × weight_i) / Σ(weight_i) + bias_correction
3.2 信号分类
📡 实时信号 (10个)
| # | 信号 | 说明 | 权重范围 |
|---|---|---|---|
| 1 | current_max | 当前午夜以来最高温 (硬下界) | 15-50% |
| 2 | inland | 内陆站回归预测 (先行指标) | 20% |
| 3 | trajectory | 温度轨迹+进度比率 | 14% |
| 4 | diurnal_curve | 7日日变化曲线预测 | 10% |
| 5 | humidity_potential | 湿度驱动升温潜力 | 6-15% |
| 6 | warming_accel | 升温加速度检测 | 5-12% |
| 7 | solar | 太阳辐射 (实测) | 6-12% |
| 8 | sunshine | 阳光指数 (Icon推算) | 7% |
| 9 | wind | 风向效应 (海风/陆风) | 5% |
| 10 | past_peak | 已过峰值锁定 (16:00+) | 50% (触发后) |
📄 预报信号 (4个)
| # | 信号 | 说明 | 权重 |
|---|---|---|---|
| 11 | forecast | HKO 官方预报 (偏差修正) | 16-18% |
| 12 | weather_text | 天气描述 NLP 解析 | 8% |
| 13 | rain_cooling | 降雨降温效应 | 8% |
| 14 | warning | 天气警告 | 10% |
📊 基准+外部信号 (4个)
| # | 信号 | 说明 | 权重 |
|---|---|---|---|
| 15 | climatology | 7日气候学均值+趋势 | 8% |
| 16 | nwp_ensemble | NWP 6模型集成 | 12% |
| 17 | shenzhen | 深圳先行指标 | 6-10% |
| 18 | early_aggressive | 早期激进预测 (8-11点) | 10-25% |
📅 气象预报信号 (Open-Meteo, 5个)
| # | 信号 | 影响规则 | 权重 |
|---|---|---|---|
| 19 | cloud_cover | ≥80%: -1.2°C, ≥65%: -0.6°C, <40%: +0.4°C | 8% |
| 20 | forecast_radiation | ≥700: +0.8°C, ≥500: +0.2°C, <300: -1.0°C | 6% |
| 21 | rain_forecast | 峰≥60%: -1.2°C, ≥40%: -0.6°C | 6% |
| 22 | cape | ≥1500: -1.5°C, ≥800: -0.6°C | 5% |
| 23 | forecast_humidity | ≥80%: -0.8°C, ≥70%: -0.3°C, <55%: +0.5°C | 5% |
📅 相似日信号
| # | 信号 | 说明 | 权重 |
|---|---|---|---|
| 24 | analog_day | 过去7天相似日加权预测 | 8-12% |
🧠 LLM 推理信号
| # | 信号 | 说明 | 基准权重 | 午夜峰值锁定后 |
|---|---|---|---|---|
| 25 | llm_reasoning | DeepSeek V4 Pro 定性分析 | 10-25% (按置信度) | ~50-55% (成为主导) |
LLM 推理信号使用 DeepSeek V4 Pro 模型,接收完整 HKO 观测 → NWP → 预报 → 历史上下文,输出结构化的温度预测。其优势包括:
3.4 概率分布生成
预测模型输出概率分布的核心逻辑:
# 从 predicted_max 和 uncertainty 生成正态分布
# 按温度 bin 离散化,每个 bin 宽度 1°C
# 应用 hard lower bound (已观测到的最高温)
# 归一化到总和 1.0
关键参数:
sigma_effective: 分布宽度,由 uncertainty + 环境因素决定HISTORICAL_MAE_FLOOR: 历史 MAE 兜底值 (0.7°C)UNIFORM_FLOOR_PCT: 均匀分布地板,避免尾部概率为 03.5 结算规则
- 28.9°C → 结算 28°C (不是四舍五入)
calendarDayTemperatureMax - °C 城市: round((F-32)*5/9) = 整数 °C
- °F 城市: 直接用 °F 整数
3.6 偏差修正
predicted_max += bias_correction # 默认 +0.7°C (历史回测)
bias_correction = max(0, recent_3day_error_avg × 0.5) # 自适应
3.7 🚩 午夜峰值锁定覆盖 (Midnight Peak Lock Override)
引入时间: 2026-05-05 | 触发事件: 5月5日HK多云有雨日,午夜最高达23.0°C后气温持续下降至20.2°C
问题: 当日内最高温出现在凌晨/午夜(非正常日间升温),且白天完全没有升温条件时,climatology(近7日均值27.9°C)和diurnal_curve(27.5°C)等历史经验信号严重拉高预测值,完全不符合当日实际天气。
检测条件(在 hko_predict.py 中,LLM 推理信号之后、权重归一化之前执行):
if (hko_max_midnight >>= current_temp + 0.8°C # 温度从午夜峰值下降
AND current_hour < 14 # 早晨/午前
AND sunshine_idx < 0.15 # 无阳光
AND forecast_wx is cool/rainy/cloudy # 天气不支持升温
AND (HKO_forecast ≈ midnight_peak # 官方预报确认
OR NWP_all_low)): # 或所有模型预测低
→ midnight_peak_locked = True
触发锁定后的权重覆盖:
| 信号 | 常规权重 | 锁定后权重调整 |
|---|---|---|
llm_reasoning | 10-25% | ×5.0 → ~54% (绝对主导) |
current_max | 15-25% | ×3.0 → ~24%, 信号值修正为 hko_max_midnight (去掉+1°C缓冲) |
nwp_ensemble | 12% | 保持不变 (保留为 ~7%) |
shenzhen | 6-10% | 保持不变 (保留为 ~5%) |
| 其他19个信号 | 合计 ~53% | ×0.15 (降至 ~10%) |
关键效果:
hko_max_midnight + 1.0 修正为 hko_max_midnight,去掉早上的激进缓冲典型触发场景举例:
四、做市引擎
4.1 信号模块: weather_signal.py
核心函数:
| 函数 | 功能 |
|---|---|
get_prediction() | 加载 hko_prediction.json (10分钟缓存) |
get_prob_distribution() | 返回 {temp: prob} 映射 |
get_fair_price(temp, comp_type) | 计算公平价格 |
get_fair_price_exact(temp) | P(T = X) |
get_fair_price_or_higher(temp) | P(T ≥ X) |
get_fair_price_or_lower(temp) | P(T ≤ X) |
compute_edge(temp, comp_type, market_price) | fair - market |
get_metadata() | 预测元数据 (置信度/不确定性等) |
get_hko_state() | 当前 HKO 观测状态 |
get_hard_lower_bound_status() | 硬下界状态 |
4.2 报价引擎: weather_quotes.py
核心函数: calculate_quote(temp, comp_type, market_mid, market_bid, market_ask, ...)
Edge 阈值: 统一 20%。仅当 |edge| ≥ 20% 时生成强信号报价。
报价模式:
buy_biased: edge > 20% → 市场低估 → 积极买入sell_biased: edge < -20% → 市场高估 → 积极卖出buy_weak / sell_weak: edge < 20% → 弱信号 (仅显示,不下单)symmetric: edge ≈ 0 → 不做市盘口对齐定价 (当前实施):
# 有流动性 (spread < 50%)
if ob_is_liquid:
if mode == 'buy_biased':
bid_price = min(best_bid + tick, fair - tick)
ask_price = best_ask
elif mode == 'sell_biased':
ask_price = max(best_ask - tick, fair + tick)
bid_price = best_bid
# 无流动性 (spread ≥ 50%) → 回退 fair-price 定价
else:
bid_price = fair - spread/2
ask_price = fair + spread/2
动态 Spread 调整:
| 因素 | 调整 |
|---|---|
| HIGH 置信度 | ×0.7 |
| LOW 置信度 | ×1.5 |
| 早上 (8-11am) | ×1.3 |
| 午后 (2-4pm) | ×0.8 |
| 傍晚 (17:00+) | ×0.6 |
| 距结算 < 2h | ×0.5 |
| 距结算 > 12h | ×1.3 |
| 硬下界触发 | ×0.7 |
| 极端天气警告 | ×2.0 |
| Orderbook 薄 (< $50) | ×1.5 |
| Orderbook 厚 (> $500) | ×0.8 |
| Edge > 5% | ×0.8 |
| 极端价格 (<5¢ 或 >95¢) | ×1.4 |
仓位大小:
| 参数 | 值 |
|---|---|
BASE_ORDER_SIZE | $10 |
MIN_ORDER_SIZE | 5 份 |
MAX_ORDER_SIZE | 100 份 |
MIN_NOTIONAL | $1 (price × size ≥ $1) |
仓位大小按档位取整: 5 → 10 → 20 → 30 → 40...
价格限制:
4.3 做市主循环: weather_mm_runner.py
运行模式:
--scan: 单次扫描 (手动触发或 cron)--loop: 持续循环扫描流程:
1. 读取最新预测 (hko_prediction.json)
2. 获取当天活跃 HK 天气合约 (Gamma API)
3. 解析合约日期 (parse_market_date)
4. 获取市场价 (Gamma API outcomePrices)
5. 获取 orderbook (CLOB API)
6. 对每个合约计算 edge + 生成报价
7. 风控检查
8. 自动交易门控 (auto_trading + 9:00-15:00)
9. 挂单/更新订单
交易时间窗口: 9:00-15:00 北京时间
auto_trading.json enabled=true)4.4 风控: weather_risk.py
| 触发条件 | 动作 |
|---|---|
| 单合约亏损 > $5 | 撤单,暂停该合约 30 分钟 |
| 日亏损 > $20 | 全部撤单,当日停止 |
| 连续 3 笔亏损 | 暂停 1 小时,检查模型 |
| 预测置信度突然下降 | 全部撤单,重新评估 |
五、下单系统
5.1 CLOB 客户端: clob_client.py
纯 REST 实现,无 py-clob-client 依赖。
认证方式:
| 方式 | 签名 | 用途 |
|---|---|---|
| L1 (EIP-712) | 钱包私钥签名订单内容 | 订单签名 |
| L2 (HMAC) | API Key + Secret HMAC | HTTP 请求认证 |
| L2 自动派生 | from_private_key() → /auth/api-key | 一键获取 L2 凭证 |
核心 API:
| 方法 | 端点 | 功能 |
|---|---|---|
get_orderbook(token_id) | GET /book | 获取盘口 |
get_midpoint(token_id) | GET /midpoint | 中间价 |
get_tick_size(token_id) | GET /tick-size | Tick 大小 |
get_neg_risk(token_id) | GET /neg-risk | 是否 NegRisk 市场 |
get_fee_rate_bps(token_id) | GET /fee-rate | 费率 |
create_order(...) | POST /order | 下单 (L1签名 + L2认证) |
cancel_order(order_id) | DELETE /order | 撤单 |
cancel_all_orders() | DELETE /cancel-all | 全部撤单 |
get_orders(token_id) | GET /data/orders | 查询挂单 (返回全部状态) |
_get_open_orders_cached(client) | 后端缓存包装 | 过滤掉 FILLED/CANCELLED/EXPIRED (15s TTL) |
get_gamma_markets(...) | Gamma API | 获取市场列表 |
5.2 EIP-712 订单签名 (v2)
2026-04-29 更新: 已从 v1 迁移到 v2。详见 docs/clob-v2-migration.md。
v2 EIP-712 Order struct (11 字段):
salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder
关键参数:
| 参数 | v2 值 | 说明 |
|---|---|---|
| Domain Name | Polymarket CTF Exchange | 不变 |
| Domain Version | "2" | v1 是 "1" |
| Chain ID | 137 (Polygon) | 不变 |
| Verifying Contract (普通) | 0xE111180...96B | v2 新地址 |
| Verifying Contract (NegRisk) | 0xe2222d...0F59 | v2 新地址 |
| Salt | int(random() time() 1000) | JS safe int (13位) |
| Side (签名) | 0=BUY, 1=SELL (uint8) | 不变 |
| Side (Payload) | "BUY" / "SELL" (字符串) | 不变 |
| Signature Type | 0=EOA, 2=POLY_GNOSIS_SAFE | v1 有 1=POLY_PROXY (不可用) |
| Timestamp | int(time.time() * 1000) | 毫秒, v1 是秒 |
| owner | API Key UUID | 不是钱包地址 |
| eth_account 版本 | >=0.12,<0.13 | 0.13.x 用非标准 EIP-712 前缀 |
MakerAmount/TakerAmount 计算: 同 v1
5.3 链上授权与余额
CLOB 余额 ≠ 链上 USDC 余额。Polymarket 的余额是独立平台记账。
| 授权类型 | 目标合约 (v2) | 用途 |
|---|---|---|
| USDC approve | 0xE111180... (v2 CTF Exchange) | BUY 下单 |
| USDC approve | 0xe2222d... (v2 NegRisk Exchange) | NegRisk 市场 |
| 平台存入 | https://polymarket.com/portfolio | CLOB 余额充值 |
| USDC approve | NegRisk Adapter | BUY 下单 (neg_risk) |
| CTF setApprovalForAll | CLOB Exchange | SELL/平仓 |
| CTF setApprovalForAll | NegRisk Adapter | SELL/平仓 (neg_risk) |
⚠️ 天气市场使用 neg_risk=true,CLOB 下单时同时检查两个合约的授权。缺任何一个都会报 "not enough balance / allowance"。
六、Web Dashboard
6.1 路由总览
| 路径 | 模板 | 功能 |
|---|---|---|
/mm/weather | mm_weather.html | MM Dashboard |
/weather/hk | weather_hk.html | HK 天气预测页面 |
/docs/ | 文件列表 | 文档索引 |
/docs/ | Markdown 渲染 | 文档阅读 |
6.2 MM Dashboard API
| 方法 | 路径 | 功能 | 认证 |
|---|---|---|---|
| GET | /api/mm/weather/status | 做市状态 (预测+报价+持仓) | 无 |
| GET | /api/mm/weather/auto-status | 自动交易开关状态 | 无 |
| POST | /api/mm/weather/toggle-auto | 切换自动交易 | 无 |
| GET | /api/mm/weather/prediction | 最新预测数据 | 无 |
| GET | /api/mm/weather/trades | 最近成交 | 无 |
| GET | /api/mm/weather/backtest | 回测结果 | 无 |
| GET | /api/mm/weather/quote-history/ | 合约报价历史 | 无 |
| GET | /api/mm/weather/balance | USDC.e/MATIC 余额 | 无 |
| POST | /api/mm/weather/place-order | 下单 (含自成交/重复单检测/撤旧下新) | L1+L2 |
| POST | /api/mm/weather/quick-order | 盘口最优价下单 | L1+L2 |
| POST | /api/mm/weather/cancel-all | 全部撤单 | L2 |
| POST | /api/mm/weather/cancel-order | 指定单撤单 | L2 |
| GET | /api/mm/weather/open-orders | 查询挂单 (自动过滤已成交/已取消) | L2 |
| POST | /api/mm/weather/run-scan | 手动触发扫描 | 无 |
| GET | /api/mm/weather/clob-book/ | CLOB 盘口代理 (3s 缓存) | 无 |
| GET | /api/mm/weather/clob-tick/ | CLOB Tick-Size 代理 (60s 缓存) | 无 |
6.3 Dashboard 功能模块
| 模块 | 内容 |
|---|---|
| 🔓 自动交易开关 | 一键开启/关闭 (默认关闭) |
| 🕘 交易时间 | 显示是否在 9:00-15:00 窗口内 |
| 📊 统计卡片 | 预测温度、HKO 实时温度、报价数、成交数、PnL |
| 💰 USDC.e 余额 | 链上查询,每 10s 刷新 |
| 📅 日期切换 | 标签页按目标日期分组 |
| 📈 概率分布 | 柱状图展示各温度概率 |
| 🃏 活跃报价 | 合约卡片 (bid/ask/fair/edge),点击展开历史 |
| 🛒 手动下单 | Buy YES / Buy NO 按钮 |
| 🤖 自动信号 | MM 引擎方向 + 盘口价 |
| 📌 挂单显示 | 💼 持仓 + 📌 挂单双栏 |
| 📋 回测 | PnL/Sharpe/Max DD/胜率 |
| 📊 5档盘口 | CLOB 深度 (YES/NO 买卖盘) |
6.4 CLOB API 代理 (CORS 解决方案)
问题: Polymarket CLOB API (clob.polymarket.com) 不返回 CORS 响应头。浏览器端 fetch() 调用会被静默拦截,导致盘口深度面板空白、下单按钮无响应等问题。
解决方案: 所有浏览器端 CLOB API 调用通过 Flask 后端代理:
浏览器 fetch('/api/mm/weather/clob-book/<token_id>')
──→ Flask 后端 @8899
──→ https://clob.polymarket.com/book?token_id=...
←── JSON data
←── JSON response (同源,无 CORS 限制)
代理端点:
/api/mm/weather/clob-book/ — 盘口深度,3s 内存缓存/api/mm/weather/clob-tick/ — Tick Size,60s 内存缓存错误处理: 代理返回 HTTP 502 而非 200,确保前端 .catch() 正常触发并保留旧数据。
前端覆盖率: mm_weather.html 中所有 CLOB 调用 (6 处) 均已通过代理,0 个直接调用残留。
6.5 价格计算规则 (前端)
// 使用 Gamma 聚合盘口价 + tick 取整
BUY → ceilTick(Gamma_bestBid + 0.0001)
SELL → floorTick(Gamma_bestAsk - 0.0001)
CLOB Tick 取整:
Tick size 由 CLOB 官方 API 按 token 返回,不是按价格分段:
GET https://clob.polymarket.com/tick-size?token_id=<token_id>
→ {"minimum_tick_size": 0.01} 或 {"minimum_tick_size": 0.001}
| Token | Tick Size | 示例 |
|---|---|---|
| 26°C YES | 0.001 | bid=0.022 → ceil = 0.023 |
| 29°C YES | 0.01 | bid=0.01 → ceil = 0.02 |
📄 详见 [挂单策略文档](order-placement-strategy.md)
6.6 盘口数据源
| 数据 | 源 | 用途 |
|---|---|---|
| YES bid/ask | CLOB /book?token_id= (YES) | 卡片显示 |
| NO bid/ask | CLOB /book?token_id= (NO) | 卡片显示 |
| 5档深度 | CLOB /book?token_id= | 限价单深度 |
| 市场中间价 | Gamma API outcomePrices | Edge 计算 |
当前使用纯 CLOB 数据源 (2026-04-26 更新)。此前用 Gamma 聚合盘口,后改为纯 CLOB 以保持数据一致性。
七、钱包架构
7.1 地址关系
| 地址 | 类型 | 用途 | 私钥 |
|---|---|---|---|
0xAf63...dfc51d | EOA | CLOB 下单签名 + 做市钱包 | ✅ |
0xe39C...d74C0B | Safe 合约 | Builder 账户 (持有资金) | ❌ |
0xE290...7c48 | EOA | Builder Code 推导地址 | ✅ Builder Code |
关系: 0xAf63... 是 Safe 0xe39C... 的 owner。
7.2 下单模式: POLY_GNOSIS_SAFE (当前)
2026-04-29 更新: 从 EOA 切换到 POLY_GNOSIS_SAFE (sig_type=2)。CLOB 余额在 Safe 名下。
1. from_private_key(PK) → 自动派生 L2 API Key
2. API Key 地址 = Signer 地址 = 0xAf63... ✅
3. create_order(token_id, price, size, side, funder_address=SAFE) → EOA 签名
4. Maker = Safe (0xe39C...) = 出资方
5. Signer = EOA (0xAf63...) = 签名方
6. signature_type = 2 (POLY_GNOSIS_SAFE)
不使用 POLY_PROXY (sig_type=1): CLOB API 不支持。使用 sig_type=2。
7.3 资金流向
Safe (0xe39C...) ─── USDC 转账 ──→ Signer EOA (0xAf63...)
│
USDC approve → NegRisk + CTF Exchange
MATIC (gas) → QuickSwap
│
└── CLOB 下单
7.4 补充资金
# 从 Safe 转 USDC
cd /tmp/poly-relayer && node transfer-usdc.mjs <amount>
# 换 MATIC (gas)
cd /tmp/poly-relayer && node swap-quicksilver.mjs
7.5 环境变量 (.env)
POLYMARKET_PRIVATE_KEY=0xc90e... # Signer EOA 私钥
POLYMARKET_WALLET_ADDRESS=0xAf63... # Signer 地址
POLYMARKET_BUILDER_API_KEY=... # Gamma API 用
POLYMARKET_BUILDER_SECRET=...
POLYMARKET_BUILDER_PASSPHRASE=...
POLYMARKET_BUILDER_CODE=...
POLYMARKET_SAFE_ADDRESS=0xe39C... # Safe 合约地址
八、风控系统
8.1 仓位限制
| 参数 | 值 |
|---|---|
| 单笔最大 | $5 (测试阶段) |
| 单合约最大敞口 | $10 |
| 总最大敞口 | $50 |
| 单方向最大 | $30 |
8.2 自动交易门控
自动交易需要同时满足:
1. auto_trading.json → enabled: true (Web UI 手动开启)
2. 当前时间在 9:00-15:00 北京时间内
3. Risk manager 未触发止损
8.3 自成交防护
8.4 流动性检测
九、部署与调度
9.1 Cron 任务
| 任务 | 频率 | 脚本 |
|---|---|---|
| HKO 数据采集 | 每 10 分钟 | hko_data_fetcher.py |
| 预测更新 | 每 10 分钟 | hko_predict.py (由 fetcher 触发) |
| MM 扫描 | 每 5 分钟 | weather_mm_runner.py --scan |
| 全城市套利扫描 | 每 30 分钟 | weather_arb_scanner.py |
| DLWP AI 预测 | 每 6 小时 | DLWP pipeline |
9.2 运行环境
附录: 踩坑记录与注意事项
以下记录开发过程中的关键错误和教训,按时间倒序排列。
A.1 概率分布 Bug: sigma 膨胀 + bin 边界效应 (2026-04-26)
现象: 预测 28.8°C (结算 28°C),但概率分布显示 P(29)=63.6% > P(28)=33.7%。
根因 (三层问题):
1. HISTORICAL_MAE_FLOOR 滥用: 模型 uncertainty=0.18 (HIGH),但被历史 MAE floor 0.7 膨胀到 sigma=0.42 (2.3倍)。当观测已确认预测时,不应再用历史 MAE 兜底。
2. Bin 边界效应: lower_bound=28.8 时,28°C bin 仅 [28.8, 29.0) 宽 0.2°C,而 29°C bin 宽 1.0°C。sigma=0.42 时 P(29) 已经是 P(28) 的 1.7 倍。
3. 归一化放大: 所有 bin 概率之和仅 ~50% (另一半在 lower_bound 以下被归零),归一化后 P(29) 从 31.5% 被放大到 63%。
修复 (hko_predict.py L1372):
if lower_bound >= predicted_max - 0.3:
if temp_decline is not None and temp_decline > 0:
sigma_effective = max(uncertainty * 0.6, 0.09)
else:
sigma_effective = max(uncertainty, 0.10)
UNIFORM_FLOOR_PCT = max(UNIFORM_FLOOR_PCT * 0.25, 0.0003)
教训:
HISTORICAL_MAE_FLOOR 是预报不确定性兜底,当观测值匹配预测时,信任模型自身 uncertaintyA.2 盘口定价 vs Fair-Price 定价 (2026-04-26)
现象: 低流动性合约 (bid=1¢, ask=99¢) 按盘口价挂单永远成交不了。
根因: MM 报价直接对齐 CLOB 盘口,但低流动性合约的盘口价毫无意义 (边界值)。
修复: 添加流动性检测:
ob_spread_pct = (market_ask - market_bid) / max(market_mid, 0.01)
ob_is_liquid = (market_bid > 0 and market_ask < 1.0 and ob_spread_pct < 0.50)
Spread > 50% → 回退 fair-price 定价。
教训: 不要假设 orderbook 总是有效。极端流动性差的市场需要用 fair price 而非盘口价。
A.3 合约类型解析: "or below" vs "or lower" (2026-04-26)
现象: 部分合约的 comp_type 解析失败。
根因: Polymarket 问题文本用 "or below" 而非 "or lower":
"Will the highest temperature in Hong Kong be 20°C or below on April 26?"
修复: 添加 'or below' in ql or 'below?' in ql 匹配。
教训: 文本解析要覆盖多种措辞变体,不要假设单一格式。
A.4 链上交易判断: NegRisk 不走标准事件 (2026-04-26)
现象: 无法通过 OrderFilled 事件判断成交。
根因: NegRisk 适配器 (0xC5d5...f80a) 不走标准 OrderFilled 事件。
正确方法: 判断 CTF token 转移 + USDC 流向。
| 交易类型 | 判断方法 |
|---|---|
| 成交 | CTF token 转移 + USDC 流向 |
| 挂单 | 无 CTF 转移,仅签名上链 |
| 赎回 | CTF token 销毁 + USDC 到账 |
教训: 不同合约架构有不同的事件模式,不能假设标准 ERC-20/721 行为。
A.5 代理钱包地址选择 (2026-04-26)
现象: 用 EOA 地址查询 Polymarket 持仓返回空。
根因: Polymarket 用户可能有多代理钱包,持仓在不同地址。
正确做法:
0xaf63f116d074ba2793cbaa83f3380f7e10dfc51d0xe39c4853a6e14045f192b65efbacecf845d74c0b0x49ce...) 不直接持有仓位教训: 查询前先确认正确的代理钱包地址。
A.6 SELL 失败: CTF setApprovalForAll 缺失 (2026-04-26)
现象: BUY 下单成功但 SELL 报 "not enough balance / allowance"。
根因: USDC approve 只覆盖 BUY。SELL 需要 CTF token 的 setApprovalForAll 授权。
需要的全部授权:
1. USDC approve → CLOB Exchange (BUY)
2. USDC approve → NegRisk Adapter (BUY)
3. CTF setApprovalForAll → CLOB Exchange (SELL)
4. CTF setApprovalForAll → NegRisk Adapter (SELL)
教训: 下单前检查所有授权,不要只检查 USDC approve。
A.7 CLOB Tick 取整: 价格被拒绝 (2026-04-25)
现象: 下单报 "breaks minimum tick size rule"。
根因: 不同 token 的 tick size 不同(0.001 vs 0.01),价格未按正确 tick 取整。
解决方案: 使用 CLOB 官方 API 查询每个 token 的 tick size:GET /tick-size?token_id=...,前端和后端均在下单前调用此接口。详见 [挂单策略文档](order-placement-strategy.md)。
修复: 添加 ceilTick() / floorTick() 函数。
教训: 下单前务必验证 tick size,不同价格区间 tick 不同。
A.8 Gamma 盘口 vs CLOB 裸盘口 (2026-04-25)
现象: 页面显示的 Gamma 盘口价与下单用的 CLOB 裸盘口价不一致。
根因: Gamma 聚合盘口 (含 AMM) ≈ 网页显示价,纯 CLOB 对低流动性市场价差极大。
演变:
教训: 理解不同数据源的含义,选择与使用场景匹配的源。
A.9 CLOB 下单 5 大签名差异 (2026-04-23)
现象: Python 下单持续报 "Invalid order payload" / "invalid signature"。
根因: 与 @polymarket/clob-client v5.8.1 的 5 个关键差异:
| # | 差异 | 错误值 | 正确值 |
|---|---|---|---|
| 1 | EIP-712 域名 | CTF Exchange | Polymarket CTF Exchange |
| 2 | 验证合约 | 固定地址 | 需查 /neg-risk 动态切换 |
| 3 | Salt | secrets.randbits(256) (78位) | int(random()time()1000) (13位) |
| 4 | Salt 在 JSON | 字符串 | 数字 |
| 5 | owner 字段 | 钱包地址 | API key UUID |
| 6 | side 在 JSON | 0 (数字) | "BUY" (字符串) |
| 7 | postOnly | 缺失 | false |
教训: 逆向工程 API 时,逐字段对比参考实现的签名和 payload,不要假设。
A.10 HMAC 签名 Base64 编码 (2026-04-23)
现象: L2 认证失败。
根因: Base64 URL-safe 编码细节:
-_ 为 +/),补齐 = padding+/ 为 -_),保留 = padding教训: Base64 编码有多种变体 (standard/URL-safe/with-without padding),API 对接时需精确匹配。
A.11 USDC vs USDC.e (2026-04-24)
现象: 换了 USDC 但 Polymarket 不能用。
根因: Polymarket 在 Polygon 上使用 USDC.e (bridged USDC, 0x2791Bca...),不是原生 USDC (0x3c499c54...)。
修复: 通过 Uniswap V3 兑换原生 USDC → USDC.e。
教训: 不同链/平台可能使用不同的 USDC 版本,转账前确认合约地址。
A.12 数据库锁冲突 (2026-04-16)
现象: Web 应用返回 500 错误,DuckDB 锁冲突。
根因: DuckDB 不支持多进程并发写入 + 每次请求创建新引擎。
修复:
教训:
A.13 结算规则理解错误 (2026-04-14)
现象: 4/13 手动结算用了 Open-Meteo 数据导致多笔交易结果错误。
正确规则:
教训:
A.14 Open-Meteo Rate Limiting (2026-04-13)
现象: 频繁 429 错误。
根因: 7 个重叠调度源,每小时 30+ 次扫描 × 14 城市 = 数百次 API 调用。
修复: 禁用重复 cron,频率 5→30 分钟。
教训:
A.15 POLY_PROXY / Safe 下单演进 (2026-04-24 → 2026-04-29)
v1 阶段 (2026-04-24):
现象: 即使官方 SDK 也返回 invalid signature。
结论: Polymarket CLOB 对 Safe 代理钱包的签名验证有 bug 或未支持。
方案: EOA 模式 (from_private_key, sig_type=0)。
v2 阶段 (2026-04-29):
CLOB v2 迁移后,发现:
1. CLOB 余额独立于链上 USDC — 需通过 polymarket.com 存入
2. Safe (0xe39C...) 在 CLOB 有余额,EOA 没有
3. sig_type=2 (POLY_GNOSIS_SAFE) 可用,sig_type=1 (POLY_PROXY) 仍不可用
4. 用 Safe 作 funder + sig_type=2 成功挂单
当前方案: POLY_GNOSIS_SAFE 模式 (funder_address=Safe, sig_type=2)。
教训:
A.16 polymarket.db 大小问题 (2026-04-13)
现象: polymarket.db 达 22.3GB,备份超时被 SIGKILL。
应对: 考虑从备份中排除或增加超时。
教训:
A.17 通知系统不一致 (2026-04-16)
现象: 脚本输出显示东京交易,通知文件记录伊斯坦布尔交易。
根因: 4 个不同通知文件分散存储,31+ 脚本各自写入。
修复: 统一通知目录 + 通知包装器 + 文件锁。
教训:
A.18 部署位置误解: 阿里云 vs 本机 (2026-04-26)
错误: 文档 (包括本文档初版) 声称预测模型运行在"阿里云服务器"上。
事实: 一切运行在本机 macOS VM 上:
/10 * hko_data_fetcher.py (本机 crontab)教训: 部署位置要以 cron/进程实际运行位置为准,不能凭印象写。
A.20 CORS 导致前端功能全部静默失败 (2026-05-02)
现象: 盘口深度面板空白、下单按钮无反应、但无任何报错。
根因: Polymarket CLOB API (clob.polymarket.com) 不返回 CORS 响应头。浏览器端 fetch() 到不同源被静默拦截 — 网络面板显示 (blocked) 但 Console 可能无报错。
影响范围: mm_weather.html 中共 6 处直接 CLOB fetch() 调用,包括:
toggleDepth — 5档盘口 (YES + NO,2处)confirmOrder — 手动下单智能定价quickOrder — 快捷下单detectTickForToken — Tick Size 检测修复: 添加 Flask 后端代理端点 (/api/mm/weather/clob-book/ 和 /api/mm/weather/clob-tick/),所有前端 CLOB 调用改为同源代理请求。代理返回 HTTP 502 时前端 .catch() 可正常处理。
教训:
fetch() 被 CORS 拦截时可能静默失败(无 visible error)A.21 预测分布正午过度自信 (2026-05-02)
现象: 12:00 时预测锁定 26°C 概率 99.7%,而实际信号指向 27-28°C (内陆最高已达 29.8°C)。
根因: hko_predict.py 中"确认预测覆盖"逻辑 (lower_bound >= predicted_max - 0.3) 在正午就触发,将 sigma 收紧到 0.114,导致概率分布极度集中。但正午温度下降可能只是云/天气波动,不是真正的峰值下降。
修复: 添加 late_enough_for_lock 时间门控:
current_hour >= 15 → 完全 sigma 收紧 (峰值已过)current_hour >= 14 且 temp_decline >= 1.0°C → 中等收紧同时修复: uncertainty 收紧逻辑 — 14:00 前即使温度下降也不降低 uncertainty。
修复后: 26°C: 72.5%, 27°C: 23.7%, 28°C: 1.3% (合理分布)
教训:
A.22 已成交挂单未从 UI 清除 (2026-05-02)
现象: CLOB 上已成交的订单仍然显示在 Dashboard 的挂单列表中,显示 (剩0.0)。
根因: 三层问题:
1. Polymarket API /data/orders 返回所有订单 (含 FILLED/CANCELLED) — 无状态过滤
2. _get_open_orders_cached() 原样返回无过滤
3. updateStatusBars() 前端渲染所有订单,不隐藏 unfilled <= 0
修复:
app.py): _get_open_orders_cached() 过滤 FILLED, CANCELLED, CANCELED, EXPIRED, CLOSED 状态,以及 size_matched >= original_size 的订单mm_weather.html): updateStatusBars() 跳过 unfilled <= 0 的订单 (安全网)教训:
A.23 深度面板 (5档盘口) 闪烁 — 彻底重构 (2026-05-02)
现象: 点击展开后闪烁、DOM 重建丢失、"加载中"文字跳动。
根因: 折叠/展开状态机复杂,renderStatus → renderQuotes 用 innerHTML 重建所有卡片时销毁深度面板 DOM,save/restore 方案治标不治本。
彻底重构 (晚 18:00):
1. 移除折叠逻辑 — 面板始终显示,无需 .show 类切换
2. 固定高度骨架 — _emptyDepthTables() 生成 5行空表占位 (min-height: 220px)
3. 无加载文字 — 用 .qc-depth-age 显示"刚刚更新"→"5秒前更新"→"1分30秒前更新"
4. 每秒更新年龄 — setInterval(_updateDepthAges, 1000) 全局滚动
5. refreshDepthPanel() — 只替换 .qc-depth-content innerHTML,不改全面板
6. renderQuotes 后统一 refresh — 不再需要 save/restore 面板状态
A.24 最优报价闪烁 — DOM 重建消除 (2026-05-02)
现象: .qc-ob (YES/NO bid/mid/ask) 显示短暂闪烁。
根因: 4 处更新点使用 obEl.innerHTML = html 或 obEl.outerHTML = html,每次都销毁+重建全部子节点,导致布局重排 + 可见闪烁。
修复: 创建 _updateObDisplay(card, bid, ask, noBid, noAsk) 函数:
节点textContent,不碰 DOM 结构innerHTMLrefreshTargetedOrderbooks、renderQuotes、refreshDepthPanel、refreshOrderbook 4 处调用A.24 设计原则总结
1. 预测模型: 历史 MAE 是预报兜底,不是确认预测兜底
2. 盘口: 低流动性市场不要信任盘口价
3. API: 逆向工程时逐字段对比参考实现
4. 结算: 永远用官方数据源,不要用替代源
5. 数据库: 选型要考虑并发模型,DuckDB 不适合高并发写入
6. 链上: 下单前检查所有授权 (USDC + CTF)
7. 文本解析: 覆盖多种措辞变体,不要假设单一格式
8. 钱包: 查询前确认正确的代理钱包地址
9. Base64: 注意 URL-safe vs standard 编码差异
10. Token: 不同链/平台可能用不同版本的 USDC
11. CORS: 浏览器跨源 API 调用需要服务端代理,静默失败很难排查
12. 预测时间门控: 确认/锁定逻辑需要考虑时间上下文,峰值前不要过早收紧分布
13. 订单状态: 不要相信 API 默认过滤,自己显式过滤 FILLED/CANCELLED
14. DOM 更新: 用 textContent 精准更新文本节点,避免 innerHTML/outerHTML 重建 DOM 导致闪烁
15. UI 状态: 需要实时更新的面板应始终可见,固定尺寸 + 时间戳优于折叠/展开 + 加载文字
A.25 5档盘口高亮自己挂单 + 剩余未成交量显示 (2026-05-05)
现象: 自己的挂单(SELL YES / BUY NO 等)没有在 5 档盘口中突出显示,无法直观区分自己的订单和他人订单。
修复:
1. _renderDepthTables — 新增 openOrders 参数,渲染时检查每行是否为我的挂单,匹配时添加 my-order CSS class(蓝色左边框 + 背景高亮)
2. _getMyOrderInfo — 新增函数,按 (token_id, price, side) 精确匹配(误差 < 0.0005),并返回剩余未成交量
3. _isMyOrder — 改为 delegate 到 _getMyOrderInfo
4. 盘口显示 — 自己的挂单行数量后追加 (剩余N) 标记
5. loadOpenOrders — 数据加载完成后触发 refreshTargetedDepthPanels(),确保高亮和剩余量即时更新
效果:
(6)A.26 5档盘口 NO 侧展示错误的订单 (2026-05-05)
现象: orderbook/23/exact 接口返回的 NO 测盘口数据显示价格最高的卖单和最低的买单,而非最有意义的(最低卖价和最高买价)。
根因: 后端 _fetch_orderbook_inner 和 api_mm_weather_orderbook_single 对 YES/NO 两个 token 都使用 bids_raw[-5:] / asks_raw[-5:]。CLOB API 返回的 bids 是升序(低→高),asks 是降序(高→低)。[-5:] 取最后 5 个:
结论: 逻辑正确。NO 侧的显示效果取决于 CLOB 当前深度。当自己的订单价格不在 top-5 最佳价范围内时,不会出现在盘口中。
优化: 通过 _isMyOrder 高亮和剩余量显示帮助区分是否自己的订单在盘口中。
A.27 CLOB SSL 重试耗尽 Worker 修复 (2026-05-05)
现象: 页面卡死,所有 API 请求需要 15-30 秒才返回,gunicorn worker 全部被占满。
根因: Polymarket CLOB API 的 SSL 握手不稳定(_ssl.c:1129 EOF 和 _ssl.c:1112 握手超时),后端对每次失败做了 3 次重试(含 sleep),导致单个 CLOB 请求可能耗时 15-20 秒。4 个 sync worker 很快全部被堵死。
修复:
1. _fetch_clob_best — CLOB 重试从 3 次减为 2 次,超时从 5s 减为 4s,sleep 从 0.3s→0.2s
2. _fetch_orderbook_inner Gamma 发现 — 重试从 3 次减为 2 次,超时从 10s 减为 8s,移除 sleep
3. api_mm_weather_orderbook_single Gamma 发现 — 同上
教训:
ThreadPoolExecutor 并发取 CLOB 数据,但 Python 的 urllib SSL 连接可能全局卡住A.28 平仓按钮持仓量不足5时不显示 + 价格不跟随盘口刷新 (2026-05-05)
现象: PM 持仓 1 个 NO token,平仓按钮不显示(因为 noCloseSz < 5 被设为 0)。持仓变化或盘口价格更新后,平仓按钮的量和价格不自动更新。
修复:
1. 移除最小 5 的限制 — yesCloseSz < 5 / noCloseSz < 5 改为 > 0 门槛,持仓大于 0 即可平仓
2. $5 notional cap 截断补全 — 当 cap 截断后剩余数量 < 5 时,一次性平全部持仓
3. 持仓刷新触发卡片重绘 — fetchPolymarketPositions() 完成后调用 refreshActiveCards(),确保按钮价格(来自 orderbook no_best_ask)和数量(来自 totalNoQty)实时更新
4. 定时器串行 — setInterval 内 fetchPolymarketPositions 用 .then() 串行化,数据回来后刷新卡片
效果:
SELL NO 1@99.0¢)和价格随持仓 + 盘口实时更新本文档随系统演进持续更新。最后更新: 2026-05-05 11:15 CST