fix: keep decision estimates independent and use timed capacity in fill fixtures
This commit is contained in:
@@ -7725,7 +7725,7 @@ where
|
||||
} else {
|
||||
remaining_qty.min(available_qty)
|
||||
};
|
||||
if !(side == OrderSide::Sell && allow_odd_lot_sell && take_qty == remaining_qty) {
|
||||
if !(side == OrderSide::Sell && allow_odd_lot_sell) {
|
||||
take_qty =
|
||||
self.round_buy_quantity(take_qty, minimum_order_quantity, order_step_size);
|
||||
}
|
||||
@@ -8397,6 +8397,7 @@ mod tests {
|
||||
vec![dated_limit_test_benchmark(first), dated_limit_test_benchmark(second)],
|
||||
).unwrap();
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
let mut portfolio = PortfolioState::new(100_000.0);
|
||||
broker.execute(first, &mut portfolio, &data, &next_open_buy_decision()).unwrap();
|
||||
@@ -8423,6 +8424,7 @@ mod tests {
|
||||
let data = DataSet::from_components(vec![limit_test_instrument()], vec![limit_test_snapshot()],
|
||||
Vec::new(), vec![limit_test_candidate(true, true)], vec![limit_test_benchmark()]).unwrap();
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
broker.upsert_open_order(test_open_order(99));
|
||||
let mut decision = StrategyDecision::default();
|
||||
@@ -8450,6 +8452,7 @@ mod tests {
|
||||
dated_limit_test_candidate(second, false, false, true, true)],
|
||||
vec![dated_limit_test_benchmark(first), dated_limit_test_benchmark(second)]).unwrap();
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::NextBarOpen);
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
let mut initial = StrategyDecision::default();
|
||||
|
||||
@@ -7028,6 +7028,7 @@ mod tests {
|
||||
let third = d(2025, 1, 6);
|
||||
let fourth = d(2025, 1, 7);
|
||||
let broker = scheduled_next_open_broker(FidcRiskControlConfig::default())
|
||||
.with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.25);
|
||||
let result = run_scheduled_round_trip_next_open_with_dataset_and_broker(
|
||||
@@ -7055,12 +7056,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_bar_open_sell_volume_limit_rejects_execution_day_zero_volume() {
|
||||
fn next_bar_open_session_audit_flags_zero_volume_without_rewriting_fills() {
|
||||
let first = d(2025, 1, 2);
|
||||
let second = d(2025, 1, 3);
|
||||
let third = d(2025, 1, 6);
|
||||
let fourth = d(2025, 1, 7);
|
||||
let broker = scheduled_next_open_broker(FidcRiskControlConfig::default())
|
||||
.with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.25);
|
||||
let result = run_scheduled_round_trip_next_open_with_dataset_and_broker(
|
||||
@@ -7081,7 +7083,10 @@ mod tests {
|
||||
broker,
|
||||
);
|
||||
|
||||
assert_round_trip_sell_canceled_with_reason(&result, "daily volume limit");
|
||||
assert!(result.fills.iter().any(|fill| fill.side == OrderSide::Sell && fill.date == fourth));
|
||||
assert_eq!(result.capacity_audit.audit_passed, Some(false));
|
||||
assert_eq!(result.capacity_audit.failed_symbol_sessions, 1);
|
||||
assert!(result.process_events.iter().any(|event| event.kind == ProcessEventKind::SessionCapacityAudit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3157,10 +3157,16 @@ impl PlatformExprStrategy {
|
||||
allow_odd_lot_sell: bool,
|
||||
current_fill_quantity: u32,
|
||||
execution_state: &ProjectedExecutionState,
|
||||
future_execution: bool,
|
||||
) -> Result<Option<u32>, BacktestError> {
|
||||
if requested_qty == 0 {
|
||||
return Ok(Some(0));
|
||||
}
|
||||
if future_execution {
|
||||
// A decision-day estimate cannot use tomorrow's liquidity to
|
||||
// change the orders created today.
|
||||
return Ok(Some(requested_qty));
|
||||
}
|
||||
|
||||
let constraints = self.config.risk_config.trading_constraints;
|
||||
let mut max_fill = requested_qty;
|
||||
@@ -3333,6 +3339,7 @@ impl PlatformExprStrategy {
|
||||
allow_odd_lot_sell,
|
||||
filled_qty,
|
||||
execution_state,
|
||||
Self::defer_projection_execution_risk(ctx, date),
|
||||
)?
|
||||
.unwrap_or(0);
|
||||
if available_qty == 0 {
|
||||
@@ -3521,6 +3528,7 @@ impl PlatformExprStrategy {
|
||||
sellable_qty >= current_qty,
|
||||
0,
|
||||
execution_state,
|
||||
Self::defer_projection_execution_risk(ctx, date),
|
||||
)?.filter(|quantity| *quantity > 0)
|
||||
{
|
||||
fill = Some(ProjectedExecutionFill {
|
||||
@@ -4153,6 +4161,7 @@ impl PlatformExprStrategy {
|
||||
false,
|
||||
0,
|
||||
execution_state,
|
||||
Self::defer_projection_execution_risk(ctx, date),
|
||||
)?.filter(|quantity| *quantity > 0)
|
||||
{
|
||||
fill = Some(ProjectedExecutionFill {
|
||||
@@ -15072,6 +15081,7 @@ mod tests {
|
||||
order_events:&[],fills:&[],
|
||||
};
|
||||
let mut cfg=PlatformExprStrategyConfig::generic();
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled=false;
|
||||
cfg.signal_symbol=symbol.into();
|
||||
cfg.rotation_enabled=false;
|
||||
cfg.signal_book=Some(book);
|
||||
@@ -15190,6 +15200,7 @@ mod tests {
|
||||
let rows = Arc::new(Mutex::new(Vec::new()));
|
||||
let strategy = Capture { inner: PlatformExprStrategy::new(config), first, rows: Arc::clone(&rows) };
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
let mut engine = BacktestEngine::new(data, strategy, broker, BacktestConfig {
|
||||
initial_cash: 10_000.0, benchmark_code: "000852.SH".to_owned(), start_date: Some(first),
|
||||
@@ -18046,6 +18057,7 @@ mod tests {
|
||||
false,
|
||||
0,
|
||||
&execution_state,
|
||||
false,
|
||||
).expect("valid volume capacity"),
|
||||
Some(2_500)
|
||||
);
|
||||
@@ -18066,6 +18078,7 @@ mod tests {
|
||||
false,
|
||||
0,
|
||||
&execution_state,
|
||||
false,
|
||||
).expect("valid remaining volume capacity"),
|
||||
Some(100)
|
||||
);
|
||||
|
||||
@@ -107,6 +107,7 @@ fn action(quantity: &str, when: &str) -> PlatformTradeAction {
|
||||
}
|
||||
fn run(policy: AutomaticTradeProtection) -> fidc_core::BacktestResult {
|
||||
let mut config = PlatformExprStrategyConfig::generic();
|
||||
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
|
||||
config.signal_symbol = "000001.SZ".into();
|
||||
config.benchmark_symbol = "000300.SH".into();
|
||||
config.rotation_enabled = false;
|
||||
@@ -276,6 +277,7 @@ fn locked_holding_keeps_its_slot_even_when_cash_can_buy_the_next_candidate() {
|
||||
)
|
||||
.unwrap();
|
||||
let mut config = PlatformExprStrategyConfig::generic();
|
||||
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
|
||||
config.signal_symbol = "000001.SZ".into();
|
||||
config.benchmark_symbol = "000300.SH".into();
|
||||
config.strategy_name = "protection_test".into();
|
||||
|
||||
@@ -4290,6 +4290,7 @@ fn platform_strategy_cannot_hide_missing_valuation_by_skipping_stop_take() {
|
||||
)
|
||||
.expect("dataset");
|
||||
let mut config = PlatformExprStrategyConfig::microcap_rotation();
|
||||
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
|
||||
config.strategy_name = "missing-row-platform-risk".to_string();
|
||||
config.benchmark_symbol = "000300.SH".to_string();
|
||||
config.signal_symbol = "000001.SZ".to_string();
|
||||
|
||||
@@ -202,6 +202,7 @@ fn execute_single_value_order(
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
.with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_strict_value_budget(true);
|
||||
let report = broker
|
||||
.execute(
|
||||
@@ -392,7 +393,7 @@ fn broker_executes_explicit_order_value_buy() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
@@ -682,7 +683,7 @@ fn broker_executes_order_shares_and_order_lots() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
@@ -802,7 +803,7 @@ fn broker_executes_target_shares_like_order_to() {
|
||||
let broker = BrokerSimulator::new(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
@@ -978,7 +979,7 @@ fn broker_executes_target_portfolio_smart_with_custom_prices() {
|
||||
let broker = BrokerSimulator::new(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
@@ -1252,7 +1253,7 @@ fn broker_executes_order_percent_and_target_percent() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
let mut percent_portfolio = PortfolioState::new(1_000_000.0);
|
||||
let percent_report = broker
|
||||
@@ -1380,7 +1381,7 @@ fn broker_uses_day_open_price_for_open_auction_matching() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::DayOpen,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.05));
|
||||
|
||||
let report = broker
|
||||
@@ -1414,7 +1415,7 @@ fn broker_uses_day_open_price_for_open_auction_matching() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broker_open_auction_uses_auction_volume_without_quote_liquidity() {
|
||||
fn broker_rejects_unproven_auction_capacity_in_a_daily_snapshot() {
|
||||
let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let data = DataSet::from_components(
|
||||
vec![Instrument {
|
||||
@@ -1511,11 +1512,10 @@ fn broker_open_auction_uses_auction_volume_without_quote_liquidity() {
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
.expect_err("a timestamped daily total is not proof of auction volume");
|
||||
|
||||
assert_eq!(report.fill_events.len(), 1);
|
||||
assert_eq!(report.fill_events[0].quantity, 200);
|
||||
assert_eq!(report.fill_events[0].price, 9.8);
|
||||
assert!(report.to_string().contains("execution-time capacity is missing"));
|
||||
assert_eq!(portfolio.cash(), 1_000_000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1710,7 +1710,7 @@ fn broker_applies_price_ratio_slippage_on_snapshot_fills() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.01));
|
||||
|
||||
let report = broker
|
||||
@@ -2337,7 +2337,7 @@ fn broker_cancels_market_order_remainder_when_intraday_quote_liquidity_exhausted
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broker_cancels_market_buy_when_minute_has_no_volume() {
|
||||
fn broker_rejects_missing_execution_capacity_instead_of_declaring_suspension() {
|
||||
let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let data = DataSet::from_components(
|
||||
vec![Instrument {
|
||||
@@ -2433,15 +2433,10 @@ fn broker_cancels_market_buy_when_minute_has_no_volume() {
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
.expect_err("missing capacity is a contract error, not a normal no-volume cancellation");
|
||||
|
||||
assert_eq!(report.fill_events.len(), 0);
|
||||
assert_eq!(report.order_events.len(), 1);
|
||||
assert_eq!(
|
||||
report.order_events[0].status,
|
||||
fidc_core::OrderStatus::Canceled
|
||||
);
|
||||
assert!(report.order_events[0].reason.contains("daily no volume"));
|
||||
assert!(report.to_string().contains("execution-time capacity is missing"));
|
||||
assert_eq!(portfolio.cash(), 1_000_000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3679,7 +3674,7 @@ fn rebalance_uses_day_open_for_open_auction_valuation() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::DayOpen,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
@@ -3864,7 +3859,7 @@ fn rebalance_optimizer_prioritizes_higher_target_weight_when_cash_is_tight() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
@@ -4049,7 +4044,7 @@ fn rebalance_optimizer_does_not_scale_targets_above_requested_weight() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
broker
|
||||
.execute(
|
||||
@@ -4163,7 +4158,7 @@ fn broker_uses_board_specific_min_quantity_and_step_size_for_buy_sizing() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_risk_config(risk_config);
|
||||
|
||||
let report = broker
|
||||
@@ -4269,7 +4264,7 @@ fn broker_allows_bjse_quantities_above_minimum_without_round_lot_step() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_risk_config(risk_config);
|
||||
|
||||
let report = broker
|
||||
@@ -4377,7 +4372,7 @@ fn broker_allows_full_odd_lot_sell_when_liquidating_position() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
@@ -4511,7 +4506,7 @@ fn same_day_sell_then_rebuy_is_rejected_by_default() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
|
||||
broker
|
||||
.execute(
|
||||
@@ -4655,7 +4650,7 @@ fn same_day_sell_then_rebuy_can_be_allowed_by_policy() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_risk_config(risk_config);
|
||||
|
||||
broker
|
||||
@@ -4708,7 +4703,7 @@ fn broker_configured_policy_can_allow_upper_limit_buy() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_risk_config(risk_config);
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
|
||||
@@ -4752,7 +4747,7 @@ fn broker_configured_policy_can_allow_lower_limit_sell() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_risk_config(risk_config);
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
portfolio
|
||||
@@ -4791,7 +4786,7 @@ fn broker_configured_policy_can_allow_lower_limit_sell() {
|
||||
fn two_day_limit_order_data(day1_open: f64, day2_open: f64) -> DataSet {
|
||||
let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let day2 = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap();
|
||||
DataSet::from_components(
|
||||
DataSet::from_components_with_actions_and_quotes(
|
||||
vec![Instrument {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
name: "Test".to_string(),
|
||||
@@ -4919,6 +4914,13 @@ fn two_day_limit_order_data(day1_open: f64, day2_open: f64) -> DataSet {
|
||||
volume: 1_000_000,
|
||||
},
|
||||
],
|
||||
Vec::new(),
|
||||
[(day1, day1_open), (day2, day2_open)].into_iter().map(|(date, price)| IntradayExecutionQuote {
|
||||
date, symbol: "000002.SZ".into(), timestamp: date.and_hms_opt(9, 30, 0).unwrap(),
|
||||
last_price: price, bid1: price, ask1: price, bid1_volume: 0, ask1_volume: 0,
|
||||
volume_delta: 100_000, amount_delta: 100_000.0 * price,
|
||||
trading_phase: Some("synthetic_observation_fixture".into()),
|
||||
}).collect(),
|
||||
)
|
||||
.expect("dataset")
|
||||
}
|
||||
@@ -4932,7 +4934,7 @@ fn broker_expires_day_limit_buy_at_market_close() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap());
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
|
||||
let day1_report = broker
|
||||
@@ -5006,7 +5008,7 @@ fn broker_ioc_limit_order_fills_available_quantity_and_cancels_remainder() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.001)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5042,7 +5044,7 @@ fn broker_ioc_limit_order_fills_available_quantity_and_cancels_remainder() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broker_persists_daily_volume_consumption_across_execute_calls() {
|
||||
fn broker_persists_observed_volume_consumption_across_execute_calls() {
|
||||
let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let day2 = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap();
|
||||
let data = two_day_limit_order_data(10.0, 10.0);
|
||||
@@ -5051,6 +5053,8 @@ fn broker_persists_daily_volume_consumption_across_execute_calls() {
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
.with_matching_type(MatchingType::MinuteLast)
|
||||
.with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.001)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5077,7 +5081,7 @@ fn broker_persists_daily_volume_consumption_across_execute_calls() {
|
||||
assert_eq!(second.order_events.len(), 1);
|
||||
assert_eq!(second.order_events[0].status, OrderStatus::Canceled);
|
||||
assert_eq!(second.order_events[0].filled_quantity, 0);
|
||||
assert!(second.order_events[0].reason.contains("daily volume limit"));
|
||||
assert!(second.order_events[0].reason.contains("intraday quote liquidity exhausted"));
|
||||
assert_eq!(portfolio.position("000002.SZ").unwrap().quantity, 100);
|
||||
|
||||
let next_day = broker
|
||||
@@ -5294,7 +5298,7 @@ fn broker_day_market_order_cancels_remainder_without_creating_invalid_open_order
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.001)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5334,7 +5338,7 @@ fn broker_fok_order_is_atomic_when_liquidity_is_insufficient() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.001)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5421,7 +5425,7 @@ fn broker_gtc_limit_order_survives_close_and_fills_next_day() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap());
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
|
||||
let day1_report = broker
|
||||
@@ -5469,7 +5473,7 @@ fn broker_gtc_partial_fills_preserve_cumulative_order_and_commission_state() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.001)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5536,7 +5540,7 @@ fn broker_modifies_gtc_limit_order_without_changing_order_identity() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap());
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
|
||||
let created = broker
|
||||
@@ -5621,7 +5625,7 @@ fn broker_modifies_partially_filled_gtc_total_and_preserves_commission_state() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.001)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5699,7 +5703,7 @@ fn broker_rejected_modify_has_zero_side_effects() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.001)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5767,7 +5771,7 @@ fn broker_accepted_modify_resets_queue_priority_but_reduction_preserves_it() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap());
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
let create = |reason: &str| StrategyDecision {
|
||||
order_intents: vec![
|
||||
@@ -5899,7 +5903,7 @@ fn broker_uses_limit_price_slippage_for_limit_orders() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_slippage_model(SlippageModel::LimitPrice);
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
|
||||
@@ -5938,7 +5942,7 @@ fn broker_rejects_limit_buy_when_final_execution_price_reaches_upper_limit() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_slippage_model(SlippageModel::LimitPrice);
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
|
||||
@@ -5984,7 +5988,7 @@ fn broker_executes_limit_value_and_limit_percent_intents() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap());
|
||||
|
||||
let mut value_portfolio = PortfolioState::new(1_000_000.0);
|
||||
let value_report = broker
|
||||
@@ -6047,7 +6051,7 @@ fn broker_cancels_open_order_by_order_id() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap());
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
|
||||
let day1_report = broker
|
||||
@@ -6225,7 +6229,7 @@ fn broker_reserves_sellable_quantity_for_open_limit_sells() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
portfolio
|
||||
.position_mut("000002.SZ")
|
||||
|
||||
@@ -142,7 +142,13 @@ fn data_with_fund_rules(
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
DataSet::from_components(instruments, market, factors, candidates, benchmarks).unwrap()
|
||||
let quotes = market.iter().filter(|row| row.symbol != "000300.SH").map(|row| fidc_core::IntradayExecutionQuote {
|
||||
date: row.date, symbol: row.symbol.clone(), timestamp: row.date.and_hms_opt(9, 30, 0).unwrap(),
|
||||
last_price: row.open, bid1: row.open, ask1: row.open, bid1_volume: 0, ask1_volume: 0,
|
||||
volume_delta: row.volume, amount_delta: row.open * row.volume as f64,
|
||||
trading_phase: Some("synthetic_observation_fixture".into()),
|
||||
}).collect();
|
||||
DataSet::from_components_with_actions_and_quotes(instruments, market, factors, candidates, benchmarks, vec![], quotes).unwrap()
|
||||
}
|
||||
fn broker(volume: bool) -> BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks> {
|
||||
let mut risk = FidcRiskControlConfig::default();
|
||||
@@ -159,6 +165,7 @@ fn broker(volume: bool) -> BrokerSimulator<ChinaAShareCostModel, ChinaEquityRule
|
||||
ChinaEquityRuleHooks,
|
||||
)
|
||||
.with_matching_type(MatchingType::NextBarOpen)
|
||||
.with_intraday_execution_start_time(chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
.with_risk_config(risk)
|
||||
}
|
||||
fn contract(signal: NaiveDate, target: usize, preserve: bool) -> FrozenStockPoolIntent {
|
||||
@@ -268,6 +275,7 @@ fn mixed_fund_and_stock_round_trip_uses_declared_ticks_and_asset_specific_fees()
|
||||
let mut costs = ChinaAShareCostModel::default();
|
||||
costs.set_transfer_fee_rate(0.00001);
|
||||
let broker = BrokerSimulator::new(costs, ChinaEquityRuleHooks)
|
||||
.with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::NextBarOpen);
|
||||
let mut account = PortfolioState::new(30_000.);
|
||||
let mut entry = contract(day(2), 1, false);
|
||||
|
||||
Reference in New Issue
Block a user