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

160 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 共享内存时间戳机制说明
## 问题背景
原始实现中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 行
```python
# 更新写入位置、最后数据长度和时间戳
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` 结构体新增字段:
```go
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 行
```go
// 解析时间戳
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 端测试
运行测试脚本验证时间戳是否正确更新:
```bash
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 客户端:
```bash
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. 采用环形缓冲区 + 序列号的无锁设计