Support successor conversions for delisted holdings
This commit is contained in:
@@ -181,6 +181,17 @@ pub struct PortfolioState {
|
||||
cash_receivables: Vec<CashReceivable>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SuccessorConversionOutcome {
|
||||
pub old_symbol: String,
|
||||
pub new_symbol: String,
|
||||
pub old_quantity: u32,
|
||||
pub new_quantity_delta: i32,
|
||||
pub new_quantity_after: u32,
|
||||
pub new_average_cost_after: f64,
|
||||
pub cash_delta: f64,
|
||||
}
|
||||
|
||||
impl PortfolioState {
|
||||
pub fn new(initial_cash: f64) -> Self {
|
||||
Self {
|
||||
@@ -290,6 +301,80 @@ impl PortfolioState {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn apply_successor_conversion(
|
||||
&mut self,
|
||||
old_symbol: &str,
|
||||
new_symbol: &str,
|
||||
ratio: f64,
|
||||
cash_per_old_share: f64,
|
||||
) -> Option<SuccessorConversionOutcome> {
|
||||
if !ratio.is_finite() || ratio <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let old_symbol_owned = old_symbol.to_string();
|
||||
let old_position = self.positions.shift_remove(old_symbol)?;
|
||||
if old_position.quantity == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let old_quantity = old_position.quantity;
|
||||
let last_price = old_position.last_price;
|
||||
let realized_pnl = old_position.realized_pnl;
|
||||
let mut converted_lots = old_position
|
||||
.lots
|
||||
.into_iter()
|
||||
.map(|lot| PositionLot {
|
||||
acquired_date: lot.acquired_date,
|
||||
quantity: round_half_up_u32(lot.quantity as f64 * ratio),
|
||||
price: lot.price / ratio,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let expected_total = round_half_up_u32(old_quantity as f64 * ratio);
|
||||
let scaled_total = converted_lots.iter().map(|lot| lot.quantity).sum::<u32>();
|
||||
if let Some(last_lot) = converted_lots.last_mut() {
|
||||
if scaled_total < expected_total {
|
||||
last_lot.quantity += expected_total - scaled_total;
|
||||
} else if scaled_total > expected_total {
|
||||
last_lot.quantity = last_lot
|
||||
.quantity
|
||||
.saturating_sub(scaled_total - expected_total);
|
||||
}
|
||||
}
|
||||
converted_lots.retain(|lot| lot.quantity > 0);
|
||||
let converted_quantity = converted_lots.iter().map(|lot| lot.quantity).sum::<u32>();
|
||||
let converted_last_price = if last_price > 0.0 {
|
||||
last_price / ratio
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let successor = self
|
||||
.positions
|
||||
.entry(new_symbol.to_string())
|
||||
.or_insert_with(|| Position::new(new_symbol));
|
||||
successor.lots.extend(converted_lots);
|
||||
successor.quantity = successor.lots.iter().map(|lot| lot.quantity).sum();
|
||||
successor.realized_pnl += realized_pnl;
|
||||
if converted_last_price > 0.0 {
|
||||
successor.last_price = converted_last_price;
|
||||
}
|
||||
successor.recalculate_average_cost();
|
||||
|
||||
Some(SuccessorConversionOutcome {
|
||||
old_symbol: old_symbol_owned,
|
||||
new_symbol: new_symbol.to_string(),
|
||||
old_quantity,
|
||||
new_quantity_delta: converted_quantity as i32,
|
||||
new_quantity_after: successor.quantity,
|
||||
new_average_cost_after: successor.average_cost,
|
||||
cash_delta: if cash_per_old_share.is_finite() {
|
||||
old_quantity as f64 * cash_per_old_share
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user