|
|
|
@@ -96,6 +96,8 @@ pub struct Position {
|
|
|
|
|
// ALV-compatible moving average including buy costs; partial sells do not rebase it.
|
|
|
|
|
pub average_cost: f64,
|
|
|
|
|
pub last_price: f64,
|
|
|
|
|
last_market_mark_date: Option<NaiveDate>,
|
|
|
|
|
current_session_date: Option<NaiveDate>,
|
|
|
|
|
realized_pnl: FixedMoney,
|
|
|
|
|
realized_entry_pnl: FixedMoney,
|
|
|
|
|
pub trading_pnl: f64,
|
|
|
|
@@ -128,6 +130,8 @@ impl Position {
|
|
|
|
|
average_price: 0.0,
|
|
|
|
|
average_cost: 0.0,
|
|
|
|
|
last_price: 0.0,
|
|
|
|
|
last_market_mark_date: None,
|
|
|
|
|
current_session_date: None,
|
|
|
|
|
realized_pnl: FixedMoney::ZERO,
|
|
|
|
|
realized_entry_pnl: FixedMoney::ZERO,
|
|
|
|
|
trading_pnl: 0.0,
|
|
|
|
@@ -250,6 +254,7 @@ impl Position {
|
|
|
|
|
self.next_lot_sequence = self.next_lot_sequence.checked_add(1).expect("position lot sequence overflow");
|
|
|
|
|
self.quantity += quantity;
|
|
|
|
|
self.last_price = normalized_mark_price(mark_price, execution_price);
|
|
|
|
|
self.last_market_mark_date = Some(date);
|
|
|
|
|
self.day_trade_quantity_delta += quantity as i32;
|
|
|
|
|
self.day_buy_quantity += quantity;
|
|
|
|
|
self.day_buy_value = self
|
|
|
|
@@ -383,6 +388,7 @@ impl Position {
|
|
|
|
|
|
|
|
|
|
self.quantity -= quantity;
|
|
|
|
|
self.last_price = normalized_mark_price(mark_price, execution_price);
|
|
|
|
|
self.last_market_mark_date = self.current_session_date;
|
|
|
|
|
self.realized_pnl = self
|
|
|
|
|
.realized_pnl
|
|
|
|
|
.checked_add(realized)
|
|
|
|
@@ -806,6 +812,7 @@ pub struct PortfolioState {
|
|
|
|
|
closing_holdings: Arc<BTreeMap<NaiveDate, ClosingHoldings>>,
|
|
|
|
|
empty_simulation_origin: Option<NaiveDate>,
|
|
|
|
|
retired_lot_sequences: BTreeMap<String, u64>,
|
|
|
|
|
current_session_date: Option<NaiveDate>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct PortfolioCheckpoint {
|
|
|
|
@@ -930,8 +937,11 @@ impl PortfolioState {
|
|
|
|
|
if position.last_price > 0.0 && position.last_price.is_finite() {
|
|
|
|
|
// A purchase already marked on this ex-date has an actual ex-date
|
|
|
|
|
// quote. Earlier marks are translated to the accounting reference.
|
|
|
|
|
if !position.last_buy_date.is_some_and(|day| day >= action.date) && position.sold_quantity() == 0 {
|
|
|
|
|
if position.last_market_mark_date.is_some_and(|day| day < action.date) {
|
|
|
|
|
position.last_price /= ratio;
|
|
|
|
|
} else if position.last_market_mark_date.is_none() {
|
|
|
|
|
position.last_price = accounting_reference.filter(|price| price.is_finite() && *price > 0.0)
|
|
|
|
|
.ok_or("corporate_action_share_accounting_reference_missing")?;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
position.last_price = accounting_reference.filter(|price| price.is_finite() && *price > 0.0)
|
|
|
|
@@ -1045,6 +1055,7 @@ impl PortfolioState {
|
|
|
|
|
closing_holdings: self.closing_holdings.clone(),
|
|
|
|
|
empty_simulation_origin: self.empty_simulation_origin,
|
|
|
|
|
retired_lot_sequences: self.retired_lot_sequences.clone(),
|
|
|
|
|
current_session_date: self.current_session_date,
|
|
|
|
|
},
|
|
|
|
|
position_order: Some(self.positions.keys().cloned().collect()),
|
|
|
|
|
}
|
|
|
|
@@ -1074,6 +1085,7 @@ impl PortfolioState {
|
|
|
|
|
closing_holdings: Arc::new(BTreeMap::new()),
|
|
|
|
|
empty_simulation_origin: None,
|
|
|
|
|
retired_lot_sequences: BTreeMap::new(),
|
|
|
|
|
current_session_date: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1178,6 +1190,8 @@ impl PortfolioState {
|
|
|
|
|
for (symbol, position) in &mut replayed.positions {
|
|
|
|
|
if let Some(current) = self.positions.get(symbol).filter(|current| current.quantity > 0) {
|
|
|
|
|
position.last_price = current.last_price;
|
|
|
|
|
position.last_market_mark_date = current.last_market_mark_date;
|
|
|
|
|
position.current_session_date = current.current_session_date;
|
|
|
|
|
position.refresh_day_pnl();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1226,9 +1240,10 @@ impl PortfolioState {
|
|
|
|
|
|
|
|
|
|
pub fn position_mut(&mut self, symbol: &str) -> &mut Position {
|
|
|
|
|
let next = self.retired_lot_sequences.get(symbol).copied().unwrap_or(0);
|
|
|
|
|
let session = self.current_session_date;
|
|
|
|
|
self.positions
|
|
|
|
|
.entry(symbol.to_string())
|
|
|
|
|
.or_insert_with(|| { let mut position = Position::new(symbol); position.next_lot_sequence = next; position })
|
|
|
|
|
.or_insert_with(|| { let mut position = Position::new(symbol); position.next_lot_sequence = next; position.current_session_date = session; position })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn apply_cash_delta(&mut self, delta: f64) -> Result<(), String> {
|
|
|
|
@@ -1339,6 +1354,8 @@ impl PortfolioState {
|
|
|
|
|
}
|
|
|
|
|
position.day_trade_cost = next_cost;
|
|
|
|
|
position.refresh_day_pnl();
|
|
|
|
|
position.current_session_date = Some(trade_date);
|
|
|
|
|
position.last_market_mark_date = Some(trade_date);
|
|
|
|
|
self.positions.insert(symbol.to_string(), position);
|
|
|
|
|
self.cash = next_cash;
|
|
|
|
|
Ok(delta)
|
|
|
|
@@ -1625,6 +1642,12 @@ impl PortfolioState {
|
|
|
|
|
self.refresh_dividend_receivables();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn begin_trading_day_at(&mut self, date: NaiveDate) {
|
|
|
|
|
self.begin_trading_day();
|
|
|
|
|
self.current_session_date = Some(date);
|
|
|
|
|
for position in self.positions.values_mut() { position.current_session_date = Some(date); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_prices(
|
|
|
|
|
&mut self,
|
|
|
|
|
date: NaiveDate,
|
|
|
|
@@ -1686,6 +1709,7 @@ impl PortfolioState {
|
|
|
|
|
symbol: position.symbol.clone(),
|
|
|
|
|
})?;
|
|
|
|
|
position.last_price = price;
|
|
|
|
|
position.last_market_mark_date = Some(date);
|
|
|
|
|
position.refresh_day_pnl();
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|