Skip to content

通达信 TickV4 分时数据解码 #47

Description

@deathknight0718

这个项目应该已经停滞很久了,我后续自建私有项目之后,也没有太多关注了,包括 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 }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions