use super::{
BlockRecord, CompanyInfoCategory, CompanyInfoCategoryItem, CompanyInfoContent, FinanceInfoData,
HistoryMinuteTime, HistoryTransaction, IndexKline, Kline, KlineData, MinuteTimeData, QuoteData,
SecurityListData, TransactionData, Xdxr, XdxrData,
};
use crate::pool::ConnectionPool;
use crate::tcp::helper::DateTime;
use crate::tcp::{Tcp, TcpConfig, Tdx};
use chrono::Datelike;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
#[derive(Debug)]
pub struct Client {
pub tcp: Tcp,
}
type BatchQueue<'a> = Arc<Mutex<VecDeque<(usize, u16, &'a str)>>>;
type BatchResults<'a> = Arc<Mutex<Vec<Option<std::io::Result<Vec<KlineData<'a>>>>>>>;
impl Client {
pub fn new() -> std::io::Result<Self> {
Ok(Self { tcp: Tcp::new()? })
}
pub fn with_config(config: &TcpConfig) -> std::io::Result<Self> {
Ok(Self {
tcp: Tcp::with_config(config)?,
})
}
pub fn reconnect(&mut self) -> std::io::Result<()> {
self.tcp.reconnect()
}
pub fn heartbeat(&mut self) -> std::io::Result<u16> {
self.tcp.heartbeat()
}
pub fn retry<T>(
&mut self,
f: impl FnMut(&mut Tcp) -> std::io::Result<T>,
attempts: usize,
) -> std::io::Result<T> {
self.tcp.retry(f, attempts)
}
pub fn quotes(&mut self, stocks: &[(u16, &str)]) -> std::io::Result<Vec<QuoteData>> {
let mut quotes = SecurityQuotesRef::new(stocks.to_vec());
quotes
.recv_parsed(&mut self.tcp)
.map_err(|e| ctx_err(e, format_args!("Client::quotes(n={})", stocks.len())))?;
Ok(quotes.result().to_vec())
}
pub fn bars<'a>(
&mut self,
market: u16,
code: &'a str,
category: u16,
start: u16,
count: u16,
) -> std::io::Result<Vec<super::KlineData<'a>>> {
let mut kline = Kline::new(market, code, category, start, count);
kline.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(
e,
format_args!("Client::bars(market={market}, code={code}, category={category})"),
)
})?;
Ok(kline.result().to_vec())
}
pub fn index_bars<'a>(
&mut self,
market: u16,
code: &'a str,
category: u16,
start: u16,
count: u16,
) -> std::io::Result<Vec<super::IndexKlineData<'a>>> {
let mut kline = IndexKline::new(market, code, category, start, count);
kline.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(
e,
format_args!("Client::index_bars(market={market}, code={code})"),
)
})?;
Ok(kline.result().to_vec())
}
pub fn k<'a>(
&mut self,
market: u16,
code: &'a str,
begin: Option<u32>,
end: Option<u32>,
) -> std::io::Result<Vec<super::KlineData<'a>>> {
fetch_k(&mut self.tcp, market, code, begin, end)
.map_err(|e| ctx_err(e, format_args!("Client::k(market={market}, code={code})")))
}
pub fn k_batch<'a>(
&self,
stocks: &[(u16, &'a str)],
begin: Option<u32>,
end: Option<u32>,
max_parallel: usize,
) -> std::io::Result<Vec<BatchKline<'a>>> {
if stocks.is_empty() {
return Ok(Vec::new());
}
let workers = match max_parallel {
0 => std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4),
n => n,
}
.min(stocks.len())
.max(1);
let pool = ConnectionPool::new(workers)?;
let queue: BatchQueue<'a> = Arc::new(Mutex::new(
stocks
.iter()
.enumerate()
.map(|(i, &(m, c))| (i, m, c))
.collect(),
));
let results: BatchResults<'a> =
Arc::new(Mutex::new((0..stocks.len()).map(|_| None).collect()));
std::thread::scope(|s| {
for _ in 0..workers {
let queue = Arc::clone(&queue);
let results = Arc::clone(&results);
let pool = pool.clone();
s.spawn(move || {
let Ok(mut conn) = pool.get_connection() else {
return;
};
loop {
let item = queue.lock().unwrap().pop_front();
let Some((idx, market, code)) = item else {
break;
};
let r = conn.execute(|tcp| {
fetch_k(tcp, market, code, begin, end).map_err(|e| {
ctx_err(e, format_args!("k_batch(market={market}, code={code})"))
})
});
results.lock().unwrap()[idx] = Some(r);
}
});
}
});
let mut out = Vec::with_capacity(stocks.len());
for (i, &(market, code)) in stocks.iter().enumerate() {
let result = results.lock().unwrap()[i].take().unwrap_or_else(|| {
Err(std::io::Error::other("worker 未能处理该股票(连接建立失败)"))
});
out.push(BatchKline {
market,
code,
result,
});
}
Ok(out)
}
pub fn k_adjusted<'a>(
&mut self,
market: u16,
code: &'a str,
adj: Adj,
begin: Option<u32>,
end: Option<u32>,
) -> std::io::Result<Vec<super::KlineData<'a>>> {
let all = fetch_k(&mut self.tcp, market, code, None, None).map_err(|e| {
ctx_err(
e,
format_args!("Client::k_adjusted(market={market}, code={code})"),
)
})?;
if all.is_empty() {
return Ok(all);
}
let xdxrs = self.xdxr(market, code)?;
let multipliers = adjusted_multipliers(&all, &xdxrs, adj);
let out: Vec<super::KlineData<'a>> = all
.into_iter()
.zip(multipliers)
.filter(|(bar, _)| {
let d = DateTime::to_u32(bar.dt.clone());
begin.is_none_or(|b| d >= b) && end.is_none_or(|e| d <= e)
})
.map(|(mut bar, m)| {
bar.open *= m;
bar.high *= m;
bar.low *= m;
bar.close *= m;
bar
})
.collect();
Ok(out)
}
pub fn minute(&mut self, market: u16, code: &str) -> std::io::Result<Vec<MinuteTimeData>> {
let ctx = format_args!("Client::minute(market={market}, code={code})");
let mut mt = super::MinuteTime::new(market, code);
mt.recv_parsed(&mut self.tcp)
.map_err(|e| ctx_err(e, ctx))?;
if !mt.result().is_empty() {
return Ok(mt.result().to_vec());
}
let mut hmt = HistoryMinuteTime::new(market, code, today_yyyymmdd());
hmt.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(e, format_args!("{ctx}(回退到今日历史分时)"))
})?;
Ok(hmt.result().to_vec())
}
pub fn history_minute(
&mut self,
market: u16,
code: &str,
date: u32,
) -> std::io::Result<Vec<MinuteTimeData>> {
let mut mt = HistoryMinuteTime::new(market, code, date);
mt.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(
e,
format_args!("Client::history_minute(market={market}, code={code}, date={date})"),
)
})?;
Ok(mt.result().to_vec())
}
pub fn transaction(
&mut self,
market: u16,
code: &str,
start: u16,
count: u16,
) -> std::io::Result<Vec<TransactionData>> {
let mut tx = super::Transaction::new(market, code, start, count);
tx.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(
e,
format_args!("Client::transaction(market={market}, code={code})"),
)
})?;
Ok(tx.result().to_vec())
}
pub fn history_transaction(
&mut self,
market: u16,
code: &str,
start: u16,
count: u16,
date: u32,
) -> std::io::Result<Vec<TransactionData>> {
let mut tx = HistoryTransaction::new(market, code, start, count, date);
tx.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(
e,
format_args!("Client::history_transaction(market={market}, code={code})"),
)
})?;
Ok(tx.result().to_vec())
}
pub fn finance(&mut self, market: u16, code: &str) -> std::io::Result<FinanceInfoData> {
let mut fin = super::FinanceInfo::new(market as u8, code);
fin.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(
e,
format_args!("Client::finance(market={market}, code={code})"),
)
})?;
Ok(fin.result().first().cloned().unwrap_or_default())
}
pub fn xdxr(&mut self, market: u16, code: &str) -> std::io::Result<Vec<super::XdxrData>> {
let mut xdxr = Xdxr::new(market, code);
xdxr.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(e, format_args!("Client::xdxr(market={market}, code={code})"))
})?;
Ok(xdxr.result().to_vec())
}
pub fn block(&mut self, block_file: &str) -> crate::Result<Vec<BlockRecord>> {
super::get_block_info(&mut self.tcp, block_file).map_err(|e| {
crate::Error::Io(std::io::Error::other(format!("Client::block({block_file}): {e}")))
})
}
pub fn stock_count(&mut self, market: u16) -> std::io::Result<u16> {
let mut sc = super::super::basic::SecurityCount::new(market);
let c = sc
.recv_parsed(&mut self.tcp)
.map_err(|e| ctx_err(e, format_args!("Client::stock_count(market={market})")))?;
Ok(*c)
}
pub fn stocks(&mut self, market: u16) -> crate::Result<Vec<SecurityListData>> {
super::stocks(&mut self.tcp, market).map_err(|e| {
crate::Error::Io(std::io::Error::other(format!("Client::stocks(market={market}): {e}")))
})
}
pub fn f10(&mut self, market: u16, code: &str) -> std::io::Result<Vec<(String, String)>> {
let ctx = format_args!("Client::f10(market={market}, code={code})");
let mut cat = CompanyInfoCategory::new(market, code);
cat.recv_parsed(&mut self.tcp)
.map_err(|e| ctx_err(e, ctx))?;
let mut result = Vec::with_capacity(cat.result().len());
for item in cat.result() {
let mut content =
CompanyInfoContent::new(market, code, &item.filename, item.start, item.length);
content.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(e, format_args!("{ctx}(栏目 {})", item.name))
})?;
result.push((item.name.clone(), std::mem::take(&mut content.data)));
}
Ok(result)
}
pub fn f10_categories(
&mut self,
market: u16,
code: &str,
) -> std::io::Result<Vec<CompanyInfoCategoryItem>> {
let mut cat = CompanyInfoCategory::new(market, code);
cat.recv_parsed(&mut self.tcp).map_err(|e| {
ctx_err(
e,
format_args!("Client::f10_categories(market={market}, code={code})"),
)
})?;
Ok(cat.result().to_vec())
}
}
fn today_yyyymmdd() -> u32 {
let today = chrono::Local::now().date_naive();
(today.year() * 10000 + today.month() as i32 * 100 + today.day() as i32) as u32
}
fn ctx_err(e: std::io::Error, ctx: std::fmt::Arguments<'_>) -> std::io::Error {
std::io::Error::new(e.kind(), format!("{ctx}: {e}"))
}
use super::SecurityQuotes as SecurityQuotesRef;
fn fetch_k<'a>(
tcp: &mut Tcp,
market: u16,
code: &'a str,
begin: Option<u32>,
end: Option<u32>,
) -> std::io::Result<Vec<KlineData<'a>>> {
const PAGE: u16 = 800;
const CATEGORY_DAY: u16 = 9;
let mut all = Vec::new();
let mut start = 0u16;
loop {
let mut kline = Kline::new(market, code, CATEGORY_DAY, start, PAGE);
kline.recv_parsed(tcp)?;
let bars = kline.result().to_vec();
let n = bars.len();
all.extend(bars);
if n < PAGE as usize {
break; }
if let Some(begin) = begin {
if let Some(oldest) = all
.iter()
.map(|b| DateTime::to_u32(b.dt.clone()))
.min()
&& oldest < begin
{
break;
}
}
start = start.saturating_add(PAGE);
if start == 0 {
break; }
}
all.retain(|bar| {
let d = DateTime::to_u32(bar.dt.clone());
begin.is_none_or(|b| d >= b) && end.is_none_or(|e| d <= e)
});
all.sort_by_key(|bar| DateTime::to_u32(bar.dt.clone()));
Ok(all)
}
#[derive(Debug)]
pub struct BatchKline<'a> {
pub market: u16,
pub code: &'a str,
pub result: std::io::Result<Vec<KlineData<'a>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Adj {
Qfq,
Hfq,
}
fn adjusted_multipliers(days: &[KlineData<'_>], xdxrs: &[XdxrData], adj: Adj) -> Vec<f64> {
let mut events: Vec<(u32, f32, f32, f32, f32)> = xdxrs
.iter()
.filter(|x| x.category == 1)
.map(|x| (x.date, x.fh_qltp, x.sg_hltp, x.pg_hzgb, x.pgj_qzgb))
.collect();
events.sort_by_key(|e| e.0);
let mut ev = events.iter().peekable();
let mut scales: Vec<f64> = Vec::with_capacity(days.len());
let mut scale = 1.0f64;
let mut preclose = days.first().map(|d| d.close).unwrap_or(0.0);
for d in days {
let date = DateTime::to_u32(d.dt.clone());
while let Some(&&(edate, fh, sg, pg, pgj)) = ev.peek() {
if edate > date {
break;
}
let preclose_adj = (preclose * 10.0 - fh as f64 + pg as f64 * pgj as f64)
/ (10.0 + pg as f64 + sg as f64);
if preclose_adj > 0.0 && preclose > 0.0 {
scale *= preclose_adj / preclose;
}
ev.next();
}
scales.push(scale);
preclose = d.close;
}
let last = *scales.last().unwrap_or(&1.0);
scales
.into_iter()
.map(|s| match adj {
Adj::Hfq => 1.0 / s,
Adj::Qfq => last / s,
})
.map(|m| if m.is_finite() { m } else { 1.0 })
.collect()
}
#[cfg(test)]
mod tests {
#[test]
fn k_range_retain_logic() {
use crate::tcp::helper::DateTime;
let d = DateTime {
year: 2026,
month: 9,
day: 1,
hour: 15,
minute: 0,
};
assert_eq!(d.to_u32(), 20260901);
}
#[test]
fn adjusted_multipliers_no_xdxr() {
use super::adjusted_multipliers;
use super::Adj;
let days = vec![
kd(20260101, 10.0),
kd(20260102, 11.0),
kd(20260105, 12.0),
];
assert_eq!(adjusted_multipliers(&days, &[], Adj::Qfq), vec![1.0, 1.0, 1.0]);
assert_eq!(adjusted_multipliers(&days, &[], Adj::Hfq), vec![1.0, 1.0, 1.0]);
}
#[test]
fn adjusted_multipliers_split() {
use super::adjusted_multipliers;
use super::Adj;
let days = vec![
kd(20260105, 20.0),
kd(20260106, 10.0), kd(20260107, 10.5),
];
let xdxrs = vec![xdxr(20260106, 0.0, 10.0, 0.0, 0.0)]; let m = adjusted_multipliers(&days, &xdxrs, Adj::Hfq);
assert!((m[0] - 1.0).abs() < 1e-9);
assert!((m[1] - 2.0).abs() < 1e-9, "除权日乘数应为 2,实际 {m:?}");
assert!((m[2] - 2.0).abs() < 1e-9);
let m = adjusted_multipliers(&days, &xdxrs, Adj::Qfq);
assert!((m[2] - 1.0).abs() < 1e-9);
assert!((m[0] - 0.5).abs() < 1e-9, "除权前乘数应为 0.5,实际 {m:?}");
}
#[test]
fn adjusted_multipliers_cash_and_rights() {
use super::adjusted_multipliers;
use super::Adj;
let days = vec![kd(20260105, 20.0), kd(20260106, 15.0), kd(20260107, 15.5)];
let xdxrs = vec![xdxr(20260106, 5.0, 0.0, 3.0, 2.0)];
let m = adjusted_multipliers(&days, &xdxrs, Adj::Hfq);
assert!(
(m[1] - (13.0 * 20.0 / 201.0)).abs() < 1e-9,
"实际 {m:?}"
);
}
#[test]
fn adjusted_multipliers_suspended_xdxr_day() {
use super::adjusted_multipliers;
use super::Adj;
let days = vec![kd(20260102, 20.0), kd(20260106, 10.0)];
let xdxrs = vec![xdxr(20260105, 0.0, 10.0, 0.0, 0.0)]; let m = adjusted_multipliers(&days, &xdxrs, Adj::Hfq);
assert!((m[0] - 1.0).abs() < 1e-9);
assert!((m[1] - 2.0).abs() < 1e-9, "停牌顺延后乘数应为 2,实际 {m:?}");
}
fn kd(date: u32, close: f64) -> super::KlineData<'static> {
let (y, m, d) = (date / 10000, date / 100 % 100, date % 100);
super::KlineData {
dt: crate::tcp::helper::DateTime {
year: y as u16,
month: m as u16,
day: d as u16,
hour: 15,
minute: 0,
},
code: "",
open: close,
close,
high: close,
low: close,
vol: 0.0,
amount: 0.0,
}
}
fn xdxr(date: u32, fh: f32, sg: f32, pg: f32, pgj: f32) -> super::XdxrData {
super::XdxrData {
market: 1,
code: "600000".into(),
date,
category: 1,
fh_qltp: fh,
pgj_qzgb: pgj,
sg_hltp: sg,
pg_hzgb: pg,
}
}
#[test]
fn today_yyyymmdd_format() {
use super::today_yyyymmdd;
let v = today_yyyymmdd();
assert!((20000101..=20991231).contains(&v), "非法日期: {v}");
let today = chrono::Local::now().date_naive();
let expected = {
use chrono::Datelike;
(today.year() * 10000 + today.month() as i32 * 100 + today.day() as i32) as u32
};
assert_eq!(v, expected);
}
}