修正滑点成交后的持仓估值

This commit is contained in:
boris
2026-06-14 02:09:44 +08:00
parent 0cfb7625bf
commit 80b34280c2
3 changed files with 88 additions and 21 deletions
+51 -14
View File
@@ -31,6 +31,7 @@ pub struct BrokerExecutionReport {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct ExecutionLeg { struct ExecutionLeg {
price: f64, price: f64,
mark_price: f64,
quantity: u32, quantity: u32,
} }
@@ -401,19 +402,37 @@ where
side: OrderSide, side: OrderSide,
quantity: Option<u32>, quantity: Option<u32>,
) -> f64 { ) -> f64 {
let raw_price = if self.execution_price_field == PriceField::Last let raw_price = self.snapshot_raw_execution_price(snapshot, side);
self.apply_slippage(snapshot, side, raw_price, quantity)
}
fn snapshot_raw_execution_price(
&self,
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
) -> f64 {
if self.execution_price_field == PriceField::Last
&& self.intraday_execution_start_time.is_some() && self.intraday_execution_start_time.is_some()
{ {
let _ = side; return snapshot.price(PriceField::Last);
snapshot.price(PriceField::Last) }
} else { match side {
match side { OrderSide::Buy => self.buy_price(snapshot),
OrderSide::Buy => self.buy_price(snapshot), OrderSide::Sell => self.sell_price(snapshot),
OrderSide::Sell => self.sell_price(snapshot), }
} }
};
self.apply_slippage(snapshot, side, raw_price, quantity) fn snapshot_mark_price(
&self,
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
) -> f64 {
let price = snapshot.price(self.execution_price_field);
if price.is_finite() && price > 0.0 {
price
} else {
self.snapshot_raw_execution_price(snapshot, side)
}
} }
fn is_open_auction_matching(&self) -> bool { fn is_open_auction_matching(&self) -> bool {
@@ -573,6 +592,14 @@ where
} }
} }
fn quote_mark_price(&self, quote: &IntradayExecutionQuote, fallback: f64) -> f64 {
if quote.last_price.is_finite() && quote.last_price > 0.0 {
quote.last_price
} else {
fallback
}
}
pub fn execute( pub fn execute(
&self, &self,
date: NaiveDate, date: NaiveDate,
@@ -2503,6 +2530,7 @@ where
fillable_qty, fillable_qty,
vec![ExecutionLeg { vec![ExecutionLeg {
price: execution_price, price: execution_price,
mark_price: self.snapshot_mark_price(snapshot, OrderSide::Sell),
quantity: fillable_qty, quantity: fillable_qty,
}], }],
) )
@@ -2588,7 +2616,7 @@ where
let net_cash = gross_amount - cost.total(); let net_cash = gross_amount - cost.total();
let realized_pnl = portfolio let realized_pnl = portfolio
.position_mut(symbol) .position_mut(symbol)
.sell(leg.quantity, leg.price) .sell_with_mark_price(leg.quantity, leg.price, leg.mark_price)
.map_err(BacktestError::Execution)?; .map_err(BacktestError::Execution)?;
if let Some(position) = portfolio.position_mut_if_exists(symbol) { if let Some(position) = portfolio.position_mut_if_exists(symbol) {
position.record_trade_cost(cost.total()); position.record_trade_cost(cost.total());
@@ -3909,6 +3937,7 @@ where
filled_qty, filled_qty,
vec![ExecutionLeg { vec![ExecutionLeg {
price: execution_price, price: execution_price,
mark_price: self.snapshot_mark_price(snapshot, OrderSide::Buy),
quantity: filled_qty, quantity: filled_qty,
}], }],
) )
@@ -3995,9 +4024,12 @@ where
let cash_out = gross_amount + cost.total(); let cash_out = gross_amount + cost.total();
portfolio.apply_cash_delta(-cash_out); portfolio.apply_cash_delta(-cash_out);
portfolio portfolio.position_mut(symbol).buy_with_mark_price(
.position_mut(symbol) date,
.buy(date, leg.quantity, leg.price); leg.quantity,
leg.price,
leg.mark_price,
);
if let Some(position) = portfolio.position_mut_if_exists(symbol) { if let Some(position) = portfolio.position_mut_if_exists(symbol) {
position.record_buy_trade_cost(leg.quantity, cost.total()); position.record_buy_trade_cost(leg.quantity, cost.total());
} }
@@ -4700,6 +4732,7 @@ where
}; };
let mut filled_qty = 0_u32; let mut filled_qty = 0_u32;
let mut gross_amount = 0.0_f64; let mut gross_amount = 0.0_f64;
let mut mark_amount = 0.0_f64;
let mut last_timestamp = None; let mut last_timestamp = None;
let mut legs = Vec::new(); let mut legs = Vec::new();
let mut budget_block_reason = None; let mut budget_block_reason = None;
@@ -4717,6 +4750,7 @@ where
else { else {
continue; continue;
}; };
let mark_price = self.quote_mark_price(quote, raw_quote_price);
let remaining_qty = requested_qty.saturating_sub(filled_qty); let remaining_qty = requested_qty.saturating_sub(filled_qty);
if remaining_qty == 0 { if remaining_qty == 0 {
break; break;
@@ -4815,10 +4849,12 @@ where
quote_price = self.execution_price_with_limit_slippage(quote_price, limit_price); quote_price = self.execution_price_with_limit_slippage(quote_price, limit_price);
gross_amount += quote_price * take_qty as f64; gross_amount += quote_price * take_qty as f64;
mark_amount += mark_price * take_qty as f64;
filled_qty += take_qty; filled_qty += take_qty;
last_timestamp = Some(quote.timestamp); last_timestamp = Some(quote.timestamp);
legs.push(ExecutionLeg { legs.push(ExecutionLeg {
price: quote_price, price: quote_price,
mark_price,
quantity: take_qty, quantity: take_qty,
}); });
@@ -4849,6 +4885,7 @@ where
legs: if matching_type == MatchingType::Vwap { legs: if matching_type == MatchingType::Vwap {
vec![ExecutionLeg { vec![ExecutionLeg {
price: gross_amount / filled_qty as f64, price: gross_amount / filled_qty as f64,
mark_price: mark_amount / filled_qty as f64,
quantity: filled_qty, quantity: filled_qty,
}] }]
} else { } else {
+34 -7
View File
@@ -66,6 +66,16 @@ impl Position {
} }
pub fn buy(&mut self, date: NaiveDate, quantity: u32, price: f64) { pub fn buy(&mut self, date: NaiveDate, quantity: u32, price: f64) {
self.buy_with_mark_price(date, quantity, price, price);
}
pub fn buy_with_mark_price(
&mut self,
date: NaiveDate,
quantity: u32,
execution_price: f64,
mark_price: f64,
) {
if quantity == 0 { if quantity == 0 {
return; return;
} }
@@ -73,19 +83,28 @@ impl Position {
self.lots.push(PositionLot { self.lots.push(PositionLot {
acquired_date: date, acquired_date: date,
quantity, quantity,
entry_price: price, entry_price: execution_price,
price, price: execution_price,
}); });
self.quantity += quantity; self.quantity += quantity;
self.last_price = price; self.last_price = normalized_mark_price(mark_price, execution_price);
self.day_trade_quantity_delta += quantity as i32; self.day_trade_quantity_delta += quantity as i32;
self.day_buy_quantity += quantity; self.day_buy_quantity += quantity;
self.day_buy_value += price * quantity as f64; self.day_buy_value += execution_price * quantity as f64;
self.recalculate_average_cost(); self.recalculate_average_cost();
self.refresh_day_pnl(); self.refresh_day_pnl();
} }
pub fn sell(&mut self, quantity: u32, price: f64) -> Result<f64, String> { pub fn sell(&mut self, quantity: u32, price: f64) -> Result<f64, String> {
self.sell_with_mark_price(quantity, price, price)
}
pub fn sell_with_mark_price(
&mut self,
quantity: u32,
execution_price: f64,
mark_price: f64,
) -> Result<f64, String> {
if quantity > self.quantity { if quantity > self.quantity {
return Err(format!( return Err(format!(
"sell quantity {} exceeds current quantity {} for {}", "sell quantity {} exceeds current quantity {} for {}",
@@ -102,7 +121,7 @@ impl Position {
}; };
let lot_sell = remaining.min(first_lot.quantity); let lot_sell = remaining.min(first_lot.quantity);
realized += (price - first_lot.price) * lot_sell as f64; realized += (execution_price - first_lot.price) * lot_sell as f64;
first_lot.quantity -= lot_sell; first_lot.quantity -= lot_sell;
remaining -= lot_sell; remaining -= lot_sell;
@@ -112,11 +131,11 @@ impl Position {
} }
self.quantity -= quantity; self.quantity -= quantity;
self.last_price = price; self.last_price = normalized_mark_price(mark_price, execution_price);
self.realized_pnl += realized; self.realized_pnl += realized;
self.day_trade_quantity_delta -= quantity as i32; self.day_trade_quantity_delta -= quantity as i32;
self.day_sell_quantity += quantity; self.day_sell_quantity += quantity;
self.day_sell_value += price * quantity as f64; self.day_sell_value += execution_price * quantity as f64;
self.recalculate_average_cost(); self.recalculate_average_cost();
self.refresh_day_pnl(); self.refresh_day_pnl();
Ok(realized) Ok(realized)
@@ -356,6 +375,14 @@ impl Position {
} }
} }
fn normalized_mark_price(mark_price: f64, fallback: f64) -> f64 {
if mark_price.is_finite() && mark_price > 0.0 {
mark_price
} else {
fallback
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PortfolioState { pub struct PortfolioState {
initial_cash: f64, initial_cash: f64,
@@ -1701,6 +1701,9 @@ fn broker_applies_tick_size_slippage_on_intraday_last_fills() {
assert_eq!(report.fill_events.len(), 1); assert_eq!(report.fill_events.len(), 1);
assert!((report.fill_events[0].price - 10.02).abs() < 1e-9); assert!((report.fill_events[0].price - 10.02).abs() < 1e-9);
let position = portfolio.position("000002.SZ").expect("position");
assert!((position.last_price - 10.0).abs() < 1e-9);
assert!((position.market_value() - position.quantity as f64 * 10.0).abs() < 1e-6);
} }
#[test] #[test]