🌡️ 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 部署位置


  • Web 服务: https://polymkt.lt.sopher.cool:4433 (本机 macOS VM, 端口 8899)
  • Flask 端口: 8899 (本机), 通过外部代理暴露为 4433
  • 数据库: DuckDB (/Volumes/data128G/polymarket-data/polymarket_analytics.duckdb) + SQLite
  • 预测模型: 运行在本机 (macOS VM),通过 cron 每 10 分钟触发



  • 二、数据管线


    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个)


    #信号说明权重范围
    1current_max当前午夜以来最高温 (硬下界)15-50%
    2inland内陆站回归预测 (先行指标)20%
    3trajectory温度轨迹+进度比率14%
    4diurnal_curve7日日变化曲线预测10%
    5humidity_potential湿度驱动升温潜力6-15%
    6warming_accel升温加速度检测5-12%
    7solar太阳辐射 (实测)6-12%
    8sunshine阳光指数 (Icon推算)7%
    9wind风向效应 (海风/陆风)5%
    10past_peak已过峰值锁定 (16:00+)50% (触发后)

    📄 预报信号 (4个)


    #信号说明权重
    11forecastHKO 官方预报 (偏差修正)16-18%
    12weather_text天气描述 NLP 解析8%
    13rain_cooling降雨降温效应8%
    14warning天气警告10%

    📊 基准+外部信号 (4个)


    #信号说明权重
    15climatology7日气候学均值+趋势8%
    16nwp_ensembleNWP 6模型集成12%
    17shenzhen深圳先行指标6-10%
    18early_aggressive早期激进预测 (8-11点)10-25%

    📅 气象预报信号 (Open-Meteo, 5个)


    #信号影响规则权重
    19cloud_cover≥80%: -1.2°C, ≥65%: -0.6°C, <40%: +0.4°C8%
    20forecast_radiation≥700: +0.8°C, ≥500: +0.2°C, <300: -1.0°C6%
    21rain_forecast峰≥60%: -1.2°C, ≥40%: -0.6°C6%
    22cape≥1500: -1.5°C, ≥800: -0.6°C5%
    23forecast_humidity≥80%: -0.8°C, ≥70%: -0.3°C, <55%: +0.5°C5%

    📅 相似日信号


    #信号说明权重
    24analog_day过去7天相似日加权预测8-12%

    🧠 LLM 推理信号


    #信号说明基准权重午夜峰值锁定后
    25llm_reasoningDeepSeek V4 Pro 定性分析10-25% (按置信度)~50-55% (成为主导)

    LLM 推理信号使用 DeepSeek V4 Pro 模型,接收完整 HKO 观测 → NWP → 预报 → 历史上下文,输出结构化的温度预测。其优势包括:

  • 综合判断矛盾信号(例如: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: 均匀分布地板,避免尾部概率为 0

  • 3.5 结算规则


  • 香港: HKO (天文台) — "Absolute Daily Max (deg. C)",向下取整 (floor)
  • - 28.9°C → 结算 28°C (不是四舍五入)

  • 其他城市: Weather Underground (WU) — 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_reasoning10-25%×5.0 → ~54% (绝对主导)
    current_max15-25%×3.0 → ~24%, 信号值修正为 hko_max_midnight (去掉+1°C缓冲)
    nwp_ensemble12%保持不变 (保留为 ~7%)
    shenzhen6-10%保持不变 (保留为 ~5%)
    其他19个信号合计 ~53%×0.15 (降至 ~10%)

    关键效果:

  • LLM + current_max 合计权重 ~78%,成为预测主导
  • climatology 从 4.3% → 几乎 0%(被压制到 0.2% 以下)
  • diurnal_curve 从 5.4% → 几乎 0%
  • current_max 信号值从 hko_max_midnight + 1.0 修正为 hko_max_midnight,去掉早上的激进缓冲
  • 预测值下降 1.0-1.5°C,更贴近实际天气

  • 典型触发场景举例:

  • 夜间冷锋过境 → 午夜达到峰值 → 白天持续多云有雨 → 气温不升反降
  • 强东北季风 + 云雨覆盖 → 午夜短暂放晴后迅速转阴
  • 台风外围 + 阴雨 → 凌晨短暂高温后持续降温



  • 四、做市引擎


    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_SIZE5 份
    MAX_ORDER_SIZE100 份
    MIN_NOTIONAL$1 (price × size ≥ $1)

    仓位大小按档位取整: 5 → 10 → 20 → 30 → 40...


    价格限制:

  • market_mid < 10¢ → 不买入 (bid=0)
  • market_mid > 90¢ → 不卖出 (ask=0)

  • 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 HMACHTTP 请求认证
    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-sizeTick 大小
    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 NamePolymarket CTF Exchange不变
    Domain Version"2"v1 是 "1"
    Chain ID137 (Polygon)不变
    Verifying Contract (普通)0xE111180...96Bv2 新地址
    Verifying Contract (NegRisk)0xe2222d...0F59v2 新地址
    Saltint(random() time() 1000)JS safe int (13位)
    Side (签名)0=BUY, 1=SELL (uint8)不变
    Side (Payload)"BUY" / "SELL" (字符串)不变
    Signature Type0=EOA, 2=POLY_GNOSIS_SAFEv1 有 1=POLY_PROXY (不可用)
    Timestampint(time.time() * 1000)毫秒, v1 是秒
    ownerAPI Key UUID不是钱包地址
    eth_account 版本>=0.12,<0.130.13.x 用非标准 EIP-712 前缀

    MakerAmount/TakerAmount 计算: 同 v1


    5.3 链上授权与余额


    CLOB 余额 ≠ 链上 USDC 余额。Polymarket 的余额是独立平台记账。


    授权类型目标合约 (v2)用途
    USDC approve0xE111180... (v2 CTF Exchange)BUY 下单
    USDC approve0xe2222d... (v2 NegRisk Exchange)NegRisk 市场
    平台存入https://polymarket.com/portfolioCLOB 余额充值
    USDC approveNegRisk AdapterBUY 下单 (neg_risk)
    CTF setApprovalForAllCLOB ExchangeSELL/平仓
    CTF setApprovalForAllNegRisk AdapterSELL/平仓 (neg_risk)

    ⚠️ 天气市场使用 neg_risk=true,CLOB 下单时同时检查两个合约的授权。缺任何一个都会报 "not enough balance / allowance"



    六、Web Dashboard


    6.1 路由总览


    路径模板功能
    /mm/weathermm_weather.htmlMM Dashboard
    /weather/hkweather_hk.htmlHK 天气预测页面
    /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/balanceUSDC.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}
    

    TokenTick Size示例
    26°C YES0.001bid=0.022 → ceil = 0.023
    29°C YES0.01bid=0.01 → ceil = 0.02

    📄 详见 [挂单策略文档](order-placement-strategy.md)

    6.6 盘口数据源


    数据用途
    YES bid/askCLOB /book?token_id= (YES)卡片显示
    NO bid/askCLOB /book?token_id= (NO)卡片显示
    5档深度CLOB /book?token_id=限价单深度
    市场中间价Gamma API outcomePricesEdge 计算

    当前使用纯 CLOB 数据源 (2026-04-26 更新)。此前用 Gamma 聚合盘口,后改为纯 CLOB 以保持数据一致性。



    七、钱包架构


    7.1 地址关系


    地址类型用途私钥
    0xAf63...dfc51dEOACLOB 下单签名 + 做市钱包
    0xe39C...d74C0BSafe 合约Builder 账户 (持有资金)
    0xE290...7c48EOABuilder 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.jsonenabled: true (Web UI 手动开启)

    2. 当前时间在 9:00-15:00 北京时间内

    3. Risk manager 未触发止损


    8.3 自成交防护


  • 新 BUY 价 ≥ 现有 SELL 价 → 拒绝 (HTTP 409)
  • 新 SELL 价 ≤ 现有 BUY 价 → 拒绝 (HTTP 409)
  • 同合约同方向自动撤旧下新

  • 8.4 流动性检测


  • Orderbook spread > 50% → 判定为无真实流动性 → 回退 fair-price 定价
  • 仅对当天合约做市 (不交易明天合约)



  • 九、部署与调度


    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 运行环境


  • 服务器: 本机 macOS VM (bot的虚拟机, Darwin arm64)
  • OS: macOS (Darwin 25.2.0)
  • Python: 3.9
  • Web: Flask (端口 8899, 通过代理暴露为 4433)
  • 数据库: DuckDB (分析) + SQLite (交易记录)
  • ⚠️ 注意: 阿里云 (aliyun.sopher.cool) 仅用作 nginx 反向代理和 SSH 隧道,不运行预测模型或 Web 服务



  • 附录: 踩坑记录与注意事项


    以下记录开发过程中的关键错误和教训,按时间倒序排列。

    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预报不确定性兜底,当观测值匹配预测时,信任模型自身 uncertainty
  • 温度下降时 sigma 应收紧到 0.6× uncertainty
  • 确认预测 (lower_bound ≥ predicted_max - 0.3) 时用不同参数组

  • A.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 用户可能有多代理钱包,持仓在不同地址。


    正确做法:

  • 当前活跃: 0xaf63f116d074ba2793cbaa83f3380f7e10dfc51d
  • 历史仓位: 0xe39c4853a6e14045f192b65efbacecf845d74c0b
  • EOA (0x49ce...) 不直接持有仓位

  • 教训: 查询前先确认正确的代理钱包地址。


    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 对低流动性市场价差极大。


    演变:

  • 最初: CLOB 裸盘口 → 价差极大,无意义
  • 改为: Gamma 聚合盘口 → 与网页一致
  • 最终: 纯 CLOB (2026-04-26) → 数据一致性

  • 教训: 理解不同数据源的含义,选择与使用场景匹配的源。


    A.9 CLOB 下单 5 大签名差异 (2026-04-23)


    现象: Python 下单持续报 "Invalid order payload" / "invalid signature"


    根因: 与 @polymarket/clob-client v5.8.1 的 5 个关键差异:


    #差异错误值正确值
    1EIP-712 域名CTF ExchangePolymarket CTF Exchange
    2验证合约固定地址需查 /neg-risk 动态切换
    3Saltsecrets.randbits(256) (78位)int(random()time()1000) (13位)
    4Salt 在 JSON字符串数字
    5owner 字段钱包地址API key UUID
    6side 在 JSON0 (数字)"BUY" (字符串)
    7postOnly缺失false

    教训: 逆向工程 API 时,逐字段对比参考实现的签名和 payload,不要假设。


    A.10 HMAC 签名 Base64 编码 (2026-04-23)


    现象: L2 认证失败。


    根因: Base64 URL-safe 编码细节:

  • Secret: URL-safe → standard (替换 -_+/),补齐 = padding
  • Signature: standard → URL-safe (替换 +/-_),保留 = 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 不支持多进程并发写入 + 每次请求创建新引擎。


    修复:

  • 单例写入引擎模式
  • 读写分离: SQLite (交易 CRUD) + DuckDB 只读 (分析查询)
  • 连接池管理 (pool_size=1, max_overflow=0)
  • 重试机制 (最多3次,指数退避)

  • 教训:

  • DuckDB 不是为高并发写入设计的
  • 生产环境需要读写分离架构
  • 数据库选型要考虑并发模型

  • A.13 结算规则理解错误 (2026-04-14)


    现象: 4/13 手动结算用了 Open-Meteo 数据导致多笔交易结果错误。


    正确规则:

  • 香港: HKO (不是 WU),向下取整 (不是四舍五入)
  • 其他城市: WU (不是 Open-Meteo/METAR)
  • Open-Meteo 与 WU 经常有 1-8°C 偏差 (Tel Aviv 差了 8°C!)

  • 教训:

  • 永远用官方结算数据源,不要用替代源
  • 取整规则 (floor vs round) 直接影响结果
  • 每个城市的结算规则可能不同

  • A.14 Open-Meteo Rate Limiting (2026-04-13)


    现象: 频繁 429 错误。


    根因: 7 个重叠调度源,每小时 30+ 次扫描 × 14 城市 = 数百次 API 调用。


    修复: 禁用重复 cron,频率 5→30 分钟。


    教训:

  • 审查所有 cron 任务避免重复调度
  • 对免费 API 要有速率意识
  • 缓存中间结果减少重复请求

  • 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)。


    教训:

  • 不是所有文档中的功能都实际可用
  • 有 fallback 方案很重要
  • 官方 SDK 也可能有同样的 bug

  • 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 上:

  • Cron 任务: /10 * hko_data_fetcher.py (本机 crontab)
  • hko_data_fetcher.py → import hko_predict → run_predict()
  • hko_data_fetcher.py → subprocess: weather_mm_runner.py --scan
  • Flask app.py: 本机端口 8899
  • 阿里云: 仅 nginx 反向代理 + SSH 隧道

  • 教训: 部署位置要以 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() 可正常处理。


    教训:

  • 跨源 API 调用不要假设一定有 CORS 支持
  • 浏览器 fetch() 被 CORS 拦截时可能静默失败(无 visible error)
  • 服务端代理是最可靠的 CORS 解决方案
  • 代理端点返回错误 status code (502) 确保前端错误处理正常

  • 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 → 中等收紧
  • 14:00 之前 → 不收紧 sigma (信任模型自身 uncertainty)

  • 同时修复: uncertainty 收紧逻辑 — 14:00 前即使温度下降也不降低 uncertainty。


    修复后: 26°C: 72.5%, 27°C: 23.7%, 28°C: 1.3% (合理分布)


    教训:

  • 预测模型的"确认"逻辑需要考虑时间上下文
  • 正午温度波动 ≠ 峰值已确定
  • 峰值通常出现在 14:00-15:00,在此之前应保持 uncertainty

  • 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 的订单 (安全网)

  • 教训:

  • 不要假设 API 返回的就是"打开"的订单 — 自己过滤状态
  • 缓存层 (15s TTL) 可能掩盖状态变化 — 下单后需要 invalidate cache
  • 前端永远做防御性过滤

  • A.23 深度面板 (5档盘口) 闪烁 — 彻底重构 (2026-05-02)


    现象: 点击展开后闪烁、DOM 重建丢失、"加载中"文字跳动。


    根因: 折叠/展开状态机复杂,renderStatusrenderQuotesinnerHTML 重建所有卡片时销毁深度面板 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 = htmlobEl.outerHTML = html,每次都销毁+重建全部子节点,导致布局重排 + 可见闪烁。


    修复: 创建 _updateObDisplay(card, bid, ask, noBid, noAsk) 函数:

  • 定位到 6 个 节点
  • 只改 textContent,不碰 DOM 结构
  • 仅首次创建或添加 NO 行时才用 innerHTML
  • 统一了 refreshTargetedOrderbooksrenderQuotesrefreshDepthPanelrefreshOrderbook 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)
  • 整个面板每秒自动刷新年龄,orderbook 更新后即重新渲染

  • A.26 5档盘口 NO 侧展示错误的订单 (2026-05-05)


    现象: orderbook/23/exact 接口返回的 NO 测盘口数据显示价格最高的卖单和最低的买单,而非最有意义的(最低卖价和最高买价)。


    根因: 后端 _fetch_orderbook_innerapi_mm_weather_orderbook_single 对 YES/NO 两个 token 都使用 bids_raw[-5:] / asks_raw[-5:]。CLOB API 返回的 bids 是升序(低→高),asks 是降序(高→低)。[-5:] 取最后 5 个:

  • bids: 最高的 5 个买单 ✅
  • asks: 最低的 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 发现 — 同上


    教训:

  • 外部 API 超时/失败时不要反复重试耗尽内部连接池
  • sync worker 池遇到阻塞式后端请求,所有并发请求都会排队,等价于单线程
  • 使用 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. 定时器串行setIntervalfetchPolymarketPositions.then() 串行化,数据回来后刷新卡片


    效果:

  • 持仓 >0 即显示平仓按钮,不再隐藏
  • 按钮上的数量(SELL NO 1@99.0¢)和价格随持仓 + 盘口实时更新
  • 手动 Buy YES/NO 后,按钮自动更新为当前持仓状态

  • 本文档随系统演进持续更新。最后更新: 2026-05-05 11:15 CST