Files
tick-computing/client_go/TIMESTAMP_MECHANISM.md
2026-04-12 23:24:43 +08:00

4.8 KiB
Raw Permalink Blame History

共享内存时间戳机制说明

问题背景

原始实现中Python 端每 30 秒更新一次共享内存数据,而 Go 客户端每秒读取一次。由于共享内存中的数据不会自动清除Go 客户端每次读取的都是同一份旧数据,导致看起来像是每秒都收到了新行情。

解决方案:时间戳版本检测

核心思路

在共享内存头部添加时间戳字段Go 客户端通过检测时间戳变化来判断数据是否真正更新。

共享内存头部结构16字节

偏移量  | 字段名         | 类型   | 说明
--------|---------------|--------|------------------
0-3     | version       | uint32 | 版本号当前为1
4-7     | write_pos     | uint32 | 写入位置指针
8-11    | last_data_len | uint32 | 最后一条数据的长度
12-15   | timestamp     | uint32 | Unix 时间戳(秒级)

工作流程

Python 端(生产者)

  1. 每次调用 publish_tick() 时:

    • 序列化数据并写入共享内存
    • 更新头部时间戳为当前 Unix 时间戳
  2. 关键代码位置:src/qmt/tick_push.py 第 93-97 行

# 更新写入位置、最后数据长度和时间戳
new_pos = current_pos + 4 + data_len
timestamp = int(time.time())  # Unix 时间戳
struct.pack_into('<I', buf, 4, new_pos)
struct.pack_into('<I', buf, 8, data_len)
struct.pack_into('<I', buf, 12, timestamp)  # 更新时间戳

Go 端(消费者)

  1. TickReader 结构体新增字段:

    type TickReader struct {
        shmName       string
        bufferSize    int
        mappedFile    windows.Handle
        view          uintptr
        lastTimestamp uint32 // 上次读取的时间戳
    }
    
  2. ReadLatestTick() 方法增加时间戳检测:

    • 读取头部时间戳
    • 与上次读取的时间戳比较
    • 如果相同,返回错误 "数据未更新"
    • 如果不同,继续读取数据并更新 lastTimestamp
  3. 关键代码位置:client_go/tick_reader.go 第 105-152 行

// 解析时间戳
currentTimestamp := binary.LittleEndian.Uint32(header[12:16])

// 检查数据是否更新
if currentTimestamp == r.lastTimestamp {
    return nil, fmt.Errorf("数据未更新")
}

// ... 读取数据 ...

// 更新最后时间戳
r.lastTimestamp = currentTimestamp

优势

  1. 高频读取无浪费Go 客户端可以每秒甚至更频繁地读取,但只在数据真正更新时才处理
  2. 实时响应:一旦 Python 端写入新数据Go 端最多延迟 1 秒就能检测到
  3. 向后兼容只需修改头部最后一个字段reserved → timestamp不影响其他逻辑
  4. 简单高效:无需额外的事件通知机制,纯轮询方式实现

测试验证

Python 端测试

运行测试脚本验证时间戳是否正确更新:

cd d:\work\quant\qmt
python test\test_timestamp.py

预期输出:

============================================================
测试共享内存时间戳机制
============================================================

[1] 第一次发布数据...
    时间戳: 1775996840 (20:27:20)

[2] 等待 2 秒...

[3] 第二次发布数据(相同内容)...
    时间戳: 1775996842 (20:27:22)

✓ 时间戳已更新: 2 秒

[4] 再次等待 2 秒...

[5] 第三次发布数据(不同内容)...
    时间戳: 1775996844 (20:27:24)

✓ 时间戳继续更新: 2 秒

============================================================
✓ 所有测试通过!
============================================================

Go 端测试

编译并运行 Go 客户端:

cd d:\work\quant\qmt\client_go
go build -o tick_reader.exe tick_reader.go
.\tick_reader.exe

预期行为:

  • Go 客户端每秒读取一次共享内存
  • 只有当 Python 端更新数据时(每 30 秒),才会打印 "接收到新行情"
  • 其余时间会跳过处理(因为时间戳未变化)

性能对比

方案 读取频率 CPU 占用 响应延迟 实现复杂度
方案1调整读取间隔 30秒/次 最高 30 秒 简单
方案2时间戳检测 1秒/次 最高 1 秒 中等
方案3事件通知 实时 最低 毫秒级 复杂

注意事项

  1. 时间戳精度:当前使用秒级时间戳,如果需要更高精度可改为毫秒级(需要 8 字节存储)
  2. 时钟回拨:理论上系统时钟回拨可能导致时间戳倒退,但在实际交易中几乎不可能发生
  3. 溢出问题uint32 时间戳可使用到 2106 年,短期内无需担心

未来优化方向

如果需要更低延迟的响应,可以考虑:

  1. 缩短 Python 端的 DEFAULT_TICK_INTERVAL(如改为 5 秒)
  2. 使用 Windows Event 对象实现真正的推送机制
  3. 采用环形缓冲区 + 序列号的无锁设计