Improve jq microcap execution semantics
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use indexmap::IndexMap;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::data::{DataSet, DataSetError, PriceField};
|
||||
@@ -124,19 +123,71 @@ impl Position {
|
||||
|
||||
self.average_cost = total_cost / self.quantity as f64;
|
||||
}
|
||||
|
||||
pub fn apply_cash_dividend(&mut self, dividend_per_share: f64) -> f64 {
|
||||
if self.quantity == 0 || !dividend_per_share.is_finite() || dividend_per_share == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
for lot in &mut self.lots {
|
||||
lot.price -= dividend_per_share;
|
||||
}
|
||||
self.average_cost -= dividend_per_share;
|
||||
self.last_price -= dividend_per_share;
|
||||
self.quantity as f64 * dividend_per_share
|
||||
}
|
||||
|
||||
pub fn apply_split_ratio(&mut self, ratio: f64) -> i32 {
|
||||
if self.quantity == 0 || !ratio.is_finite() || ratio <= 0.0 || (ratio - 1.0).abs() < 1e-9
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
let old_quantity = self.quantity;
|
||||
let mut scaled_lots = self
|
||||
.lots
|
||||
.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 = scaled_lots.iter().map(|lot| lot.quantity).sum::<u32>();
|
||||
if let Some(last_lot) = scaled_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);
|
||||
}
|
||||
}
|
||||
scaled_lots.retain(|lot| lot.quantity > 0);
|
||||
|
||||
self.lots = scaled_lots;
|
||||
self.quantity = self.lots.iter().map(|lot| lot.quantity).sum();
|
||||
self.last_price /= ratio;
|
||||
self.recalculate_average_cost();
|
||||
self.quantity as i32 - old_quantity as i32
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PortfolioState {
|
||||
cash: f64,
|
||||
positions: BTreeMap<String, Position>,
|
||||
positions: IndexMap<String, Position>,
|
||||
cash_receivables: Vec<CashReceivable>,
|
||||
}
|
||||
|
||||
impl PortfolioState {
|
||||
pub fn new(initial_cash: f64) -> Self {
|
||||
Self {
|
||||
cash: initial_cash,
|
||||
positions: BTreeMap::new(),
|
||||
positions: IndexMap::new(),
|
||||
cash_receivables: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +195,7 @@ impl PortfolioState {
|
||||
self.cash
|
||||
}
|
||||
|
||||
pub fn positions(&self) -> &BTreeMap<String, Position> {
|
||||
pub fn positions(&self) -> &IndexMap<String, Position> {
|
||||
&self.positions
|
||||
}
|
||||
|
||||
@@ -152,6 +203,10 @@ impl PortfolioState {
|
||||
self.positions.get(symbol)
|
||||
}
|
||||
|
||||
pub fn position_mut_if_exists(&mut self, symbol: &str) -> Option<&mut Position> {
|
||||
self.positions.get_mut(symbol)
|
||||
}
|
||||
|
||||
pub fn position_mut(&mut self, symbol: &str) -> &mut Position {
|
||||
self.positions
|
||||
.entry(symbol.to_string())
|
||||
@@ -166,6 +221,29 @@ impl PortfolioState {
|
||||
self.positions.retain(|_, position| !position.is_flat());
|
||||
}
|
||||
|
||||
pub fn add_cash_receivable(&mut self, receivable: CashReceivable) {
|
||||
self.cash_receivables.push(receivable);
|
||||
}
|
||||
|
||||
pub fn settle_cash_receivables(&mut self, date: NaiveDate) -> Vec<CashReceivable> {
|
||||
let mut settled = Vec::new();
|
||||
let mut pending = Vec::new();
|
||||
for receivable in self.cash_receivables.drain(..) {
|
||||
if receivable.payable_date <= date {
|
||||
self.cash += receivable.amount;
|
||||
settled.push(receivable);
|
||||
} else {
|
||||
pending.push(receivable);
|
||||
}
|
||||
}
|
||||
self.cash_receivables = pending;
|
||||
settled
|
||||
}
|
||||
|
||||
pub fn cash_receivables(&self) -> &[CashReceivable] {
|
||||
&self.cash_receivables
|
||||
}
|
||||
|
||||
pub fn update_prices(
|
||||
&mut self,
|
||||
date: NaiveDate,
|
||||
@@ -173,16 +251,17 @@ impl PortfolioState {
|
||||
field: PriceField,
|
||||
) -> Result<(), DataSetError> {
|
||||
for position in self.positions.values_mut() {
|
||||
let price = data
|
||||
.price(date, &position.symbol, field)
|
||||
.ok_or_else(|| DataSetError::MissingSnapshot {
|
||||
let price = data.price(date, &position.symbol, field).ok_or_else(|| {
|
||||
DataSetError::MissingSnapshot {
|
||||
kind: match field {
|
||||
PriceField::Open => "open price",
|
||||
PriceField::Close => "close price",
|
||||
PriceField::Last => "last price",
|
||||
},
|
||||
date,
|
||||
symbol: position.symbol.clone(),
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
position.last_price = price;
|
||||
}
|
||||
Ok(())
|
||||
@@ -214,6 +293,30 @@ impl PortfolioState {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn positions_preserve_insertion_order() {
|
||||
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let mut portfolio = PortfolioState::new(10_000.0);
|
||||
portfolio.position_mut("603657.SH").buy(date, 100, 10.0);
|
||||
portfolio.position_mut("001266.SZ").buy(date, 100, 10.0);
|
||||
portfolio.position_mut("601798.SH").buy(date, 100, 10.0);
|
||||
|
||||
let symbols = portfolio.positions().keys().cloned().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
symbols,
|
||||
vec![
|
||||
"603657.SH".to_string(),
|
||||
"001266.SZ".to_string(),
|
||||
"601798.SH".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct HoldingSummary {
|
||||
#[serde(with = "date_format")]
|
||||
@@ -227,6 +330,15 @@ pub struct HoldingSummary {
|
||||
pub realized_pnl: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CashReceivable {
|
||||
pub symbol: String,
|
||||
pub ex_date: NaiveDate,
|
||||
pub payable_date: NaiveDate,
|
||||
pub amount: f64,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
mod date_format {
|
||||
use chrono::NaiveDate;
|
||||
use serde::Serializer;
|
||||
@@ -240,3 +352,11 @@ mod date_format {
|
||||
serializer.serialize_str(&date.format(FORMAT).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn round_half_up_u32(value: f64) -> u32 {
|
||||
if !value.is_finite() || value <= 0.0 {
|
||||
0
|
||||
} else {
|
||||
value.round() as u32
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user