use crate::bytes_helper::u16_from_le_bytes;
use crate::tcp::Tdx;
#[derive(Debug, Clone)]
pub struct MinuteTime<'d> {
pub send: Box<[u8]>,
pub market: u16,
pub code: &'d str,
pub response: Vec<u8>,
pub data: Vec<MinuteTimeData>,
}
impl<'d> MinuteTime<'d> {
pub fn new(market: u16, code: &'d str) -> Self {
assert_eq!(code.len(), 6, "股票代码必须是6位");
let mut send = [0u8; Self::LEN];
send[0..12].copy_from_slice(Self::SEND);
send[12..14].copy_from_slice(&market.to_le_bytes());
send[14..20].copy_from_slice(code.as_bytes());
Self {
send: send.into(),
market,
code,
response: Vec::new(),
data: Vec::new(),
}
}
}
impl<'a> Tdx for MinuteTime<'a> {
type Item = [MinuteTimeData];
const SEND: &'static [u8] = &[
0x0c, 0x1b, 0x08, 0x00, 0x01, 0x01, 0x0e, 0x00, 0x0e, 0x00, 0x1d,
0x05, ];
const TAG: &'static str = "分时数据";
const LEN: usize = 12 + 2 + 6 + 4;
fn send(&mut self) -> &[u8] {
&self.send
}
fn parse(&mut self, v: Vec<u8>) {
self.data = Vec::new();
if v.len() < 4 {
self.response = v;
return;
}
let num_points = u16_from_le_bytes(&v, 0);
let mut pos = 4;
let mut last_price = 0i32;
for _ in 0..num_points {
if v.len() - pos < 3 {
break;
}
let Some(price_raw) = price_checked(&v, &mut pos) else {
self.response = v;
return; };
let Some(_reversed1) = price_checked(&v, &mut pos) else {
self.response = v;
return;
};
let Some(vol) = price_checked(&v, &mut pos) else {
self.response = v;
return;
};
last_price += price_raw;
let price = last_price as f64 / 100.0;
self.data.push(MinuteTimeData { price, vol });
}
let points_complete = self.data.len() == num_points as usize;
let bytes_consumed = points_complete && pos + 8 >= v.len();
let prices_valid = self
.data
.iter()
.all(|d| (0.01..=100000.0).contains(&d.price));
if !points_complete || !bytes_consumed || !prices_valid {
self.data = Vec::new(); }
self.response = v;
}
fn result(&self) -> &Self::Item {
&self.data
}
}
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct MinuteTimeData {
pub price: f64,
pub vol: i32,
}
pub(crate) fn price_checked(arr: &[u8], pos: &mut usize) -> Option<i32> {
let mut shl = 6;
let mut bit = *arr.get(*pos)? as i32;
let mut res = bit & 0x3f;
let sign = (bit & 0x40) == 0;
while (bit & 0x80) != 0 {
*pos += 1;
bit = *arr.get(*pos)? as i32;
res += (bit & 0x7f) << shl;
shl += 7;
}
*pos += 1;
Some(if sign { res } else { -res })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_minute_time_new() {
let minute = MinuteTime::new(0, "000001");
assert_eq!(minute.market, 0);
assert_eq!(minute.code, "000001");
assert_eq!(minute.send.len(), 24);
}
#[test]
fn test_minute_time_new_shanghai() {
let minute = MinuteTime::new(1, "600000");
assert_eq!(minute.market, 1);
assert_eq!(minute.code, "600000");
}
#[test]
fn test_minute_time_send_bytes() {
let minute = MinuteTime::new(0, "000001");
assert_eq!(
&minute.send[0..12],
&[
0x0c, 0x1b, 0x08, 0x00, 0x01, 0x01, 0x0e, 0x00, 0x0e, 0x00, 0x1d, 0x05
]
);
assert_eq!(&minute.send[12..14], &[0x00, 0x00]);
assert_eq!(&minute.send[14..20], b"000001");
assert_eq!(&minute.send[20..24], &[0x00, 0x00, 0x00, 0x00]);
}
#[test]
#[should_panic(expected = "股票代码必须是6位")]
fn test_minute_time_invalid_code() {
MinuteTime::new(0, "00001");
}
#[test]
fn test_connection() {
if std::env::var("RUSTDX_SKIP_INTEGRATION_TESTS").is_ok() {
println!("⚠️ 跳过集成测试 (RUSTDX_SKIP_INTEGRATION_TESTS 已设置)");
return;
}
println!("⚠️ 集成测试需要手动验证(需要实际TCP连接)");
}
#[test]
fn parse_legacy_format() {
let mut v = vec![0x02, 0x00, 0x00, 0x00];
v.extend_from_slice(&[0x84, 0x0e, 0x00, 0xa4, 0x01]); v.extend_from_slice(&[0x32, 0x00, 0x90, 0x01]); let mut mt = MinuteTime::new(0, "000001");
mt.parse(v);
assert_eq!(mt.result().len(), 2);
assert_eq!(mt.result()[0].price, 9.0);
assert_eq!(mt.result()[0].vol, 100);
assert_eq!(mt.result()[1].price, 9.5);
assert_eq!(mt.result()[1].vol, 80);
}
#[test]
fn parse_new_protocol_returns_empty() {
let hex = "1f000000003030303030319d02a81244460847a5f1c409e8128cf21cba0208a4864d84da0c89981000bfaf0700018c1a851b4102ae1b9e3142038f29a31543048d20861144058940ad16cd1700000000";
let v: Vec<u8> = (0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
.collect();
let mut mt = MinuteTime::new(0, "000001");
mt.parse(v);
assert!(
mt.result().is_empty(),
"新协议响应应返回空数据,实际解析出 {} 个点",
mt.result().len()
);
}
#[test]
fn parse_truncated_returns_empty() {
let mut v = vec![0x0a, 0x00, 0x00, 0x00];
v.extend_from_slice(&[0x84, 0x0e, 0x00, 0x64]); let mut mt = MinuteTime::new(0, "000001");
mt.parse(v);
assert!(mt.result().is_empty());
}
#[test]
fn parse_invalid_price_returns_empty() {
let mut v = vec![0x01, 0x00, 0x00, 0x00];
v.extend_from_slice(&[0x80, 0xb4, 0x89, 0x13, 0x00, 0xa4, 0x01]);
let mut mt = MinuteTime::new(0, "000001");
mt.parse(v);
assert!(mt.result().is_empty());
}
}