From af4cd25f478a906ac688ccc872bcaa25387cb9ba Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 17 Jul 2026 23:22:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=A4=8D=E6=9D=83=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E6=9C=AA=E6=9D=A5=E6=95=B0=E6=8D=AE=E6=B1=A1=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/data.rs | 103 ++++++++++++++---- .../fidc-core/src/platform_expr_strategy.rs | 5 +- 2 files changed, 85 insertions(+), 23 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 80c6a1a..41849d0 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -491,8 +491,8 @@ struct SymbolPriceSeries { #[derive(Debug, Clone)] struct AdjustedCloseSeries { dates: Vec, - backward_factors: Vec, - back_adjusted_closes: Vec, + backward_factors: Vec>, + back_adjusted_closes: Vec>, } impl AdjustedCloseSeries { @@ -503,22 +503,18 @@ impl AdjustedCloseSeries { let mut backward_factors = Vec::with_capacity(market.dates.len()); let mut back_adjusted_closes = Vec::with_capacity(market.dates.len()); for (date, close) in market.dates.iter().zip(&market.closes) { - if !close.is_finite() || *close <= 0.0 { - return None; - } let factor = factor_by_date .get(date) .and_then(|rows| { find_arc_by_symbol(rows, &market.symbol, |row| row.symbol.as_str()) }) - .and_then(|snapshot| { - factor_numeric_value(snapshot, "adjustment_factor_backward1") - })?; - if !factor.is_finite() || factor <= 0.0 { - return None; - } + .and_then(|snapshot| factor_numeric_value(snapshot, "adjustment_factor_backward1")) + .filter(|factor| factor.is_finite() && *factor > 0.0); + let back_adjusted_close = factor + .filter(|_| close.is_finite() && *close > 0.0) + .map(|factor| close * factor); backward_factors.push(factor); - back_adjusted_closes.push(close * factor); + back_adjusted_closes.push(back_adjusted_close); } Some(Self { dates: market.dates.clone(), @@ -539,15 +535,12 @@ impl AdjustedCloseSeries { if end < lookback { return None; } - let base_factor = *self.backward_factors.get(end - 1)?; - if !base_factor.is_finite() || base_factor <= 0.0 { - return None; - } + let base_factor = self.backward_factors.get(end - 1).copied().flatten()?; let start = end - lookback; let sum = self.back_adjusted_closes[start..end] .iter() .copied() - .sum::(); + .try_fold(0.0, |sum, value| value.map(|value| sum + value))?; if !sum.is_finite() { return None; } @@ -566,6 +559,7 @@ impl AdjustedCloseSeries { self.back_adjusted_closes .get(index) .copied() + .flatten() .filter(|value| value.is_finite() && *value > 0.0) } } @@ -3710,8 +3704,8 @@ mod tests { dates: (0..30) .map(|index| start + chrono::Duration::days(index as i64)) .collect(), - backward_factors: vec![1.0; 30], - back_adjusted_closes: values, + backward_factors: vec![Some(1.0); 30], + back_adjusted_closes: values.into_iter().map(Some).collect(), }; let date = *series.dates.last().expect("last date"); @@ -3721,6 +3715,77 @@ mod tests { ); } + #[test] + fn future_missing_adjustment_factor_does_not_invalidate_historical_window() { + let dates = [ + NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap(), + NaiveDate::parse_from_str("2025-01-03", "%Y-%m-%d").unwrap(), + NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap(), + NaiveDate::parse_from_str("2025-01-07", "%Y-%m-%d").unwrap(), + ]; + let data = DataSet::from_components( + vec![Instrument { + symbol: "000001.SZ".to_string(), + name: "000001.SZ".to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(dates[0]), + delisted_at: None, + status: "active".to_string(), + }], + dates + .iter() + .enumerate() + .map(|(index, date)| { + market_row( + &date.format("%Y-%m-%d").to_string(), + 10.0 + index as f64, + 100, + ) + }) + .collect(), + dates + .iter() + .map(|date| DailyFactorSnapshot { + date: *date, + symbol: "000001.SZ".to_string(), + market_cap_bn: 10.0, + free_float_cap_bn: 8.0, + pe_ttm: 10.0, + turnover_ratio: None, + effective_turnover_ratio: None, + extra_factors: if *date == dates[3] { + BTreeMap::new() + } else { + BTreeMap::from([("adjustment_factor_backward1".to_string(), 1.0)]) + }, + }) + .collect(), + Vec::new(), + dates + .iter() + .map(|date| BenchmarkSnapshot { + date: *date, + benchmark: "000852.SH".to_string(), + open: 100.0, + close: 100.0, + prev_close: 100.0, + volume: 1_000_000, + }) + .collect(), + ) + .expect("dataset"); + + assert_eq!( + data.market_current_numeric_moving_average(dates[2], "000001.SZ", "close", 3), + Some(11.0) + ); + assert_eq!( + data.market_current_numeric_moving_average(dates[3], "000001.SZ", "close", 3), + None + ); + } + #[test] fn decision_volume_average_includes_paused_zero_volume_days() { let mut paused = market_row("2025-01-03", 11.0, 0); diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index e411e89..212006e 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -28800,10 +28800,7 @@ mod tests { effective_turnover_ratio: None, extra_factors: BTreeMap::from([ ("adjustment_factor_backward1".to_string(), 1.0), - ( - "ma5_current_back_adjusted_close".to_string(), - current_close, - ), + ("ma5_current_back_adjusted_close".to_string(), current_close), ("avg_volume5_current".to_string(), current_volume), ]), },