修正滑点成交后的持仓估值
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user