修正股票日线复权滚动因子口径

This commit is contained in:
boris
2026-07-17 09:47:47 +08:00
parent a734cbeaec
commit 8f098e4da1
2 changed files with 211 additions and 7 deletions
+203 -5
View File
@@ -486,6 +486,76 @@ struct SymbolPriceSeries {
volume_prefix: Vec<f64>, volume_prefix: Vec<f64>,
} }
#[derive(Debug, Clone)]
struct AdjustedCloseSeries {
dates: Vec<NaiveDate>,
backward_factors: Vec<f64>,
back_adjusted_closes: Vec<f64>,
}
impl AdjustedCloseSeries {
fn new(
market: &SymbolPriceSeries,
factor_by_date: &BTreeMap<NaiveDate, Vec<Arc<DailyFactorSnapshot>>>,
) -> Option<Self> {
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;
}
backward_factors.push(factor);
back_adjusted_closes.push(close * factor);
}
Some(Self {
dates: market.dates.clone(),
backward_factors,
back_adjusted_closes,
})
}
fn current_moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> {
if lookback == 0 {
return None;
}
let end = match self.dates.binary_search(&date) {
Ok(index) => index + 1,
Err(0) => return None,
Err(index) => index,
};
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 start = end - lookback;
let sum = self.back_adjusted_closes[start..end]
.iter()
.copied()
.sum::<f64>();
if !sum.is_finite() {
return None;
}
Some(normalize_rolling_factor(
sum / lookback as f64 / base_factor,
12,
))
}
}
impl SymbolPriceSeries { impl SymbolPriceSeries {
fn new<'a, I>(symbol: String, rows: I) -> Self fn new<'a, I>(symbol: String, rows: I) -> Self
where where
@@ -701,8 +771,14 @@ impl SymbolPriceSeries {
return None; return None;
} }
let start = end - lookback; let start = end - lookback;
let sum = self.volume_prefix[end] - self.volume_prefix[start]; let sum = self.volumes[start..end]
Some(sum / lookback as f64) .iter()
.map(|value| *value as f64)
.sum::<f64>();
if !sum.is_finite() {
return None;
}
Some(normalize_rolling_factor(sum / lookback as f64, 6))
} }
fn decision_volume_values(&self, date: NaiveDate, lookback: usize) -> Option<Vec<f64>> { fn decision_volume_values(&self, date: NaiveDate, lookback: usize) -> Option<Vec<f64>> {
@@ -948,6 +1024,7 @@ pub struct DataSet {
order_book_depth_index: HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>, order_book_depth_index: HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>,
benchmark_by_date: BTreeMap<NaiveDate, BenchmarkSnapshot>, benchmark_by_date: BTreeMap<NaiveDate, BenchmarkSnapshot>,
market_series_by_symbol: Arc<RwLock<HashMap<String, Arc<SymbolPriceSeries>>>>, market_series_by_symbol: Arc<RwLock<HashMap<String, Arc<SymbolPriceSeries>>>>,
adjusted_close_series_by_symbol: Arc<RwLock<HashMap<String, Arc<AdjustedCloseSeries>>>>,
benchmark_series_cache: BenchmarkPriceSeries, benchmark_series_cache: BenchmarkPriceSeries,
eligible_universe_by_date: Arc<OnceLock<BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>>>>, eligible_universe_by_date: Arc<OnceLock<BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>>>>,
benchmark_code: String, benchmark_code: String,
@@ -1158,6 +1235,7 @@ impl DataSet {
order_book_depth_index, order_book_depth_index,
benchmark_by_date, benchmark_by_date,
market_series_by_symbol: Arc::new(RwLock::new(HashMap::new())), market_series_by_symbol: Arc::new(RwLock::new(HashMap::new())),
adjusted_close_series_by_symbol: Arc::new(RwLock::new(HashMap::new())),
benchmark_series_cache, benchmark_series_cache,
eligible_universe_by_date: Arc::new(OnceLock::new()), eligible_universe_by_date: Arc::new(OnceLock::new()),
benchmark_code, benchmark_code,
@@ -1241,6 +1319,31 @@ impl DataSet {
) )
} }
fn adjusted_close_series(&self, symbol: &str) -> Option<Arc<AdjustedCloseSeries>> {
if let Some(series) = self
.adjusted_close_series_by_symbol
.read()
.expect("adjusted close series cache lock poisoned")
.get(symbol)
.cloned()
{
return Some(series);
}
let market = self.market_series(symbol)?;
let series = Arc::new(AdjustedCloseSeries::new(&market, &self.factor_by_date)?);
let mut cache = self
.adjusted_close_series_by_symbol
.write()
.expect("adjusted close series cache lock poisoned");
Some(
cache
.entry(symbol.to_string())
.or_insert_with(|| Arc::clone(&series))
.clone(),
)
}
pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> { pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> {
self.factor_by_date self.factor_by_date
.get(&date) .get(&date)
@@ -2308,9 +2411,9 @@ impl DataSet {
) -> Option<f64> { ) -> Option<f64> {
let field = normalize_field(field); let field = normalize_field(field);
match field.as_str() { match field.as_str() {
"close" | "prev_close" | "stock_close" | "price" => { "close" | "prev_close" | "stock_close" | "price" => self
self.market_moving_average(date, symbol, lookback, PriceField::Close) .adjusted_close_series(symbol)
} .and_then(|series| series.current_moving_average(date, lookback)),
"volume" | "stock_volume" => self "volume" | "stock_volume" => self
.market_series(symbol) .market_series(symbol)
.and_then(|series| series.current_volume_moving_average(date, lookback)) .and_then(|series| series.current_volume_moving_average(date, lookback))
@@ -2908,6 +3011,11 @@ fn prefix_sums(values: &[f64]) -> Vec<f64> {
prefix prefix
} }
fn normalize_rolling_factor(value: f64, decimals: i32) -> f64 {
let scale = 10_f64.powi(decimals);
(value * scale).round() / scale
}
mod optional_date_format { mod optional_date_format {
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::{self, Deserialize, Deserializer, Serializer}; use serde::{self, Deserialize, Deserializer, Serializer};
@@ -3307,6 +3415,96 @@ mod tests {
); );
} }
#[test]
fn current_close_average_uses_backward_adjustment_factor_and_current_base() {
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(),
];
let factors = [1.0, 1.0, 2.0];
let closes = [10.0, 11.0, 6.0];
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()
.zip(closes)
.map(|(date, close)| market_row(&date.format("%Y-%m-%d").to_string(), close, 100))
.collect(),
dates
.iter()
.zip(factors)
.map(|(date, factor)| 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: BTreeMap::from([(
"adjustment_factor_backward1".to_string(),
factor,
)]),
})
.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(5.5)
);
assert_ne!(
data.market_current_numeric_moving_average(dates[2], "000001.SZ", "close", 3),
data.market_moving_average(dates[2], "000001.SZ", 3, PriceField::Close)
);
}
#[test]
fn adjusted_close_average_normalization_prevents_strict_crossover_drift() {
let pattern = [
2.953, 1.093, 2.717, 1.579, 1.289, 1.236, 1.617, 2.632, 1.361, 2.163,
];
let start = NaiveDate::parse_from_str("2025-01-01", "%Y-%m-%d").unwrap();
let values = (0..30)
.map(|index| pattern[index % pattern.len()])
.collect::<Vec<_>>();
let series = AdjustedCloseSeries {
dates: (0..30)
.map(|index| start + chrono::Duration::days(index as i64))
.collect(),
backward_factors: vec![1.0; 30],
back_adjusted_closes: values,
};
let date = *series.dates.last().expect("last date");
assert_eq!(
series.current_moving_average(date, 10),
series.current_moving_average(date, 30)
);
}
#[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);
@@ -20916,7 +20916,10 @@ mod tests {
pe_ttm: 8.0, pe_ttm: 8.0,
turnover_ratio: Some(1.0), turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0), effective_turnover_ratio: Some(1.0),
extra_factors: BTreeMap::from([("Mixed_Factor".to_string(), index as f64 + 5.0)]), extra_factors: BTreeMap::from([
("Mixed_Factor".to_string(), index as f64 + 5.0),
("adjustment_factor_backward1".to_string(), 1.0),
]),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let candidate_rows = dates let candidate_rows = dates
@@ -30069,7 +30072,10 @@ mod tests {
pe_ttm: 8.0, pe_ttm: 8.0,
turnover_ratio: Some(22.0), turnover_ratio: Some(22.0),
effective_turnover_ratio: Some(18.0), effective_turnover_ratio: Some(18.0),
extra_factors: BTreeMap::new(), extra_factors: BTreeMap::from([(
"adjustment_factor_backward1".to_string(),
1.0,
)]),
}) })
.collect(), .collect(),
dates dates