use super::{KlineData, FinanceInfoData};
use crate::tcp::helper::DateTime;
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationLevel {
Ok,
Warning(String),
Error(String),
}
#[derive(Debug, Clone)]
pub struct DataLocation {
pub code: String,
pub date: Option<DateTime>,
pub field: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub level: ValidationLevel,
pub details: Vec<String>,
pub suggestions: Vec<String>,
pub location: Option<DataLocation>,
}
impl ValidationResult {
pub fn ok(message: impl Into<String>) -> Self {
Self {
level: ValidationLevel::Ok,
details: vec![message.into()],
suggestions: vec![],
location: None,
}
}
pub fn warning(message: impl Into<String>, details: Vec<String>, suggestions: Vec<String>) -> Self {
Self {
level: ValidationLevel::Warning(message.into()),
details,
suggestions,
location: None,
}
}
pub fn error(message: impl Into<String>, details: Vec<String>, suggestions: Vec<String>) -> Self {
Self {
level: ValidationLevel::Error(message.into()),
details,
suggestions,
location: None,
}
}
pub fn is_valid(&self) -> bool {
matches!(self.level, ValidationLevel::Ok)
}
}
pub fn validate_kline_continuity(
data: &[KlineData],
code: &str,
) -> ValidationResult {
if data.is_empty() {
return ValidationResult::error(
"数据为空",
vec!["K线数据不包含任何记录".to_string()],
vec!["请检查网络连接后重新获取".to_string()],
);
}
if data.len() == 1 {
return ValidationResult::ok("只有一条数据,跳过连续性检查");
}
let mut issues = Vec::new();
let mut prev_dt = None;
for bar in data {
if let Some(prev) = prev_dt {
let days_diff = calculate_date_diff(&prev, &bar.dt);
if days_diff > 3 {
issues.push(format!(
"日期跳变: {:04}-{:02}-{:02} 到 {:04}-{:02}-{:02} (间隔 {} 天,可能缺失交易日)",
prev.year, prev.month, prev.day,
bar.dt.year, bar.dt.month, bar.dt.day,
days_diff
));
}
}
prev_dt = Some(bar.dt.clone());
}
if issues.is_empty() {
ValidationResult::ok(format!(
"K线数据连续性检查通过(共 {} 条)",
data.len()
))
} else {
ValidationResult::warning(
format!("发现 {} 个可能的日期不连续", issues.len()),
issues,
vec![
"使用增量更新补全缺失数据".to_string(),
"或检查是否为节假日/停牌期".to_string(),
],
)
}
}
fn calculate_date_diff(dt1: &DateTime, dt2: &DateTime) -> i32 {
let days1 = dt1.year as i32 * 360 + dt1.month as i32 * 30 + dt1.day as i32;
let days2 = dt2.year as i32 * 360 + dt2.month as i32 * 30 + dt2.day as i32;
(days2 - days1).abs()
}
pub fn validate_finance_consistency(
data: &FinanceInfoData,
) -> ValidationResult {
let mut issues = Vec::new();
if data.zongguben < data.liutongguben {
issues.push(format!(
"总股本({:.0}) 小于流通股本({:.0}),违背常理",
data.zongguben, data.liutongguben
));
}
if data.jingzichan > data.zongzichan {
issues.push(format!(
"净资产({:.0}) 大于总资产({:.0}),数据异常",
data.jingzichan, data.zongzichan
));
}
if data.jingzichan < 0.0 {
issues.push(format!(
"净资产为负({:.0}),可能已资不抵债",
data.jingzichan
));
}
if data.liutongguben > 0.0 && data.zongguben > 0.0 {
let ratio = data.liutongguben / data.zongguben;
if ratio > 1.0 {
issues.push(format!(
"流通股本比例异常: {:.1}% (不应超过100%)",
ratio * 100.0
));
}
}
if issues.is_empty() {
ValidationResult::ok(format!(
"财务数据一致性检查通过(股票:{})",
data.code
))
} else {
ValidationResult::error(
format!("财务数据存在 {} 个一致性问题", issues.len()),
issues,
vec![
"检查原始数据源".to_string(),
"联系数据提供方确认".to_string(),
"或排除数据异常的上市公司".to_string(),
],
)
}
}
pub fn detect_anomalies(
data: &[KlineData],
threshold: f64,
) -> ValidationResult {
if data.is_empty() {
return ValidationResult::error(
"数据为空",
vec!["无法检测异常值".to_string()],
vec!["请先获取数据".to_string()],
);
}
if data.len() < 10 {
return ValidationResult::warning(
"数据量不足".to_string(),
vec![format!("当前只有 {} 条数据,建议至少 10 条", data.len())],
vec!["获取更多数据后重新检测".to_string()],
);
}
let mut anomalies = Vec::new();
for i in 1..data.len() {
let prev = &data[i - 1];
let curr = &data[i];
if prev.close > 0.0 {
let change_pct = (curr.close - prev.close).abs() / prev.close;
if change_pct > 0.20 {
anomalies.push(format!(
"{:04}-{:02}-{:02} 价格异常波动: {:.2}% (前收:{:.2}, 今收:{:.2})",
curr.dt.year, curr.dt.month, curr.dt.day,
change_pct * 100.0,
prev.close,
curr.close
));
}
}
}
let volumes: Vec<f64> = data.iter().map(|k| k.vol).collect();
let mean_vol = volumes.iter().sum::<f64>() / volumes.len() as f64;
let variance = volumes.iter()
.map(|v| (v - mean_vol).powi(2))
.sum::<f64>() / volumes.len() as f64;
let std_vol = variance.sqrt();
if std_vol > 0.0 && mean_vol > 0.0 {
for bar in data {
let z_score = (bar.vol - mean_vol) / std_vol;
if z_score.abs() > threshold {
anomalies.push(format!(
"{:04}-{:02}-{:02} 成交量异常: {:.0} (Z-score: {:.1}, 均值: {:.0})",
bar.dt.year, bar.dt.month, bar.dt.day,
bar.vol, z_score, mean_vol
));
}
}
}
for bar in data {
if bar.high < bar.low {
anomalies.push(format!(
"{:04}-{:02}-{:02} 最高价({:.2}) 低于最低价({:.2})",
bar.dt.year, bar.dt.month, bar.dt.day,
bar.high, bar.low
));
}
if bar.close > bar.high {
anomalies.push(format!(
"{:04}-{:02}-{:02} 收盘价({:.2}) 高于最高价({:.2})",
bar.dt.year, bar.dt.month, bar.dt.day,
bar.close, bar.high
));
}
if bar.close < bar.low {
anomalies.push(format!(
"{:04}-{:02}-{:02} 收盘价({:.2}) 低于最低价({:.2})",
bar.dt.year, bar.dt.month, bar.dt.day,
bar.close, bar.low
));
}
if bar.open <= 0.0 || bar.close <= 0.0 {
anomalies.push(format!(
"{:04}-{:02}-{:02} 价格为零或负数(开:{:.2}, 收:{:.2})",
bar.dt.year, bar.dt.month, bar.dt.day,
bar.open, bar.close
));
}
}
if anomalies.is_empty() {
ValidationResult::ok(format!(
"未检测到明显异常(共 {} 条数据,阈值: {:.1}σ)",
data.len(),
threshold
))
} else {
ValidationResult::warning(
format!("检测到 {} 个异常值", anomalies.len()),
anomalies,
vec![
"检查是否为除权除息日".to_string(),
"检查是否发布重大公告(复牌等)".to_string(),
"或调整阈值参数以降低敏感度".to_string(),
],
)
}
}
pub trait Validatable {
fn validate(&self) -> ValidationResult;
fn is_valid(&self) -> bool {
self.validate().is_valid()
}
}
impl<'a> Validatable for KlineData<'a> {
fn validate(&self) -> ValidationResult {
let mut issues = Vec::new();
if self.open <= 0.0 {
issues.push("开盘价必须大于0".to_string());
}
if self.close <= 0.0 {
issues.push("收盘价必须大于0".to_string());
}
if self.high < self.low {
issues.push("最高价不能低于最低价".to_string());
}
if self.close > self.high {
issues.push("收盘价不能高于最高价".to_string());
}
if self.close < self.low {
issues.push("收盘价不能低于最低价".to_string());
}
if self.vol < 0.0 {
issues.push("成交量不能为负".to_string());
}
if issues.is_empty() {
ValidationResult::ok(format!("{:04}-{:02}-{:02} 数据验证通过",
self.dt.year, self.dt.month, self.dt.day))
} else {
ValidationResult::error(
"单条数据验证失败".to_string(),
issues,
vec!["请检查数据源".to_string()],
)
}
}
}
impl Validatable for FinanceInfoData {
fn validate(&self) -> ValidationResult {
validate_finance_consistency(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_datetime(y: u16, m: u16, d: u16) -> DateTime {
DateTime {
year: y,
month: m,
day: d,
hour: 15,
minute: 0,
}
}
fn create_test_kline_data(dates: Vec<DateTime>, close_prices: Vec<f64>) -> Vec<KlineData<'static>> {
dates.into_iter()
.zip(close_prices.into_iter())
.map(|(dt, close)| KlineData {
dt,
code: "600000",
open: close * 0.98,
close,
high: close * 1.02,
low: close * 0.97,
vol: 1000000.0,
amount: close * 1000000.0,
})
.collect()
}
#[test]
fn test_validate_kline_continuity_normal() {
let dates = vec![
create_test_datetime(2024, 1, 1),
create_test_datetime(2024, 1, 2),
create_test_datetime(2024, 1, 3),
create_test_datetime(2024, 1, 4),
];
let prices = vec![10.0, 10.1, 10.2, 10.3];
let data = create_test_kline_data(dates, prices);
let result = validate_kline_continuity(&data, "600000");
assert!(result.is_valid());
assert!(matches!(result.level, ValidationLevel::Ok));
}
#[test]
fn test_validate_kline_continuity_missing_dates() {
let dates = vec![
create_test_datetime(2024, 1, 1),
create_test_datetime(2024, 1, 6), ];
let prices = vec![10.0, 10.1];
let data = create_test_kline_data(dates, prices);
let result = validate_kline_continuity(&data, "600000");
assert!(!result.is_valid());
assert!(matches!(result.level, ValidationLevel::Warning(_)));
assert!(!result.details.is_empty());
}
#[test]
fn test_validate_kline_continuity_empty() {
let data: Vec<KlineData> = vec![];
let result = validate_kline_continuity(&data, "600000");
assert!(!result.is_valid());
assert!(matches!(result.level, ValidationLevel::Error(_)));
}
#[test]
fn test_validate_kline_continuity_single() {
let dates = vec![create_test_datetime(2024, 1, 1)];
let prices = vec![10.0];
let data = create_test_kline_data(dates, prices);
let result = validate_kline_continuity(&data, "600000");
assert!(result.is_valid());
}
#[test]
fn test_finance_consistency_normal() {
let mut data = FinanceInfoData::default();
data.code = "600000".to_string();
data.zongguben = 1000000000.0; data.liutongguben = 800000000.0; data.jingzichan = 50000000000.0; data.zongzichan = 100000000000.0;
let result = validate_finance_consistency(&data);
assert!(result.is_valid());
}
#[test]
fn test_finance_consistency_invalid() {
let mut data = FinanceInfoData::default();
data.code = "600000".to_string();
data.zongguben = 1000.0;
data.liutongguben = 2000.0; data.jingzichan = 500.0;
data.zongzichan = 1000.0;
let result = validate_finance_consistency(&data);
assert!(!result.is_valid());
assert!(!result.details.is_empty());
}
#[test]
fn test_detect_anomalies_empty() {
let data: Vec<KlineData> = vec![];
let result = detect_anomalies(&data, 3.0);
assert!(!result.is_valid());
assert!(matches!(result.level, ValidationLevel::Error(_)));
}
#[test]
fn test_detect_anomalies_insufficient_data() {
let dates = vec![
create_test_datetime(2024, 1, 1),
create_test_datetime(2024, 1, 2),
];
let prices = vec![10.0, 10.1];
let data = create_test_kline_data(dates, prices);
let result = detect_anomalies(&data, 3.0);
assert!(!result.is_valid());
assert!(matches!(result.level, ValidationLevel::Warning(_)));
}
#[test]
fn test_detect_anomalies_price_spike() {
let dates: Vec<DateTime> = (0..15)
.map(|i| create_test_datetime(2024, 1, 1 + i))
.collect();
let prices: Vec<f64> = (0..15).map(|_| 10.0).collect();
let mut data = create_test_kline_data(dates, prices);
data[10].close = 15.0; data[10].high = 15.5;
let result = detect_anomalies(&data, 3.0);
assert!(!result.is_valid());
assert!(result.details.iter().any(|s| s.contains("价格异常波动")));
}
#[test]
fn test_detect_anomalies_logic_error() {
let dates: Vec<DateTime> = (0..15)
.map(|i| create_test_datetime(2024, 1, 1 + i))
.collect();
let prices: Vec<f64> = (0..15).map(|i| 10.0 + i as f64 * 0.1).collect();
let mut data = create_test_kline_data(dates, prices);
data[5].high = 9.0; data[5].low = 10.0;
let result = detect_anomalies(&data, 3.0);
assert!(!result.is_valid());
assert!(result.details.iter().any(|s| s.contains("最高价")));
}
#[test]
fn test_kline_data_validatable() {
let dt = create_test_datetime(2024, 1, 1);
let data = KlineData {
dt,
code: "600000",
open: 10.0,
close: 10.5,
high: 11.0,
low: 9.5,
vol: 1000000.0,
amount: 10500000.0,
};
assert!(data.is_valid());
}
#[test]
fn test_kline_data_invalid() {
let dt = create_test_datetime(2024, 1, 1);
let data = KlineData {
dt,
code: "600000",
open: 10.0,
close: 10.5,
high: 9.0, low: 10.0,
vol: 1000000.0,
amount: 10500000.0,
};
assert!(!data.is_valid());
}
}