From 5f2bbc9a3714076491d48724f8bc4b16e6e55a66 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 7 Oct 2021 00:39:43 +0800 Subject: [PATCH] fix #1; update rustdx version in rustdx-cmd --- .gitignore | 1 + README.md | 10 -- rustdx-cmd/Cargo.toml | 2 +- src/file/day/mod.rs | 61 +++++----- src/file/gbbq/fq.rs | 164 +++++++++++++++----------- tests/fq.rs | 18 +++ tests/snapshots/fq__day_sz000001.snap | 34 ++++++ 7 files changed, 185 insertions(+), 105 deletions(-) create mode 100644 tests/fq.rs create mode 100644 tests/snapshots/fq__day_sz000001.snap diff --git a/.gitignore b/.gitignore index 083e419..e907462 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ assets target +rustdx.log rustdx-cmd/rustdx.log rustdx-cmd/stocks.csv rustdx-cmd/eastmoney.csv diff --git a/README.md b/README.md index 4c30dec..fdaefd1 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,6 @@ 具体文档待补充。 -## TODO - -近期计划: -- [] 【rustdx】增加 day 前复权测试用例(有些特例 #000001# 上市日因子不是 1,需要 fix bug) -- [] 【rustdx-cmd】针对 ClickHouse 和复权情况下,数据录入与导出子命令 - -远期计划: -- [] 【rustdx】增加数据源 -- [] 【rustdx-cmd】拆分非 tdx 数据源到新 crate - ## rustdx-cmd ### 安装 diff --git a/rustdx-cmd/Cargo.toml b/rustdx-cmd/Cargo.toml index 9d6ddc9..1bbf951 100644 --- a/rustdx-cmd/Cargo.toml +++ b/rustdx-cmd/Cargo.toml @@ -28,7 +28,7 @@ simplelog = {version ="0.10", optional = true} miniz_oxide = {version ="0.4", optional = true} [dependencies.rustdx] -version = "0.2.1" +version = "0.2.3" optional = true path = "../" diff --git a/src/file/day/mod.rs b/src/file/day/mod.rs index 4e0fed8..5e32ed8 100644 --- a/src/file/day/mod.rs +++ b/src/file/day/mod.rs @@ -1,6 +1,7 @@ use std::path::Path; #[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] pub mod fq; /// 解析 `*.day` 文件中的一条日线数据,即其 32 个字节所代表的所有信息。 @@ -12,14 +13,14 @@ pub mod fq; /// [`serde_type::Day`]: crate::serde_type::Day #[derive(Debug, Clone, Copy)] pub struct Day { - pub date: u32, - pub code: u32, - pub open: f32, - pub high: f32, - pub low: f32, - pub close: f32, + pub date: u32, + pub code: u32, + pub open: f32, + pub high: f32, + pub low: f32, + pub close: f32, pub amount: f32, - pub vol: u32, + pub vol: u32, } impl Day { @@ -46,18 +47,20 @@ impl Day { /// 日:20210810%10000%100 = 10 pub fn from_bytes(code: u32, arr: &[u8]) -> Self { use crate::bytes_helper::{f32_from_le_bytes, u32_from_le_bytes}; - Self { date: u32_from_le_bytes(arr, 0), - open: u32_from_le_bytes(arr, 4) as f32 / 100., - high: u32_from_le_bytes(arr, 8) as f32 / 100., - low: u32_from_le_bytes(arr, 12) as f32 / 100., - close: u32_from_le_bytes(arr, 16) as f32 / 100., - amount: f32_from_le_bytes(arr, 20), - vol: u32_from_le_bytes(arr, 24), - code } + Self { + date: u32_from_le_bytes(arr, 0), + open: u32_from_le_bytes(arr, 4) as f32 / 100., + high: u32_from_le_bytes(arr, 8) as f32 / 100., + low: u32_from_le_bytes(arr, 12) as f32 / 100., + close: u32_from_le_bytes(arr, 16) as f32 / 100., + amount: f32_from_le_bytes(arr, 20), + vol: u32_from_le_bytes(arr, 24), + code, + } } /// 一次性以**同步**方式读取单个 `*.day` 文件所有数据,然后转化成 Vec。 - pub fn file_into_vec>(code: u32, p: P) -> Result, crate::Error> { + pub fn file_into_vec>(code: u32, p: P) -> crate::Result> { Ok(std::fs::read(p)?.chunks_exact(32).map(|b| Self::from_bytes(code, b)).collect()) } @@ -73,20 +76,24 @@ impl Day { /// 6 位字符串的股票代码;%Y-%m-%d 字符串格式的日期;f64 类型的成交额;u64 类型的 vol 。 #[cfg(feature = "serde")] pub fn into_serde_type(self) -> crate::serde_type::Day { - crate::serde_type::Day { code: format!("{:06}", self.code), - date: self.date_string(), - open: self.open, - high: self.high, - low: self.low, - close: self.close, - // 单位:元 - amount: self.amount, - // 转换成手:方便与其他数据源汇合 - vol: self.vol as f32 / 100., } + crate::serde_type::Day { + code: format!("{:06}", self.code), + date: self.date_string(), + open: self.open, + high: self.high, + low: self.low, + close: self.close, + // 单位:元 + amount: self.amount, + // 转换成手:方便与其他数据源汇合 + vol: self.vol as f32 / 100., + } } /// `%Y-%m-%d` 格式的日期 - pub fn date_string(&self) -> String { crate::bytes_helper::date_string(self.date) } + pub fn date_string(&self) -> String { + crate::bytes_helper::date_string(self.date) + } /// `[年, 月, 日]` 格式的日期 pub fn ymd_arr(&self) -> [u32; 3] { diff --git a/src/file/gbbq/fq.rs b/src/file/gbbq/fq.rs index 8284ad2..39d62c6 100644 --- a/src/file/gbbq/fq.rs +++ b/src/file/gbbq/fq.rs @@ -4,37 +4,44 @@ use crate::file::day::Day; #[cfg(feature = "serde")] #[derive(Debug, Clone, serde::Deserialize)] pub struct Factor { - pub date: String, - pub code: String, + pub date: String, + pub code: String, #[serde(rename(deserialize = "close"))] pub preclose: f64, - pub factor: f64, + pub factor: f64, } #[cfg(feature = "serde")] impl Factor { /// 根据前收、收盘价(最新价)和前一日的因子计算当日的复权因子。 #[inline] - pub fn compute_factor(&self, close: f64) -> f64 { self.factor * (close / self.preclose) } + pub fn compute_factor(&self, close: f64) -> f64 { + self.factor * (close / self.preclose) + } } #[derive(Debug, Clone, Copy)] pub struct Fq { - pub code: u32, + pub code: u32, /// 年月日 - pub date: u32, - pub factor: f64, - pub close: f64, + pub date: u32, + pub factor: f64, + pub close: f64, pub preclose: f64, - pub trading: bool, - pub xdxr: bool, + pub trading: bool, + pub xdxr: bool, } impl Fq { - /// preclose: days.clone().next()?.close - /// factor: 1. - pub fn new(days: impl Iterator + ExactSizeIterator + Clone, g1: &[Gbbq]) - -> Option> { + /// 从上市日开始计算复权。 + /// + /// ## 注意 + /// 1. 上市日因子为 1。所以如果存在除权日先于上市日,直接舍弃先于上市日的除权日, + /// 比如 #000001#、#601975#。 + /// 2. 解析后的交易日天数、除权日天数会在 debug build 下校验。 + pub fn new( + days: impl Iterator + ExactSizeIterator + Clone, g1: &[Gbbq], + ) -> Option> { let count = days.len(); let mut fqs: Vec = Vec::with_capacity(count + 128); let mut preclose = days.clone().next()?.close as f64; @@ -42,11 +49,17 @@ impl Fq { let mut gbbq = g1.iter(); let mut xdxr = gbbq.next()?; let mut last = false; + let mut first = 0usize; - for d in days { + for (i, d) in days.enumerate() { while !last && d.date > xdxr.date { // 因为停牌或某种原因导致下个交易日晚于除权日 - fqs.push(Self::_0(d, xdxr, preclose, &mut factor, false, true)); + if i == 0 { + // 为了让上市日因子为 1 + first += 1; + } else { + fqs.push(Self::_0(d, xdxr, preclose, &mut factor, false, true)); + } if let Some(x) = gbbq.next() { xdxr = x; } else if !last { @@ -69,27 +82,33 @@ impl Fq { } // 确保所有数据都被正确解析:必须满足两个条件 - debug_assert_eq!(count, - fqs.iter().filter(|f| f.trading).count(), - "{} 交易日天数不正确", - g1[0].code); - debug_assert_eq!(if g1[g1.len() - 1].date <= fqs.last().unwrap().date { - g1.len() - } else { - g1.iter().take_while(|g| g.date <= fqs.last().unwrap().date).count() - }, - fqs.iter().filter(|f| f.xdxr).count(), - "{} 除权除息日天数不正确:\nleft: {:?}\nright: {:?}", - g1[0].code, - g1, - fqs.iter().filter(|f| f.xdxr).collect::>()); + debug_assert_eq!( + count, + fqs.iter().filter(|f| f.trading).count(), + "{} 交易日天数不正确", + g1[0].code + ); + debug_assert_eq!( + if g1.last().unwrap().date <= fqs.last().unwrap().date { + g1.len() + } else { + // gbbq 最新日期大于 day 文件最新日期 + g1.iter().take_while(|g| g.date <= fqs.last().unwrap().date).count() + }, + fqs.iter().filter(|f| f.xdxr).count() + first, + "{} 除权除息日天数不正确:\nleft: {:?}\nright: {:?}", + g1[0].code, + g1, + fqs.iter().filter(|f| f.xdxr).collect::>() + ); Some(fqs) } - pub fn concat(days: impl Iterator + ExactSizeIterator + Clone, g1: &[Gbbq], - mut preclose: f64, mut factor: f64) - -> Option> { + pub fn concat( + days: impl Iterator + ExactSizeIterator + Clone, g1: &[Gbbq], + mut preclose: f64, mut factor: f64, + ) -> Option> { let count = days.len(); let mut fqs: Vec = Vec::with_capacity(count + 128); let mut gbbq = g1.iter(); @@ -122,19 +141,24 @@ impl Fq { } // 确保所有数据都被正确解析:必须满足两个条件 - debug_assert_eq!(count, - fqs.iter().filter(|f| f.trading).count(), - "{} 交易日天数不正确", - g1[0].code); - debug_assert_eq!(g1.iter() - .filter(|g| g.date >= fqs.first().unwrap().date - && g.date <= fqs.last().unwrap().date) - .count(), - fqs.iter().filter(|f| f.xdxr).count(), - "{} 除权除息日天数不正确:\nleft: {:?}\nright: {:?}", - g1[0].code, - g1, - fqs.iter().filter(|f| f.xdxr).collect::>()); + debug_assert_eq!( + count, + fqs.iter().filter(|f| f.trading).count(), + "{} 交易日天数不正确", + g1[0].code + ); + debug_assert_eq!( + g1.iter() + .filter( + |g| g.date >= fqs.first().unwrap().date && g.date <= fqs.last().unwrap().date + ) + .count(), + fqs.iter().filter(|f| f.xdxr).count(), + "{} 除权除息日天数不正确:\nleft: {:?}\nright: {:?}", + g1[0].code, + g1, + fqs.iter().filter(|f| f.xdxr).collect::>() + ); Some(fqs) } @@ -142,32 +166,38 @@ impl Fq { pub fn no_gbbq(days: impl Iterator + ExactSizeIterator + Clone) -> Option> { let mut preclose = days.clone().next()?.close as f64; let mut factor = 1.; - Some(days.map(|d| { - let close = d.close as f64; - factor *= close / preclose; - let fq = Self { close, - factor, - preclose, - trading: true, - xdxr: false, - code: d.code, - date: d.date }; - preclose = close; - fq - }) - .collect()) + Some( + days.map(|d| { + let close = d.close as f64; + factor *= close / preclose; + let fq = Self { + close, + factor, + preclose, + trading: true, + xdxr: false, + code: d.code, + date: d.date, + }; + preclose = close; + fq + }) + .collect(), + ) } #[inline] fn _0(d: Day, g: &Gbbq, preclose: f64, factor: &mut f64, trading: bool, xdxr: bool) -> Self { let [preclose, close, pct] = g.compute_pre_pct(d.close, preclose, xdxr); *factor *= pct; - Self { close, - preclose, - trading, - xdxr, - code: d.code, - date: d.date, - factor: *factor } + Self { + close, + preclose, + trading, + xdxr, + code: d.code, + date: d.date, + factor: *factor, + } } } diff --git a/tests/fq.rs b/tests/fq.rs new file mode 100644 index 0000000..827af84 --- /dev/null +++ b/tests/fq.rs @@ -0,0 +1,18 @@ +use insta::assert_debug_snapshot; + +#[test] +fn day_sz000001() -> rustdx::Result<()> { + use rustdx::file::{ + day::Day, + gbbq::{Fq, Gbbq}, + }; + let day_src = std::fs::read("tests/assets/sz000001.day")?; + let days = day_src.chunks_exact(32).map(|arr| Day::from_bytes(1, arr)); + + let mut gbbq_src = std::fs::read("tests/assets/gbbq")?; + let stock_gbbq = Gbbq::filter_hashmap(Gbbq::iter(&mut gbbq_src[4..])); + + let fq = Fq::new(days, stock_gbbq.get(&1).unwrap()).unwrap(); + assert_debug_snapshot!(&fq[..3]); + Ok(()) +} diff --git a/tests/snapshots/fq__day_sz000001.snap b/tests/snapshots/fq__day_sz000001.snap new file mode 100644 index 0000000..67c750f --- /dev/null +++ b/tests/snapshots/fq__day_sz000001.snap @@ -0,0 +1,34 @@ +--- +source: tests/fq.rs +expression: "&fq[..3]" + +--- +[ + Fq { + code: 1, + date: 19910403, + factor: 1.0, + close: 49.0, + preclose: 49.0, + trading: true, + xdxr: false, + }, + Fq { + code: 1, + date: 19910404, + factor: 0.9951020065619021, + close: 48.7599983215332, + preclose: 49.0, + trading: true, + xdxr: false, + }, + Fq { + code: 1, + date: 19910405, + factor: 0.9902040909747688, + close: 48.52000045776367, + preclose: 48.7599983215332, + trading: true, + xdxr: false, + }, +]