refactor: isolate historical slippage calibration and propagate pricing errors
This commit is contained in:
+137
-74
@@ -300,37 +300,63 @@ impl DynamicSlippageConfig {
|
||||
|
||||
pub(crate) fn ratio(
|
||||
&self,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
raw_price: f64,
|
||||
calibration: &HistoricalSlippageCalibration,
|
||||
order_value: Option<f64>,
|
||||
) -> f64 {
|
||||
let daily_amount = (snapshot.volume as f64 * raw_price).max(0.0);
|
||||
let impact_ratio = match order_value {
|
||||
Some(value) if value.is_finite() && value > 0.0 && daily_amount > 0.0 => {
|
||||
value / daily_amount
|
||||
Some(value) if value.is_finite() && value > 0.0 => {
|
||||
value / calibration.turnover_proxy
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
let volatility_base = if snapshot.prev_close.is_finite() && snapshot.prev_close > 0.0 {
|
||||
snapshot.prev_close
|
||||
} else {
|
||||
raw_price
|
||||
};
|
||||
let volatility = if snapshot.high.is_finite()
|
||||
&& snapshot.low.is_finite()
|
||||
&& volatility_base.is_finite()
|
||||
&& volatility_base > 0.0
|
||||
{
|
||||
((snapshot.high - snapshot.low).abs() / volatility_base).max(0.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let ratio =
|
||||
impact_ratio * self.impact_coefficient + volatility * self.volatility_coefficient;
|
||||
let ratio = impact_ratio * self.impact_coefficient
|
||||
+ calibration.range_ratio * self.volatility_coefficient;
|
||||
ratio.clamp(0.0, self.max_ratio)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct HistoricalSlippageCalibration {
|
||||
source_date: NaiveDate,
|
||||
turnover_proxy: f64,
|
||||
range_ratio: f64,
|
||||
}
|
||||
|
||||
impl HistoricalSlippageCalibration {
|
||||
pub(crate) fn for_execution(data: &DataSet, date: NaiveDate, symbol: &str) -> Result<Self, BacktestError> {
|
||||
let missing = || BacktestError::Execution(format!(
|
||||
"historical_slippage_calibration_missing symbol={symbol} execution_date={date} policy=previous_completed_session"
|
||||
));
|
||||
let previous_date = data.previous_trading_date(date, 1).ok_or_else(missing)?;
|
||||
let row = data.market(previous_date, symbol).ok_or_else(missing)?;
|
||||
Self::from_completed_snapshot(row, date)
|
||||
}
|
||||
|
||||
fn from_completed_snapshot(
|
||||
row: &crate::data::DailyMarketSnapshot,
|
||||
execution_date: NaiveDate,
|
||||
) -> Result<Self, BacktestError> {
|
||||
let turnover_proxy = row.volume as f64 * row.close;
|
||||
let range_ratio = (row.high - row.low) / row.prev_close;
|
||||
if row.date >= execution_date
|
||||
|| [row.high, row.low, row.close, row.prev_close, turnover_proxy]
|
||||
.into_iter().any(|value| !value.is_finite() || value <= 0.0)
|
||||
|| row.high < row.low
|
||||
|| !range_ratio.is_finite()
|
||||
{
|
||||
return Err(BacktestError::Execution(format!(
|
||||
"historical_slippage_calibration_invalid symbol={} source_date={} execution_date={} volume={} high={} low={} close={} prev_close={}",
|
||||
row.symbol, row.date, execution_date, row.volume, row.high, row.low, row.close, row.prev_close,
|
||||
)));
|
||||
}
|
||||
Ok(Self {
|
||||
source_date: row.date,
|
||||
turnover_proxy,
|
||||
range_ratio,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DynamicSlippageConfig {
|
||||
fn default() -> Self {
|
||||
Self::new(0.5, 0.3, 0.01)
|
||||
@@ -343,7 +369,7 @@ pub enum SlippageModel {
|
||||
PriceRatio(f64),
|
||||
TickSize(f64),
|
||||
LimitPrice,
|
||||
Dynamic(DynamicSlippageConfig),
|
||||
HistoricalVolumeVolatility(DynamicSlippageConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -1109,12 +1135,28 @@ where
|
||||
|
||||
fn snapshot_execution_price(
|
||||
&self,
|
||||
data: &DataSet,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
side: OrderSide,
|
||||
quantity: Option<u32>,
|
||||
) -> f64 {
|
||||
) -> Result<f64, BacktestError> {
|
||||
let raw_price = self.snapshot_raw_execution_price(snapshot, side);
|
||||
self.apply_slippage(snapshot, side, raw_price, quantity)
|
||||
let calibration = self.slippage_calibration(data, snapshot)?;
|
||||
self.apply_slippage(snapshot, side, raw_price, quantity, calibration.as_ref())
|
||||
}
|
||||
|
||||
fn slippage_calibration(
|
||||
&self,
|
||||
data: &DataSet,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
) -> Result<Option<HistoricalSlippageCalibration>, BacktestError> {
|
||||
if !matches!(self.slippage_model, SlippageModel::HistoricalVolumeVolatility(_))
|
||||
|| self.is_open_auction_matching()
|
||||
|| self.is_post_close_fixed_price(snapshot.date)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
HistoricalSlippageCalibration::for_execution(data, snapshot.date, &snapshot.symbol).map(Some)
|
||||
}
|
||||
|
||||
fn snapshot_raw_execution_price(
|
||||
@@ -1184,17 +1226,18 @@ where
|
||||
side: OrderSide,
|
||||
raw_price: f64,
|
||||
quantity: Option<u32>,
|
||||
) -> f64 {
|
||||
calibration: Option<&HistoricalSlippageCalibration>,
|
||||
) -> Result<f64, BacktestError> {
|
||||
if !raw_price.is_finite() || raw_price <= 0.0 {
|
||||
return raw_price;
|
||||
return Ok(raw_price);
|
||||
}
|
||||
|
||||
if self.is_open_auction_matching() {
|
||||
return self.clamp_execution_price(snapshot, side, raw_price);
|
||||
return Ok(self.clamp_execution_price(snapshot, side, raw_price));
|
||||
}
|
||||
|
||||
if self.is_post_close_fixed_price(snapshot.date) {
|
||||
return self.clamp_execution_price(snapshot, side, raw_price);
|
||||
return Ok(self.clamp_execution_price(snapshot, side, raw_price));
|
||||
}
|
||||
|
||||
let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64));
|
||||
@@ -1216,8 +1259,12 @@ where
|
||||
}
|
||||
}
|
||||
SlippageModel::LimitPrice => raw_price,
|
||||
SlippageModel::Dynamic(config) => {
|
||||
let ratio = config.ratio(snapshot, raw_price, order_value);
|
||||
SlippageModel::HistoricalVolumeVolatility(config) => {
|
||||
let calibration = calibration.filter(|value| value.source_date < snapshot.date)
|
||||
.ok_or_else(|| BacktestError::Execution(format!(
|
||||
"historical_slippage_calibration_required symbol={} execution_date={}", snapshot.symbol, snapshot.date,
|
||||
)))?;
|
||||
let ratio = config.ratio(calibration, order_value);
|
||||
match side {
|
||||
OrderSide::Buy => raw_price * (1.0 + ratio),
|
||||
OrderSide::Sell => raw_price * (1.0 - ratio),
|
||||
@@ -1231,7 +1278,7 @@ where
|
||||
adjusted *= 1.0 + self.sell_then_buy_delay_slippage_rate;
|
||||
}
|
||||
|
||||
self.clamp_execution_price(snapshot, side, adjusted)
|
||||
Ok(self.clamp_execution_price(snapshot, side, adjusted))
|
||||
}
|
||||
|
||||
fn clamp_execution_price(
|
||||
@@ -1266,8 +1313,9 @@ where
|
||||
side: OrderSide,
|
||||
raw_price: f64,
|
||||
quantity: Option<u32>,
|
||||
) -> f64 {
|
||||
self.apply_slippage(snapshot, side, raw_price, quantity)
|
||||
calibration: Option<&HistoricalSlippageCalibration>,
|
||||
) -> Result<f64, BacktestError> {
|
||||
self.apply_slippage(snapshot, side, raw_price, quantity, calibration)
|
||||
}
|
||||
|
||||
fn matching_type_for_algo_request(
|
||||
@@ -1577,7 +1625,7 @@ where
|
||||
.unwrap_or(0);
|
||||
if target_qty > current_qty {
|
||||
let requested_qty = target_qty - current_qty;
|
||||
if !self.can_afford_minimum_buy(date, portfolio, data, &symbol) {
|
||||
if !self.can_afford_minimum_buy(date, portfolio, data, &symbol)? {
|
||||
if report.diagnostics.len() < 32 {
|
||||
report.diagnostics.push(format!(
|
||||
"rebalance_buy_reduced symbol={} provisional={} final={} current={} reason=actual_cash_after_sells",
|
||||
@@ -3385,7 +3433,7 @@ where
|
||||
price,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
))
|
||||
)?)
|
||||
} else {
|
||||
self.round_buy_quantity(
|
||||
(target_value / price).floor() as u32,
|
||||
@@ -3441,15 +3489,17 @@ where
|
||||
let buy_execution_price = data
|
||||
.market(date, &symbol)
|
||||
.map(|snapshot| {
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(buy_quantity))
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, Some(buy_quantity))
|
||||
})
|
||||
.transpose()?
|
||||
.filter(|execution_price| execution_price.is_finite() && *execution_price > 0.0)
|
||||
.unwrap_or(price);
|
||||
let sell_execution_price = data
|
||||
.market(date, &symbol)
|
||||
.map(|snapshot| {
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Sell, Some(sell_quantity))
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, Some(sell_quantity))
|
||||
})
|
||||
.transpose()?
|
||||
.filter(|execution_price| execution_price.is_finite() && *execution_price > 0.0)
|
||||
.unwrap_or(price);
|
||||
if desired_qty < current_qty
|
||||
@@ -3779,7 +3829,7 @@ where
|
||||
continue;
|
||||
}
|
||||
let buy_qty = target_qty - current_qty;
|
||||
if !self.can_afford_minimum_buy(date, portfolio, data, symbol) {
|
||||
if !self.can_afford_minimum_buy(date, portfolio, data, symbol)? {
|
||||
if report.diagnostics.len() < 32 {
|
||||
report.diagnostics.push(format!(
|
||||
"rebalance_buy_reduced symbol={} provisional={} final={} current={} reason=actual_cash_after_sells",
|
||||
@@ -4283,9 +4333,9 @@ where
|
||||
portfolio: &PortfolioState,
|
||||
data: &DataSet,
|
||||
symbol: &str,
|
||||
) -> bool {
|
||||
) -> Result<bool, BacktestError> {
|
||||
let Some(snapshot) = data.market(date, symbol) else {
|
||||
return true;
|
||||
return Ok(true);
|
||||
};
|
||||
let minimum_order_quantity = self.minimum_order_quantity(data, symbol);
|
||||
let order_step_size = self.order_step_size(data, symbol);
|
||||
@@ -4295,14 +4345,14 @@ where
|
||||
order_step_size,
|
||||
);
|
||||
if minimum_buy_quantity == 0 {
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
let minimum_execution_price =
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(minimum_buy_quantity));
|
||||
Self::fixed_cash_fits(
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, Some(minimum_buy_quantity))?;
|
||||
Ok(Self::fixed_cash_fits(
|
||||
self.estimated_buy_cash_out(date, minimum_execution_price, minimum_buy_quantity),
|
||||
portfolio.cash(),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
fn process_sell(
|
||||
@@ -4710,7 +4760,7 @@ where
|
||||
None,
|
||||
algo_request,
|
||||
limit_price,
|
||||
);
|
||||
)?;
|
||||
let (filled_qty, execution_legs, next_cursor, liquidity_consumption) = if let Some(fill) =
|
||||
fill
|
||||
{
|
||||
@@ -4724,7 +4774,7 @@ where
|
||||
)
|
||||
} else {
|
||||
let execution_price =
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Sell, Some(fillable_qty));
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, Some(fillable_qty))?;
|
||||
if let Some(reason) =
|
||||
self.execution_limit_rejection_reason(snapshot, OrderSide::Sell, execution_price)
|
||||
{
|
||||
@@ -6438,7 +6488,7 @@ where
|
||||
value_gross_limit,
|
||||
algo_request,
|
||||
limit_price,
|
||||
);
|
||||
)?;
|
||||
let (filled_qty, execution_legs, next_cursor, liquidity_consumption) = if let Some(fill) =
|
||||
fill
|
||||
{
|
||||
@@ -6452,7 +6502,7 @@ where
|
||||
)
|
||||
} else {
|
||||
let execution_price =
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(constrained_qty));
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, Some(constrained_qty))?;
|
||||
if let Some(reason) =
|
||||
self.execution_limit_rejection_reason(snapshot, OrderSide::Buy, execution_price)
|
||||
{
|
||||
@@ -6494,10 +6544,11 @@ where
|
||||
let mut blocked_by_final_price = false;
|
||||
if filled_qty > 0 {
|
||||
execution_price = self.snapshot_execution_price(
|
||||
data,
|
||||
snapshot,
|
||||
OrderSide::Buy,
|
||||
Some(filled_qty),
|
||||
);
|
||||
)?;
|
||||
match self.execution_price_with_limit_slippage_or_rejection(
|
||||
snapshot,
|
||||
OrderSide::Buy,
|
||||
@@ -7085,7 +7136,7 @@ where
|
||||
fallback_price: f64,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
) -> u32 {
|
||||
) -> Result<u32, BacktestError> {
|
||||
let snapshot = data.market(date, symbol);
|
||||
let mut quantity = self.value_buy_quantity(
|
||||
date,
|
||||
@@ -7097,8 +7148,9 @@ where
|
||||
for _ in 0..8 {
|
||||
let execution_price = snapshot
|
||||
.map(|snapshot| {
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(quantity))
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, Some(quantity))
|
||||
})
|
||||
.transpose()?
|
||||
.filter(|price| price.is_finite() && *price > 0.0)
|
||||
.unwrap_or(fallback_price);
|
||||
let resolved = self.value_buy_quantity(
|
||||
@@ -7109,27 +7161,28 @@ where
|
||||
order_step_size,
|
||||
);
|
||||
if resolved == quantity {
|
||||
return quantity;
|
||||
return Ok(quantity);
|
||||
}
|
||||
quantity = resolved;
|
||||
}
|
||||
while quantity >= minimum_order_quantity.max(1) {
|
||||
let execution_price = snapshot
|
||||
.map(|snapshot| {
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(quantity))
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, Some(quantity))
|
||||
})
|
||||
.transpose()?
|
||||
.filter(|price| price.is_finite() && *price > 0.0)
|
||||
.unwrap_or(fallback_price);
|
||||
if Self::fixed_cash_fits(
|
||||
self.estimated_buy_cash_out(date, execution_price, quantity),
|
||||
value_budget,
|
||||
) {
|
||||
return quantity;
|
||||
return Ok(quantity);
|
||||
}
|
||||
quantity =
|
||||
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||
}
|
||||
0
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn decrement_order_quantity(
|
||||
@@ -7396,14 +7449,14 @@ where
|
||||
gross_limit: Option<f64>,
|
||||
algo_request: Option<&AlgoExecutionRequest>,
|
||||
limit_price: Option<f64>,
|
||||
) -> Option<ExecutionFill> {
|
||||
) -> Result<Option<ExecutionFill>, BacktestError> {
|
||||
let matching_type = self.matching_type_for_algo_request(algo_request);
|
||||
let post_close_window = self.post_close_execution_window(date);
|
||||
let use_intraday_quotes = post_close_window.is_some()
|
||||
|| algo_request.is_some()
|
||||
|| self.matching_type_uses_intraday_quotes();
|
||||
if !use_intraday_quotes {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let runtime_start_time = self.runtime_intraday_start_time.get();
|
||||
@@ -7430,6 +7483,7 @@ where
|
||||
end_cursor
|
||||
};
|
||||
let quotes = data.execution_quotes_on(date, symbol);
|
||||
let calibration = self.slippage_calibration(data, snapshot)?;
|
||||
|
||||
if let Some(fill) = self.select_execution_fill_with_ledger(
|
||||
symbol,
|
||||
@@ -7448,8 +7502,9 @@ where
|
||||
gross_limit,
|
||||
limit_price,
|
||||
execution_ledger,
|
||||
) {
|
||||
return Some(fill);
|
||||
calibration.as_ref(),
|
||||
)? {
|
||||
return Ok(Some(fill));
|
||||
}
|
||||
|
||||
if post_close_window.is_some()
|
||||
@@ -7464,7 +7519,7 @@ where
|
||||
.or(self.intraday_execution_start_time)
|
||||
.map(|start_time| date.and_time(start_time) + Duration::seconds(1))
|
||||
.unwrap_or_else(|| date.and_hms_opt(0, 0, 1).expect("valid midnight"));
|
||||
return Some(ExecutionFill {
|
||||
return Ok(Some(ExecutionFill {
|
||||
quantity: 0,
|
||||
next_cursor,
|
||||
legs: Vec::new(),
|
||||
@@ -7475,10 +7530,10 @@ where
|
||||
end_cursor,
|
||||
matching_type == MatchingType::MinuteLast && start_cursor.is_some(),
|
||||
)),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn empty_intraday_quote_reason(
|
||||
@@ -7542,7 +7597,9 @@ where
|
||||
gross_limit,
|
||||
limit_price,
|
||||
&IntradayExecutionLedger::default(),
|
||||
None,
|
||||
)
|
||||
.expect("test quote selection without historical calibration")
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -7564,9 +7621,10 @@ where
|
||||
gross_limit: Option<f64>,
|
||||
limit_price: Option<f64>,
|
||||
execution_ledger: &IntradayExecutionLedger,
|
||||
) -> Option<ExecutionFill> {
|
||||
calibration: Option<&HistoricalSlippageCalibration>,
|
||||
) -> Result<Option<ExecutionFill>, BacktestError> {
|
||||
if requested_qty == 0 {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let quote_quantity_limited =
|
||||
@@ -7714,7 +7772,7 @@ where
|
||||
}
|
||||
|
||||
let mut quote_price =
|
||||
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty));
|
||||
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty), calibration)?;
|
||||
if let Some(reason) = self.execution_limit_rejection_reason(snapshot, side, quote_price)
|
||||
{
|
||||
execution_block_reason.get_or_insert(reason);
|
||||
@@ -7734,7 +7792,7 @@ where
|
||||
if let Some(cash) = cash_limit {
|
||||
while take_qty > 0 {
|
||||
quote_price =
|
||||
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty));
|
||||
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty), calibration)?;
|
||||
if !quote_price.is_finite() || quote_price <= 0.0 {
|
||||
budget_block_reason = Some("invalid execution price");
|
||||
take_qty = 0;
|
||||
@@ -7786,7 +7844,7 @@ where
|
||||
}
|
||||
|
||||
quote_price =
|
||||
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty));
|
||||
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty), calibration)?;
|
||||
quote_price = self.execution_price_with_limit_slippage(quote_price, limit_price);
|
||||
if let Some(reason) = self.execution_limit_rejection_reason(snapshot, side, quote_price)
|
||||
{
|
||||
@@ -7844,7 +7902,7 @@ where
|
||||
if let Some(reason) = execution_block_reason
|
||||
&& !saw_non_blocked_execution_price
|
||||
{
|
||||
return Some(ExecutionFill {
|
||||
return Ok(Some(ExecutionFill {
|
||||
quantity: 0,
|
||||
next_cursor: execution_block_timestamp
|
||||
.expect("blocked execution quote timestamp")
|
||||
@@ -7852,12 +7910,12 @@ where
|
||||
legs: Vec::new(),
|
||||
liquidity_consumption: Vec::new(),
|
||||
unfilled_reason: Some(reason),
|
||||
});
|
||||
}));
|
||||
}
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Some(ExecutionFill {
|
||||
Ok(Some(ExecutionFill {
|
||||
quantity: filled_qty,
|
||||
next_cursor: last_timestamp.unwrap() + Duration::seconds(1),
|
||||
legs: if matching_type == MatchingType::Vwap {
|
||||
@@ -7881,7 +7939,7 @@ where
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn quote_has_executable_liquidity(
|
||||
@@ -8442,6 +8500,11 @@ mod tests {
|
||||
let mut snapshot = dated_limit_test_snapshot(date);
|
||||
snapshot.close = 10.0;
|
||||
snapshot.upper_limit = 20.0;
|
||||
let data = DataSet::from_components(
|
||||
vec![limit_test_instrument()], vec![snapshot.clone()], Vec::new(),
|
||||
vec![dated_limit_test_candidate(date, false, false, true, true)],
|
||||
vec![dated_limit_test_benchmark(date)],
|
||||
).unwrap();
|
||||
|
||||
for (hour, minute) in [(14, 59), (15, 31)] {
|
||||
broker
|
||||
@@ -8452,7 +8515,7 @@ mod tests {
|
||||
EquityExecutionPhase::ContinuousAuction
|
||||
);
|
||||
assert_eq!(
|
||||
broker.snapshot_execution_price(&snapshot, OrderSide::Buy, Some(100)),
|
||||
broker.snapshot_execution_price(&data, &snapshot, OrderSide::Buy, Some(100)).unwrap(),
|
||||
12.5
|
||||
);
|
||||
}
|
||||
@@ -8465,7 +8528,7 @@ mod tests {
|
||||
EquityExecutionPhase::PostCloseFixedPrice
|
||||
);
|
||||
assert_eq!(
|
||||
broker.snapshot_execution_price(&snapshot, OrderSide::Buy, Some(100)),
|
||||
broker.snapshot_execution_price(&data, &snapshot, OrderSide::Buy, Some(100)).unwrap(),
|
||||
10.0
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user