diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 6c12830..076fdde 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -596,7 +596,7 @@ where self.load_missing_execution_quotes( execution_date, Some(*quote_time), - Some(*quote_time), + None, &mut symbols, )?; } diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 9a48a88..2e23d92 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -9061,6 +9061,135 @@ mod tests { assert_eq!(strategy.marked_total_value(&ctx, date), 2_000.0); } + #[test] + fn platform_aiquant_target_adjust_uses_scheduled_quote_for_position_value() { + let date = d(2023, 5, 8); + let symbol = "603101.SH"; + let data = DataSet::from_components_with_actions_and_quotes( + vec![Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: "SH".to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + vec![DailyMarketSnapshot { + date, + symbol: symbol.to_string(), + timestamp: Some("2023-05-08 10:40:00".to_string()), + day_open: 5.86, + open: 5.86, + high: 5.90, + low: 5.76, + close: 5.81, + last_price: 5.81, + bid1: 5.82, + ask1: 5.83, + prev_close: 5.85, + volume: 1_000_000, + tick_volume: 262, + bid1_volume: 54, + ask1_volume: 143, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 6.44, + lower_limit: 5.27, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 20.0, + free_float_cap_bn: 20.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }], + vec![CandidateEligibility { + date, + symbol: symbol.to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1002.0, + prev_close: 998.0, + volume: 1_000_000, + }], + Vec::new(), + vec![IntradayExecutionQuote { + date, + symbol: symbol.to_string(), + timestamp: date.and_hms_opt(10, 39, 59).unwrap(), + last_price: 5.83, + bid1: 5.82, + ask1: 5.83, + bid1_volume: 54, + ask1_volume: 143, + volume_delta: 262, + amount_delta: 152_501.0, + trading_phase: Some("continuous".to_string()), + }], + ) + .expect("dataset"); + let subscriptions = BTreeSet::new(); + let mut portfolio = PortfolioState::new(8_261_416.62); + portfolio.position_mut(symbol).buy(date, 21_200, 5.8817); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 1, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: Some(date.and_hms_opt(10, 40, 0).unwrap()), + order_events: &[], + fills: &[], + }; + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.aiquant_transaction_cost = true; + cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap()); + cfg.commission_rate = Some(0.0003); + cfg.minimum_commission = Some(5.0); + let strategy = PlatformExprStrategy::new(cfg); + let mut projected = portfolio.clone(); + let mut execution_state = super::ProjectedExecutionState::default(); + let target_value = 9_996_284.62 * 0.5 / 40.0; + + let filled = strategy + .project_target_value( + &ctx, + &mut projected, + date, + symbol, + target_value, + &mut execution_state, + ) + .expect("target adjustment should buy"); + + assert_eq!(filled, 200); + assert_eq!( + projected.position(symbol).map(|position| position.quantity), + Some(21_400) + ); + } + #[test] fn platform_aiquant_intraday_selection_filters_limits_on_execution_date() { let factor_date = d(2023, 11, 10); diff --git a/crates/fidc-core/tests/decision_quote_preload.rs b/crates/fidc-core/tests/decision_quote_preload.rs index 614e79e..bfc77fe 100644 --- a/crates/fidc-core/tests/decision_quote_preload.rs +++ b/crates/fidc-core/tests/decision_quote_preload.rs @@ -52,7 +52,7 @@ impl Strategy for DecisionQuoteReader { .data .execution_quotes_on(ctx.execution_date, "000001.SZ") .iter() - .any(|quote| quote.timestamp.time() == t(10, 40, 0) && quote.last_price == 11.0); + .any(|quote| quote.timestamp.time() == t(10, 39, 59) && quote.last_price == 11.0); assert!( quote_loaded_before_decision, "engine must load declared decision quote before strategy.on_day" @@ -199,6 +199,10 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() { }; let mut engine = BacktestEngine::new(data, DecisionQuoteReader::default(), broker, config) .with_execution_quote_loader(move |request| { + assert_eq!( + request.end_time, None, + "decision quote preload must request latest quote at or before start_time" + ); Ok(request .symbols .into_iter() @@ -207,7 +211,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() { symbol, timestamp: request .date - .and_time(request.start_time.unwrap_or(t(10, 40, 0))), + .and_time(t(10, 39, 59)), last_price: if request.date == second { 11.0 } else { 10.0 }, bid1: if request.date == second { 11.0 } else { 10.0 }, ask1: if request.date == second { 11.0 } else { 10.0 },