🌡️ Weather Prediction-Based Market Making Strategy
利用 hko_predict.py 概率分布做市策略文档
作者: Spark ⚡ | 日期: 2026-04-23 | 最后更新: 2026-04-29 10:45
一、核心思想
当前系统拥有一个 21 信号集成的预测模型(hko_predict.py),输出每个温度结果的概率分布:
P(T=28°C) = 80.8%
P(T=29°C) = 11.0%
P(T=30°C) = 2.2%
P(T=31°C) = 1.5%
...
与此同时,Polymarket 上每个温度都有一个独立的二元合约市场。市场做市策略的核心是:当市场价格偏离预测概率超过阈值时,在偏离侧提供流动性(挂限价单),赚取买卖价差 + alpha。
与传统做市不同:我们不是被动地在两边挂单,而是基于预测信号进行非对称报价——在 mispricing 方向加大仓位、收紧价差,在另一侧放宽或撤单。
二、理论框架:从概率分布到公平价格
2.1 合约类型与公平价格映射
Polymarket 天气合约有三种有效类型(由 weather_arb_scanner.py 自动判定):
| 合约类型 | 含义 | 公平价格公式 |
|---|---|---|
| exact | 结算温度 = X°C | fair = P(T = X) |
| or_higher | 结算温度 ≥ X°C | fair = Σ P(T ≥ X) |
| or_lower (floor) | 结算温度 ≤ X°C | fair = Σ P(T ≤ X) |
2.2 结算规则(香港)
- 28.9°C → 结算 28°C
- 这是关键!直接影响概率分布的解读
2.3 概率分布修正
由于 floor 规则,预测模型输出的 settlement_pred 已经是 floor 后的值。概率分布 prob_distribution 中的每个温度就是最终的结算温度。
当前预测示例(2026-04-23 11:50):
Predicted Max: 28.0°C → Settlement: 28°C
Confidence: MEDIUM | Uncertainty: 0.35
Fair prices:
exact 28°C: 0.808 (80.8¢)
or_higher 28°C: 0.808 + 0.110 + ... ≈ 0.990 (99¢)
or_higher 29°C: 0.110 + 0.022 + ... ≈ 0.192 (19.2¢)
exact 29°C: 0.110 (11.0¢)
exact 30°C: 0.022 (2.2¢)
exact 27°C: 0.000 (0¢) ← 低于 hard lower bound (28.0°C)
二-b. 预测模型鲁棒性机制(2026-04-29 新增)
Memory Floor(记忆下限)
lower_bound(prev_date == today)信号锚点独立化(2026-04-29 10:33)
9 个信号原先以 forecast_max 为锚点,形成 ~35% 的虚假预报一致性。
_clim_base(过去7天平均)作为独立锚点lower_bound 钳制bias_correction 归零(2026-04-29 10:33)
快速升温改为常规信号(2026-04-29 10:33)
skip_ensemble=True 绕过全部规则信号信号上限裁剪(Signal Cap)
早起小时(8-10am)轨迹推算和快速升温信号极易被微小数据差分放大:
progress_prediction 在 hist_progress=0.15 时放大 6.7 倍cap = min(forecast_max+3, inland_max+1, current_max+5)快速升温安全阀
MAX_REALISTIC_RATE = 4.0°C/h(HKO 物理极限)MIN_WINDOW_HOURS = 0.1h(6 分钟最小窗口,防 dt→0 爆炸)预测平滑
hko_temp=None(API 部分故障)→ 跳过预测,不覆盖文件hko_data_fetcher.py(cron 每分钟),hko_live_predict.py 仅监测相似日限制
close_matches 为空时不使用 best_match 降级,避免噪音📋 完整信号 Review: [weather-signal-review-2026-04-29.md](weather-signal-review-2026-04-29.md)
冷锋检测修复(2026-04-30 08:22)
问题
清晨8点模型将正常昼夜温差误判为冷锋,导致预测锁死在22°C(98.7%概率),完全偏离HKO官方预报(26°C)和市场定价(25°C @ 83.5¢)。
根因:cold_front_peak 信号触发条件过宽——hko_max_midnight - hko_temp ≥ 1.5°C 在任何正常日清晨都会满足(凌晨2-4点峰值 → 清晨6-8点谷值是标准昼夜曲线)。
修复(三层防线)
门控层(新增):
cold front, northeast monsoon, temperature fell, northerly surge, markedly coolercooler(HKO每天"Cooler in the morning"属正常描述)、cool air、cooler weatherforecast_max 比昨日实际最高低 ≥ 3°C 也会触发温度层(门槛提高):
hko_max_midnight - hko_temp 门槛从 1.5°C → 3.0°C(门控通过后)气象验证层(确认条件放宽):
内陆站早间修正(2026-04-30 08:22)
问题
内陆站(上水、打鼓岭等)早上8点尚未开始日间升温,lead_pred = 0.88 × avg_lead + 3.0 给出 ~22°C 的严重低估。回归公式校准于午后数据,早晨使用会拖低整体预测。
修复
forecast_max + avg_fc_bias,权重从 30% → 15%效果:内陆信号从 21.9°C → 24.6°C,不再拖低早间预测。
三、做市策略设计
3.1 报价逻辑总览
┌──────────────────────────────────────────────────────────┐
│ 预测模型 → prob_distribution → fair_price(t) │
│ ↓ │
│ Polymarket CLOB → market_price(t), orderbook(t) │
│ ↓ │
│ edge = fair_price - market_price │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ |edge| > threshold? │ │
│ │ YES → 非对称报价 │ │
│ │ edge > 0: market 低估 → 收紧买价,放宽卖价 │ │
│ │ edge < 0: market 高估 → 收紧卖价,放宽买价 │ │
│ │ NO → 双边对称报价(纯做市) │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
3.2 Edge 阈值
⚠️ 当前实施: 统一使用 20% 阈值。仅当 |edge| ≥ 20% 时才生成报价,避免低 alpha 噪音。
| 参数 | 值 | 说明 |
|---|---|---|
thresholds['min'] | 20% | 最小 edge 阈值(所有合约统一) |
thresholds['strong'] | 30% | 强信号(加大仓位) |
max_edge_cap | 90% | edge 上限(天气市场大 edge 可能是真实机会) |
设计原理: 天气合约流动性低、spread 宽,低 edge(<20%)的收益可能被交易成本吃掉。提高阈值保证每次交易有足够 alpha 缓冲。
3.3 报价规则(当前实施)
✅ 已实施: 报价直接对齐 orderbook 盘口(best bid/ask),而非从 fair price 派生。
设 best_bid 为 orderbook 买一价,best_ask 为 orderbook 卖一价,tick 通过 CLOB 官方 API 获取(每个 token 固定,不是按价格分段):
GET https://clob.polymarket.com/tick-size?token_id=<token_id>
→ {"minimum_tick_size": 0.01} 或 {"minimum_tick_size": 0.001}
📄 详见 [挂单策略文档](order-placement-strategy.md)
原则
1. 报价放在 orderbook 最优价 ± 1 tick,使自己成为新的 best bid/ask
2. 绝不穿越 fair price:不高于 fair 买,不低于 fair 卖
3. Edge < 20% → 不报价(返回 None)
情况 A: edge > 0(市场低估,buy_biased)
bid_price = best_bid + tick ← 加 1 tick,成为新买一
(但不超过 fair_price,不穿越 fair)
ask_price = best_ask - tick ← 减 1 tick,向 fair 靠近
(但不低于 fair_price)
bid_size 增大(BASE_ORDER_SIZE × 1.5~2.0)
ask_size 减小(BASE_ORDER_SIZE × 0.3~0.5)
情况 B: edge < 0(市场高估,sell_biased)
bid_price = best_bid + tick ← 加 1 tick,向 fair 靠近
(但不超过 fair_price)
ask_price = best_ask - tick ← 减 1 tick,成为新卖一
(但不低于 fair_price)
bid_size 减小(BASE_ORDER_SIZE × 0.3~0.5)
ask_size 增大(BASE_ORDER_SIZE × 1.5~2.0)
情况 C: |edge| < 20%(无报价)
返回 None,不挂任何单
3.4 动态 spread 调整
Spread 不是固定的,需要根据以下因素调整:
| 因素 | 调整规则 |
|---|---|
| 预测置信度 | HIGH → spread × 0.7, LOW → spread × 1.5 |
| 时间 | 早上 (8-11am) → spread × 1.3, 午后 (2-4pm) → spread × 0.8 |
| 温度接近 hard bound | 当前已达温度 ≥ 预测温度 → spread × 0.6(确定性高) |
| 极端天气 | 台风/暴雨警告 → spread × 2.0 |
| orderbook 深度 | 深度薄 → spread × 1.5, 深度厚 → spread × 0.8 |
| 距结算时间 | < 2h → spread × 0.5, > 12h → spread × 1.3 |
四、仓位与风控
4.1 订单参数与仓位限制
| 参数 | 值 | 说明 |
|---|---|---|
BASE_ORDER_SIZE | $10 | 基础订单金额 |
MAX_ORDER_SIZE | $100 | 单笔最大订单 |
MIN_ORDER_SIZE | 5 | 最小挂单量(Polymarket 限制) |
MIN_NOTIONAL | $1 | 单笔最小对价(price × size ≥ $1) |
| 单合约最大敞口 | $50 (paper) | 初始保守 |
| 总最大敞口 | $200 | 跨所有合约 |
| 单方向最大 | $100 | 所有 YES 或所有 NO |
| 每日最大亏损 | $20 | 硬止损 |
MIN_NOTIONAL 强制逻辑: 如果 price × size < $1,自动增大 size 到 $1 / max(price, 0.001),上限 MAX_ORDER_SIZE。
4.2 库存管理
做市会产生库存。需要主动管理:
net_exposure = Σ(yes_tokens × entry_price) - Σ(no_tokens × entry_price)
如果 net_exposure > 0(持有过多 YES):
→ 提高 ask 积极性(降价出),降低 bid 积极性
如果 net_exposure < 0(持有过多 NO):
→ 提高 bid 积极性(提价收),降低 ask 积极性
库存偏移公式:
inventory_skew = -net_exposure × 0.01 ← 每 $1 净敞口偏移 1%
capped at ±5%
4.3 合约选择优先级
不是所有温度合约都适合做市。优先级排序:
| 优先级 | 条件 | 原因 |
|---|---|---|
| 🥇 最高 | 主导温度 (prob > 60%) | 流动性好、确定性高、交易量大 |
| 🥈 高 | 次高温度 (prob 10-30%) | 有 alpha 但风险可控 |
| 🥉 中 | 相邻温度 (prob 2-10%) | 可能被错误定价 |
| ❌ 低 | 尾部温度 (prob < 2%) | 流动性极差,滑点大 |
| ❌ 不做 | 已被 hard bound 排除的温度 | 概率为 0,但可能有套利机会 |
4.4 止损规则
| 触发条件 | 动作 |
|---|---|
| 单合约亏损 > $5 | 撤单,暂停该合约 30 分钟 |
| 日亏损 > $20 | 全部撤单,当日停止 |
| 连续 3 笔亏损 | 暂停 1 小时,检查模型 |
| 预测置信度突然下降 | 全部撤单,重新评估 |
五、实施架构
5.1 数据流
┌─────────────────┐ 每10分钟 ┌──────────────────┐
│ hko_predict.py │ ─────────────→ │ hko_prediction. │
│ (21信号模型) │ │ json │
└─────────────────┘ └────────┬─────────┘
│
┌─────────────▼─────────────┐
│ Weather MM Engine │
│ (新增: weather_mm.py) │
│ │
│ 1. 读取 prob_distribution │
│ 2. 计算 fair_price(t) │
│ 3. 查询 Polymarket 价格 │
│ 4. 计算 edge │
│ 5. 生成非对称报价 │
│ 6. 风控检查 │
│ 7. 挂单 (paper/live) │
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ Polymarket CLOB API │
│ (GET orderbook, POST order│
└───────────────────────────┘
5.2 运行频率
| 阶段 | 频率 | 原因 |
|---|---|---|
| 预测更新 | 每 10 分钟 | 与 hko_predict.py 同步 |
| 价格扫描 + 报价 | 每 5 分钟 | Cron 调度 weather_mm_runner.py --scan |
| 运行时间窗口 | 9:00-15:00 北京时间 | 仅在此时段允许下单(扫描可全天运行) |
| 全量重算 | 每次扫描时 | fair price 变化时重新报价 |
5.3 与现有 MM 系统集成
现有 src/mm/ 系统已有成熟的框架(SignalEngine → QuoteEngine → PaperTrader),天气 MM 可以作为新的 Signal 源接入:
# 新增: src/mm/signals.py 中添加
class WeatherPredictionSignal:
"""从 hko_predict.py 读取概率分布,转换为方向信号"""
def get_fair_prices(self) -> Dict[int, float]:
"""返回 {temp: fair_price} 映射"""
...
def get_edge(self, temp: int, market_price: float, comp_type: str) -> float:
"""计算 edge = fair_price - market_price"""
...
def to_direction_signal(self, edge: float) -> float:
"""edge → [-1, +1] 方向信号"""
...
六、关键风险与应对
6.1 模型风险
风险: 预测模型出错,80% 概率说 28°C,结果实际 29°C
应对:
uncertainty 和 confidence 字段6.2 流动性风险
风险: 天气合约流动性差,无法及时进出
应对:
6.3 结算风险
风险: 对 floor 规则理解错误导致 fair price 偏差
应对:
settlement_pred 已经是 floor 后的值6.4 信息不对称风险
风险: 我们可能不是唯一有预测模型的人,其他参与者可能有更好的信息
应对:
七、回测与验证
7.1 回测方法
1. 历史预测 vs 实际: 用过去 30 天的预测概率分布 vs 实际结算温度
2. 模拟做市: 用历史 Polymarket 价格 + 历史预测,模拟报价和成交
3. PnL 分解: 拆分为 spread 收益 + alpha 收益 + 库存损益
7.2 关键指标
| 指标 | 目标 | 说明 |
|---|---|---|
| 预测准确率 | > 60% (主导温度) | 概率分布校准 |
| 日均成交笔数 | 5-20 | 做市活跃度 |
| 日均 PnL | +$5-20 (paper) | 初始目标 |
| Sharpe | > 1.0 | 风险调整收益 |
| 最大回撤 | < $30 | 风控有效 |
| 报价成交率 | 30-50% | 报价合理性 |
八、实施状态
Phase 1: 数据对接 ✅
src/mm/weather_signal.py — 读取 hko_prediction.jsonfair_price() 函数 — 概率分布 → 公平价格Phase 2: 报价引擎 ✅
src/mm/weather_quotes.py — 非对称报价逻辑(盘口对齐)Phase 3: Paper Trading ✅
data/mm/ 目录)Phase 4: 回测验证 ✅
src/mm/weather_backtest.pyPhase 5: 上线监控 ✅
/mm/weather — 实时状态、报价、成交、仓位、回测/weather/hk → /mm/weather 快捷跳转链接Phase 8: 自动交易控制 ✅
auto_trading.json,enabled: falsePhase 6: 手动下单 ✅
Phase 7: L2 凭证 + CLOB 下单 ✅
from_private_key() 通过 L1 EIP-712 签名自动获取= padding).env: L2 凭证由 from_private_key 自动派生Phase 9: 钱包架构 + 实盘下单 ✅ (2026-04-24)
from_private_key → API Key == Signer → CLOB 接受invalid signature,确认不可用_get_cached_clob_client() 避免每次重新派生 (~4s)/api/mm/weather/place-order)Phase 10: 智能挂单 UI ✅ (2026-04-24)
Phase 11: 价格优化 & 测试安全 ✅ (2026-04-25)
floor(5/price)DELETE /order + body {"orderID": "..."}(camelCase),撤后刷新缓存 - MAX_DIRECTIONAL_EXPOSURE 5→10(对齐 per-contract $10)
- BASE_ORDER_SIZE 20→5, MAX_ORDER_SIZE 200→10(匹配测试上限)
九、代码结构(已实施)
src/mm/
├── weather_signal.py # ✅ 天气预测信号源
│ ├── load_prediction() # 读取 hko_prediction.json
│ ├── get_fair_price(temp, comp_type) # 计算公平价格
│ ├── get_edge(temp, market_price) # 计算 edge
│ └── get_prob_distribution() # 概率分布
│
├── weather_quotes.py # ✅ 天气报价引擎
│ ├── calculate_quote(temp, fair, market, orderbook)
│ │ → 盘口对齐报价(best_bid/best_ask ± 1 tick)
│ ├── _calculate_sizes(edge, bid_price, ask_price, inventory)
│ │ → MIN_NOTIONAL=$1, MIN_ORDER_SIZE=5, MAX_ORDER_SIZE=100
│ └── _get_edge_thresholds() # edge ≥ 20%
│
├── weather_mm_runner.py # ✅ 做市主循环
│ ├── main_loop():
│ │ 1. 读取最新预测
│ │ 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. 挂单/更新订单(paper mode,仅在门控通过时)
│ └── Cron: /5 9-15 Asia/Shanghai
│
├── weather_risk.py # ✅ 天气专用风控
│ ├── check_hard_bound() # 温度已达预测值?
│ ├── check_weather_warnings() # 极端天气?
│ ├── check_time_decay() # 距结算时间
│ └── daily_loss_limit() # 日亏损限制
│
├── weather_backtest.py # ✅ 回测框架
│
├── clob_client.py # ✅ CLOB API 客户端
│ ├── get_orderbook() # 获取盘口
│ ├── create_order_l1() # L1 钱包签名下单 (EIP-712)
│ ├── create_order() # L2 HMAC 签名下单
│ ├── cancel_order() # 撤单
│ ├── cancel_all_orders() # 全部撤单
│ └── get_orders() # 查询挂单
│
└── 数据文件:
├── data/hko_prediction.json # 预测概率分布
├── data/mm/weather_quotes.jsonl # 历史报价日志
├── data/mm/weather_fills.jsonl # 成交记录(含 live 订单)
├── data/mm/weather_positions.json # 持仓状态
├── data/mm/weather_state.json # 运行时状态
├── data/mm/weather_risk_state.json # 风控状态
└── data/mm/auto_trading.json # 自动交易开关状态
Web 端:
├── /mm/weather # MM Dashboard (Flask template)
│ ├── 实时统计、概率分布柱状图
│ ├── 日期标签页切换(04-23 / 04-24)
│ ├── 活跃报价列表(点击展开历史)
│ ├── 🟢 Buy YES / 🔴 Buy NO 手动下单按钮
│ ├── 🗑️ Cancel All 撤单按钮
│ └── API 端点:
│ ├── GET /api/mm/weather/status
│ ├── GET /api/mm/weather/quote-history/<temp>/<comp_type>
│ ├── POST /api/mm/weather/place-order # 按报价下单
│ ├── POST /api/mm/weather/quick-order # 盘口最优价下单
│ ├── POST /api/mm/weather/cancel-all # 全部撤单
│ ├── POST /api/mm/weather/cancel-order # 指定单撤单
│ ├── GET /api/mm/weather/open-orders # 查询挂单
│ └── POST /api/mm/weather/run-scan # 手动触发扫描
└── /weather/hk # HK 天气页面 (→ 📊 MM 跳转链接)
十、示例:今日报价推演
基于当前预测(2026-04-23 15:12 HKT):
预测: 29°C, 置信度 HIGH, uncertainty 0.12
当前 HKO 温度: 29.1°C (已达预测值!)
Hard lower bound: 29°C → T < 29°C 概率为 0
合约 fair price 与报价(实际扫描输出)
| 合约 | 类型 | Fair | Market | Edge | 动作 | Bid | Ask | Size |
|---|---|---|---|---|---|---|---|---|
| T=29°C | exact | 0.903 | 0.500 | +40.3% | 🟢 BUY | 0.001 | 0.998 | $50/$11 |
| T=30°C | exact | 0.097 | 0.100 | -0.3% | ❌ 无(edge<20%) | — | — | — |
| T≥31°C | or_higher | 0.010 | 0.500 | -49.0% | 🔴 SELL | 0.001 | 0.999 | $10/$50 |
| T≥28°C | or_higher | 1.000 | 0.003 | +99.7% | 🟢 BUY | 0.002 | 0.999 | $50/$10 |
⚠️ T28_or_higher 市场价 $0.003 是 stale 数据(hard bound 确认 T≥29°C,fair=1.0)
关键观察
1. 20% 阈值过滤: 4 个活跃合约中只有 3 个生成报价,T30 exact 因 edge 仅 -0.3% 被过滤
2. 盘口对齐: Bid/Ask 直接放在 orderbook best bid/ask ± 1 tick
3. T28_or_higher 特殊情况: fair=1.0 但市场价仅 $0.003(流动性极差),edge 99.7% 触发 buy_biased
4. MIN_NOTIONAL: Bid $0.001 × 50 = $0.05 < $1,但 size 已到 MAX_ORDER_SIZE 上限
十一、注意事项
1. 不要与现有 arbitrage scanner 冲突:arb scanner 做的是方向性押注(买低估卖高估),做市策略是提供流动性赚 spread。两者可以共存但仓位要分开管理。
2. 预测更新时撤单重报:每次 hko_predict.py 更新后,fair price 会变化,必须撤掉旧单重新报价。
3. 天气警告特殊处理:台风/暴雨/酷热警告会显著改变温度走势,此时应扩大 spread 或暂停做市。
4. 下午 4 点后谨慎:HKO 的 daily max 通常在 2-4pm 确定,4pm 后不确定性大幅降低,spread 应收紧。
5. 监控 HKO 官方预报变化:如果 HKO 更新了官方预报(通常 11:30am 和 4:30pm),模型会重新运行,fair price 可能显著变化。
十二、Web Dashboard
MM Dashboard: /mm/weather
实时展示做市系统状态:
| 模块 | 内容 |
|---|---|
| 🔓 自动交易开关 | 一键开启/关闭自动交易(默认关闭),需手动激活 |
| 🕘 交易时间 | 显示当前是否在 9:00-15:00 下单窗口内 |
| 📊 统计卡片 | 预测温度、HKO 实时温度、活跃报价数、成交数、PnL、持仓 |
| 📅 日期切换 | 顶部标签页按目标日期分组(如 04-23 / 04-24),点击切换 |
| 📈 概率分布 | 柱状图展示各温度概率 |
| 🃏 活跃报价 | 每个合约的 bid/ask/fair/edge,点击展开挂单历史 |
| 🛒 手动下单 | 🟢 Buy YES / 🔴 Buy NO 按钮,取 Gamma 盘口价 +1 tick |
| 🤖 自动信号 | MM 引擎方向 + Gamma 盘口价 +1 tick,仅 BUY 无持仓,SELL 需持仓 |
| 📌 挂单显示 | 页面底部双栏:💼 持仓 + 📌 挂单,实时显示数量和状态 |
| 🔔 最近成交 | 最近 15 笔成交记录 |
| 💼 持仓 | 当前各合约持仓和敞口 |
| 📋 回测 | 回测结果(PnL/Sharpe/Max DD/胜率) |
自动交易控制逻辑
扫描(报价生成): 全天运行,每5分钟
下单(成交模拟): 必须同时满足以下条件才触发
1. auto_trading.json → enabled = true(Web UI 手动开启)
2. 当前时间在 9:00-15:00 北京时间内
3. Risk manager 未触发止损
状态显示:
手动下单流程
1. 点击合约卡片上的 🟢 Buy YES 或 🔴 Buy NO
2. 系统使用页面上已加载的 Gamma 聚合盘口(非 CLOB 裸盘口)
3. 弹出确认框显示:
- 挂单价格(盘口价 + 1 tick,已取整到 CLOB 有效 tick)
- 金额、盘口买一/卖一、Spread、市场价
4. 确认后通过 from_private_key EOA 模式下单
5. 下单结果实时显示在按钮上(✅ 成功 / ❌ 失败)
价格计算规则(2026-04-25 更新):
BUY → ceilTick(Gamma_bestBid + 0.0001)
SELL → floorTick(Gamma_bestAsk - 0.0001)
CLOB Tick 取整(Polymarket 强制要求):
| 价格范围 | Tick Size | 示例 |
|---|---|---|
| ≥ 0.10 | 0.01 | bid=0.42 → ceil(0.4201) = 0.43 |
| ≥ 0.01 | 0.001 | bid=0.05 → ceil(0.0501) = 0.051 |
| < 0.01 | 0.0001 | bid=0.005 → ceil(0.0051) = 0.0052 |
⚠️ 之前直接用 CLOB 裸盘口(clob.polymarket.com/book),与页面显示的 Gamma 盘口不一致,且未做 tick 取整导致 breaks minimum tick size rule 错误。已修复。
智能挂单系统 (Phase 10, 2026-04-24, 更新 2026-04-25)
2026-04-25 更新:
DELETE /order + body {"orderID": "..."}已有挂单显示:每个合约卡片显示当前挂单:
📌 BUY YES 50 @ 35.0¢ ✖ — 带单独撤单按钮重复挂单保护:
🔄 更新 BUY ...)自成交防护:
API 端点汇总
| 方法 | 路径 | 功能 | 认证 |
|---|---|---|---|
| GET | /api/mm/weather/status | 当前做市状态(含 auto_trading 信息) | 无 |
| GET | /api/mm/weather/auto-status | 自动交易开关状态 | 无 |
| POST | /api/mm/weather/toggle-auto | 切换自动交易开关(开/关) | 无 |
| GET | /api/mm/weather/quote-history/ | 合约挂单历史 | 无 |
| 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 | 手动触发扫描 | 无 |
认证方式
| 方式 | 说明 | 配置项 |
|---|---|---|
| L1 (EIP-712) | 钱包私钥签名订单内容 | POLYMARKET_PRIVATE_KEY |
| L2 (HMAC) | 由 from_private_key 自动派生,缓存 5 分钟 | 无需手动配置 |
✅ 已解决: EOA 模式下单成功。详见下方「钱包架构与下单流程」。
跳转链接
/weather/hk 右上角 📊 MM → /mm/weather十三、钱包架构与下单流程 (2026-04-24)
13.1 地址架构
本系统涉及三个地址,各有不同用途:
| 地址 | 类型 | 用途 | 私钥 |
|---|---|---|---|
0xAf63...dfc51d | EOA (外部账户) | CLOB 下单签名 + 做市钱包 | ✅ POLYMARKET_PRIVATE_KEY |
0xe39C...d74C0B | Safe 智能合约 | Builder 账户(持有资金) | ❌ 无私钥(合约钱包) |
0xE290...7c48 | EOA | Builder Code 推导地址(未使用) | ✅ Builder Code |
关系:
0xAf63... 是 Safe 0xe39C... 的 owner(getOwners() 返回)0x60a9...) 是 Polymarket Builder 配置,推导出 0xE290...0xAf63...(EOA 模式),不用 Safe/Builder13.2 下单模式:EOA(当前使用)
1. from_private_key(PK) → 自动派生 L2 API Key (70be0740-...)
2. API Key 地址 = Signer 地址 = 0xAf63... ✅ 匹配
3. create_order(token_id, price, size, side) → EOA 签名
4. Maker = Signer = 0xAf63...(链上 USDC 需在此地址)
不使用 POLY_PROXY 模式:虽然 0xAf63... 是 Safe 的 owner,但 Polymarket CLOB API 不接受 POLY_PROXY 签名(即使官方 SDK 也返回 invalid signature)。
13.3 资金流向
Builder Safe (0xe39C...) ─── USDC 转账 ──→ Signer EOA (0xAf63...)
│ │
│ USDC approve → NegRisk Adapter
│ USDC approve → CTF Exchange
│ │
└── Relayer: QuickSwap └── CLOB 下单
USDC → MATIC (gas)
初始化步骤(已完成 2026-04-24):
1. ✅ 通过 Relayer SDK 从 Safe 转 $100 USDC 到 Signer
2. ✅ 通过 Relayer SDK 用 QuickSwap 换 MATIC($0.5 USDC → 5.2 MATIC)
3. ✅ Signer 链上 approve USDC 给 NegRisk Adapter (0xd91E...)
4. ✅ Signer 链上 approve USDC 给 CTF Exchange (0x4bFb...)
5. ✅ 通过 Relayer SDK 从 Safe 设置 ConditionalTokens approve(CTF + NegRisk + Adapter)
13.4 配置文件 (.env)
# CLOB 下单用(L2 凭证由 from_private_key 自动派生)
POLYMARKET_PRIVATE_KEY=0xc90e... # Signer EOA 私钥
POLYMARKET_WALLET_ADDRESS=0xAf63... # Signer 地址(USDC 在此)
# Builder 凭证(Relayer/Gamma API 用,不用于 CLOB 下单)
POLYMARKET_BUILDER_API_KEY=019dbed3-...
POLYMARKET_BUILDER_SECRET=lA9VMq...
POLYMARKET_BUILDER_PASSPHRASE=b9c5e1...
POLYMARKET_BUILDER_CODE=0x60a957...
POLYMARKET_SAFE_ADDRESS=0xe39C... # Safe 合约地址
13.5 Web 下单流程(Flask)
# 1. 缓存 CLOB 客户端(from_private_key 需 ~4s,缓存 5 分钟)
client = _get_cached_clob_client(private_key) # TTL=300s
# 2. EOA 模式下单(无 funder_address)
result = client.create_order(token_id, price, size, side, private_key=pk)
# 3. 响应示例
{"success": true, "order": {"orderID": "0x...", "status": "live"}}
13.6 补充资金
当 Signer USDC 余额不足时,从 Builder Safe 补充:
# 使用 Relayer SDK 从 Safe 转 USDC(无需 gas)
cd /tmp/poly-relayer && node transfer-usdc.mjs <amount>
# 如需 MATIC(链上 approve 等操作需要 gas)
cd /tmp/poly-relayer && node swap-quickswap.mjs # 0.5 USDC → MATIC
13.7 已知限制
/order 端点0x2791...),不是 native USDC十四、CLOB 下单调试记录 (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 | 验证合约地址 | 固定 0x4bFb...982E | 需查 GET /neg-risk,若 true 则用 0xC5d5...f80a |
| 3 | Salt 生成 | secrets.randbits(256) (78位) | int(random() time() 1000) (13位,JS safe int) |
| 4 | Salt 在 JSON | 字符串 | 数字 (匹配 Number.parseInt) |
| 5 | owner 字段 | 钱包地址 | API key UUID |
| 6 | side 在 JSON | 0 (数字) | "BUY" (字符串) |
| 7 | postOnly | 缺失 | false |
调试方法:
1. 用 Node.js 生成参考签名: signer._signTypedData(domain, types, value)
2. 用 Python 生成对比签名: encode_typed_data + Account.sign_message
3. 逐字段对比直到签名匹配
4. 用 http.client 直连绕过 Cloudflare(需设置 User-Agent: @polymarket/clob-client)
合约地址规则:
# 每个市场可能有不同的 exchange 合约
def get_verifying_contract(token_id):
neg_risk = GET /neg-risk?token_id={token_id}
if neg_risk:
return '0xC5d563A36AE78145C45a50134d48A1215220f80a' # NegRiskExchange
return '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E' # CTF Exchange
费率规则:
# 每个市场有独立的费率
fee_rate = GET /fee-rate?token_id={token_id}
# 签名时用 fee_rate,payload 中 feeRateBps 为字符串
十五、2026-04-26 实战调试与改进
15.1 代理钱包地址确认
Polymarket 用户可能拥有多个代理钱包(proxy wallet),不同钱包存储不同时期的仓位:
| 代理钱包 | 用途 | 说明 |
|---|---|---|
0xaf63f116d074ba2793cbaa83f3380f7e10dfc51d | 当前活跃 | MM bot 的 maker 地址,存当前持仓 |
0xe39c4853a6e14045f192b65efbacecf845d74c0b | 历史仓位 | 旧持仓(已结算/已赎回) |
教训: 查询 Polymarket 持仓/活动时,必须使用正确的代理钱包地址。EOA 地址(如 0x49ce...)不直接持有仓位。
15.2 下单/平仓必需的链上授权
平仓(SELL)失败的根本原因:CTF 合约缺少对 CLOB 交易所合约的 setApprovalForAll 授权。
Polymarket CLOB 下单需要以下授权(全部以代理钱包 0xaf63f116... 身份):
| 授权类型 | 目标合约 | 地址 | 用途 |
|---|---|---|---|
| USDC approve | CLOB Exchange | 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296 | BUY 下单 |
| USDC approve | NegRisk Adapter | 0xC5d563A36AE78145C45a50134d48A1215220f80a | BUY 下单(neg_risk 市场) |
| CTF setApprovalForAll | CLOB Exchange | 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296 | SELL/平仓 |
| CTF setApprovalForAll | NegRisk Adapter | 0xC5d563A36AE78145C45a50134d48A1215220f80a | SELL/平仓(neg_risk 市场) |
注意:天气市场使用 neg_risk=true,但 CLOB 下单时同时检查两个合约的授权。缺任何一个都会报 "not enough balance / allowance"。
CLOB 下单代码已验证正确:
15.3 链上交易类型判断
Polymarket 使用 NegRisk 适配器合约(0xC5d563A36AE78145C45a50134d48A1215220f80a),不走标准 OrderFilled 事件。
| 交易类型 | 判断方法 | 合约地址 |
|---|---|---|
| 成交 | CTF token 转移 + USDC 流向(买家→卖家+手续费) | Proxy 0xb768... |
| 挂单 | 无 CTF 转移,仅签名上链 | Proxy 0xb768... |
| 赎回 (Redeem) | CTF token 销毁 + USDC 到账 | Proxy 0xb768... |
Redeem 说明: 市场结算后,winning token 可赎回为 USDC。需要发起链上交易,支付 Polygon gas 费(约 $0.19-0.24)。赎回后持仓从 Polymarket 消失。
注意: weather_fills.jsonl 记录的是挂单行为(status: "live"),不是成交记录。需通过 Polymarket positions API 验证是否真正成交。
15.4 盘口定价修正
问题
对于流动性极差的合约(bid=0.01, ask=0.99,价差 98%),MM 按盘口价挂单导致:
修复
在 weather_quotes.py 的 calculate_quote() 和 mm_weather.html 的 quickOrder() 中加流动性检测:
# weather_quotes.py
ob_spread_pct = (market_ask - market_bid) / max(market_mid, 0.01)
ob_is_liquid = has_ob and market_bid > 0 and market_ask < 1.0 and ob_spread_pct < 0.50
if ob_is_liquid:
# 正常盘口定价
else:
# 回退 fair-price 定价
// mm_weather.html quickOrder
const obSpread = (bestBid - bestAsk);
const hasLiquidity = bestBid > 0.001 && bestAsk < 0.999 && obSpread < 0.50;
if (hasLiquidity) {
// 盘口价 +1 tick
} else {
// gamma 价 × 0.85 (BUY) / × 1.15 (SELL)
}
阈值: spread > 50% 判定为无真实流动性,回退 fair-price 定价。
15.5 合约类型解析修复
Polymarket 天气市场的问题文本使用 "or below" 而非 "or lower":
"Will the highest temperature in Hong Kong be 20°C or below on April 26?"
app.py orderbook API 的 comp_type 解析已添加 'or below' in ql or 'below?' in ql 匹配。
15.6 Web Dashboard 功能增强
| 功能 | 说明 | ||||
|---|---|---|---|---|---|
| 合约排序 | 按温度升序排列(原为 \ | edge\ | 降序) | ||
| Impossible 折叠 | 已不可能的合约默认折叠,点击展开 | ||||
| 单合约盘口刷新 | 🔄 按钮,调用 /api/mm/weather/orderbook/ | ||||
| YES/NO 双行盘口 | YES bid \ | spread \ | ask + NO bid \ | spread \ | ask |
| 5档盘口深度 | 📋 按钮展开 CLOB 5 档深度(YES/NO 买卖盘) | ||||
| 持仓状态栏 | qc-status-bar 显示 PM 持仓 + 挂单 + 平仓按钮 | ||||
| 实时更新 | 每 10 秒只更新活跃合约,保持展开/折叠状态 | ||||
| 下单前验盘口 | 挂单前实时取盘口,价格变化时警告 |
15.7 做市价格限制
基于实际盘口价格(不是 fair price):
| 条件 | 行为 |
|---|---|
| market_mid < 10¢ | 不买入(bid=0),可卖出 |
| market_mid > 90¢ | 不卖出(ask=0),可买入 |
代码位置:weather_quotes.py 的 calculate_quote()
15.8 CLOB 盘口数据源
| 数据 | 源 | 用途 |
|---|---|---|
| YES bid/ask | Gamma API bestBid/bestAsk | 合并盘口(含 AMM),卡片显示 |
| NO bid/ask | CLOB /book?token_id= | 纯 CLOB 盘口 |
| 5档深度 | CLOB /book?token_id= | 纯限价单,不含 AMM |
注意:Gamma 合并价(含 AMM)≈ Polymarket 网页显示价。纯 CLOB 对低流动性市场价差极大(如 1¢/99¢)。
已改为纯 CLOB 数据源(2026-04-26 15:18):
bestBid/bestAsk5档深度显示规则:
本文档随实际实施持续更新。
15.9 概率分布 Bug 修复(2026-04-26 16:28)
问题
概率分布与模型预测自相矛盾:
predicted_max = 28.8°C → settlement_pred = 28°C
但概率分布:
28°C: 33.7% ← 预测说结算 28,但只给 34%
29°C: 63.6% ← 给 29°C 更高的概率!
30°C: 1.2%
31-34°C: 各 0.4%(等概率,显然不对)
29°C edge = 32% - 6.5% ≈ 25-30%(模型说 32%,市场说 6.5%)
LLM 推理明确说:
"温度已从最高点下降0.3°C,且时间已过典型峰值时段(13-15时),新高可能性极小。29°C风险约15%。"
但分布却给 29°C 63.6%——LLM 和数学分布完全脱节。
根因(三层问题)
第一层:sigma 被历史 MAE floor 抬高
模型 uncertainty = 0.18(HIGH 置信度)
HISTORICAL_MAE_FLOOR = 0.7
sigma_floor_mult = 0.6(past_peak)
sigma_effective = max(0.18, 0.7 × 0.6) = 0.42 ← 2.3 倍于模型自身估计!
HISTORICAL_MAE_FLOOR 是为预报不确定性设计的兜底,但观测值已经匹配预测值时,不应该再用历史 MAE 来膨胀 sigma。
第二层:bin 边界效应 + floor_lb 截断
lower_bound = 28.8°C(已观测到 28.8°C)
floor_lb = 28
28°C 结算 bin 需要 max ∈ [28.8, 29.0),只有 0.2°C 宽
29°C 结算 bin 需要 max ∈ [29.0, 30.0),有 1.0°C 宽
sigma=0.42 时:
P(28.8 ≤ max < 29.0) = Φ(0.476) - Φ(0) = 0.683 - 0.5 = 18.3%
P(29.0 ≤ max < 30.0) = Φ(2.857) - Φ(0.476) = 0.998 - 0.683 = 31.5%
未归一化时 P(29) 就已经是 P(28) 的 1.7 倍!
第三层:normalization 放大
所有 bin 概率之和仅 ~50%(另一半在 lower_bound 以下,被归零)
归一化到 100% 后:
P(28) = 18.3% / 50% = 36.6%
P(29) = 31.5% / 50% = 63.0%
P(29) 从 31.5%(已偏高)被放大到 63%!
修复(两次迭代)
Fix 1: 确认预测时信任模型自身 uncertainty
# hko_predict.py L1372
if lower_bound >= predicted_max - 0.3:
sigma_effective = max(uncertainty, 0.10)
UNIFORM_FLOOR_PCT = max(UNIFORM_FLOOR_PCT * 0.25, 0.0003)
效果:sigma 从 0.42 → 0.19,分布变为 28°C=67% / 29°C=32%。
但 29°C 仍有 32%,edge 仍 ~30%——温度在下降(28.8→28.0),新高概率应该趋零。
Fix 2: 温度下降时进一步收紧 sigma
if lower_bound >= predicted_max - 0.3:
if temp_decline is not None and temp_decline > 0:
# 温度从已确认峰值下降 → max 已锁定
sigma_effective = max(uncertainty * 0.6, 0.09)
else:
sigma_effective = max(uncertainty, 0.10)
效果:sigma 从 0.19 → 0.11(0.19 × 0.6),分布变为:
| 修复前 | Fix 1 | Fix 2 | |
|---|---|---|---|
| 28°C | 33.7% | 67.3% | 95.6% |
| 29°C | 63.6% | 32.2% | 3.9% |
| 29°C Edge | ~30% | ~30% | +1.7% |
| uncertainty | 0.19 | 0.19 | 0.09 |
设计原则
1. HISTORICAL_MAE_FLOOR 是预报兜底,不是确认预测兜底:当观测值已匹配预测时,应信任模型自身 uncertainty
2. 温度下降 = 新高概率趋零:峰值已过 + 温度下降时,sigma 应收紧到 0.6× uncertainty
3. normalization 是条件概率,数学上正确:问题不在 normalization,而在 sigma 太宽导致原始概率就已偏高
代码位置
polymarket/scripts/hko_predict.py L1372-L1382(confirmed prediction override)