这个项目应该已经停滞很久了,我后续自建私有项目之后,也没有太多关注了,包括 pytdx 的代码已经废弃,由于我个人项目目前已经采用新的数据渠道,通达信这个开源的数据渠道对我意义已经不大。看在还有人关注这个项目,我就分享一下比较关键的 Tick V4 HTC 数据解析关键代码。数据渠道可以通过每日分时总包获取。
/*
* MIT License
*
* Copyright (c) 2025 Foliage Develop Team
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use crate::dbio::BinaryDumpWriter;
use anyhow::Result;
use anyhow::ensure;
use chrono::NaiveDate;
use flate2::read::ZlibDecoder;
use qaas_core::codec;
use qaas_core::market::Exchange;
use qaas_core::market::Seid;
use std::io::Read;
use std::path::Path;
// ---------------------------------------------------------------------------
pub struct Record {
pub seid: Seid,
pub date: NaiveDate,
pub data: Vec<u8>,
}
impl Record {
fn from_file(path: impl AsRef<Path>) -> Result<Vec<Self>> {
let bytes = std::fs::read(path)?;
let head = &bytes[0x00..0x10];
let size = codec::le_u32(head, 0x0C) as usize;
let mut packs = Vec::with_capacity(size);
let mut cursor = 0x10;
for _ in 0..size {
let pack_head = &bytes[cursor..cursor + 0x1E];
let exchange = match codec::le_u08(&bytes, cursor) {
0 => Exchange::SZ,
1 => Exchange::SH,
2 => Exchange::BJ,
_ => Exchange::NA,
};
let code = codec::le_code(&bytes, cursor + 0x01, 0x06);
let date = codec::le_date(&bytes, cursor + 0x08);
let seid = Seid::of(exchange, code);
let size_decoded = codec::le_u32(&pack_head, 0x0C) as usize;
let size_encoded = codec::le_u32(&pack_head, 0x10) as usize;
let pack_body = &bytes[cursor + 0x1E..cursor + 0x1E + size_encoded];
let mut decoder = ZlibDecoder::new(&pack_body[..]);
let mut decoded = vec![0u8; size_decoded];
decoder.read_exact(&mut decoded)?;
packs.push(Self { seid, date, data: decoded });
cursor += 0x1E + size_encoded;
}
Ok(packs)
}
}
// ---------------------------------------------------------------------------
pub fn process(path: &Path, output: &Path) -> Result<()> {
ensure!(path.exists() && path.is_file(), "Specified path is not a file: {}", path.display());
let packs: Vec<Record> = Record::from_file(path)?;
if packs.is_empty() {
println!("No pack change records found in file");
return Ok(());
}
println!("Total {} pack change records read", packs.len());
let mut writer = BinaryDumpWriter::from_path(output)?;
for pack in packs {
writer.begin_row(3)?;
writer.write_i32(pack.seid.0 as i32)?;
writer.write_date(pack.date)?;
writer.write_byte(&pack.data)?;
}
writer.finish()?;
println!("Data written to file: {}", output.display());
Ok(())
}
// ----------------------------------------------------------------------------
#[test]
fn test_htcs() -> Result<()> {
process(Path::new("../../.data/20260312.htc"), Path::new("../../.data/20260312.dmp"))
}
#[test]
fn test_decode() -> Result<()> {
let vtc_file = std::fs::File::open("../../.data/2688533.bin")?;
let mut vtc_reader = std::io::BufReader::new(vtc_file);
let mut vtc_data = vec![];
vtc_reader.read_to_end(&mut vtc_data)?;
let pack = qaas_core::tick::TickPack::from_byte(&vtc_data)?;
let mut writer = csv::Writer::from_writer(vec![]);
super::write_to_csv(&pack.vt[..20].to_vec(), &mut writer)?;
insta::assert_snapshot!("ticks", String::from_utf8(writer.into_inner()?)?);
Ok(())
}
#[test]
fn test_vint() -> Result<()> {
let mut cursor = 0;
let data = vec![0x07u8, 0x84u8, 0x80u8, 0x88u8, 0xFEu8, 0x83u8, 0x8Au8, 0x80u8, 0x86u8, 0x81u8, 0x81u8];
let val0 = qaas_core::varint::cursor_be_i32(&data, &mut cursor);
let val1 = qaas_core::varint::cursor_be_i32(&data, &mut cursor);
let val2 = qaas_core::varint::cursor_be_i32(&data, &mut cursor);
println!("val0={:?}, val1={:?}, val2={:?}, cursor={:?}", val0, val1, val2, cursor);
Ok(())
}
/*
* MIT License
*
* Copyright (c) 2025 Foliage Develop Team
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// ---------------------------------------------------------------------------
pub fn cursor_be_i32(data: &[u8], cursor: &mut usize) -> i32 {
let slice = &data[*cursor..];
let b0 = slice[0];
let mut val = if b0 & 0x40 != 0 { -1_i32 } else { 0_i32 };
if b0 & 0x80 != 0 {
val = (val << 7) | (b0 & 0x7F) as i32;
*cursor += 1;
return val;
}
val = (val << 7) | b0 as i32;
let mut i = 1;
loop {
let b = slice[i];
if b & 0x80 != 0 {
val = (val << 7) | (b & 0x7F) as i32;
*cursor += i + 1;
return val;
}
val = (val << 7) | b as i32;
i += 1;
}
}
pub fn cursor_be_u32(data: &[u8], cursor: &mut usize) -> u32 {
let slice = &data[*cursor..];
let b0 = slice[0];
if b0 & 0x80 != 0 {
*cursor += 1;
return (b0 & 0x7F) as u32;
}
let mut val = b0 as u32;
let mut i = 1;
loop {
let b = slice[i];
if b & 0x80 != 0 {
val = (val << 7) | (b & 0x7F) as u32;
*cursor += i + 1;
return val;
}
val = (val << 7) | b as u32;
i += 1;
}
}
// ---------------------------------------------------------------------------
pub fn cursor_be_i64(data: &[u8], cursor: &mut usize) -> i64 {
let slice = &data[*cursor..];
let b0 = slice[0];
let mut val = if b0 & 0x40 != 0 { -1_i64 } else { 0_i64 };
if b0 & 0x80 != 0 {
val = (val << 7) | (b0 & 0x7F) as i64;
*cursor += 1;
return val;
}
val = (val << 7) | b0 as i64;
let mut i = 1;
loop {
let b = slice[i];
if b & 0x80 != 0 {
val = (val << 7) | (b & 0x7F) as i64;
*cursor += i + 1;
return val;
}
val = (val << 7) | b as i64;
i += 1;
}
}
pub fn cursor_be_u64(data: &[u8], cursor: &mut usize) -> u64 {
let slice = &data[*cursor..];
let b0 = slice[0];
if b0 & 0x80 != 0 {
*cursor += 1;
return (b0 & 0x7F) as u64;
}
let mut val = b0 as u64;
let mut i = 1;
loop {
let b = slice[i];
if b & 0x80 != 0 {
val = (val << 7) | (b & 0x7F) as u64;
*cursor += i + 1;
return val;
}
val = (val << 7) | b as u64;
i += 1;
}
}
// ---------------------------------------------------------------------------
pub fn cursor_le_i32(slice: &[u8], cursor: &mut usize) -> i32 {
if *cursor >= slice.len() {
return 0;
}
let v0 = slice[*cursor];
let mut v = (v0 & 0x3F) as i32;
let sign = (v0 & 0x40) != 0;
let mut shift = 6;
if (v0 & 0x80) != 0 {
loop {
*cursor += 1;
if *cursor >= slice.len() {
break;
}
let next = slice[*cursor];
v |= ((next & 0x7F) as i32) << shift;
shift += 7;
if (next & 0x80) == 0 {
break;
}
}
}
*cursor += 1;
if sign { -v } else { v }
}
pub fn cursor_le_f64(slice: &[u8], cursor: &mut usize) -> f64 {
let l = slice[*cursor + 0] as i32; // offset 0: 低位 (L)
let m = slice[*cursor + 1] as i32; // offset 1: 中位 (M)
let h = slice[*cursor + 2] as i32; // offset 2: 高位 (H)
let mut e = slice[*cursor + 3] as i32; // offset 3: 指数 (E)
let sign = (e & 0x80) != 0; // 指数的最高位为符号位
e = e & 0x7F; // 清除符号位,保留指数值
// Step E: 计算指数底
// 公式: 2^(E*2 - 0x7F)
let eb = e * 2 - 0x7F;
let ev = 2.0_f64.powi(eb);
// Step H: 计算高位尾数
let hv: f64;
// 检查 H 的最高位 (0x80) 是否为 1
let hf = (h & 0x80) != 0;
if hf {
// 如果 H >= 0x80 (最高位为1)
let term1 = 128.0 * 2.0_f64.powi(eb - 7);
let term2 = (h & 0x7F) as f64 * 2.0_f64.powi(eb - 6);
hv = term1 + term2;
} else {
// 如果 H < 0x80
hv = h as f64 * 2.0_f64.powi(eb - 7);
}
// Step M & L: 计算中低位尾数
// 基础权重分别为 Exp-0x0F 和 Exp-0x17
let mut mv = m as f64 * 2.0_f64.powi(eb - 0x0F);
let mut lv = l as f64 * 2.0_f64.powi(eb - 0x17);
// 5. 进位修正
// 如果 H 触发了进位标志,M 和 L 的权重要翻倍
if hf {
mv *= 2.0;
lv *= 2.0;
}
// 6. 累加最终结果
let v = ev + hv + mv + lv;
*cursor += 4; // 移动游标
if sign { -v } else { v }
}
这个项目应该已经停滞很久了,我后续自建私有项目之后,也没有太多关注了,包括 pytdx 的代码已经废弃,由于我个人项目目前已经采用新的数据渠道,通达信这个开源的数据渠道对我意义已经不大。看在还有人关注这个项目,我就分享一下比较关键的 Tick V4 HTC 数据解析关键代码。数据渠道可以通过每日分时总包获取。