修正股票日线复权滚动因子口径
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user