diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 379d94f..28e7d3a 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -1710,6 +1710,31 @@ impl PlatformExprStrategy { } } + fn context_position_value_for_remaining_buy_cash( + &self, + ctx: &StrategyContext<'_>, + date: NaiveDate, + symbol: &str, + ) -> f64 { + let Some(position) = ctx.portfolio.position(symbol) else { + return 0.0; + }; + if position.quantity == 0 { + return 0.0; + } + let mark_price = self + .aiquant_scheduled_last_price(ctx, date, symbol) + .or_else(|| ctx.data.price(date, symbol, PriceField::Last)) + .or_else(|| ctx.data.price_on_or_before(date, symbol, PriceField::Last)) + .filter(|price| price.is_finite() && *price > 0.0) + .unwrap_or(position.last_price); + if mark_price.is_finite() && mark_price > 0.0 { + mark_price * position.quantity as f64 + } else { + position.market_value() + } + } + fn remaining_buy_cash_per_slot( &self, ctx: &StrategyContext<'_>, @@ -1732,7 +1757,11 @@ impl PlatformExprStrategy { .iter() .filter(|symbol| value_symbols.contains(*symbol)) .map(|symbol| { - self.projected_position_value_at_execution_price(ctx, projected, date, symbol) + if self.config.aiquant_transaction_cost { + self.context_position_value_for_remaining_buy_cash(ctx, date, symbol) + } else { + self.projected_position_value_at_execution_price(ctx, projected, date, symbol) + } }) .filter(|value| value.is_finite() && *value > 0.0) .sum::(); @@ -12038,6 +12067,138 @@ mod tests { ))); } + #[test] + fn platform_aiquant_remaining_buy_cash_uses_strategy_visible_positions() { + let prev_date = d(2025, 2, 2); + let date = d(2025, 2, 3); + let symbols = ["000001.SZ", "000002.SZ"]; + let data = DataSet::from_components( + symbols + .iter() + .map(|symbol| Instrument { + symbol: (*symbol).to_string(), + name: (*symbol).to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }) + .collect(), + symbols + .iter() + .map(|symbol| DailyMarketSnapshot { + date, + symbol: (*symbol).to_string(), + timestamp: Some("2025-02-03 10:40:00".to_string()), + day_open: 10.0, + open: 10.0, + high: 10.2, + low: 9.8, + close: 10.0, + last_price: 10.0, + bid1: 10.0, + ask1: 10.0, + prev_close: 10.0, + volume: 1_000_000, + tick_volume: 10_000, + bid1_volume: 2_000, + ask1_volume: 2_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 11.0, + lower_limit: 9.0, + price_tick: 0.01, + }) + .collect(), + symbols + .iter() + .enumerate() + .map(|(index, symbol)| DailyFactorSnapshot { + date, + symbol: (*symbol).to_string(), + market_cap_bn: 10.0 + index as f64, + free_float_cap_bn: 10.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }) + .collect(), + symbols + .iter() + .map(|symbol| 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, + }) + .collect(), + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1000.0, + prev_close: 1000.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + + let mut portfolio = PortfolioState::new(10_000.0); + portfolio + .position_mut("000001.SZ") + .buy(prev_date, 900, 10.0); + let mut projected = portfolio.clone(); + projected + .position_mut("000001.SZ") + .sell(400, 10.0) + .expect("projected same-bar adjustment"); + let subscriptions = BTreeSet::new(); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 2, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: None, + order_events: &[], + fills: &[], + }; + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.aiquant_transaction_cost = true; + let strategy = PlatformExprStrategy::new(cfg); + let working_symbols = BTreeSet::from(["000001.SZ".to_string()]); + let value_symbols = working_symbols.clone(); + + let cash = strategy.remaining_buy_cash_per_slot( + &ctx, + &projected, + date, + 10_000.0, + 2, + &working_symbols, + &value_symbols, + 0.0, + ); + + assert!( + (cash - 1_000.0).abs() < 1e-6, + "remaining cash should use strategy-visible 900-share position, got {cash}" + ); + } + #[test] fn platform_weak_market_skips_positive_adjust_for_stop_loss_position() { let prev_date = d(2025, 2, 2);