┌─────────────────────────────────────────────────────────────────────┐
│ 本地 Windows (D:\code_p16v\03_stock_gtht\) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ fytrade.db │ │ stkinfo.txt │ │ TdxW.exe 进程内存 │ │
│ │ (SQLite) │ │ (GBK编码) │ │ (minidump 提取) │ │
│ └──────┬──────┘ └──────┬───────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ ┌──────┴────────────────┴─────────────────────┴───────────┐ │
│ │ htzq.py (Python 3.10) │ │
│ │ · PositionReader → 解析持仓二进制 │ │
│ │ · PriceFetcher → 新浪实时行情 │ │
│ │ · AccountBalanceCache → 总资产/现金余额 │ │
│ │ · ChangeDetector → 对比变化检测 │ │
│ │ · CostCalculator → 交易成本计算 │ │
│ │ · DatabaseUploader → PostgreSQL 上传 │ │
│ │ · HTZQApp → Tkinter UI │ │
│ └──────────────────────┬───────────────────────────────────┘ │
│ │ psycopg2 (SSL) │
└─────────────────────────┼───────────────────────────────────────────┘
│
┌───────────────┴────────────────┐
│ 腾讯云 CVM 150.158.87.41 │
│ ┌──────────────────────────┐ │
│ │ PostgreSQL 16 (appdb) │ │
│ │ · ht_position │ │
│ │ · ht_analysis_log │ │
│ │ · ht_mv_history │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────┴───────────────┐ │
│ │ Flask :5001 │ │
│ │ api_server.py │ │
│ │ · /api/htzq │ │
│ │ · /api/htzq/analysis │ │
│ │ · /api/htzq/costs │ │
│ │ · /api/quote │ │
│ │ · /api/intraday │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────┴───────────────┐ │
│ │ Nginx :443 │ │
│ │ · /htzq/ → index.html │ │
│ │ · /api/* → Flask proxy │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
│
┌───────────┴───────────┐
│ 外部行情 API │
│ · 新浪 hq.sinajs.cn │
│ · 腾讯 qt.gtimg.cn │
└───────────────────────┘
| 文件 | 绝对路径 | 说明 |
|---|---|---|
| 主程序 | D:\code_p16v\03_stock_gtht\HTZQ\htzq.py | 提取+上传+UI |
| 启动器 | D:\code_p16v\03_stock_gtht\HTZQ\htzq.bat | Windows 双击启动(GBK 编码) |
| 交易数据库 | D:\code_p16v\03_stock_gtht\newVer\RichEZ\Bin\cache\fytrade.db | 富易客户端 SQLite |
| 股票名称 | D:\code_p16v\03_stock_gtht\newVer\RichEZ\Bin\data\stkinfo.txt | GBK 编码,分号分隔 |
| 账户信息 | D:\code_p16v\03_stock_gtht\newVer\RichEZ\Bin\wt\ff_myyyb.ini | Base64 编码 |
| 现金缓存 | D:\code_p16v\03_stock_gtht\HTZQ\htzq_cash_cache.json | 总资产/现金本地持久化 |
| 前端页面 | D:\code_p16v\03_stock_gtht\HTZQ\index.html | 部署到服务器 /www/wwwroot/wheelfly.cn/htzq/ |
| 表名 | 用途 | 编码 |
|---|---|---|
today_position_<客户号>_ENC | 当前持仓(base64 + 二进制) | BASE64 |
history_trade_<客户号>_ENC | 历史成交(77+ 笔) | BASE64 |
today_order_<客户号>_ENC | 当日委托 | BASE64 |
从 today_position_*_ENC 的 data 字段 base64 解码后:
Offset Size Type 字段 示例
──────────────────────────────────────────────────
0 4 uint32 LE 会话ID 507691
4 4 uint32 LE 股票代码(整型) 767 → "000767"
8 1 byte 市场码(ASCII) '2'=深圳, '1'=上海
9 2 uint16 LE 当前持仓(股) 1,200
11 1 byte 分隔符 0x00
12 1 byte 分隔符 0x00
13 2 uint16 LE 可用数量(股) 1,200
15 1 byte 分隔符 0x00
16 1 byte 分隔符 0x00
17 2 uint16 LE 冻结数量(股) 1,200
19 1 byte 分隔符 0x00
20 1 byte 标识 0x00
21 8 double LE 缓存价格(过时!) 4.8183
──────────────────────────────────────────────────
29 字节总计
⚠️ offset 21 的价格是客户端缓存价,不可用作成交价。实际成交价从 history_trade_*_ENC 表读取。
从 history_trade_*_ENC 的 data 字段 base64 解码后:
Offset Size Type 字段 说明
──────────────────────────────────────────────────
0 4 uint32 LE date_start 起始日期
4 4 uint32 LE date_end 结束日期
8 4 uint32 LE date_trade 成交日期 (YYYYMMDD)
12 4 uint32 LE field_12 未知
16 4 uint32 LE field_16 未知
20 4 uint32 LE field_20 未知
24 4 uint32 LE stock_code 股票代码(整型)
28 3 ascii market_marker "10"=SH, "20"/"30"=SZ
31 8 double LE price ★ 实际成交价 ★
39 4 uint32 LE qty 成交数量
43 4 uint32 LE qty2 未知数量
47 8 double LE price2 未知价格
price (offset 31) 是券商记录的实际成交价,唯一可靠的价格来源。
fytrade.db 和所有本地文件中不含总资产和可用资金。这些数据通过加密 gRPC 从券商服务器获取,仅存在于 TdxW.exe 进程内存中。
获取方式:通过 Windows dbghelp.MiniDumpWriteDump API 创建 TdxW.exe 进程 minidump(约 875MB,2-3 秒),在其中搜索 double 值匹配。
搜索逻辑:
股票市值的 IEEE 754 double 编码total ≈ stock_mv + cash 的数对缓存策略:
htzq_cash_cache.json总资产 = 缓存现金 + 实时股票市值stock_market_value <= 0,零值搜索无意义且会卡死)D:\code_p16v\03_stock_gtht\HTZQ\htzq_cash_cache.jsonGET http://hq.sinajs.cn/list=sz000767,sh603993
Referer: https://finance.sina.com.cn
返回格式(GBK 编码):
var hq_str_sz000767="晋控电力,5.14,5.07,5.58,5.58,5.07,5.58,5.58,371234,200000000,...";
字段索引(逗号分隔):
| 索引 | 字段 | 说明 |
|---|---|---|
| 0 | 股票名称 | 中文(GBK) |
| 1 | 今开 | 开盘价 |
| 2 | 昨收 | 昨日收盘价 |
| 3 | 现价 | ★ 当前价格 |
| 4 | 最高 | 当日最高 |
| 5 | 最低 | 当日最低 |
| 30 | 日期 | YYYY-MM-DD |
| 31 | 时间 | HH:MM:SS |
市场前缀规则:
| 代码开头 | 新浪前缀 |
|---|---|
| 0xxxxx, 3xxxxx | sz (深圳) |
| 6xxxxx | sh (上海) |
GET http://qt.gtimg.cn/q=sz000767
返回当日 242 个分时数据点(09:30-15:00),用于前端 SVG 分时曲线图表。
CostCalculator 中的交易价格选取顺序:
| 优先级 | 来源 | 字段 | 说明 |
|---|---|---|---|
| 1 (最高) | history_trade_*_ENC | price (offset 31) | 券商实际成交价 |
| 2 | 市值变化反推 | delta_mv / delta_qty | 估值近似 |
| 3 | 新浪实时行情 | parts[3] | 当前市价(非成交价) |
| 4 | 持仓均价 | curr_mv / curr_qty | 最后手段 |
# UI 模式(Tkinter 界面 + 自动刷新)
双击 htzq.bat
# 命令行单次上传
python htzq.py --headless
# 自动循环(每3分钟)
python htzq.py --auto
1. PositionReader.read_positions()
├── 读取 fytrade.db → today_position_*_ENC
├── Base64 解码 → 29 字节二进制解析
├── 过滤已清仓残留(price=0, avail_qty=0 但 hold_qty>0)
└── 返回 positions[], cust_id
2. PriceFetcher.get_realtime_prices()
├── 调用新浪 API 获取实时价格
└── 返回 {code: {price, name, prev_close, open, high, low, time}}
3. AccountBalanceCache
├── 有持仓 + 有缓存: 总资产 = 缓存现金 + 实时市值
├── 无持仓 + 有缓存: 跳过内存读取,使用缓存
└── 无缓存: minidump 提取(仅一次)
4. DatabaseUploader.upload_positions()
├── _get_prev_snapshot()
│ ├── 从 ht_analysis_log 获取上一批次摘要
│ └── 从 ht_position 获取上一批次持仓明细
├── ChangeDetector.detect()
│ ├── 对比两次快照的持仓数量和市值
│ ├── delta_qty > 0 → BUY
│ ├── delta_qty < 0 → SELL
│ └── 首次上传不标记为交易
├── 三段式(盘前/盘中/盘后)去重覆盖
│ ├── 盘中无变化 → 每日首访记录1次
│ └── 同 session 旧记录 → DELETE + INSERT
├── CostCalculator.calculate()
│ ├── 优先使用 history_trade 实际成交价
│ └── 逐笔计算佣金/印花税/过户费
├── CostReconciler.reconcile()
│ └── 对比预期 vs 实际现金流,反推真实费率
└── INSERT 三张表
├── ht_position (每只股票一行,空仓时无记录)
├── ht_analysis_log (每次上传一行)
└── ht_mv_history (每只股票一行)
UI 模式下默认每 3 分钟自动执行「刷新 + 上传」。刷新内容包括:
fytrade.db 持仓数据conn = psycopg2.connect(
host="150.158.87.41",
port=5432,
database="appdb",
user="appuser",
password="AppUser2024!pg",
sslmode="require"
)
每只股票每次上传一行。空仓时此表无新记录(这是导致幻影交易 bug 的根源,已修复)。
CREATE TABLE ht_position (
id SERIAL PRIMARY KEY,
record_time TIMESTAMP DEFAULT NOW(),
cust_id VARCHAR(20), -- 客户号 (1210839165)
total_assets NUMERIC(18,2), -- 账户总资产
cash_balance NUMERIC(18,2), -- 可用资金
total_market_value NUMERIC(18,2), -- 股票总市值
stock_code VARCHAR(10), -- 股票代码
stock_name VARCHAR(50), -- 股票名称
market VARCHAR(4), -- 市场 (SZ/SH)
current_price NUMERIC(10,4), -- 当前价格(实时)
hold_quantity INTEGER, -- 持仓数量
avail_quantity INTEGER, -- 可用数量
other_quantity INTEGER, -- 冻结数量
market_value NUMERIC(18,2), -- 个股市值
raw_flag INTEGER, -- 原始标识
raw_session INTEGER, -- 原始会话ID
batch_id VARCHAR(32), -- 批次ID (MD5前8位)
price_source VARCHAR(10) -- 价格来源 (sina/stale)
);
每次上传必定写入一行(即使空仓/无变化)。这是上一批次查询的可靠来源。
CREATE TABLE ht_analysis_log (
id SERIAL PRIMARY KEY,
batch_id VARCHAR(32) NOT NULL,
record_time TIMESTAMP DEFAULT NOW(),
cust_id VARCHAR(20),
total_assets NUMERIC(18,2),
cash_balance NUMERIC(18,2),
total_market_value NUMERIC(18,2),
delta_total_assets NUMERIC(18,2),
delta_cash NUMERIC(18,2),
delta_market_value NUMERIC(18,2),
position_count INTEGER, -- 持仓股票数
position_summary JSONB, -- {_session, stocks: [{code,name,qty,price,mv,...}]}
has_trade BOOLEAN, -- 是否有交易
trade_detail JSONB, -- [{stock_code, direction, price, qty, amount, commission, ...}]
estimated_commission NUMERIC(18,2),
estimated_stamp_tax NUMERIC(18,2),
estimated_transfer_fee NUMERIC(18,2),
estimated_total_cost NUMERIC(18,2),
cost_discrepancy NUMERIC(18,2), -- 预期vs实际现金流差异
expected_cash_change NUMERIC(18,2),
actual_cash_change NUMERIC(18,2),
reconciled_rate NUMERIC(10,6), -- 反推真实费率
rate_match BOOLEAN,
raw_snapshot_ids TEXT -- ht_position 关联ID列表
);
每只股票每次上传一行,用于 API 返回 latest 持仓详情。
CREATE TABLE ht_mv_history (
id SERIAL PRIMARY KEY,
record_time TIMESTAMP DEFAULT NOW(),
batch_id VARCHAR(32),
cust_id VARCHAR(20),
stock_code VARCHAR(10),
stock_name VARCHAR(50),
current_price NUMERIC(10,4),
hold_quantity INTEGER,
avail_quantity INTEGER,
market_value NUMERIC(18,2),
delta_quantity INTEGER DEFAULT 0,
delta_mv NUMERIC(18,2),
price_source VARCHAR(10)
);
ht_analysis_log (1) ──batch_id── (N) ht_position
ht_analysis_log (1) ──batch_id── (N) ht_mv_history
关键约束:ht_analysis_log 永远有记录;ht_position 和 ht_mv_history 在空仓时无记录。
┌──────────────────────────────────────────┐
│ 海通证券 账户持仓 │
│ ┌─────────┬─────────┬─────────┐ │
│ │ 总资产 │ 股票市值 │ 可用资金 │ │
│ │ ¥37,930 │ ¥0 │ ¥37,930 │ │
│ └─────────┴─────────┴─────────┘ │
│ │
│ 持仓明细 更新: 2026-06-03... │
│ ┌──────────────────────────────────┐ │
│ │ 代码 │名称│市场│价格│持仓│可用│市值│ │
│ │ 点击股票 → 实时行情弹窗 │ │
│ └──────────────────────────────────┘ │
│ │
│ 资金明细日志 │
│ ┌──────────────────────────────────┐ │
│ │ 时间 │类型│总资产│现金│市值│持仓...│ │
│ │ 每行显示该时间点的历史持仓+交易 │ │
│ └──────────────────────────────────┘ │
│ │
│ 交易日志 │
│ ┌──────────────────────────────────┐ │
│ │ 日期 │代码│名称│方向│数量│价格...│ │
│ │ 佣金 │印花税│过户费 独立列显示 │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘
| 端点 | 方法 | 数据源 | 说明 |
|---|---|---|---|
/api/htzq | GET | ht_analysis_log + ht_position | 账户总览 + 当前持仓明细 |
/api/htzq/analysis?days=30 | GET | ht_analysis_log + ht_mv_history | 资金时序 + 每行持仓 + 每行交易 |
/api/htzq/costs?days=180 | GET | ht_analysis_log (has_trade=true) | 全部历史交易明细 |
/api/quote?code=000767 | GET | 新浪 hq.sinajs.cn | 实时报价 |
/api/intraday?code=000767 | GET | 腾讯 qt.gtimg.cn | 分时数据(242点) |
loadData() 页面加载
├── fetch /api/htzq → data
│ ├── 渲染总览卡片 (total_assets / stock_mv / cash)
│ └── 渲染持仓明细表 (data.positions)
├── fetch /api/htzq/analysis?days=30 → ar
│ ├── 渲染资金明细日志 (ar.fund_flow)
│ │ └── 每行使用 f.positions.stocks(该时间点持仓)
│ │ └── 有交易时追加交易详情 + 费用
│ └── 空仓时正确处理(非"—"覆盖全部历史行)
├── loadTrades()
│ └── fetch /api/htzq/costs?days=180
│ └── 渲染交易日志表(佣金/印花税/过户费分列)
└── 自动刷新: setInterval(loadData, 5分钟)
点击持仓表格中的股票行弹出实时行情卡片:
| 费用类型 | 费率 | 说明 |
|---|---|---|
| 佣金 | 0.025% (万2.5) | 最低 5 元 |
| 印花税 | 0.05% (万5) | 仅卖出收取 |
| 过户费 | 0.001% (万0.1) | 仅上海市场 |
通过对比预期现金流变化和实际现金流变化反推券商真实费率:
预期现金流 = 买入(-) / 卖出(+) 金额 ± 估算费用
实际现金流 = curr_cash - prev_cash
差异 = 实际 - 预期
如果 |差异| >= 0.5 → 怀疑费率不一致
反推真实佣金 = (估算佣金 + 按比例分配的差异) / 成交金额
pos = ar.positions(最新持仓)渲染所有历史行,空仓时 pos=[],全显示 —f.positions.stocks(position_summary JSONB 中的历史数据)CostCalculator.calculate() 优先使用新浪实时价(6.11)而非 history_trade 的实际成交价(6.07)get_recent_trade_prices() 读取 history_trade_*_ENC offset 31 double,作为最高优先级_get_prev_snapshot() 从 ht_position 查上一批次,空仓时 ht_position 无新记录,回退到古老批次(还有持仓),ChangeDetector 误判为又一次卖出ht_analysis_log 查上一批次(每次上传必定写入)read_from_memory_once(0) 在 875MB dump 中搜索 8 个零字节(double 0.0),命中数百万次stock_market_value <= 0 直接返回 None;搜索循环加 MAX_MATCHES=100 限制| 文件 | 服务器路径 |
|---|---|
| Flask API | /www/wwwroot/wheelfly.cn/api/api_server.py |
| 前端页面 | /www/wwwroot/wheelfly.cn/htzq/index.html |
| 技术文档 | /www/wwwroot/wheelfly.cn/htzq/htzqweb/index.html |
| systemd 服务 | /etc/systemd/system/temp-api.service |
systemctl restart temp-api # 重启 Flask(API 代码变更后)
systemctl status temp-api # 查看状态
# 前端 HTML 变更无需重启,直接刷新浏览器
# SSH 工具 (本地)
cd key-cloud-tencent/ROCKY94_LAUNCH
# 上传 API
scp -i id_ed25519 .../api_server.py root@150.158.87.41:/www/wwwroot/wheelfly.cn/api/
# 上传前端
scp -i id_ed25519 .../index.html root@150.158.87.41:/www/wwwroot/wheelfly.cn/htzq/
# 重启服务
python rocky94_launch.py exec "systemctl restart temp-api"
location /api/ {
proxy_pass http://127.0.0.1:5001/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 10s;
proxy_read_timeout 10s;
}
CSP: connect-src *(允许前端跨域请求 API)
| 包 | 用途 |
|---|---|
psycopg2-binary | PostgreSQL 连接 |
ctypes | Windows API 调用(minidump) |
tkinter | UI 界面(Python 内置) |
sqlite3 | fytrade.db 读取(Python 内置) |
struct | 二进制解析(Python 内置) |
base64 | 解码(Python 内置) |
| 依赖 | 说明 |
|---|---|
| Python 3.10 | D:\Program Files\Python310\python.exe |
| TdxW.exe | 国泰海通富易客户端(需运行中才能读内存) |
| fytrade.db | 客户端 SQLite 数据库 |
| 新浪行情 API | 免费 HTTP 接口,无需注册 |
| PostgreSQL 16 | 远程 SSL 连接,需 IP 白名单 |
文档生成: 2026-06-04 · 返回持仓页面