给贵金属行情监控或者黄金期货量化策略Code Review

用户头像Fxdund
2026-10-15 发布

做贵金属行情监控或者黄金期货量化策略的人,最后大概率都会走到同一条路上:用Python写一套行情采集程序,对接实时行情API,把国际金价、COMEX黄金期货数据拉进自己的系统。这个方向本身没什么门槛,真正的门槛在接入细节——一个WebSocket连接里的鉴权时序、心跳机制、时间戳处理,就能让一套看起来正常的采集程序在凌晨三点悄悄断流。
贵金属行情监控或者黄金期货量化.jpg

最近朋友发来一段自己写的黄金期货行情采集代码,说"有时候数据断着断着就没了,也没报错"。我把代码从头到尾翻了一遍,问题不止一个。这篇不写教程,就是一次真实的代码审查过程:原版代码放出来,问题一条条列,修复方式对着文档写。如果你也在写期货行情采集、实时行情推送或者贵金属数据管道,大概率能对号入座。

原版代码

import json
import time
import requests
import websocket

API_BASE = "//api.itick.org"
TOKEN = "your_token_here"
headers = {"accept": "application/json", "token": TOKEN}

def get_quote():
    resp = requests.get(
        f"{API_BASE}/future/quote",
        headers=headers,
        params={"region": "US", "code": "GC"}
    )
    return resp.json()["data"]

def on_message(ws, message):
    payload = json.loads(message)
    data = payload.get("data") or {}
    if data.get("type") == "quote":
        ts = data["t"] // 1000
        print(f"{data['s']}: {data['last']} ({data['chp']}%) time={ts}")

def on_open(ws):
    # 连接一建立就订阅
    ws.send(json.dumps({
        "ac": "subscribe",
        "params": "GC$US",
        "types": "quote"
    }))

ws = websocket.WebSocketApp(
    "wss://api.itick.org/future",
    header=[f"token: {TOKEN}"],
    on_message=on_message,
    on_open=on_open
)
ws.run_forever()

这段代码能跑,乍一看没什么问题。但仔细过一遍,六个问题,其中三个能解释"数据静默消失"。

审查结果:六个问题

问题1:连接建立就订阅,鉴权还没完成

on_open 触发只是TCP和WebSocket握手完成,不代表服务端鉴权通过。文档里明确写了:要等收到 resAc:"auth" 且 code:1 的消息后才能发送订阅。这段代码连接一建好就发subscribe,服务端此时可能还没完成token校验,订阅直接被拒,而且不报错——这就是"没数据但程序还在跑"的典型原因。

这个坑在WebSocket实时推送类接口里非常常见。REST接口是"发请求等响应",错了会返回错误码;WebSocket是长连接,很多接入问题不返回任何提示,只能靠消息类型自己判断。所以写实时行情API接入的时候,必须把"鉴权成功"当成一个显式的状态节点,而不是假设连接成功就能用。

修复:加一个 authenticated 状态位,只有收到鉴权成功消息才允许订阅。

authenticated = False

def on_message(ws, message):
    global authenticated
    payload = json.loads(message)

    if payload.get("resAc") == "auth" and payload.get("code") == 1 and not authenticated:
        authenticated = True
        ws.send(json.dumps({
            "ac": "subscribe",
            "params": "GC$US",
            "types": "quote"
        }))
        return

问题2:没有心跳,长连接被服务端断开

期货WebSocket要求每30秒发送一次心跳({"ac":"ping","params":"时间戳"}),不消费消息、不发心跳,服务端会主动断开。这段代码完全没有心跳逻辑,跑几个小时后连接被服务端回收,程序不退出、不重连,看起来"数据没了"。

黄金期货跟股票最大的不同就是交易时间长,基本是7×23小时连续交易,中间只休市约一小时。这样的场景下,连接长期在线是常态,心跳机制就不是"可选项"而是"必选项"。很多做贵金属行情监控的人一开始都忽略心跳,等发现数据断了,往往已经过去一两个小时。

修复:订阅成功后启动心跳线程。

import threading

def heartbeat(ws):
    while True:
        time.sleep(30)
        ws.send(json.dumps({
            "ac": "ping",
            "params": str(int(time.time() * 1000))
        }))

# 在鉴权成功分支里启动
threading.Thread(target=heartbeat, args=(ws,), daemon=True).start()

问题3:时间戳被当成秒用

data["t"] 是毫秒时间戳,代码里 // 1000 转成了秒,但后面直接拿来当普通时间打印,没有转时区。黄金期货美盘时段对应的是北京时间晚上,用UTC时间戳直接展示,凌晨告警的"几点"全是错的,做交易时段判断更是全乱。

时间戳问题在做跨市场、跨品种数据的时候尤其突出。黄金、白银、铂金、钯金这些贵金属的交易时段横跨亚盘、欧盘、美盘,一个监控系统里可能同时处理多个市场的行情,时间基准必须统一、转换必须显式。毫秒转秒、UTC转本地、夏令时处理,这三件事建议封装成独立的工具函数,别在业务代码里随手除一下了事。

修复:显式转成本地时间。

from datetime import datetime

ts = data["t"] / 1000
local_time = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
print(f"{data['s']}: {data['ld']} ({data['chp']}%) time={local_time}")

问题4:字段名记错了

代码里用的是 data['last'],实际报价字段是 ld(最新价)。拿错字段的后果是:程序在跑、连接也活着、数据也在推,但你永远拿不到正确的价格——这种bug最坑,因为没有任何报错。

字段名这件事,不同数据源差异很大。有的接口用 last,有的用 price,有的用 close;同样的"涨跌幅",可能是 chp、change_pct 或者 percent_change。接任何金融数据API,第一步都应该是把返回字段文档完整读一遍,把要用的字段列成一张映射表,再写代码。期货行情接口的报价字段是 s/ld/chp/o/h/l/v/tu/ts,K线字段是 t/o/h/l/c/v/tu,这几个记牢能省很多排查时间。

修复:字段名对照文档——报价字段是 s/ld/chp/o/h/l/v/tu/ts,K线字段是 t/o/h/l/c/v/tu。

问题5:没有断线重连

run_forever() 在连接异常退出后,程序直接结束。黄金期货交易23小时,凌晨断一次线没人盯着重启。生产环境必须加重连。

重连逻辑要注意一点:重连之后要重置 authenticated 状态。新连接建立后需要重新鉴权,如果状态位还是旧的True,订阅请求会在鉴权完成前被发出,等于问题1的坑再踩一遍。这也是为什么很多实时行情客户端会把这个状态位放在连接循环外面、每次连接重置。

修复:包装一层循环,断线自动重连,重连后重置 authenticated 状态(因为新连接要重新鉴权)。

while True:
    authenticated = False
    ws = websocket.WebSocketApp(
        "wss://api.itick.org/future",
        header=[f"token: {TOKEN}"],
        on_message=on_message
    )
    try:
        ws.run_forever()
    except Exception:
        pass
    print("连接断开,5秒后重连...")
    time.sleep(5)

问题6:把休市当成接口挂了

COMEX黄金每天休市约1小时,这个窗口没有成交数据。代码里没有任何交易状态判断,一旦这个时段"没数据",第一反应往往是"接口坏了"。报价里其实有 ts 字段标交易状态,先判断状态再判断数据。

贵金属的交易规则跟股票不太一样:股票是固定时段,休市时间长,大家有预期;黄金这种近全天交易的品种,休市窗口短,反而容易被忽视。做行情监控的时候,把 ts 交易状态判断加进去,同时把"休市时段"和"真实故障"区分开,能少很多次半夜被误报警叫醒的情况。

修复:

if data.get("type") == "quote":
    if data.get("ts") == 0:
        print("休市/停牌,忽略")
        return
    # 正常处理行情

审查清单速查

把上面六个问题整理成一张自查表,写完代码逐条过一遍:

# 检查项 错误表现 正确做法
1 鉴权时序 无数据但程序在跑 等 resAc:"auth" + code:1 再订阅
2 心跳机制 几小时后连接被回收 每30秒发 {"ac":"ping","params":"时间戳"}
3 时间戳单位 时间显示错误、时段判断错乱 毫秒转秒 + 显式转本地时区
4 字段名 拿到错误价格无报错 对照文档:报价 ld、K线 c
5 断线重连 凌晨断线后程序静默结束 循环重连 + 重置鉴权状态
6 交易状态 休市当故障,误报不断 先判断 ts 再处理行情

这张表也适用于其他品种的实时行情接入——股票、外汇、指数、期货,接入框架是同一套,差异只在代码格式和交易时段。

修复后的完整代码

import json
import threading
import time
from datetime import datetime
import websocket

WS_URL = "wss://api.itick.org/future"
TOKEN = "your_token_here"
authenticated = False


def on_message(ws, message):
    global authenticated
    payload = json.loads(message)

    if payload.get("resAc") == "auth" and payload.get("code") == 1 and not authenticated:
        authenticated = True
        ws.send(json.dumps({
            "ac": "subscribe",
            "params": "GC$US",
            "types": "quote"
        }))
        threading.Thread(target=heartbeat, args=(ws,), daemon=True).start()
        return

    if payload.get("resAc") == "pong":
        return

    data = payload.get("data") or {}
    if data.get("type") == "quote":
        if data.get("ts") == 0:
            return  # 休市
        ts = datetime.fromtimestamp(data["t"] / 1000).strftime("%H:%M:%S")
        print(f"{data['s']}: {data['ld']} ({data['chp']}%) {ts}")


def heartbeat(ws):
    while True:
        time.sleep(30)
        ws.send(json.dumps({"ac": "ping", "params": str(int(time.time() * 1000))}))


while True:
    authenticated = False
    ws = websocket.WebSocketApp(
        WS_URL,
        header=[f"token: {TOKEN}"],
        on_message=on_message
    )
    try:
        ws.run_forever()
    except Exception:
        pass
    print("连接断开,5秒后重连...")
    time.sleep(5)

这套代码还能怎么扩展

审查完只是第一步。把这段代码放进真实的黄金行情监控系统,通常还要补三块:

多品种扩展。 只盯GC不够,白银、铂金、钯金常常要一起看。WebSocket订阅支持逗号分隔多个标的(GC$US,SI$US),一个连接就能覆盖整个贵金属组合,不用开多个连接。REST侧也有批量报价接口 /future/quotes,参数用 codes(复数)传逗号分隔的产品代码,一次请求拿多个品种的快照,比循环调单品种接口高效得多。

历史K线补齐。 监控需要实时行情,回测和均线计算需要历史数据。REST的 /future/kline 接口用 kType 指定周期(1=1分钟、2=5分钟、5=1小时、8=日K、9=周K、10=月K),limit 控制返回条数。把日K拉进本地数据库,收盘后增量更新,监控和回测就能共用一套数据。

告警触发。 实时行情拿到手,后面接什么逻辑都方便:突破均线提醒、价格区间监控、相对强弱计算。数据层的问题解决之后,策略层的开发才谈得上专注。

审查心得

这段代码的问题很有代表性:绝大多数"数据莫名其妙没了"的场景,不是接口挂了,而是接入层的小问题——鉴权时序、心跳、字段名、时区、断线重连。每个问题单看都很小,但叠加起来就是"程序在跑、数据在断、你还在找原因"。

写行情采集代码的时候,我现在的习惯是写完先自查这五件事:鉴权等了吗、心跳发了吗、字段名对文档了吗、时间戳转了吗、断线会重连吗。五件事过一遍,接入层的坑基本就避开了。

字段和参数的细节,写代码时边写边翻官方文档对照,比自己记一遍靠谱。
GitHub:https://github.com/itick-org/

评论