修正股票日线复权滚动因子口径
This commit is contained in:
@@ -486,6 +486,76 @@ struct SymbolPriceSeries {
|
||||
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 {
|
||||
fn new<'a, I>(symbol: String, rows: I) -> Self
|
||||
where
|
||||
@@ -701,8 +771,14 @@ impl SymbolPriceSeries {
|
||||
return None;
|
||||
}
|
||||
let start = end - lookback;
|
||||
let sum = self.volume_prefix[end] - self.volume_prefix[start];
|
||||
Some(sum / lookback as f64)
|
||||
let sum = self.volumes[start..end]
|
||||
.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>> {
|
||||
@@ -948,6 +1024,7 @@ pub struct DataSet {
|
||||
order_book_depth_index: HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>,
|
||||
benchmark_by_date: BTreeMap<NaiveDate, BenchmarkSnapshot>,
|
||||
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,
|
||||
eligible_universe_by_date: Arc<OnceLock<BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>>>>,
|
||||
benchmark_code: String,
|
||||
@@ -1158,6 +1235,7 @@ impl DataSet {
|
||||
order_book_depth_index,
|
||||
benchmark_by_date,
|
||||
market_series_by_symbol: Arc::new(RwLock::new(HashMap::new())),
|
||||
adjusted_close_series_by_symbol: Arc::new(RwLock::new(HashMap::new())),
|
||||
benchmark_series_cache,
|
||||
eligible_universe_by_date: Arc::new(OnceLock::new()),
|
||||
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> {
|
||||
self.factor_by_date
|
||||
.get(&date)
|
||||
@@ -2308,9 +2411,9 @@ impl DataSet {
|
||||
) -> Option<f64> {
|
||||
let field = normalize_field(field);
|
||||
match field.as_str() {
|
||||
"close" | "prev_close" | "stock_close" | "price" => {
|
||||
self.market_moving_average(date, symbol, lookback, PriceField::Close)
|
||||
}
|
||||
"close" | "prev_close" | "stock_close" | "price" => self
|
||||
.adjusted_close_series(symbol)
|
||||
.and_then(|series| series.current_moving_average(date, lookback)),
|
||||
"volume" | "stock_volume" => self
|
||||
.market_series(symbol)
|
||||
.and_then(|series| series.current_volume_moving_average(date, lookback))
|
||||
@@ -2908,6 +3011,11 @@ fn prefix_sums(values: &[f64]) -> Vec<f64> {
|
||||
prefix
|
||||
}
|
||||
|
||||
fn normalize_rolling_factor(value: f64, decimals: i32) -> f64 {
|
||||
let scale = 10_f64.powi(decimals);
|
||||
(value * scale).round() / scale
|
||||
}
|
||||
|
||||
mod optional_date_format {
|
||||
use chrono::NaiveDate;
|
||||
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]
|
||||
fn decision_volume_average_includes_paused_zero_volume_days() {
|
||||
let mut paused = market_row("2025-01-03", 11.0, 0);
|
||||
|
||||
Reference in New Issue
Block a user