建立手工成交观察合同与原子回放游标
This commit is contained in:
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user