fix: value unavailable execution days without fills
This commit is contained in:
@@ -6700,6 +6700,15 @@ where
|
|||||||
if let Some(price) = prices.get(symbol).copied().filter(|price| *price > 0.0) {
|
if let Some(price) = prices.get(symbol).copied().filter(|price| *price > 0.0) {
|
||||||
return Ok(price);
|
return Ok(price);
|
||||||
}
|
}
|
||||||
|
if data.market(date, symbol).is_none()
|
||||||
|
&& let Some(previous_date) = date.pred_opt()
|
||||||
|
&& let Some(price) =
|
||||||
|
data.price_on_or_before(previous_date, symbol, PriceField::Close)
|
||||||
|
&& price.is_finite()
|
||||||
|
&& price > 0.0
|
||||||
|
{
|
||||||
|
return Ok(price);
|
||||||
|
}
|
||||||
return Err(BacktestError::MissingPrice {
|
return Err(BacktestError::MissingPrice {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
@@ -10348,6 +10357,93 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn target_portfolio_smart_uses_prior_close_only_for_missing_day_valuation() {
|
||||||
|
let trade_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
|
let missing_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).expect("valid date");
|
||||||
|
let symbol = "000001.SZ";
|
||||||
|
let data = DataSet::from_components_with_actions_and_quotes(
|
||||||
|
vec![limit_test_instrument()],
|
||||||
|
vec![limit_test_snapshot()],
|
||||||
|
Vec::new(),
|
||||||
|
vec![limit_test_candidate(true, true)],
|
||||||
|
vec![limit_test_benchmark()],
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
.expect("valid dataset");
|
||||||
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks,
|
||||||
|
PriceField::Open,
|
||||||
|
)
|
||||||
|
.with_volume_limit(false)
|
||||||
|
.with_liquidity_limit(false);
|
||||||
|
let mut portfolio = PortfolioState::new(100_000.0);
|
||||||
|
portfolio.position_mut(symbol).buy(trade_date, 1_000, 10.0);
|
||||||
|
let mut report = BrokerExecutionReport::default();
|
||||||
|
|
||||||
|
broker
|
||||||
|
.process_target_portfolio_smart(
|
||||||
|
missing_date,
|
||||||
|
&mut portfolio,
|
||||||
|
&data,
|
||||||
|
&BTreeMap::new(),
|
||||||
|
None,
|
||||||
|
Some(&BTreeMap::new()),
|
||||||
|
"target_portfolio_missing_execution_day",
|
||||||
|
&mut BTreeMap::new(),
|
||||||
|
&mut IntradayExecutionLedger::default(),
|
||||||
|
&mut None,
|
||||||
|
&mut BTreeMap::new(),
|
||||||
|
&mut report,
|
||||||
|
)
|
||||||
|
.expect("prior close may value the holding but must not execute the order");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
portfolio.position(symbol).map(|position| position.quantity),
|
||||||
|
Some(1_000)
|
||||||
|
);
|
||||||
|
assert!(report.fill_events.is_empty());
|
||||||
|
assert!(report.order_events.iter().any(|event| {
|
||||||
|
event.symbol == symbol
|
||||||
|
&& event.side == OrderSide::Sell
|
||||||
|
&& event.status == OrderStatus::Rejected
|
||||||
|
&& event.reason.contains("market snapshot is missing")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_valuation_missing_with_current_market_still_fails_closed() {
|
||||||
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
|
let data = DataSet::from_components_with_actions_and_quotes(
|
||||||
|
vec![limit_test_instrument()],
|
||||||
|
vec![limit_test_snapshot()],
|
||||||
|
Vec::new(),
|
||||||
|
vec![limit_test_candidate(true, true)],
|
||||||
|
vec![limit_test_benchmark()],
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
.expect("valid dataset");
|
||||||
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks,
|
||||||
|
PriceField::Open,
|
||||||
|
);
|
||||||
|
|
||||||
|
let error = broker
|
||||||
|
.rebalance_valuation_price_with_overrides(
|
||||||
|
date,
|
||||||
|
"000001.SZ",
|
||||||
|
&data,
|
||||||
|
Some(&BTreeMap::new()),
|
||||||
|
)
|
||||||
|
.expect_err("a present execution-day market row cannot use stale valuation fallback");
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("custom valuation"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn market_share_buy_skips_quantity_that_rounds_below_one_lot() {
|
fn market_share_buy_skips_quantity_that_rounds_below_one_lot() {
|
||||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
|
|||||||
Reference in New Issue
Block a user