建立手工成交观察合同与原子回放游标

This commit is contained in:
boris
2026-09-14 07:30:19 +08:00
parent 8e7ae69b0b
commit 5e11f3da22
6 changed files with 1126 additions and 9 deletions
+129 -9
View File
@@ -138,18 +138,28 @@ impl Position {
if quantity == 0 {
return;
}
let gross_amount = fixed_money_or_panic(execution_price * quantity as f64, "position buy gross amount");
self.buy_with_fixed_gross(date,quantity,execution_price,mark_price,gross_amount);
}
fn buy_with_fixed_gross(
&mut self,
date: NaiveDate,
quantity: u32,
execution_price: f64,
mark_price: f64,
gross_amount: FixedMoney,
) {
let previous_quantity = self.quantity;
self.last_buy_date = Some(self.last_buy_date.map_or(date, |previous| previous.max(date)));
self.last_buy_date = Some(
self.last_buy_date
.map_or(date, |previous| previous.max(date)),
);
if previous_quantity == 0 {
self.opened_date = Some(date);
}
let previous_average_price = self.average_price;
let previous_average_cost = self.average_cost;
let gross_amount = fixed_money_or_panic(
execution_price * quantity as f64,
"position buy gross amount",
);
self.lots.push(PositionLot {
acquired_date: date,
quantity,
@@ -200,6 +210,20 @@ impl Position {
quantity: u32,
execution_price: f64,
mark_price: f64,
) -> Result<f64, String> {
if quantity > self.quantity {
return Err(format!("sell quantity {} exceeds current quantity {} for {}",quantity,self.quantity,self.symbol));
}
let total_proceeds = fixed_money(execution_price * quantity as f64,"position sell gross amount")?;
self.sell_with_fixed_gross(quantity,execution_price,mark_price,total_proceeds)
}
fn sell_with_fixed_gross(
&mut self,
quantity: u32,
execution_price: f64,
mark_price: f64,
total_proceeds: FixedMoney,
) -> Result<f64, String> {
if quantity > self.quantity {
return Err(format!(
@@ -208,10 +232,6 @@ impl Position {
));
}
let total_proceeds = fixed_money(
execution_price * quantity as f64,
"position sell gross amount",
)?;
let mut remaining = quantity;
let mut remaining_proceeds = total_proceeds;
let mut realized = FixedMoney::ZERO;
@@ -796,6 +816,106 @@ impl PortfolioState {
Ok(())
}
/// Apply one fully observed external fill atomically. Its money is already
/// quantized from the original decimal amounts, not from a float product.
pub(crate) fn apply_observed_manual_fill(
&mut self,
trade_date: NaiveDate,
symbol: &str,
side: crate::events::OrderSide,
quantity: u32,
price: f64,
mark_price: f64,
gross: FixedMoney,
fees: FixedMoney,
) -> Result<FixedMoney, String> {
use crate::events::OrderSide;
if symbol.trim().is_empty()
|| quantity == 0
|| quantity > i32::MAX as u32
|| !price.is_finite()
|| price <= 0.
|| !mark_price.is_finite()
|| mark_price <= 0.
|| gross <= FixedMoney::ZERO
|| fees < FixedMoney::ZERO
{
return Err("invalid observed manual fill".into());
}
let mut position = self
.positions
.get(symbol)
.cloned()
.unwrap_or_else(|| Position::new(symbol));
let delta = match side {
OrderSide::Buy => gross.checked_add(fees).and_then(FixedMoney::checked_neg),
OrderSide::Sell => gross.checked_sub(fees),
}
.ok_or("manual fill cash delta overflow")?;
let next_cash = self
.cash
.checked_add(delta)
.filter(|cash| *cash >= FixedMoney::ZERO)
.ok_or("manual fill disagrees with shadow available cash")?;
let next_cost = position
.day_trade_cost
.checked_add(fees)
.ok_or("manual trade cost overflow")?;
match side {
OrderSide::Buy => {
let total_quantity = position
.quantity
.checked_add(quantity)
.ok_or("manual position quantity overflow")?;
FixedMoney::from_f64(mark_price * f64::from(total_quantity))
.ok_or("manual marked position value overflow")?;
position
.day_buy_quantity
.checked_add(quantity)
.ok_or("manual daily buy quantity overflow")?;
position
.day_trade_quantity_delta
.checked_add(quantity as i32)
.ok_or("manual daily quantity delta overflow")?;
position
.day_buy_value
.checked_add(gross)
.ok_or("manual daily buy value overflow")?;
let total_basis = gross.checked_add(fees).ok_or("manual lot basis overflow")?;
position
.total_cost_basis()
.checked_add(total_basis)
.ok_or("manual aggregate position basis overflow")?;
position.buy_with_fixed_gross(trade_date, quantity, price, mark_price, gross);
position
.lots
.last_mut()
.ok_or("manual buy produced no lot")?
.cost_basis = total_basis;
position.average_cost += fees.to_f64() / f64::from(position.quantity);
}
OrderSide::Sell => {
if quantity > position.sellable_qty(trade_date) {
return Err("manual fill disagrees with shadow sellable holdings or T+1".into());
}
position
.day_sell_quantity
.checked_add(quantity)
.ok_or("manual daily sell quantity overflow")?;
position
.day_trade_quantity_delta
.checked_sub(quantity as i32)
.ok_or("manual daily quantity delta overflow")?;
position.sell_with_fixed_gross(quantity, price, mark_price, gross)?;
}
}
position.day_trade_cost = next_cost;
position.refresh_day_pnl();
self.positions.insert(symbol.to_string(), position);
self.cash = next_cash;
Ok(delta)
}
pub fn prune_flat_positions(&mut self) {
let mut sold_symbols = Vec::new();
self.positions.retain(|symbol, position| {