修正复权序列未来数据污染
This commit is contained in:
@@ -491,8 +491,8 @@ struct SymbolPriceSeries {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct AdjustedCloseSeries {
|
struct AdjustedCloseSeries {
|
||||||
dates: Vec<NaiveDate>,
|
dates: Vec<NaiveDate>,
|
||||||
backward_factors: Vec<f64>,
|
backward_factors: Vec<Option<f64>>,
|
||||||
back_adjusted_closes: Vec<f64>,
|
back_adjusted_closes: Vec<Option<f64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdjustedCloseSeries {
|
impl AdjustedCloseSeries {
|
||||||
@@ -503,22 +503,18 @@ impl AdjustedCloseSeries {
|
|||||||
let mut backward_factors = Vec::with_capacity(market.dates.len());
|
let mut backward_factors = Vec::with_capacity(market.dates.len());
|
||||||
let mut back_adjusted_closes = 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) {
|
for (date, close) in market.dates.iter().zip(&market.closes) {
|
||||||
if !close.is_finite() || *close <= 0.0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let factor = factor_by_date
|
let factor = factor_by_date
|
||||||
.get(date)
|
.get(date)
|
||||||
.and_then(|rows| {
|
.and_then(|rows| {
|
||||||
find_arc_by_symbol(rows, &market.symbol, |row| row.symbol.as_str())
|
find_arc_by_symbol(rows, &market.symbol, |row| row.symbol.as_str())
|
||||||
})
|
})
|
||||||
.and_then(|snapshot| {
|
.and_then(|snapshot| factor_numeric_value(snapshot, "adjustment_factor_backward1"))
|
||||||
factor_numeric_value(snapshot, "adjustment_factor_backward1")
|
.filter(|factor| factor.is_finite() && *factor > 0.0);
|
||||||
})?;
|
let back_adjusted_close = factor
|
||||||
if !factor.is_finite() || factor <= 0.0 {
|
.filter(|_| close.is_finite() && *close > 0.0)
|
||||||
return None;
|
.map(|factor| close * factor);
|
||||||
}
|
|
||||||
backward_factors.push(factor);
|
backward_factors.push(factor);
|
||||||
back_adjusted_closes.push(close * factor);
|
back_adjusted_closes.push(back_adjusted_close);
|
||||||
}
|
}
|
||||||
Some(Self {
|
Some(Self {
|
||||||
dates: market.dates.clone(),
|
dates: market.dates.clone(),
|
||||||
@@ -539,15 +535,12 @@ impl AdjustedCloseSeries {
|
|||||||
if end < lookback {
|
if end < lookback {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let base_factor = *self.backward_factors.get(end - 1)?;
|
let base_factor = self.backward_factors.get(end - 1).copied().flatten()?;
|
||||||
if !base_factor.is_finite() || base_factor <= 0.0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let start = end - lookback;
|
let start = end - lookback;
|
||||||
let sum = self.back_adjusted_closes[start..end]
|
let sum = self.back_adjusted_closes[start..end]
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.sum::<f64>();
|
.try_fold(0.0, |sum, value| value.map(|value| sum + value))?;
|
||||||
if !sum.is_finite() {
|
if !sum.is_finite() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -566,6 +559,7 @@ impl AdjustedCloseSeries {
|
|||||||
self.back_adjusted_closes
|
self.back_adjusted_closes
|
||||||
.get(index)
|
.get(index)
|
||||||
.copied()
|
.copied()
|
||||||
|
.flatten()
|
||||||
.filter(|value| value.is_finite() && *value > 0.0)
|
.filter(|value| value.is_finite() && *value > 0.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3710,8 +3704,8 @@ mod tests {
|
|||||||
dates: (0..30)
|
dates: (0..30)
|
||||||
.map(|index| start + chrono::Duration::days(index as i64))
|
.map(|index| start + chrono::Duration::days(index as i64))
|
||||||
.collect(),
|
.collect(),
|
||||||
backward_factors: vec![1.0; 30],
|
backward_factors: vec![Some(1.0); 30],
|
||||||
back_adjusted_closes: values,
|
back_adjusted_closes: values.into_iter().map(Some).collect(),
|
||||||
};
|
};
|
||||||
let date = *series.dates.last().expect("last date");
|
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]
|
#[test]
|
||||||
fn decision_volume_average_includes_paused_zero_volume_days() {
|
fn decision_volume_average_includes_paused_zero_volume_days() {
|
||||||
let mut paused = market_row("2025-01-03", 11.0, 0);
|
let mut paused = market_row("2025-01-03", 11.0, 0);
|
||||||
|
|||||||
@@ -28800,10 +28800,7 @@ mod tests {
|
|||||||
effective_turnover_ratio: None,
|
effective_turnover_ratio: None,
|
||||||
extra_factors: BTreeMap::from([
|
extra_factors: BTreeMap::from([
|
||||||
("adjustment_factor_backward1".to_string(), 1.0),
|
("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),
|
("avg_volume5_current".to_string(), current_volume),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user