perf(data): type adjustment factor snapshots
This commit is contained in:
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user