use crate::bytes_helper::{u16_from_le_bytes, u32_from_le_bytes};
use crate::tcp::{Tdx, helper::price};
#[derive(Debug, Clone)]
pub struct SecurityQuotes<'d> {
pub send: Box<[u8]>,
pub stocks: Vec<(u16, &'d str)>,
pub response: Vec<u8>,
pub data: Vec<QuoteData>,
}
impl<'d> Default for SecurityQuotes<'d> {
fn default() -> Self {
Self::new(vec![(0, "000001")])
}
}
impl<'d> SecurityQuotes<'d> {
pub fn new(stocks: Vec<(u16, &'d str)>) -> Self {
let count = stocks.len();
assert!(count > 0 && count <= 80, "股票数量必须在1-80之间");
for (_, code) in &stocks {
assert_eq!(code.len(), 6, "股票代码必须是6位");
}
let pkg_len = (count * 7 + 12) as u16;
let mut send = [0u8; Self::LEN];
send[0..22].copy_from_slice(Self::SEND);
send[6..8].copy_from_slice(&pkg_len.to_le_bytes());
send[8..10].copy_from_slice(&pkg_len.to_le_bytes());
send[20..22].copy_from_slice(&(count as u16).to_le_bytes());
let mut pos = 22;
for (market, code) in &stocks {
send[pos] = *market as u8;
send[pos + 1..pos + 7].copy_from_slice(code.as_bytes());
pos += 7;
}
Self {
send: send.into(),
stocks,
response: Vec::new(),
data: Vec::with_capacity(count),
}
}
}
impl<'a> Tdx for SecurityQuotes<'a> {
type Item = [QuoteData];
const SEND: &'static [u8] = &[
0x0c, 0x01, 0x20, 0x63, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, ];
const TAG: &'static str = "股票行情快照";
const LEN: usize = 22 + 80 * 7;
fn send(&mut self) -> &[u8] {
let actual_len = 22 + self.stocks.len() * 7;
&self.send[..actual_len]
}
fn parse(&mut self, v: Vec<u8>) {
if v.len() < 4 {
self.response = v;
self.data = Vec::new();
return;
}
let mut pos = 0;
pos += 2;
let num_stocks = u16_from_le_bytes(&v, pos);
pos += 2;
self.data = Vec::with_capacity(num_stocks as usize);
for i in 0..num_stocks {
if pos + 9 > v.len() {
debug_assert!(false, "行情数据不完整,只解析了 {i}/{num_stocks} 只股票");
break;
}
let quote = parse_quote(&v, &mut pos);
self.data.push(quote);
}
self.response = v;
}
fn result(&self) -> &Self::Item {
&self.data
}
}
fn parse_quote(data: &[u8], pos: &mut usize) -> QuoteData {
let _market = data[*pos] as u16;
*pos += 1;
let code_bytes = &data[*pos..*pos + 6];
*pos += 6;
let code = unsafe { std::str::from_utf8_unchecked(code_bytes) };
let code = String::from(code); let _active1 = u16_from_le_bytes(data, *pos);
*pos += 2;
let price_raw = price(data, pos);
let last_close_diff = price(data, pos);
let open_diff = price(data, pos);
let high_diff = price(data, pos);
let low_diff = price(data, pos);
let reversed_bytes0 = price(data, pos);
let _reversed_bytes1 = price(data, pos);
let vol = price(data, pos);
let cur_vol = price(data, pos);
let amount_raw = u32_from_le_bytes(data, *pos);
*pos += 4;
let amount = crate::tcp::helper::vol_amount(amount_raw as i32);
let s_vol = price(data, pos);
let b_vol = price(data, pos);
let _reversed_bytes2 = price(data, pos);
let _reversed_bytes3 = price(data, pos);
let bid1 = price(data, pos);
let ask1 = price(data, pos);
let bid1_vol = price(data, pos);
let ask1_vol = price(data, pos);
let bid2 = price(data, pos);
let ask2 = price(data, pos);
let bid2_vol = price(data, pos);
let ask2_vol = price(data, pos);
let bid3 = price(data, pos);
let ask3 = price(data, pos);
let bid3_vol = price(data, pos);
let ask3_vol = price(data, pos);
let bid4 = price(data, pos);
let ask4 = price(data, pos);
let bid4_vol = price(data, pos);
let ask4_vol = price(data, pos);
let bid5 = price(data, pos);
let ask5 = price(data, pos);
let bid5_vol = price(data, pos);
let ask5_vol = price(data, pos);
let _reversed_bytes4 = u16_from_le_bytes(data, *pos);
*pos += 2;
let _reversed_bytes5 = price(data, pos);
let _reversed_bytes6 = price(data, pos);
let _reversed_bytes7 = price(data, pos);
let _reversed_bytes8 = price(data, pos);
let reversed_bytes9 = u16_from_le_bytes(data, *pos) as i16;
let _active2 = u16_from_le_bytes(data, *pos + 2);
*pos += 4;
let base = price_raw as f64;
let price = base / 100.0;
let last_close = (base + last_close_diff as f64) / 100.0;
let open = (base + open_diff as f64) / 100.0;
let high = (base + high_diff as f64) / 100.0;
let low = (base + low_diff as f64) / 100.0;
let change = price - last_close;
let change_percent = if last_close > 0.0 {
change / last_close * 100.0
} else {
0.0
};
QuoteData {
code,
price,
last_close,
open,
high,
low,
vol: vol as f64,
cur_vol: cur_vol as f64,
amount,
s_vol: s_vol as f64,
b_vol: b_vol as f64,
change,
change_percent,
speed: reversed_bytes9 as f64 / 100.0,
servertime: servertime_string(reversed_bytes0),
bid1: (base + bid1 as f64) / 100.0,
ask1: (base + ask1 as f64) / 100.0,
bid1_vol: bid1_vol as f64,
ask1_vol: ask1_vol as f64,
bid2: (base + bid2 as f64) / 100.0,
ask2: (base + ask2 as f64) / 100.0,
bid2_vol: bid2_vol as f64,
ask2_vol: ask2_vol as f64,
bid3: (base + bid3 as f64) / 100.0,
ask3: (base + ask3 as f64) / 100.0,
bid3_vol: bid3_vol as f64,
ask3_vol: ask3_vol as f64,
bid4: (base + bid4 as f64) / 100.0,
ask4: (base + ask4 as f64) / 100.0,
bid4_vol: bid4_vol as f64,
ask4_vol: ask4_vol as f64,
bid5: (base + bid5 as f64) / 100.0,
ask5: (base + ask5 as f64) / 100.0,
bid5_vol: bid5_vol as f64,
ask5_vol: ask5_vol as f64,
}
}
fn servertime_string(v: i32) -> String {
if v <= 0 {
return String::new();
}
let s = v.to_string();
if s.len() < 7 {
return String::new();
}
let (head, tail) = s.split_at(s.len() - 6);
let hour: u32 = head.parse().unwrap_or(u32::MAX);
let minute: u32 = tail[..2].parse().unwrap_or(u32::MAX);
let sec_raw: f64 = tail[2..].parse().unwrap_or(f64::NAN);
let seconds = sec_raw * 60.0 / 10000.0;
if hour > 23 || minute > 59 || !(0.0..60.0).contains(&seconds) {
return String::new();
}
format!("{hour:02}:{minute:02}:{seconds:06.3}")
}
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct QuoteData {
pub code: String,
pub price: f64,
pub last_close: f64,
pub open: f64,
pub high: f64,
pub low: f64,
pub vol: f64,
pub cur_vol: f64,
pub amount: f64,
pub s_vol: f64,
pub b_vol: f64,
pub change: f64,
pub change_percent: f64,
pub speed: f64,
pub servertime: String,
pub bid1: f64,
pub ask1: f64,
pub bid1_vol: f64,
pub ask1_vol: f64,
pub bid2: f64,
pub ask2: f64,
pub bid2_vol: f64,
pub ask2_vol: f64,
pub bid3: f64,
pub ask3: f64,
pub bid3_vol: f64,
pub ask3_vol: f64,
pub bid4: f64,
pub ask4: f64,
pub bid4_vol: f64,
pub ask4_vol: f64,
pub bid5: f64,
pub ask5: f64,
pub bid5_vol: f64,
pub ask5_vol: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_quotes_default() {
let quotes = SecurityQuotes::default();
assert_eq!(quotes.stocks.len(), 1);
assert_eq!(quotes.stocks[0].0, 0);
assert_eq!(quotes.stocks[0].1, "000001");
}
#[test]
fn test_security_quotes_new() {
let stocks = vec![(0, "000001"), (1, "600000")];
let quotes = SecurityQuotes::new(stocks);
assert_eq!(quotes.stocks.len(), 2);
}
#[test]
#[should_panic(expected = "股票数量必须在1-80之间")]
fn test_security_quotes_empty() {
SecurityQuotes::new(vec![]);
}
#[test]
fn parse_full_response() {
let hex = "0114010000303030303031fe08a61242440a45bb9fe30ae612a9853d0d31160e4e91981e98ed1e00bfaf0700019f12074102a724ab3b4203911295164304a8288a234405a33296457501000000000000fe08";
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 quotes = SecurityQuotes::default();
quotes.parse(v);
assert_eq!(quotes.data.len(), 1);
let q = "es.data[0];
assert_eq!(q.code, "000001");
assert_eq!(q.price, 11.9);
assert_eq!(q.last_close, 11.88);
assert_eq!(q.open, 11.86);
assert_eq!(q.high, 12.0);
assert_eq!(q.low, 11.85);
assert_eq!(q.vol, 500073.0); assert_eq!(q.cur_vol, 13.0);
assert_eq!(q.s_vol, 247313.0);
assert_eq!(q.b_vol, 252760.0);
assert_eq!(q.amount, 595954752.0);
assert_eq!(q.bid1, 11.9);
assert_eq!(q.ask1, 11.91);
assert_eq!(q.bid2, 11.89);
assert_eq!(q.ask2, 11.92);
assert_eq!(q.bid3, 11.88);
assert_eq!(q.ask3, 11.93);
assert_eq!(q.bid4, 11.87);
assert_eq!(q.ask4, 11.94);
assert_eq!(q.bid5, 11.86);
assert_eq!(q.ask5, 11.95);
assert_eq!(q.bid1_vol, 1183.0);
assert_eq!(q.ask1_vol, 7.0);
assert!((q.change - 0.02).abs() < 1e-9);
assert!((q.change_percent - 0.1684).abs() < 1e-4);
assert_eq!(q.servertime, "11:29:52.866");
}
#[test]
fn test_servertime_string() {
assert_eq!(servertime_string(11298811), "11:29:52.866");
assert_eq!(servertime_string(9561810), "09:56:10.860"); assert_eq!(servertime_string(0), "");
assert_eq!(servertime_string(-1), "");
assert_eq!(servertime_string(123), ""); }
}