Support successor conversions for delisted holdings

This commit is contained in:
boris
2026-04-22 22:13:32 -07:00
parent 6606ef86bc
commit 32e29fdf9a
4 changed files with 447 additions and 33 deletions

View File

@@ -231,6 +231,12 @@ pub struct CorporateAction {
pub issue_price: f64,
pub reform: bool,
pub adjust_factor: Option<f64>,
#[serde(default)]
pub successor_symbol: Option<String>,
#[serde(default)]
pub successor_ratio: Option<f64>,
#[serde(default)]
pub successor_cash: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -284,6 +290,26 @@ impl CorporateAction {
|| (self.split_ratio() - 1.0).abs() > f64::EPSILON
|| self.issue_quantity.abs() > f64::EPSILON
|| self.reform
|| self.has_successor_conversion()
}
pub fn has_successor_conversion(&self) -> bool {
self.successor_symbol
.as_ref()
.is_some_and(|symbol| !symbol.trim().is_empty())
&& self.successor_ratio_value() > 0.0
}
pub fn successor_ratio_value(&self) -> f64 {
self.successor_ratio
.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
.unwrap_or(1.0)
}
pub fn successor_cash_value(&self) -> f64 {
self.successor_cash
.filter(|cash| cash.is_finite())
.unwrap_or(0.0)
}
}
@@ -329,7 +355,10 @@ impl SymbolPriceSeries {
let closes = sorted.iter().map(|row| row.close).collect::<Vec<_>>();
let prev_closes = sorted.iter().map(|row| row.prev_close).collect::<Vec<_>>();
let last_prices = sorted.iter().map(|row| row.last_price).collect::<Vec<_>>();
let volumes = sorted.iter().map(|row| row.volume as f64).collect::<Vec<_>>();
let volumes = sorted
.iter()
.map(|row| row.volume as f64)
.collect::<Vec<_>>();
let open_prefix = prefix_sums(&opens);
let close_prefix = prefix_sums(&closes);
let prev_close_prefix = prefix_sums(&prev_closes);
@@ -513,7 +542,12 @@ impl BenchmarkPriceSeries {
self.moving_average_for(date, lookback, PriceField::Close)
}
fn moving_average_for(&self, date: NaiveDate, lookback: usize, field: PriceField) -> Option<f64> {
fn moving_average_for(
&self,
date: NaiveDate,
lookback: usize,
field: PriceField,
) -> Option<f64> {
if lookback == 0 {
return None;
}
@@ -875,12 +909,7 @@ impl DataSet {
.and_then(|series| series.decision_volume_moving_average(date, lookback))
}
pub fn factor_numeric_value(
&self,
date: NaiveDate,
symbol: &str,
field: &str,
) -> Option<f64> {
pub fn factor_numeric_value(&self, date: NaiveDate, symbol: &str, field: &str) -> Option<f64> {
self.factor(date, symbol)
.and_then(|snapshot| factor_numeric_value(snapshot, field))
}
@@ -922,16 +951,14 @@ impl DataSet {
lookback: usize,
) -> Option<f64> {
match field {
"close" | "prev_close" | "stock_close" | "price" => {
self.market_series_by_symbol
.get(symbol)
.and_then(|series| series.decision_close_rolling_average(date, lookback))
}
"volume" | "stock_volume" => {
self.market_series_by_symbol
.get(symbol)
.and_then(|series| series.decision_volume_rolling_average(date, lookback))
}
"close" | "prev_close" | "stock_close" | "price" => self
.market_series_by_symbol
.get(symbol)
.and_then(|series| series.decision_close_rolling_average(date, lookback)),
"volume" | "stock_volume" => self
.market_series_by_symbol
.get(symbol)
.and_then(|series| series.decision_volume_rolling_average(date, lookback)),
"open" => self.market_moving_average(date, symbol, lookback, PriceField::Open),
"last" | "last_price" => {
self.market_moving_average(date, symbol, lookback, PriceField::Last)
@@ -1178,6 +1205,13 @@ fn read_corporate_actions(path: &Path) -> Result<Vec<CorporateAction>, DataSetEr
issue_price: row.parse_optional_f64(6 + offset).unwrap_or(0.0),
reform: row.parse_optional_bool(7 + offset).unwrap_or(false),
adjust_factor: row.parse_optional_f64(8 + offset),
successor_symbol: row
.fields
.get(9 + offset)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty()),
successor_ratio: row.parse_optional_f64(10 + offset),
successor_cash: row.parse_optional_f64(11 + offset),
});
}
Ok(snapshots)