perf(data): type adjustment factor snapshots

This commit is contained in:
boris
2026-09-07 17:53:36 +08:00
parent 9714c051c5
commit 04b45adf98
12 changed files with 371 additions and 52 deletions
+98 -28
View File
@@ -14,7 +14,7 @@ use crate::futures::FuturesTradingParameter;
use crate::instrument::Instrument;
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig};
const BACKWARD_ADJUSTMENT_FACTOR_FIELD: &str = "adjustment_factor_backward1";
pub(crate) const BACKWARD_ADJUSTMENT_FACTOR_FIELD: &str = "adjustment_factor_backward1";
mod date_format {
use chrono::NaiveDate;
@@ -96,6 +96,18 @@ pub enum DataSetError {
row_count: usize,
symbol_id_count: usize,
},
#[error("factor field {field} must use its typed column on {date} / {symbol}")]
ReservedTypedFactorInExtraMap {
date: NaiveDate,
symbol: String,
field: &'static str,
},
#[error("invalid backward adjustment factor {value} on {date} / {symbol}")]
InvalidBackwardAdjustmentFactor {
date: NaiveDate,
symbol: String,
value: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -200,6 +212,8 @@ pub struct DailyFactorSnapshot {
pub turnover_ratio: Option<f64>,
pub effective_turnover_ratio: Option<f64>,
#[serde(default)]
pub adjustment_factor_backward1: Option<f64>,
#[serde(default)]
pub extra_factors: NumericFactorMap,
}
@@ -659,12 +673,7 @@ impl AdjustedCloseSeries {
let factor = factor_rows
.get(factor_index)
.filter(|snapshot| snapshot.date == *date)
.and_then(|snapshot| {
snapshot
.extra_factors
.get(BACKWARD_ADJUSTMENT_FACTOR_FIELD)
.copied()
})
.and_then(|snapshot| snapshot.adjustment_factor_backward1)
.filter(|factor| factor.is_finite() && *factor > 0.0);
let back_adjusted_close = factor
.filter(|_| close.is_finite() && *close > 0.0)
@@ -1602,7 +1611,7 @@ impl DataSet {
|row| row.symbol.as_str(),
)?;
sort_rows_by_symbol_if_needed(&mut bundle.market, |row| row.symbol.as_str());
bundle.factors = normalize_factor_snapshots(bundle.factors);
bundle.factors = normalize_factor_snapshots(bundle.factors)?;
sort_rows_by_symbol_if_needed(&mut bundle.factors, |row| row.symbol.as_str());
sort_rows_by_symbol_if_needed(&mut bundle.candidates, |row| row.symbol.as_str());
if !bundle.market.is_empty() {
@@ -1764,7 +1773,7 @@ impl DataSet {
} else {
let mut market_by_date = group_by_date(market, |item| item.date);
sort_groups_by_symbol(&mut market_by_date, |item| item.symbol.as_str());
let factors = normalize_factor_snapshots(factors);
let factors = normalize_factor_snapshots(factors)?;
let mut factor_by_date = group_by_date(factors, |item| item.date);
sort_groups_by_symbol(&mut factor_by_date, |item| item.symbol.as_str());
let mut candidate_by_date = group_by_date(candidates, |item| item.date);
@@ -4230,6 +4239,7 @@ fn factor_numeric_value(snapshot: &DailyFactorSnapshot, field: &str) -> Option<f
"pe_ttm" => Some(snapshot.pe_ttm),
"turnover_ratio" => snapshot.turnover_ratio,
"effective_turnover_ratio" => snapshot.effective_turnover_ratio,
BACKWARD_ADJUSTMENT_FACTOR_FIELD => snapshot.adjustment_factor_backward1,
"ths_market_value_stock" | "ths_market_value_stock_bn" => snapshot
.extra_factors
.get(field.as_ref())
@@ -4342,10 +4352,31 @@ fn normalized_field(field: &str) -> Cow<'_, str> {
}
}
fn normalize_factor_snapshots(factors: Vec<DailyFactorSnapshot>) -> Vec<DailyFactorSnapshot> {
fn normalize_factor_snapshots(
factors: Vec<DailyFactorSnapshot>,
) -> Result<Vec<DailyFactorSnapshot>, DataSetError> {
factors
.into_iter()
.map(|mut snapshot| {
if snapshot
.extra_factors
.contains_key(BACKWARD_ADJUSTMENT_FACTOR_FIELD)
{
return Err(DataSetError::ReservedTypedFactorInExtraMap {
date: snapshot.date,
symbol: snapshot.symbol,
field: BACKWARD_ADJUSTMENT_FACTOR_FIELD,
});
}
if let Some(value) = snapshot.adjustment_factor_backward1
&& (!value.is_finite() || value <= 0.0)
{
return Err(DataSetError::InvalidBackwardAdjustmentFactor {
date: snapshot.date,
symbol: snapshot.symbol,
value,
});
}
let already_normalized = snapshot.extra_factors.iter().all(|(field, value)| {
let trimmed = field.as_ref().trim().trim_matches('"').trim_matches('\'');
!trimmed.is_empty()
@@ -4354,7 +4385,7 @@ fn normalize_factor_snapshots(factors: Vec<DailyFactorSnapshot>) -> Vec<DailyFac
&& value.is_finite()
});
if already_normalized {
return snapshot;
return Ok(snapshot);
}
snapshot.extra_factors = snapshot
.extra_factors
@@ -4372,7 +4403,7 @@ fn normalize_factor_snapshots(factors: Vec<DailyFactorSnapshot>) -> Vec<DailyFac
}
})
.collect();
snapshot
Ok(snapshot)
})
.collect()
}
@@ -5304,6 +5335,7 @@ mod tests {
pe_ttm: 0.0,
turnover_ratio: Some(0.02),
effective_turnover_ratio: Some(0.01),
adjustment_factor_backward1: None,
extra_factors: NumericFactorMap::from([(Cow::Borrowed("quality"), close)]),
};
let candidate_row = CandidateEligibility {
@@ -5487,6 +5519,7 @@ mod tests {
pe_ttm: 0.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: NumericFactorMap::new(),
};
let candidate = |symbol: &str| CandidateEligibility {
@@ -5720,10 +5753,8 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
extra_factors: NumericFactorMap::from([(
Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD),
1.0,
)]),
adjustment_factor_backward1: Some(1.0),
extra_factors: NumericFactorMap::new(),
})
.collect(),
Vec::new(),
@@ -5935,6 +5966,7 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: NumericFactorMap::new(),
})
.collect::<Vec<_>>();
@@ -6240,11 +6272,16 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: Some(1.25),
extra_factors: BTreeMap::from([("custom_factor".into(), 3.5)]),
};
assert_eq!(factor_numeric_value(&snapshot, " MARKET_CAP "), Some(12.5));
assert_eq!(factor_numeric_value(&snapshot, "CUSTOM_FACTOR"), Some(3.5));
assert_eq!(
factor_numeric_value(&snapshot, "ADJUSTMENT_FACTOR_BACKWARD1"),
Some(1.25)
);
}
#[test]
@@ -6258,8 +6295,10 @@ mod tests {
pe_ttm: 1.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: BTreeMap::from([(Cow::Borrowed("amount"), 10.0)]),
}]);
}])
.expect("normalize clean factor snapshot");
assert!(matches!(
clean[0].extra_factors.keys().next(),
Some(Cow::Borrowed("amount"))
@@ -6273,15 +6312,45 @@ mod tests {
pe_ttm: 1.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors: BTreeMap::from([
(Cow::Owned(" CUSTOM_FACTOR ".to_string()), 2.0),
(Cow::Borrowed("bad_nan"), f64::NAN),
]),
}]);
}])
.expect("normalize dirty factor snapshot");
assert_eq!(dirty[0].extra_factors.get("custom_factor"), Some(&2.0));
assert!(!dirty[0].extra_factors.contains_key("bad_nan"));
}
#[test]
fn factor_snapshot_rejects_legacy_or_invalid_adjustment_storage() {
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
let snapshot = |adjustment_factor_backward1, extra_factors| DailyFactorSnapshot {
date,
symbol: "000001.SZ".to_string(),
market_cap_bn: 1.0,
free_float_cap_bn: 1.0,
pe_ttm: 1.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1,
extra_factors,
};
assert!(matches!(
normalize_factor_snapshots(vec![snapshot(
Some(1.0),
BTreeMap::from([(Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD), 1.0)]),
)]),
Err(DataSetError::ReservedTypedFactorInExtraMap { .. })
));
assert!(matches!(
normalize_factor_snapshots(vec![snapshot(Some(0.0), BTreeMap::new())]),
Err(DataSetError::InvalidBackwardAdjustmentFactor { .. })
));
}
#[test]
fn symbol_price_series_test_constructor_sorts_unsorted_rows() {
let series = SymbolPriceSeries::new(
@@ -6374,6 +6443,7 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
adjustment_factor_backward1: None,
extra_factors,
}
})
@@ -6432,10 +6502,8 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
extra_factors: BTreeMap::from([(
Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD),
1.0,
)]),
adjustment_factor_backward1: Some(1.0),
extra_factors: BTreeMap::new(),
})
.collect(),
Vec::new(),
@@ -6728,7 +6796,8 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
extra_factors: BTreeMap::from([("adjustment_factor_backward1".into(), factor)]),
adjustment_factor_backward1: Some(factor),
extra_factors: BTreeMap::new(),
})
.collect(),
Vec::new(),
@@ -6825,11 +6894,8 @@ mod tests {
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".into(), 1.0)])
},
adjustment_factor_backward1: (*date != dates[3]).then_some(1.0),
extra_factors: BTreeMap::new(),
})
.collect(),
Vec::new(),
@@ -6931,6 +6997,7 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
};
let candidate = |symbol: &str| CandidateEligibility {
@@ -7029,6 +7096,7 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}],
Vec::new(),
@@ -7113,6 +7181,7 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
}],
vec![CandidateEligibility {
@@ -7164,6 +7233,7 @@ mod tests {
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
adjustment_factor_backward1: None,
extra_factors: BTreeMap::new(),
};