1use byteorder::{ByteOrder, LittleEndian};
7use chrono::{DateTime, FixedOffset, NaiveDate, Utc};
8use rusty_leveldb::{DB, LdbIterator, Options};
9use std::path::Path;
10use std::str;
11use thiserror::Error;
12
13#[derive(Debug, Clone)]
15pub struct DividendRecord {
16 pub ex_dividend_date: NaiveDate,
18 pub record_date: Option<NaiveDate>,
20 pub interest: f64,
22 pub stock_bonus: f64,
24 pub stock_gift: f64,
26 pub allot_num: f64,
28 pub allot_price: f64,
30 pub gugai: f64,
32 pub unknown64_raw: f64,
34 pub adjust_factor: f64,
36 pub timestamp_raw: i64,
38}
39
40#[derive(Debug, Error)]
42pub enum DividendError {
43 #[error("无法打开 LevelDB: {0}")]
45 OpenDb(String),
46 #[error("无法创建 LevelDB 迭代器")]
48 IteratorUnavailable,
49 #[error("非法的分红 Key: {0}")]
51 InvalidKey(String),
52 #[error("分红 Key 不是有效 UTF-8")]
54 InvalidUtf8Key,
55 #[error("无效的分红时间戳: {0}")]
57 InvalidTimestamp(i64),
58 #[error("无法解析分红 Value: {0}")]
60 InvalidValue(String),
61}
62
63pub struct DividendDb {
65 db: DB,
66}
67
68impl DividendDb {
69 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, DividendError> {
85 let mut options = Options::default();
86 options.create_if_missing = false;
87
88 match DB::open(path, options) {
89 Ok(db) => Ok(Self { db }),
90 Err(e) => Err(DividendError::OpenDb(e.to_string())),
91 }
92 }
93
94 pub fn query(
111 &mut self,
112 market: &str,
113 code: &str,
114 ) -> Result<Vec<DividendRecord>, DividendError> {
115 let mut results = Vec::new();
116
117 let prefix = format!("{}|{}|", market, code);
118 let prefix_bytes = prefix.as_bytes();
119
120 let mut iter = self
121 .db
122 .new_iter()
123 .map_err(|_| DividendError::IteratorUnavailable)?;
124
125 iter.seek(prefix_bytes);
126
127 while let Some((key, value)) = iter.next() {
128 if !key.starts_with(prefix_bytes) {
129 break;
130 }
131
132 let ts_key = match Self::parse_key_timestamp(&key)? {
133 Some(ts_key) => ts_key,
134 None => continue,
135 };
136
137 if ts_key == 0 || ts_key > 3_000_000_000_000 {
138 continue;
139 }
140
141 if let Some(record) = Self::parse_value(&value)? {
142 results.push(record);
143 }
144 }
145
146 Ok(results)
147 }
148
149 fn parse_value(data: &[u8]) -> Result<Option<DividendRecord>, DividendError> {
156 if data.is_empty() {
157 return Ok(None);
158 }
159 if data.len() < 96 {
160 return Err(DividendError::InvalidValue(format!(
161 "value too short: expected at least 96 bytes, got {}",
162 data.len()
163 )));
164 }
165
166 let ts_val = LittleEndian::read_i64(&data[8..16]);
167 if ts_val <= 0 {
168 return Err(DividendError::InvalidTimestamp(ts_val));
169 }
170 let interest = LittleEndian::read_f64(&data[16..24]);
171 let stock_bonus = LittleEndian::read_f64(&data[24..32]);
172 let stock_gift = LittleEndian::read_f64(&data[32..40]);
173 let allot_num = LittleEndian::read_f64(&data[40..48]);
174 let allot_price = LittleEndian::read_f64(&data[48..56]);
175 let gugai = LittleEndian::read_f64(&data[56..64]);
176 let unknown64_raw = LittleEndian::read_f64(&data[64..72]);
177 let adjust_factor = LittleEndian::read_f64(&data[72..80]);
178
179 let record_date = Self::parse_yyyymmdd_u32(LittleEndian::read_u32(&data[80..84]));
180 let ex_dividend_date = Self::parse_yyyymmdd_u32(LittleEndian::read_u32(&data[88..92]))
181 .or_else(|| Self::date_from_timestamp_bj(ts_val))
182 .ok_or_else(|| DividendError::InvalidTimestamp(ts_val))?;
183
184 Ok(Some(DividendRecord {
185 ex_dividend_date,
186 record_date,
187 interest,
188 stock_bonus,
189 stock_gift,
190 allot_num,
191 allot_price,
192 gugai,
193 unknown64_raw,
194 adjust_factor,
195 timestamp_raw: ts_val,
196 }))
197 }
198
199 fn parse_key_timestamp(key: &[u8]) -> Result<Option<i64>, DividendError> {
200 let key_str = str::from_utf8(key).map_err(|_| DividendError::InvalidUtf8Key)?;
201 let parts: Vec<&str> = key_str.split('|').collect();
202 if parts.len() < 4 {
203 return Err(DividendError::InvalidKey(key_str.to_string()));
204 }
205
206 let ts = parts
207 .last()
208 .ok_or_else(|| DividendError::InvalidKey(key_str.to_string()))?
209 .parse::<i64>()
210 .map_err(|_| DividendError::InvalidKey(key_str.to_string()))?;
211
212 if ts == 0 || ts > 3_000_000_000_000 {
213 return Ok(None);
214 }
215
216 Ok(Some(ts))
217 }
218
219 fn parse_yyyymmdd_u32(raw: u32) -> Option<NaiveDate> {
220 if raw == 0 {
221 return None;
222 }
223
224 let year = (raw / 10_000) as i32;
225 let month = (raw / 100 % 100) as u32;
226 let day = (raw % 100) as u32;
227 NaiveDate::from_ymd_opt(year, month, day)
228 }
229
230 fn date_from_timestamp_bj(ts_val: i64) -> Option<NaiveDate> {
231 let seconds = ts_val / 1000;
232 let nanoseconds = (ts_val % 1000) * 1_000_000;
233 let dt_utc = DateTime::<Utc>::from_timestamp(seconds, nanoseconds as u32)?;
234 let bj = FixedOffset::east_opt(8 * 3600)?;
235 Some(dt_utc.with_timezone(&bj).date_naive())
236 }
237}
238
239#[test]
240fn test_dividend() {
241 let db_path = "/mnt/data/trade/qmtdata/datadir/DividData";
243
244 let mut qmt_db = match DividendDb::new(db_path) {
246 Ok(db) => db,
247 Err(e) => {
248 eprintln!("错误: {}", e);
249 return;
250 }
251 };
252
253 println!("正在查询 SH.185222 ...");
255 let records = qmt_db.query("SH", "185222").expect("query dividend");
256
257 if records.is_empty() {
258 eprintln!("未找到记录或解析失败。");
259 }
260
261 for record in records {
262 println!("--------------------------------");
263 println!("除权日 : {}", record.ex_dividend_date);
264 println!("登记日 : {:?}", record.record_date);
265 println!("每股红利 : {:.4}", record.interest);
266 println!("每股送转 : {:.4}", record.stock_bonus);
267 println!("每股转赠 : {:.4}", record.stock_gift);
268 println!("配股数量 : {:.4}", record.allot_num);
269 println!("配股价格 : {:.4}", record.allot_price);
270 println!("股改值 : {:.4}", record.gugai);
271 println!("复权系数 : {:.6}", record.adjust_factor);
272 }
273}
274
275#[test]
276fn test_parse_dividend_value_cash_dates_and_factor() {
277 let raw = decode_hex(
278 "2087c6faff7f000000488fa1850100005c8fc2f5285c09400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ce8853b1786f03fdfaf340100000000e0af340100000000",
279 )
280 .unwrap();
281
282 let record = DividendDb::parse_value(&raw)
283 .expect("should parse")
284 .expect("record");
285 assert_eq!(
286 record.ex_dividend_date,
287 NaiveDate::from_ymd_opt(2023, 1, 12).unwrap()
288 );
289 assert_eq!(
290 record.record_date,
291 Some(NaiveDate::from_ymd_opt(2023, 1, 11).unwrap())
292 );
293 assert_eq!(record.interest, 3.17);
294 assert_eq!(record.stock_bonus, 0.0);
295 assert_eq!(record.stock_gift, 0.0);
296 assert_eq!(record.allot_num, 0.0);
297 assert_eq!(record.allot_price, 0.0);
298 assert_eq!(record.gugai, 0.0);
299 assert!((record.adjust_factor - 1.032737).abs() < 1e-9);
300}
301
302#[test]
303fn test_parse_dividend_value_bonus_gift_and_rights_issue() {
304 let bonus_raw = decode_hex(
305 "2087c6faff7f000000e4f9da630100009a9999999999b93f0000000000000000000000000000e03f0000000000000000000000000000000000000000000000000000000000000000b56b425a6350f83f7fee33010000000080ee330100000000",
306 )
307 .unwrap();
308 let bonus_record = DividendDb::parse_value(&bonus_raw)
309 .expect("should parse")
310 .expect("record");
311 assert_eq!(
312 bonus_record.ex_dividend_date,
313 NaiveDate::from_ymd_opt(2018, 6, 8).unwrap()
314 );
315 assert_eq!(
316 bonus_record.record_date,
317 Some(NaiveDate::from_ymd_opt(2018, 6, 7).unwrap())
318 );
319 assert_eq!(bonus_record.interest, 0.1);
320 assert_eq!(bonus_record.stock_bonus, 0.0);
321 assert_eq!(bonus_record.stock_gift, 0.5);
322 assert_eq!(bonus_record.allot_num, 0.0);
323 assert_eq!(bonus_record.allot_price, 0.0);
324 assert_eq!(bonus_record.gugai, 0.0);
325 assert!((bonus_record.adjust_factor - 1.519626).abs() < 1e-9);
326
327 let rights_raw = decode_hex(
328 "2087c6faff7f00000040675d27010000000000000000000000000000000000000000000000000000a4703d0ad7a3c03f3333333333b3214000000000000000000000000000000000ae9b525e2be1f03fd0b43201000000000000000000000000",
329 )
330 .unwrap();
331 let rights_record = DividendDb::parse_value(&rights_raw)
332 .expect("should parse")
333 .expect("record");
334 assert_eq!(
335 rights_record.ex_dividend_date,
336 NaiveDate::from_ymd_opt(2010, 3, 15).unwrap()
337 );
338 assert_eq!(
339 rights_record.record_date,
340 Some(NaiveDate::from_ymd_opt(2010, 3, 4).unwrap())
341 );
342 assert_eq!(rights_record.interest, 0.0);
343 assert_eq!(rights_record.stock_bonus, 0.0);
344 assert_eq!(rights_record.stock_gift, 0.0);
345 assert!((rights_record.allot_num - 0.13).abs() < 1e-12);
346 assert!((rights_record.allot_price - 8.85).abs() < 1e-12);
347 assert_eq!(rights_record.gugai, 0.0);
348 assert!((rights_record.adjust_factor - 1.054973).abs() < 1e-9);
349}
350
351#[test]
352fn test_parse_dividend_value_gugai_slot() {
353 let raw = decode_hex(
354 "2087c6faff7f000000583e5b940100005c8fc2f5285c0940000000000000000000000000000000000000000000000000000000000000000000000000000059400000000000000000199293895b85f03ffefd34010000000000fe340100000000",
355 )
356 .unwrap();
357
358 let record = DividendDb::parse_value(&raw)
359 .expect("should parse")
360 .expect("record");
361 assert_eq!(
362 record.ex_dividend_date,
363 NaiveDate::from_ymd_opt(2025, 1, 12).unwrap()
364 );
365 assert_eq!(
366 record.record_date,
367 Some(NaiveDate::from_ymd_opt(2025, 1, 10).unwrap())
368 );
369 assert_eq!(record.interest, 3.17);
370 assert_eq!(record.stock_bonus, 0.0);
371 assert_eq!(record.stock_gift, 0.0);
372 assert_eq!(record.allot_num, 0.0);
373 assert_eq!(record.allot_price, 0.0);
374 assert_eq!(record.gugai, 100.0);
375 assert!((record.adjust_factor - 1.032558).abs() < 1e-9);
376}
377
378#[test]
379fn test_dividend_open_missing_db_returns_typed_error() {
380 match DividendDb::new("/definitely/missing/dividend-db") {
381 Err(DividendError::OpenDb(_)) => {}
382 Err(other) => panic!("unexpected error: {other}"),
383 Ok(_) => panic!("expected missing db to fail"),
384 }
385}
386
387#[test]
388fn test_parse_dividend_key_timestamp_rejects_invalid_key() {
389 let err = DividendDb::parse_key_timestamp(b"SH|185222").unwrap_err();
390 assert!(matches!(err, DividendError::InvalidKey(_)));
391}
392
393#[cfg(test)]
394fn decode_hex(input: &str) -> Result<Vec<u8>, String> {
395 if input.len() % 2 != 0 {
396 return Err("hex length must be even".to_string());
397 }
398
399 let mut out = Vec::with_capacity(input.len() / 2);
400 let bytes = input.as_bytes();
401 let mut i = 0;
402 while i < bytes.len() {
403 let hi = (bytes[i] as char)
404 .to_digit(16)
405 .ok_or_else(|| format!("invalid hex at {}", i))?;
406 let lo = (bytes[i + 1] as char)
407 .to_digit(16)
408 .ok_or_else(|| format!("invalid hex at {}", i + 1))?;
409 out.push(((hi << 4) | lo) as u8);
410 i += 2;
411 }
412 Ok(out)
413}