diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 94b4d38..3c3f5bf 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -185,7 +185,6 @@ pub struct BrokerSimulator { strict_value_budget: bool, rebalance_cash_mode: RebalanceCashMode, sell_then_buy_delay_slippage_rate: f64, - aiquant_execution_rules: bool, same_day_buy_close_mark_at_fill: bool, risk_config: FidcRiskControlConfig, same_day_sold_symbols: RefCell>>, @@ -216,7 +215,6 @@ impl BrokerSimulator { strict_value_budget: true, rebalance_cash_mode: RebalanceCashMode::default(), sell_then_buy_delay_slippage_rate: 0.0, - aiquant_execution_rules: false, same_day_buy_close_mark_at_fill: false, risk_config: FidcRiskControlConfig::default(), same_day_sold_symbols: RefCell::new(BTreeMap::new()), @@ -251,7 +249,6 @@ impl BrokerSimulator { strict_value_budget: true, rebalance_cash_mode: RebalanceCashMode::default(), sell_then_buy_delay_slippage_rate: 0.0, - aiquant_execution_rules: false, same_day_buy_close_mark_at_fill: false, risk_config: FidcRiskControlConfig::default(), same_day_sold_symbols: RefCell::new(BTreeMap::new()), @@ -305,11 +302,6 @@ impl BrokerSimulator { self } - pub fn with_aiquant_execution_rules(mut self, enabled: bool) -> Self { - self.aiquant_execution_rules = enabled; - self - } - pub fn with_same_day_buy_close_mark_at_fill(mut self, enabled: bool) -> Self { self.same_day_buy_close_mark_at_fill = enabled; self @@ -444,7 +436,7 @@ where return execution_price; } } - if self.aiquant_execution_rules && self.execution_price_field == PriceField::Last { + if self.execution_price_field == PriceField::Last { let start_cursor = self .runtime_intraday_start_time .get() @@ -1075,6 +1067,8 @@ where if current_qty > target_qty { let requested_qty = current_qty - target_qty; + let fill_start = report.fill_events.len(); + let order_start = report.order_events.len(); self.process_sell( date, portfolio, @@ -1093,6 +1087,23 @@ where None, &mut report, )?; + let filled_quantity = report.fill_events[fill_start..] + .iter() + .filter(|fill| fill.symbol == symbol && fill.side == OrderSide::Sell) + .map(|fill| fill.quantity) + .sum::(); + if filled_quantity < requested_qty && report.diagnostics.len() < 32 { + let denial_reason = report.order_events[order_start..] + .iter() + .rev() + .find(|event| event.symbol == symbol && event.side == OrderSide::Sell) + .map(|event| event.reason.as_str()) + .unwrap_or("sell_not_fully_filled"); + report.diagnostics.push(format!( + "rebalance_target_denied symbol={} side=sell requested={} filled={} reason={}", + symbol, requested_qty, filled_quantity, denial_reason + )); + } } } @@ -1104,6 +1115,15 @@ 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 report.diagnostics.len() < 32 { + report.diagnostics.push(format!( + "rebalance_buy_reduced symbol={} provisional={} final={} current={} reason=actual_cash_after_sells", + symbol, target_qty, current_qty, current_qty + )); + } + continue; + } self.process_buy( date, portfolio, @@ -2732,6 +2752,25 @@ where &mut local_report, )?; } + let filled_quantity = local_report + .fill_events + .iter() + .filter(|fill| fill.symbol == *symbol && fill.side == OrderSide::Sell) + .map(|fill| fill.quantity) + .sum::(); + if filled_quantity < sell_qty && local_report.diagnostics.len() < 32 { + let denial_reason = local_report + .order_events + .iter() + .rev() + .find(|event| event.symbol == *symbol && event.side == OrderSide::Sell) + .map(|event| event.reason.as_str()) + .unwrap_or("sell_not_fully_filled"); + local_report.diagnostics.push(format!( + "rebalance_target_denied symbol={} side=sell requested={} filled={} reason={}", + symbol, sell_qty, filled_quantity, denial_reason + )); + } Self::extend_report(report, local_report); } @@ -2745,6 +2784,15 @@ where continue; } let buy_qty = target_qty - current_qty; + 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", + symbol, target_qty, current_qty, current_qty + )); + } + continue; + } let mut local_report = BrokerExecutionReport::default(); if let Some(limit_price) = self.required_custom_order_price(date, symbol, limit_prices)? @@ -2886,7 +2934,7 @@ where Ok(()) } - fn aiquant_limit_check_price( + fn execution_limit_check_price( &self, snapshot: &crate::data::DailyMarketSnapshot, side: OrderSide, @@ -2898,7 +2946,7 @@ where } } - fn aiquant_order_limit_check_price( + fn execution_order_limit_check_price( &self, date: NaiveDate, data: &DataSet, @@ -2922,7 +2970,7 @@ where false, ) .and_then(|quote| self.select_quote_reference_price(snapshot, quote, side, matching_type)) - .unwrap_or_else(|| self.aiquant_limit_check_price(snapshot, side)) + .unwrap_or_else(|| self.execution_limit_check_price(snapshot, side)) } #[cfg(test)] @@ -2933,11 +2981,7 @@ where candidate: &crate::data::CandidateEligibility, instrument: Option<&Instrument>, ) -> RuleCheck { - let check_price = if self.aiquant_execution_rules { - self.aiquant_limit_check_price(snapshot, OrderSide::Buy) - } else { - ChinaAShareRiskControl::buy_check_price(snapshot, self.execution_price_field) - }; + let check_price = self.execution_limit_check_price(snapshot, OrderSide::Buy); if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason_with_config( date, candidate, @@ -2948,7 +2992,7 @@ where ) { return RuleCheck::reject(reason); } - if !self.aiquant_execution_rules && !self.rules.duplicates_standard_china_risk() { + if !self.rules.duplicates_standard_china_risk() { return self .rules .can_buy(date, snapshot, candidate, self.execution_price_field); @@ -2966,18 +3010,14 @@ where instrument: Option<&Instrument>, algo_request: Option<&AlgoExecutionRequest>, ) -> RuleCheck { - let check_price = if self.aiquant_execution_rules { - self.aiquant_order_limit_check_price( - date, - data, - symbol, - snapshot, - OrderSide::Buy, - algo_request, - ) - } else { - ChinaAShareRiskControl::buy_check_price(snapshot, self.execution_price_field) - }; + let check_price = self.execution_order_limit_check_price( + date, + data, + symbol, + snapshot, + OrderSide::Buy, + algo_request, + ); if let Some(reason) = self.same_day_rebuy_rejection_reason(date, symbol) { return RuleCheck::reject(reason); } @@ -2991,7 +3031,7 @@ where ) { return RuleCheck::reject(reason); } - if !self.aiquant_execution_rules && !self.rules.duplicates_standard_china_risk() { + if !self.rules.duplicates_standard_china_risk() { return self .rules .can_buy(date, snapshot, candidate, self.execution_price_field); @@ -3010,36 +3050,25 @@ where position: &crate::portfolio::Position, algo_request: Option<&AlgoExecutionRequest>, ) -> RuleCheck { - if self.risk_config.static_rules.respect_allow_buy_sell - && !self.aiquant_execution_rules - && !candidate.allow_sell - { - return RuleCheck::reject("sell_disabled"); - } - let check_price = if self.aiquant_execution_rules { - self.aiquant_order_limit_check_price( - date, - data, - symbol, - snapshot, - OrderSide::Sell, - algo_request, - ) - } else { - ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field) - }; + let check_price = self.execution_order_limit_check_price( + date, + data, + symbol, + snapshot, + OrderSide::Sell, + algo_request, + ); let adjusted_candidate; - let candidate_for_check = if self.aiquant_execution_rules - && self.aiquant_sell_allow_flag_is_stale_lower_limit(candidate, snapshot, check_price) - { - adjusted_candidate = crate::data::CandidateEligibility { - allow_sell: true, - ..candidate.clone() + let candidate_for_check = + if self.sell_allow_flag_is_stale_lower_limit(candidate, snapshot, check_price) { + adjusted_candidate = crate::data::CandidateEligibility { + allow_sell: true, + ..candidate.clone() + }; + &adjusted_candidate + } else { + candidate }; - &adjusted_candidate - } else { - candidate - }; if let Some(reason) = ChinaAShareRiskControl::sell_rejection_reason_with_config( date, candidate_for_check, @@ -3051,7 +3080,7 @@ where ) { return RuleCheck::reject(reason); } - if !self.aiquant_execution_rules && !self.rules.duplicates_standard_china_risk() { + if !self.rules.duplicates_standard_china_risk() { return self.rules.can_sell( date, snapshot, @@ -3063,7 +3092,7 @@ where RuleCheck::allow() } - fn aiquant_sell_allow_flag_is_stale_lower_limit( + fn sell_allow_flag_is_stale_lower_limit( &self, candidate: &crate::data::CandidateEligibility, snapshot: &crate::data::DailyMarketSnapshot, @@ -3082,11 +3111,11 @@ where &self, date: NaiveDate, portfolio: &PortfolioState, - data: &DataSet, + _data: &DataSet, symbol: &str, current_qty: u32, - minimum_order_quantity: u32, - order_step_size: u32, + _minimum_order_quantity: u32, + _order_step_size: u32, ) -> u32 { if current_qty == 0 { return 0; @@ -3094,93 +3123,23 @@ where let Some(position) = portfolio.position(symbol) else { return 0; }; - if self.aiquant_execution_rules { - let sellable = position - .sellable_qty(date) - .saturating_sub(self.reserved_open_sell_quantity(symbol, None)); - return current_qty.saturating_sub(sellable.min(current_qty)); - } - let Ok(snapshot) = data.require_market(date, symbol) else { - return current_qty; - }; - let Ok(candidate) = data.require_candidate(date, symbol) else { - return current_qty; - }; - let rule = self.sell_rule_check_for_order( - date, - data, - symbol, - snapshot, - candidate, - data.instrument(symbol), - position, - None, - ); - if !rule.allowed { - return current_qty; - } let sellable = position .sellable_qty(date) .saturating_sub(self.reserved_open_sell_quantity(symbol, None)); - let sell_limit = match self.market_fillable_quantity( - snapshot, - OrderSide::Sell, - sellable.min(current_qty), - minimum_order_quantity, - order_step_size, - 0, - sellable >= current_qty, - ) { - Ok(quantity) => quantity.min(sellable).min(current_qty), - Err(_) => 0, - }; - current_qty.saturating_sub(sell_limit) + current_qty.saturating_sub(sellable.min(current_qty)) } fn maximum_target_quantity( &self, - date: NaiveDate, + _date: NaiveDate, _portfolio: &PortfolioState, - data: &DataSet, - symbol: &str, - current_qty: u32, - minimum_order_quantity: u32, - order_step_size: u32, + _data: &DataSet, + _symbol: &str, + _current_qty: u32, + _minimum_order_quantity: u32, + _order_step_size: u32, ) -> u32 { - if self.aiquant_execution_rules { - return u32::MAX; - } - let Ok(snapshot) = data.require_market(date, symbol) else { - return current_qty; - }; - let Ok(candidate) = data.require_candidate(date, symbol) else { - return current_qty; - }; - let rule = self.buy_rule_check_for_order( - date, - data, - symbol, - snapshot, - candidate, - data.instrument(symbol), - None, - ); - if !rule.allowed { - return current_qty; - } - let additional_limit = match self.market_fillable_quantity( - snapshot, - OrderSide::Buy, - u32::MAX, - minimum_order_quantity, - order_step_size, - 0, - false, - ) { - Ok(quantity) => quantity, - Err(_) => 0, - }; - current_qty.saturating_add(additional_limit) + u32::MAX } fn estimated_sell_net_cash(&self, date: NaiveDate, price: f64, quantity: u32) -> f64 { @@ -3298,6 +3257,32 @@ where gross + cost.total() } + fn can_afford_minimum_buy( + &self, + date: NaiveDate, + portfolio: &PortfolioState, + data: &DataSet, + symbol: &str, + ) -> bool { + let Some(snapshot) = data.market(date, symbol) else { + return true; + }; + let minimum_order_quantity = self.minimum_order_quantity(data, symbol); + let order_step_size = self.order_step_size(data, symbol); + let minimum_buy_quantity = self.round_buy_quantity( + minimum_order_quantity, + minimum_order_quantity, + order_step_size, + ); + if minimum_buy_quantity == 0 { + return false; + } + let minimum_execution_price = + self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(minimum_buy_quantity)); + self.estimated_buy_cash_out(date, minimum_execution_price, minimum_buy_quantity) + <= portfolio.cash() + 1e-6 + } + fn process_sell( &self, date: NaiveDate, @@ -3977,13 +3962,8 @@ where return Ok(()); }; - let current_value = if self.aiquant_execution_rules { - let valuation_price = self.target_value_valuation_price(date, data, symbol, snapshot); - valuation_price * current_qty as f64 - } else { - let valuation_price = self.target_value_valuation_price(date, data, symbol, snapshot); - valuation_price * current_qty as f64 - }; + let valuation_price = self.target_value_valuation_price(date, data, symbol, snapshot); + let current_value = valuation_price * current_qty as f64; let cash_delta = target_value.max(0.0) - current_value; if cash_delta.abs() > f64::EPSILON { @@ -4256,8 +4236,7 @@ where commission_state: &mut BTreeMap, report: &mut BrokerExecutionReport, ) -> Result<(), BacktestError> { - let price = if self.aiquant_execution_rules && limit_price.is_finite() && limit_price > 0.0 - { + let price = if limit_price.is_finite() && limit_price > 0.0 { limit_price } else { data.market(date, symbol) @@ -7645,7 +7624,7 @@ mod tests { } #[test] - fn target_value_valuation_uses_daily_snapshot_but_value_order_sizing_uses_intraday_minute() { + fn scheduled_target_value_valuation_and_sizing_use_same_intraday_price() { let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date"); let broker = BrokerSimulator::new_with_execution_price( ChinaAShareCostModel::default(), @@ -7672,7 +7651,7 @@ mod tests { assert_eq!( broker.target_value_valuation_price(date, &data, "000001.SZ", snapshot), - 10.0 + 11.0 ); assert_eq!( broker.value_sell_sizing_price(date, &data, "000001.SZ", snapshot), @@ -8138,7 +8117,7 @@ mod tests { } #[test] - fn aiquant_target_portfolio_smart_defers_buy_risk_during_target_sizing() { + fn target_portfolio_smart_defers_buy_risk_until_order_validation() { 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()], @@ -8154,30 +8133,17 @@ mod tests { let mut target_weights = BTreeMap::new(); target_weights.insert("000001.SZ".to_string(), 0.50); - let default_broker = BrokerSimulator::new_with_execution_price( + let broker = BrokerSimulator::new_with_execution_price( ChinaAShareCostModel::default(), ChinaEquityRuleHooks, PriceField::Open, ) .with_volume_limit(false) .with_liquidity_limit(false); - let (default_targets, _) = default_broker + let (targets, _) = broker .target_quantities(date, &portfolio, &data, &target_weights) - .expect("default target quantities"); - assert_eq!(default_targets.get("000001.SZ").copied().unwrap_or(0), 0); - - let aiquant_broker = BrokerSimulator::new_with_execution_price( - ChinaAShareCostModel::default(), - ChinaEquityRuleHooks, - PriceField::Open, - ) - .with_aiquant_execution_rules(true) - .with_volume_limit(false) - .with_liquidity_limit(false); - let (aiquant_targets, _) = aiquant_broker - .target_quantities(date, &portfolio, &data, &target_weights) - .expect("aiquant target quantities"); - assert_eq!(aiquant_targets.get("000001.SZ").copied(), Some(49_900)); + .expect("target quantities"); + assert_eq!(targets.get("000001.SZ").copied(), Some(49_900)); } #[test] @@ -8198,7 +8164,6 @@ mod tests { ChinaEquityRuleHooks, PriceField::Open, ) - .with_aiquant_execution_rules(true) .with_volume_limit(false) .with_liquidity_limit(false); let mut portfolio = PortfolioState::new(1_000_000.0); @@ -8294,7 +8259,6 @@ mod tests { ChinaEquityRuleHooks, PriceField::Open, ) - .with_aiquant_execution_rules(true) .with_volume_limit(false) .with_liquidity_limit(false) .with_inactive_limit(false); @@ -8660,7 +8624,6 @@ mod tests { ChinaEquityRuleHooks, PriceField::Open, ) - .with_aiquant_execution_rules(true) .with_rebalance_cash_mode(mode) .with_volume_limit(false) .with_liquidity_limit(false) @@ -9093,16 +9056,12 @@ mod tests { Some(1_000) ); assert!(report.fill_events.is_empty()); - assert!(report.order_events.is_empty()); - assert!( - report - .diagnostics - .iter() - .any(|item| item.contains("rebalance_target_clipped") - && item.contains("000001.SZ")), - "{:?}", - report.diagnostics - ); + assert!(report.order_events.iter().any(|event| { + event.symbol == symbol + && event.side == OrderSide::Sell + && event.status == OrderStatus::Rejected + && event.reason.contains("missing") + })); } #[test] @@ -9204,7 +9163,6 @@ mod tests { .with_intraday_execution_start_time(date.and_hms_opt(10, 40, 0).unwrap().time()) .with_slippage_model(SlippageModel::PriceRatio(0.002)) .with_strict_value_budget(true) - .with_aiquant_execution_rules(true) .with_volume_limit(false) .with_liquidity_limit(false) .with_inactive_limit(false); @@ -9627,8 +9585,7 @@ mod tests { ChinaAShareCostModel::default(), ChinaEquityRuleHooks, PriceField::Last, - ) - .with_aiquant_execution_rules(true); + ); let aiquant_rule = aiquant_broker.buy_rule_check(date, &snapshot, &candidate, None); assert!(!aiquant_rule.allowed); assert_eq!(aiquant_rule.reason.as_deref(), Some("buy_disabled")); @@ -9691,7 +9648,6 @@ mod tests { ChinaEquityRuleHooks, PriceField::Last, ) - .with_aiquant_execution_rules(true) .with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time()) .with_volume_limit(false) .with_liquidity_limit(false) @@ -9781,7 +9737,6 @@ mod tests { ChinaEquityRuleHooks, PriceField::Last, ) - .with_aiquant_execution_rules(true) .with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time()) .with_volume_limit(false) .with_liquidity_limit(false) diff --git a/crates/fidc-core/src/cost.rs b/crates/fidc-core/src/cost.rs index 8afa29a..4aff6ef 100644 --- a/crates/fidc-core/src/cost.rs +++ b/crates/fidc-core/src/cost.rs @@ -5,8 +5,6 @@ use chrono::NaiveDate; use crate::events::OrderSide; use crate::risk_control::TradingConstraintConfig; -pub const STOCK_PIT_TAX_CHANGE_DATE: (i32, u32, u32) = (2023, 8, 28); - #[derive(Debug, Clone, Copy)] pub struct TradingCost { pub commission: f64, @@ -45,26 +43,11 @@ pub struct ChinaAShareCostModel { impl Default for ChinaAShareCostModel { fn default() -> Self { - Self { - commission_rate: 0.0008, - stamp_tax_rate_before_change: 0.001, - stamp_tax_rate_after_change: 0.0005, - stamp_tax_change_date: default_stamp_tax_change_date(), - minimum_commission: 5.0, - } + Self::from_trading_constraints(TradingConstraintConfig::default()) } } impl ChinaAShareCostModel { - pub fn aiquant_default() -> Self { - Self { - commission_rate: 0.0003, - stamp_tax_rate_before_change: 0.0005, - stamp_tax_rate_after_change: 0.0005, - ..Self::default() - } - } - pub fn from_trading_constraints(config: TradingConstraintConfig) -> Self { Self { commission_rate: config.commission_rate, @@ -135,15 +118,6 @@ impl ChinaAShareCostModel { } } -fn default_stamp_tax_change_date() -> NaiveDate { - NaiveDate::from_ymd_opt( - STOCK_PIT_TAX_CHANGE_DATE.0, - STOCK_PIT_TAX_CHANGE_DATE.1, - STOCK_PIT_TAX_CHANGE_DATE.2, - ) - .expect("valid pit tax change date") -} - impl CostModel for ChinaAShareCostModel { fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost { if gross_amount <= 0.0 { @@ -192,8 +166,8 @@ mod tests { use super::*; #[test] - fn aiquant_default_matches_current_backtest_fee_model() { - let model = ChinaAShareCostModel::aiquant_default(); + fn default_matches_configurable_trading_constraints() { + let model = ChinaAShareCostModel::default(); let date = NaiveDate::from_ymd_opt(2025, 11, 11).expect("valid date"); assert!((model.commission_for(248_059.812) - 74.4179436).abs() < 1e-9); diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 613fd34..c910f15 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -376,7 +376,6 @@ pub struct PlatformExprStrategyConfig { pub calendar_rebalance_interval: bool, pub max_holding_days: Option, pub weak_market_shrink_overweight_threshold: Option, - pub aiquant_transaction_cost: bool, pub commission_rate: Option, pub minimum_commission: Option, pub stamp_tax_rate_before_change: Option, @@ -455,7 +454,6 @@ fn band_low(index_close) { calendar_rebalance_interval: false, max_holding_days: None, weak_market_shrink_overweight_threshold: None, - aiquant_transaction_cost: false, commission_rate: None, minimum_commission: None, stamp_tax_rate_before_change: None, @@ -466,7 +464,7 @@ fn band_low(index_close) { sell_then_buy_delay_slippage_rate: 0.0, risk_config: FidcRiskControlConfig::default(), slippage_model: SlippageModel::None, - matching_type: MatchingType::MinuteLast, + matching_type: MatchingType::CurrentBarClose, quote_quantity_limit: true, current_day_precomputed_factors: false, prefer_precomputed_rolling_factors: false, @@ -1622,9 +1620,7 @@ impl PlatformExprStrategy { if let Some(value) = self.config.commission_rate { model.commission_rate = value; } - if let Some(value) = self.config.minimum_commission - && (value > 0.0 || !self.config.aiquant_transaction_cost) - { + if let Some(value) = self.config.minimum_commission { model.minimum_commission = value; } if let Some(value) = self.config.stamp_tax_rate_before_change { @@ -1650,8 +1646,8 @@ impl PlatformExprStrategy { if position.quantity == 0 { continue; } - let mark_price = if self.config.aiquant_transaction_cost { - self.aiquant_scheduled_last_price(ctx, date, &position.symbol) + let mark_price = if self.uses_intraday_execution_quotes() { + self.scheduled_last_price(ctx, date, &position.symbol) .or_else(|| ctx.data.price(date, &position.symbol, PriceField::Last)) .or_else(|| { ctx.data @@ -1790,9 +1786,21 @@ impl PlatformExprStrategy { } } + fn uses_intraday_execution_quotes(&self) -> bool { + matches!( + self.config.matching_type, + MatchingType::MinuteLast + | MatchingType::MinuteBestOwn + | MatchingType::MinuteBestCounterparty + | MatchingType::Vwap + | MatchingType::Twap + ) || (self.config.matching_type == MatchingType::CurrentBarClose + && self.config.intraday_execution_time.is_some()) + } + fn projected_execution_price(&self, market: &DailyMarketSnapshot, side: OrderSide) -> f64 { let price_field = self.projected_execution_price_field(); - if self.config.aiquant_transaction_cost && price_field != PriceField::Open { + if self.uses_intraday_execution_quotes() && price_field != PriceField::Open { let last = market.price(PriceField::Last); if last.is_finite() && last > 0.0 { return last; @@ -1819,11 +1827,11 @@ impl PlatformExprStrategy { OrderSide::Sell => market.sell_price(PriceField::Open), }; } - if self.config.aiquant_transaction_cost { + if self.uses_intraday_execution_quotes() { let scheduled_price = match side { - OrderSide::Buy => self.aiquant_scheduled_buy_price(ctx, date, symbol), + OrderSide::Buy => self.scheduled_buy_price(ctx, date, symbol), OrderSide::Sell => { - self.aiquant_scheduled_sell_price_at_time(ctx, date, symbol, execution_time) + self.scheduled_sell_price_at_time(ctx, date, symbol, execution_time) } }; return scheduled_price.unwrap_or_else(|| market.price(PriceField::Last)); @@ -1851,7 +1859,7 @@ impl PlatformExprStrategy { _symbol: &str, _execution_state: &ProjectedExecutionState, ) -> NaiveDateTime { - if self.config.aiquant_transaction_cost { + if self.uses_intraday_execution_quotes() { return date.and_time(self.intraday_execution_start_time()); } if let Some(active_datetime) = ctx.active_datetime @@ -1876,23 +1884,23 @@ impl PlatformExprStrategy { ) } - fn aiquant_scheduled_quote<'a>( + fn scheduled_quote<'a>( &self, ctx: &'a StrategyContext<'_>, date: NaiveDate, symbol: &str, ) -> Option<&'a crate::data::IntradayExecutionQuote> { - self.aiquant_scheduled_quote_at_time(ctx, date, symbol, None) + self.scheduled_quote_at_time(ctx, date, symbol, None) } - fn aiquant_scheduled_quote_at_time<'a>( + fn scheduled_quote_at_time<'a>( &self, ctx: &'a StrategyContext<'_>, date: NaiveDate, symbol: &str, execution_time: Option, ) -> Option<&'a crate::data::IntradayExecutionQuote> { - if !self.config.aiquant_transaction_cost { + if !self.uses_intraday_execution_quotes() { return None; } let start_cursor = self.projected_execution_start_cursor_at_time( @@ -1909,37 +1917,36 @@ impl PlatformExprStrategy { .max_by_key(|quote| quote.timestamp) } - fn aiquant_scheduled_buy_price( + fn scheduled_buy_price( &self, ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str, ) -> Option { - self.aiquant_scheduled_quote(ctx, date, symbol) + self.scheduled_quote(ctx, date, symbol) .and_then(|quote| self.projected_quote_raw_price(quote, OrderSide::Buy)) } - fn aiquant_scheduled_sell_price_at_time( + fn scheduled_sell_price_at_time( &self, ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str, execution_time: Option, ) -> Option { - self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time) + self.scheduled_quote_at_time(ctx, date, symbol, execution_time) .and_then(|quote| self.projected_quote_raw_price(quote, OrderSide::Sell)) } - fn aiquant_scheduled_last_price( + fn scheduled_last_price( &self, ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str, ) -> Option { - self.aiquant_scheduled_quote(ctx, date, symbol) - .and_then(|quote| { - (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price) - }) + self.scheduled_quote(ctx, date, symbol).and_then(|quote| { + (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price) + }) } fn projected_apply_slippage( @@ -2423,7 +2430,7 @@ impl PlatformExprStrategy { execution_time, ) .or_else(|| { - if self.config.aiquant_transaction_cost + if self.uses_intraday_execution_quotes() && !Self::defer_projection_execution_risk(ctx, date) { return None; @@ -2498,19 +2505,8 @@ impl PlatformExprStrategy { return self.project_target_zero(ctx, projected, date, symbol, execution_state); } let market = ctx.data.market(date, symbol)?; - let current_value = if self.config.aiquant_transaction_cost { - self.projected_target_value_current_position_value(ctx, projected, date, symbol) - } else { - let valuation_price = if market.close.is_finite() && market.close > 0.0 { - market.close - } else { - self.projected_execution_price(market, OrderSide::Buy) - }; - if !valuation_price.is_finite() || valuation_price <= 0.0 { - return None; - } - valuation_price * current_qty as f64 - }; + let current_value = + self.projected_target_value_current_position_value(ctx, projected, date, symbol); if !current_value.is_finite() || current_value <= 0.0 { return None; } @@ -2530,7 +2526,7 @@ impl PlatformExprStrategy { return None; } let sizing_price = self - .aiquant_scheduled_quote(ctx, date, symbol) + .scheduled_quote(ctx, date, symbol) .and_then(|quote| { if quote.last_price.is_finite() && quote.last_price > 0.0 { Some(quote.last_price) @@ -2612,14 +2608,10 @@ impl PlatformExprStrategy { let Some(market) = ctx.data.market(date, symbol) else { return position.market_value(); }; - if self.config.aiquant_transaction_cost { - if let Some(price) = self - .aiquant_scheduled_quote(ctx, date, symbol) - .and_then(|quote| { - (quote.last_price.is_finite() && quote.last_price > 0.0) - .then_some(quote.last_price) - }) - { + if self.uses_intraday_execution_quotes() { + if let Some(price) = self.scheduled_quote(ctx, date, symbol).and_then(|quote| { + (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price) + }) { return position.quantity as f64 * price; } } @@ -2672,7 +2664,7 @@ impl PlatformExprStrategy { } let context_position = ctx.portfolio.position(symbol); let mark_price = self - .aiquant_scheduled_last_price(ctx, date, symbol) + .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)) .or_else(|| context_position.map(|position| position.last_price)) @@ -2741,7 +2733,7 @@ impl PlatformExprStrategy { projected: &mut PortfolioState, projected_execution_state: &mut ProjectedExecutionState, order_intents: &mut Vec, - aiquant_available_cash: &mut f64, + available_cash: &mut f64, slot_working_symbols: &mut BTreeSet, same_bar_buy_symbols: &mut BTreeSet, pending_full_close_symbols: &BTreeSet, @@ -2805,7 +2797,7 @@ impl PlatformExprStrategy { Some(deferred_target_values), slot_blocking_symbols, ); - let available_buy_cash = slot_buy_cash.min(*aiquant_available_cash); + let available_buy_cash = slot_buy_cash.min(*available_cash); if debug_daily_top_up { daily_top_up_debug_notes.push(format!( "daily_top_up_budget date={} working={} value_symbols={} same_bar_buys={} active_value={:.4} pending_buy_value={:.4} target_budget={:.4} slot_buy_cash={:.4} available_cash={:.4} portfolio_cash={:.4}", @@ -2818,7 +2810,7 @@ impl PlatformExprStrategy { target_budget, slot_buy_cash, available_buy_cash, - *aiquant_available_cash + *available_cash )); } if slot_buy_cash <= 0.0 || available_buy_cash < slot_buy_cash * 0.5 { @@ -2897,7 +2889,7 @@ impl PlatformExprStrategy { }); self.remember_position_entry_date(symbol, signal_date); let spent = (cash_before_buy - projected.cash()).max(0.0); - *aiquant_available_cash = (*aiquant_available_cash - spent).max(0.0); + *available_cash = (*available_cash - spent).max(0.0); slot_working_symbols.insert(symbol.clone()); same_bar_buy_symbols.insert(symbol.clone()); if order_result.filled_quantity == 0 { @@ -2911,7 +2903,7 @@ impl PlatformExprStrategy { buy_cash, order_result.filled_quantity, spent, - *aiquant_available_cash + *available_cash )); } submitted_any = true; @@ -2973,13 +2965,9 @@ impl PlatformExprStrategy { { return *target_value; } - if self.config.aiquant_transaction_cost { - self.projected_strategy_visible_position_value_for_remaining_buy_cash( - ctx, projected, date, symbol, - ) - } else { - self.projected_position_value_at_execution_price(ctx, projected, date, symbol) - } + self.projected_strategy_visible_position_value_for_remaining_buy_cash( + ctx, projected, date, symbol, + ) }) .filter(|value| value.is_finite() && *value > 0.0) .sum::() @@ -3020,17 +3008,14 @@ impl PlatformExprStrategy { { return ProjectedOrderValueResult::not_submitted(); } - let raw_sizing_price = if self.config.aiquant_transaction_cost { - self.aiquant_scheduled_last_price(ctx, date, symbol) + let raw_sizing_price = if self.uses_intraday_execution_quotes() { + self.scheduled_last_price(ctx, date, symbol) .unwrap_or_else(|| self.projected_execution_price(market, OrderSide::Buy)) } else { self.projected_execution_price(market, OrderSide::Buy) }; - let sizing_price = if self.config.aiquant_transaction_cost { - self.projected_apply_slippage(market, OrderSide::Buy, raw_sizing_price, None) - } else { - raw_sizing_price - }; + let sizing_price = + self.projected_apply_slippage(market, OrderSide::Buy, raw_sizing_price, None); if !sizing_price.is_finite() || sizing_price <= 0.0 { return ProjectedOrderValueResult::not_submitted(); } @@ -3420,7 +3405,7 @@ impl PlatformExprStrategy { execution_time: NaiveTime, ) -> Result, BacktestError> { if self - .aiquant_scheduled_quote_at_time(ctx, date, symbol, Some(execution_time)) + .scheduled_quote_at_time(ctx, date, symbol, Some(execution_time)) .is_none() { return Ok(None); @@ -3493,11 +3478,11 @@ impl PlatformExprStrategy { let market = ctx.data.require_market(date, symbol)?; let feature_market = ctx.data.market(factor_date, symbol).unwrap_or(market); - let intraday_same_day_factor = self.config.aiquant_transaction_cost + let intraday_same_day_factor = self.uses_intraday_execution_quotes() && factor_date == date && !ctx.is_lagged_execution(); let decision_quote = if use_intraday_quote { - self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time) + self.scheduled_quote_at_time(ctx, date, symbol, execution_time) } else { None }; @@ -6268,9 +6253,7 @@ impl PlatformExprStrategy { } else { previous_factor_date }; - let selection_date = if self.config.aiquant_transaction_cost - && self.config.intraday_execution_time.is_some() - && !ctx.is_lagged_execution() + let selection_date = if self.uses_intraday_execution_quotes() && !ctx.is_lagged_execution() { ctx.execution_date } else { @@ -7936,9 +7919,6 @@ impl PlatformExprStrategy { let Ok(candidate) = ctx.data.require_candidate(date, symbol) else { return false; }; - if !self.config.aiquant_transaction_cost && !candidate.allow_sell { - return false; - } let check_price = self.projected_risk_check_price( ctx, date, @@ -8970,10 +8950,7 @@ impl PlatformExprStrategy { .unwrap_or(position.average_cost); let stop_take_base_price = match self.config.stop_take_reference_price_mode { PlatformStopTakeReferencePriceMode::PositionCostBasis => { - if self.config.aiquant_transaction_cost - && position.average_cost.is_finite() - && position.average_cost > 0.0 - { + if position.average_cost.is_finite() && position.average_cost > 0.0 { position.average_cost } else { entry_avg_price @@ -9000,7 +8977,7 @@ impl PlatformExprStrategy { return Ok((false, false)); } let scheduled_time = self.intraday_execution_start_time(); - if self.config.aiquant_transaction_cost + if self.uses_intraday_execution_quotes() && matches!( self.config.matching_type, MatchingType::MinuteLast @@ -9010,7 +8987,7 @@ impl PlatformExprStrategy { | MatchingType::Twap ) && self - .aiquant_scheduled_quote_at_time(ctx, signal_date, symbol, Some(scheduled_time)) + .scheduled_quote_at_time(ctx, signal_date, symbol, Some(scheduled_time)) .is_none() { return Ok((false, false)); @@ -9136,9 +9113,7 @@ impl PlatformExprStrategy { }) }); } - if self.config.aiquant_transaction_cost - && self.config.matching_type == MatchingType::NextBarOpen - { + if self.config.matching_type == MatchingType::NextBarOpen { let market = ctx.data.require_market(signal_date, symbol)?; if market.close.is_finite() && market.close > 0.0 { return Ok(market.close); @@ -9155,7 +9130,7 @@ impl Strategy for PlatformExprStrategy { fn decision_quote_times(&self) -> Vec { let mut times = BTreeSet::new(); - if self.config.aiquant_transaction_cost || self.config.intraday_execution_time.is_some() { + if self.uses_intraday_execution_quotes() { times.insert(self.intraday_execution_start_time()); } if self.config.delayed_limit_open_exit_enabled { @@ -9265,14 +9240,14 @@ impl Strategy for PlatformExprStrategy { }; let marked_total_value = self.signal_visible_total_value(ctx, projection_date, defer_execution_risk); - let mut aiquant_total_value = if marked_total_value.is_finite() && marked_total_value > 0.0 - { - marked_total_value - } else if day.total_value.is_finite() && day.total_value > 0.0 { - day.total_value - } else { - ctx.portfolio.total_value() - }; + let mut strategy_visible_total_value = + if marked_total_value.is_finite() && marked_total_value > 0.0 { + marked_total_value + } else if day.total_value.is_finite() && day.total_value > 0.0 { + day.total_value + } else { + ctx.portfolio.total_value() + }; let (band_low, band_high) = if self.config.rotation_enabled && !in_skip_window { self.market_cap_band(ctx, &day)? } else { @@ -9467,7 +9442,7 @@ impl Strategy for PlatformExprStrategy { continue; } if self - .aiquant_scheduled_quote_at_time( + .scheduled_quote_at_time( ctx, projection_date, &symbol, @@ -9538,7 +9513,7 @@ impl Strategy for PlatformExprStrategy { let projected_total = self.marked_total_value_for_portfolio(ctx, &projected, projection_date); if projected_total.is_finite() && projected_total > 0.0 { - aiquant_total_value = projected_total; + strategy_visible_total_value = projected_total; } } @@ -9605,7 +9580,7 @@ impl Strategy for PlatformExprStrategy { }); } - let mut aiquant_available_cash = if delayed_sold_symbols.is_empty() { + let mut available_cash = if delayed_sold_symbols.is_empty() { ctx.portfolio.cash() } else { projected.cash() @@ -9625,7 +9600,7 @@ impl Strategy for PlatformExprStrategy { && !periodic_rebalance && trading_ratio > 0.0 && selection_limit > 0; - let daily_top_up_target_budget = aiquant_total_value * trading_ratio; + let daily_top_up_target_budget = strategy_visible_total_value * trading_ratio; let mut daily_top_up_pending_buy_value = 0.0_f64; let mut deferred_daily_target_values = BTreeMap::::new(); let mut pending_full_close_symbols = BTreeSet::::new(); @@ -9684,7 +9659,7 @@ impl Strategy for PlatformExprStrategy { .is_some(); if close_submitted { self.refresh_available_cash_after_projected_sell( - &mut aiquant_available_cash, + &mut available_cash, &projected, ); if Self::projected_position_is_flat(&projected, &position.symbol) { @@ -9704,58 +9679,56 @@ impl Strategy for PlatformExprStrategy { } } } - if self.config.aiquant_transaction_cost { - for position in ctx.portfolio.positions().values() { - if position.quantity == 0 - || delayed_sold_symbols.contains(&position.symbol) - || self.pending_highlimit_holdings.contains(&position.symbol) - || unresolved_delisted_symbols.contains(&position.symbol) - { - continue; - } - if self.regular_sell_should_wait_due_to_highlimit( + for position in ctx.portfolio.positions().values() { + if position.quantity == 0 + || delayed_sold_symbols.contains(&position.symbol) + || self.pending_highlimit_holdings.contains(&position.symbol) + || unresolved_delisted_symbols.contains(&position.symbol) + { + continue; + } + if self.regular_sell_should_wait_due_to_highlimit( + ctx, + projection_date, + &position.symbol, + self.intraday_execution_start_time(), + )? { + continue; + } + if self.candidate_requires_risk_level_forced_exit( + ctx, + projection_date, + &position.symbol, + risk_level_forced_exit_time, + defer_execution_risk, + )? { + pending_full_close_symbols.insert(position.symbol.clone()); + self.pending_full_close_symbols + .insert(position.symbol.clone()); + pending_risk_level_forced_exit_symbols.insert(position.symbol.clone()); + continue; + } + let can_sell = defer_execution_risk + || self.can_sell_position(ctx, execution_date, &position.symbol); + if !can_sell { + continue; + } + if self.config.delayed_limit_open_exit_enabled { + let stock = match self.stock_state_at_time( ctx, projection_date, &position.symbol, - self.intraday_execution_start_time(), - )? { + Some(self.intraday_execution_start_time()), + ) { + Ok(stock) => stock, + Err(BacktestError::Data(crate::data::DataSetError::MissingSnapshot { + .. + })) => continue, + Err(error) => return Err(error), + }; + if stock.upper_limit > 0.0 && stock.last >= stock.upper_limit { continue; } - if self.candidate_requires_risk_level_forced_exit( - ctx, - projection_date, - &position.symbol, - risk_level_forced_exit_time, - defer_execution_risk, - )? { - pending_full_close_symbols.insert(position.symbol.clone()); - self.pending_full_close_symbols - .insert(position.symbol.clone()); - pending_risk_level_forced_exit_symbols.insert(position.symbol.clone()); - continue; - } - let can_sell = defer_execution_risk - || self.can_sell_position(ctx, execution_date, &position.symbol); - if !can_sell { - continue; - } - if self.config.delayed_limit_open_exit_enabled { - let stock = match self.stock_state_at_time( - ctx, - projection_date, - &position.symbol, - Some(self.intraday_execution_start_time()), - ) { - Ok(stock) => stock, - Err(BacktestError::Data(crate::data::DataSetError::MissingSnapshot { - .. - })) => continue, - Err(error) => return Err(error), - }; - if stock.upper_limit > 0.0 && stock.last >= stock.upper_limit { - continue; - } - } } } @@ -9785,10 +9758,7 @@ impl Strategy for PlatformExprStrategy { ) .is_some(); if close_submitted { - self.refresh_available_cash_after_projected_sell( - &mut aiquant_available_cash, - &projected, - ); + self.refresh_available_cash_after_projected_sell(&mut available_cash, &projected); if Self::projected_position_is_flat(&projected, &symbol) { same_day_sold_symbols.insert(symbol.clone()); slot_working_symbols.remove(&symbol); @@ -9801,8 +9771,7 @@ impl Strategy for PlatformExprStrategy { let stop_take_exit_signal_symbols = current_stop_take_exit_symbols.clone(); - if self.config.aiquant_transaction_cost - && self.config.rotation_enabled + if self.config.rotation_enabled && self.config.daily_position_target_adjust_enabled && trading_ratio > 0.0 && (self.config.target_portfolio_daily_enabled || trading_ratio < 1.0) @@ -9813,7 +9782,7 @@ impl Strategy for PlatformExprStrategy { && (!ctx.portfolio.positions().is_empty() || (persistent_model_lifecycle && !self.position_entry_dates.is_empty())) { - if aiquant_total_value.is_finite() && aiquant_total_value > 0.0 { + if strategy_visible_total_value.is_finite() && strategy_visible_total_value > 0.0 { for position in ctx.portfolio.positions().values() { if position.quantity == 0 || delayed_sold_symbols.contains(&position.symbol) @@ -9841,8 +9810,9 @@ impl Strategy for PlatformExprStrategy { &position.symbol, )?; let stock_scale = self.buy_scale(ctx, &day, &decision_stock)?; - let target_value = - aiquant_total_value * trading_ratio / selection_limit as f64 * stock_scale; + let target_value = strategy_visible_total_value * trading_ratio + / selection_limit as f64 + * stock_scale; if !target_value.is_finite() || target_value <= 0.0 { continue; } @@ -9941,7 +9911,7 @@ impl Strategy for PlatformExprStrategy { &symbol, )?; let stock_scale = self.buy_scale(ctx, &day, &decision_stock)?; - let target_value = aiquant_total_value * trading_ratio + let target_value = strategy_visible_total_value * trading_ratio / selection_limit as f64 * stock_scale; if !target_value.is_finite() || target_value <= 0.0 { @@ -9977,7 +9947,7 @@ impl Strategy for PlatformExprStrategy { } } } - aiquant_available_cash = projected.cash(); + available_cash = projected.cash(); } } @@ -10012,7 +9982,7 @@ impl Strategy for PlatformExprStrategy { .is_some(); if close_submitted { self.refresh_available_cash_after_projected_sell( - &mut aiquant_available_cash, + &mut available_cash, &projected, ); slot_working_symbols.remove(&position.symbol); @@ -10047,7 +10017,7 @@ impl Strategy for PlatformExprStrategy { &mut projected, &mut projected_execution_state, &mut order_intents, - &mut aiquant_available_cash, + &mut available_cash, &mut slot_working_symbols, &mut same_bar_buy_symbols, &pending_full_close_symbols, @@ -10120,7 +10090,7 @@ impl Strategy for PlatformExprStrategy { .is_some(); if close_submitted { self.refresh_available_cash_after_projected_sell( - &mut aiquant_available_cash, + &mut available_cash, &projected, ); slot_working_symbols.remove(&position.symbol); @@ -10159,7 +10129,7 @@ impl Strategy for PlatformExprStrategy { &mut projected, &mut projected_execution_state, &mut order_intents, - &mut aiquant_available_cash, + &mut available_cash, &mut slot_working_symbols, &mut same_bar_buy_symbols, &pending_full_close_symbols, @@ -10208,7 +10178,7 @@ impl Strategy for PlatformExprStrategy { .is_some(); if close_submitted { self.refresh_available_cash_after_projected_sell( - &mut aiquant_available_cash, + &mut available_cash, &projected, ); if Self::projected_position_is_flat(&projected, &position.symbol) { @@ -10246,7 +10216,7 @@ impl Strategy for PlatformExprStrategy { &mut projected, &mut projected_execution_state, &mut order_intents, - &mut aiquant_available_cash, + &mut available_cash, &mut slot_working_symbols, &mut same_bar_buy_symbols, &pending_full_close_symbols, @@ -10290,7 +10260,7 @@ impl Strategy for PlatformExprStrategy { .is_some(); if close_submitted { self.refresh_available_cash_after_projected_sell( - &mut aiquant_available_cash, + &mut available_cash, &projected, ); if Self::projected_position_is_flat(&projected, &position.symbol) { @@ -10327,7 +10297,7 @@ impl Strategy for PlatformExprStrategy { &mut projected, &mut projected_execution_state, &mut order_intents, - &mut aiquant_available_cash, + &mut available_cash, &mut slot_working_symbols, &mut same_bar_buy_symbols, &pending_full_close_symbols, @@ -10380,8 +10350,9 @@ impl Strategy for PlatformExprStrategy { &symbol, )?; let stock_scale = self.buy_scale(ctx, &day, &decision_stock)?; - let target_value = - aiquant_total_value * trading_ratio / selection_limit as f64 * stock_scale; + let target_value = strategy_visible_total_value * trading_ratio + / selection_limit as f64 + * stock_scale; if !target_value.is_finite() || target_value <= 0.0 { continue; } @@ -10443,7 +10414,7 @@ impl Strategy for PlatformExprStrategy { &mut projected, &mut projected_execution_state, &mut order_intents, - &mut aiquant_available_cash, + &mut available_cash, &mut slot_working_symbols, &mut same_bar_buy_symbols, &pending_full_close_symbols, @@ -10470,7 +10441,6 @@ impl Strategy for PlatformExprStrategy { .keys() .cloned() .collect::>(); - let pre_rebalance_cash = projected.cash(); for symbol in pre_rebalance_symbols.iter() { if unresolved_delisted_symbols.contains(symbol) { continue; @@ -10513,13 +10483,7 @@ impl Strategy for PlatformExprStrategy { slot_working_symbols.remove(symbol); } - aiquant_available_cash = match self.effective_rebalance_cash_mode() { - RebalanceCashMode::PreOpenCash => pre_rebalance_cash, - RebalanceCashMode::SamePointNet | RebalanceCashMode::SellThenBuy => { - projected.cash() - } - }; - let target_budget = aiquant_total_value * trading_ratio; + let target_budget = strategy_visible_total_value * trading_ratio; let fixed_buy_cash = target_budget / selection_limit as f64; let mut rebalance_working_symbols = slot_working_symbols.clone(); if self.config.release_slot_on_exit_signal { @@ -10528,8 +10492,6 @@ impl Strategy for PlatformExprStrategy { slot_working_symbols.remove(symbol); } } - let rebalance_value_symbols = rebalance_working_symbols.clone(); - let mut rebalance_pending_buy_value = 0.0_f64; for symbol in stock_list.iter().take(selection_limit) { if unresolved_delisted_symbols.contains(symbol) { continue; @@ -10544,108 +10506,50 @@ impl Strategy for PlatformExprStrategy { symbol, )?; let stock_scale = self.buy_scale(ctx, &day, &decision_stock)?; - let target_cash = fixed_buy_cash * stock_scale; let released_exit_position = projected.positions().contains_key(symbol) && !rebalance_working_symbols.contains(symbol) && !same_day_sold_symbols.contains(symbol) && !pending_full_close_symbols.contains(symbol); if projected.positions().contains_key(symbol) && !released_exit_position { - if self.config.aiquant_transaction_cost && !rebalance_existing_positions { + if !rebalance_existing_positions { continue; } - if self.config.aiquant_transaction_cost { - let target_value = target_budget / selection_limit as f64 * stock_scale; - let before_qty = projected - .position(symbol) - .map(|position| position.quantity) - .unwrap_or(0); - let mut trial_projected = projected.clone(); - let mut trial_execution_state = projected_execution_state.clone(); - self.project_target_value( - ctx, - &mut trial_projected, - projection_date, - symbol, - target_value, - &mut trial_execution_state, - ); - let after_qty = trial_projected - .position(symbol) - .map(|position| position.quantity) - .unwrap_or(0); - if after_qty != before_qty { - projected = trial_projected; - projected_execution_state = trial_execution_state; - } - if Self::should_emit_rebalance_target_value( - defer_execution_risk, - before_qty, - after_qty, - ) { - order_intents.push(OrderIntent::TargetValue { - symbol: symbol.clone(), - target_value, - reason: "periodic_rebalance_target_adjust".to_string(), - }); - if after_qty > before_qty { - intraday_attempted_buys.insert(symbol.clone()); - } - } - aiquant_available_cash = projected.cash().max(0.0); - continue; - } - if same_day_sold_symbols.contains(symbol) - || unresolved_stop_loss_symbols.contains(symbol) - || intraday_attempted_buys.contains(symbol) - { - continue; - } - let current_value = self.projected_position_value_at_execution_price( + let target_value = target_budget / selection_limit as f64 * stock_scale; + let before_qty = projected + .position(symbol) + .map(|position| position.quantity) + .unwrap_or(0); + let mut trial_projected = projected.clone(); + let mut trial_execution_state = projected_execution_state.clone(); + self.project_target_value( ctx, - &projected, + &mut trial_projected, projection_date, symbol, + target_value, + &mut trial_execution_state, ); - let buy_cash = (target_cash - current_value).min(aiquant_available_cash); - if buy_cash <= 0.0 { - continue; + let after_qty = trial_projected + .position(symbol) + .map(|position| position.quantity) + .unwrap_or(0); + if after_qty != before_qty { + projected = trial_projected; + projected_execution_state = trial_execution_state; } - if !defer_execution_risk - && self - .buy_rejection_reason( - ctx, - execution_date, - symbol, - &self.stock_state(ctx, execution_date, symbol)?, - )? - .is_some() - { - continue; - } - if !self.stock_passes_expr(ctx, &day, &decision_stock)? { - continue; - } - let cash_before_buy = projected.cash(); - let order_result = self.project_order_value( - ctx, - &mut projected, - projection_date, - symbol, - buy_cash, - &mut projected_execution_state, - ); - if order_result.was_submitted() { - order_intents.push(OrderIntent::Value { + if Self::should_emit_rebalance_target_value( + defer_execution_risk, + before_qty, + after_qty, + ) { + order_intents.push(OrderIntent::TargetValue { symbol: symbol.clone(), - value: buy_cash, - reason: "periodic_rebalance_buy".to_string(), + target_value, + reason: "periodic_rebalance_target_adjust".to_string(), }); - self.remember_position_entry_date(symbol, signal_date); - let spent = (cash_before_buy - projected.cash()).max(0.0); - aiquant_available_cash = (aiquant_available_cash - spent).max(0.0); - intraday_attempted_buys.insert(symbol.clone()); - rebalance_working_symbols.insert(symbol.clone()); - slot_working_symbols.insert(symbol.clone()); + if after_qty > before_qty { + intraday_attempted_buys.insert(symbol.clone()); + } } continue; } @@ -10662,77 +10566,10 @@ impl Strategy for PlatformExprStrategy { { continue; } - if self.config.aiquant_transaction_cost { - let target_value = fixed_buy_cash * stock_scale; - if target_value <= 0.0 { - continue; - } - if !defer_execution_risk - && self - .buy_rejection_reason( - ctx, - execution_date, - symbol, - &self.stock_state(ctx, execution_date, symbol)?, - )? - .is_some() - { - continue; - } - if !self.stock_passes_expr(ctx, &day, &decision_stock)? { - continue; - } - self.project_order_value( - ctx, - &mut projected, - projection_date, - symbol, - target_value, - &mut projected_execution_state, - ); - order_intents.push(OrderIntent::TargetValue { - symbol: symbol.clone(), - target_value, - reason: "periodic_rebalance_buy".to_string(), - }); - self.remember_position_entry_date(symbol, signal_date); - aiquant_available_cash = projected.cash().max(0.0); - rebalance_working_symbols.insert(symbol.clone()); - slot_working_symbols.insert(symbol.clone()); + let target_value = fixed_buy_cash * stock_scale; + if target_value <= 0.0 { continue; } - let slot_buy_cash = self.remaining_buy_cash_per_slot( - ctx, - &projected, - projection_date, - target_budget, - selection_limit, - &rebalance_working_symbols, - &rebalance_value_symbols, - rebalance_pending_buy_value, - None, - &pending_full_close_symbols, - ); - let target_cash = slot_buy_cash * stock_scale; - let buy_cash = target_cash.min(aiquant_available_cash); - if buy_cash <= 0.0 { - if debug_projection { - projection_debug_notes.push(format!( - "periodic_buy_skip_nonpositive_cash symbol={} target_cash={:.4} available_cash={:.4} stock_scale={:.4}", - symbol, target_cash, aiquant_available_cash, stock_scale - )); - } - break; - } - if self.config.aiquant_transaction_cost && buy_cash < target_cash * 0.5 { - if debug_projection { - projection_debug_notes.push(format!( - "periodic_buy_skip_insufficient_cash symbol={} buy_cash={:.4} target_cash={:.4} available_cash={:.4}", - symbol, buy_cash, target_cash, aiquant_available_cash - )); - } - break; - } if !defer_execution_risk && self .buy_rejection_reason( @@ -10745,140 +10582,26 @@ impl Strategy for PlatformExprStrategy { { continue; } - let stock_expr_pass = self.stock_passes_expr(ctx, &day, &decision_stock)?; - if !stock_expr_pass { - if debug_projection { - projection_debug_notes.push(format!( - "periodic_buy_skip_stock_expr symbol={} ma5={:.4} ma10={:.4} ma30={:.4} volume_ma5={:.4} volume_ma100={:.4}", - symbol, - decision_stock.stock_ma5, - decision_stock.stock_ma10, - decision_stock.stock_ma30, - decision_stock.stock_volume_ma5, - decision_stock.stock_volume_ma100 - )); - } + if !self.stock_passes_expr(ctx, &day, &decision_stock)? { continue; } - let cash_before_buy = projected.cash(); - let order_result = self.project_order_value( + self.project_order_value( ctx, &mut projected, projection_date, symbol, - buy_cash, + target_value, &mut projected_execution_state, ); - if order_result.was_submitted() { - order_intents.push(OrderIntent::Value { - symbol: symbol.clone(), - value: buy_cash, - reason: "periodic_rebalance_buy".to_string(), - }); - self.remember_position_entry_date(symbol, signal_date); - let spent = (cash_before_buy - projected.cash()).max(0.0); - aiquant_available_cash = (aiquant_available_cash - spent).max(0.0); - rebalance_working_symbols.insert(symbol.clone()); - slot_working_symbols.insert(symbol.clone()); - rebalance_pending_buy_value += buy_cash; - } else if debug_projection { - let market = ctx.data.market(projection_date, symbol); - let candidate = ctx.data.candidate(projection_date, symbol); - let factor = ctx.data.factor(selection_factor_date, symbol); - let quotes = ctx.data.execution_quotes_on(projection_date, symbol); - let quote_count = quotes.len(); - let quote_cursor = self.projected_execution_start_cursor( - ctx, - projection_date, - symbol, - &projected_execution_state, - ); - let latest_quote = quotes - .iter() - .filter(|quote| quote.timestamp <= quote_cursor) - .max_by_key(|quote| quote.timestamp); - let sizing_price = market.map(|market| { - let raw = if self.config.aiquant_transaction_cost { - self.aiquant_scheduled_last_price(ctx, projection_date, symbol) - .unwrap_or_else(|| { - self.projected_execution_price(market, OrderSide::Buy) - }) - } else { - self.projected_execution_price(market, OrderSide::Buy) - }; - self.projected_apply_slippage(market, OrderSide::Buy, raw, None) - }); - let projected_qty = sizing_price - .map(|price| { - self.value_buy_quantity( - buy_cash, - price, - self.projected_minimum_order_quantity(ctx, symbol), - self.projected_order_step_size(ctx, symbol), - ) - }) - .unwrap_or_default(); - let fillable_qty = market - .and_then(|market| { - self.projected_market_fillable_quantity( - market, - latest_quote, - symbol, - OrderSide::Buy, - projected_qty, - self.projected_round_lot(ctx, symbol), - self.projected_minimum_order_quantity(ctx, symbol), - self.projected_order_step_size(ctx, symbol), - false, - 0, - &projected_execution_state, - ) - }) - .unwrap_or_default(); - projection_debug_notes.push(format!( - "periodic_buy_project_zero symbol={} buy_cash={:.4} target_cash={:.4} available_cash={:.4} projection_date={} execution_date={} market={} factor={} candidate={} has_day_quotes={} quote_count={} quote_cursor={} quote_ts={} quote_last={:.4} quote_volume_delta={} quote_bid1_volume={} quote_ask1_volume={} sizing_price={:.4} projected_qty={} fillable_qty={} quote_quantity_limit={} volume_limit={} liquidity_limit={} volume_percent={:.4} market_volume={} minute_volume={} paused={} allow_buy={} is_kcb={} upper_limit={:.4} lower_limit={:.4}", - symbol, - buy_cash, - target_cash, - aiquant_available_cash, - projection_date, - execution_date, - market.is_some(), - factor.is_some(), - candidate.is_some(), - ctx.data.has_execution_quotes_on_date(projection_date), - quote_count, - quote_cursor, - latest_quote - .map(|quote| quote.timestamp.to_string()) - .unwrap_or_else(|| "-".to_string()), - latest_quote - .map(|quote| quote.last_price) - .unwrap_or(f64::NAN), - latest_quote - .map(|quote| quote.volume_delta) - .unwrap_or_default(), - latest_quote.map(|quote| quote.bid1_volume).unwrap_or_default(), - latest_quote.map(|quote| quote.ask1_volume).unwrap_or_default(), - sizing_price.unwrap_or(f64::NAN), - projected_qty, - fillable_qty, - self.config.quote_quantity_limit, - self.config.risk_config.trading_constraints.volume_limit_enabled, - self.config - .risk_config - .trading_constraints - .liquidity_limit_enabled, - self.config.risk_config.trading_constraints.volume_percent, - market.map(|item| item.volume).unwrap_or(0), - market.map(|item| item.minute_volume).unwrap_or(0), - market.map(|item| item.paused).unwrap_or(false), - candidate.map(|item| item.allow_buy).unwrap_or(false), - candidate.map(|item| item.is_kcb).unwrap_or(false), - market.map(|item| item.upper_limit).unwrap_or(f64::NAN), - market.map(|item| item.lower_limit).unwrap_or(f64::NAN) - )); - } + order_intents.push(OrderIntent::TargetValue { + symbol: symbol.clone(), + target_value, + reason: "periodic_rebalance_buy".to_string(), + }); + self.remember_position_entry_date(symbol, signal_date); + rebalance_working_symbols.insert(symbol.clone()); + slot_working_symbols.insert(symbol.clone()); + continue; } } if self.config.hold_until_exit_enabled && periodic_rebalance { @@ -10963,7 +10686,7 @@ impl Strategy for PlatformExprStrategy { selection_universe_factor_date, selection_factor_date, execution_date, - aiquant_total_value, + strategy_visible_total_value, marked_total_value, day.total_value ), @@ -11263,7 +10986,6 @@ mod tests { cfg.stock_filter_expr = "close > 0".to_string(); cfg.stop_loss_expr.clear(); cfg.take_profit_expr.clear(); - cfg.aiquant_transaction_cost = true; cfg.daily_replacement_limit = 3; cfg.selection_buffer_multiple = 2.0; cfg.rebalance_existing_positions = false; @@ -11571,7 +11293,6 @@ mod tests { let mut aiquant_cfg = PlatformExprStrategyConfig::microcap_rotation(); aiquant_cfg.matching_type = MatchingType::NextBarOpen; - aiquant_cfg.aiquant_transaction_cost = true; let aiquant_strategy = PlatformExprStrategy::new(aiquant_cfg); assert_eq!( aiquant_strategy.projected_execution_price(&market, OrderSide::Buy), @@ -11673,7 +11394,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.matching_type = MatchingType::NextBarOpen; cfg.risk_config.trading_constraints.volume_limit_enabled = false; cfg.risk_config.trading_constraints.liquidity_limit_enabled = false; @@ -11800,7 +11520,6 @@ mod tests { let mut aiquant_cfg = PlatformExprStrategyConfig::microcap_rotation(); aiquant_cfg.matching_type = MatchingType::NextBarOpen; - aiquant_cfg.aiquant_transaction_cost = true; let aiquant_strategy = PlatformExprStrategy::new(aiquant_cfg); assert!( aiquant_strategy.can_sell_position_at_time(&ctx, date, symbol, None), @@ -12585,7 +12304,6 @@ mod tests { #[test] fn platform_expr_cost_model_uses_commission_override() { let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.commission_rate = Some(0.0003); cfg.minimum_commission = Some(5.0); let strategy = PlatformExprStrategy::new(cfg); @@ -12595,14 +12313,13 @@ mod tests { } #[test] - fn platform_expr_aiquant_cost_model_ignores_zero_minimum_commission_override() { + fn platform_expr_cost_model_honors_zero_minimum_commission() { let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.commission_rate = Some(0.0003); cfg.minimum_commission = Some(0.0); let strategy = PlatformExprStrategy::new(cfg); - assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9); + assert!((strategy.buy_commission(1_000.0) - 0.3).abs() < 1e-9); } #[test] @@ -12957,7 +12674,6 @@ mod tests { assert_eq!(strategy.value_buy_quantity(4_848.0, 11.93, 100, 100), 400); let mut aiquant_cfg = PlatformExprStrategyConfig::microcap_rotation(); - aiquant_cfg.aiquant_transaction_cost = true; aiquant_cfg.commission_rate = Some(0.0003); aiquant_cfg.minimum_commission = Some(5.0); let aiquant_strategy = PlatformExprStrategy::new(aiquant_cfg); @@ -13075,7 +12791,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.commission_rate = Some(0.0003); cfg.minimum_commission = Some(5.0); cfg.strict_value_budget = true; @@ -13218,7 +12933,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.commission_rate = Some(0.0003); cfg.minimum_commission = Some(5.0); cfg.strict_value_budget = true; @@ -13453,7 +13167,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.commission_rate = Some(0.0003); cfg.minimum_commission = Some(5.0); cfg.strict_value_budget = true; @@ -13680,7 +13393,6 @@ mod tests { 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()); let strategy = PlatformExprStrategy::new(cfg); @@ -14109,7 +13821,6 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.rotation_enabled = false; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap()); cfg.delayed_limit_open_exit_enabled = true; cfg.delayed_limit_open_exit_time = Some(NaiveTime::from_hms_opt(10, 31, 0).unwrap()); @@ -14405,7 +14116,6 @@ mod tests { let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = signal_symbol.to_string(); cfg.benchmark_symbol = "932000.CSI".to_string(); - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); cfg.delayed_limit_open_exit_enabled = true; cfg.delayed_limit_open_exit_time = Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()); @@ -14692,7 +14402,6 @@ mod tests { let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = signal_symbol.to_string(); cfg.benchmark_symbol = "932000.CSI".to_string(); - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); cfg.delayed_limit_open_exit_enabled = true; cfg.delayed_limit_open_exit_time = Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()); @@ -14947,7 +14656,6 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.rotation_enabled = false; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); cfg.delayed_limit_open_exit_enabled = true; cfg.delayed_limit_open_exit_time = Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()); @@ -15002,7 +14710,7 @@ mod tests { } #[test] - fn platform_take_profit_uses_strategy_entry_price_not_fee_cost_basis() { + fn platform_take_profit_default_uses_position_cost_basis() { let prev_date = d(2025, 3, 13); let date = d(2025, 3, 14); let symbol = "600561.SH"; @@ -15107,14 +14815,7 @@ mod tests { let decision = strategy.on_day(&ctx).expect("platform decision"); assert!( - decision.order_intents.iter().any(|intent| matches!( - intent, - OrderIntent::TargetValue { - symbol: intent_symbol, - target_value, - reason, - } if intent_symbol == symbol && *target_value == 0.0 && reason == "take_profit_exit" - )), + decision.order_intents.is_empty(), "{:?}", decision.order_intents ); @@ -15266,7 +14967,6 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.rotation_enabled = false; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).expect("time")); cfg.signal_symbol = symbol.to_string(); cfg.stop_loss_expr = "0.92".to_string(); @@ -15290,7 +14990,6 @@ mod tests { let mut signal_price_cfg = PlatformExprStrategyConfig::microcap_rotation(); signal_price_cfg.rotation_enabled = false; - signal_price_cfg.aiquant_transaction_cost = true; signal_price_cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).expect("time")); signal_price_cfg.signal_symbol = symbol.to_string(); @@ -15463,7 +15162,6 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.rotation_enabled = false; - cfg.aiquant_transaction_cost = true; cfg.matching_type = MatchingType::NextBarOpen; cfg.signal_symbol = symbol.to_string(); cfg.prelude = "let stop_loss = 0.92;".to_string(); @@ -15573,7 +15271,6 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.rotation_enabled = false; - cfg.aiquant_transaction_cost = true; cfg.matching_type = MatchingType::MinuteLast; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).expect("time")); cfg.signal_symbol = symbol.to_string(); @@ -15695,7 +15392,6 @@ mod tests { let subscriptions = BTreeSet::new(); let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.rotation_enabled = false; - cfg.aiquant_transaction_cost = true; cfg.matching_type = MatchingType::MinuteLast; cfg.risk_config.trading_constraints.volume_limit_enabled = true; cfg.risk_config.trading_constraints.volume_percent = 0.25; @@ -16311,7 +16007,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.stock_filter_expr = "true".to_string(); cfg.benchmark_short_ma_days = 1; @@ -16331,11 +16026,11 @@ mod tests { assert!((stock.free_float_cap - 9.0).abs() < 1e-9); assert!((stock.free_float_cap_bn - 9.0).abs() < 1e-9); assert_eq!(stock.listed_days, 263); - assert_eq!(stock.close, 10.0); - assert!(stock.high.is_nan()); - assert!(stock.low.is_nan()); - assert!(stock.volume.is_nan()); - assert!(!stock.touched_upper_limit); + assert_eq!(stock.close, 20.0); + assert_eq!(stock.high, 22.0); + assert_eq!(stock.low, 10.0); + assert_eq!(stock.volume, 120_000.0); + assert!(stock.touched_upper_limit); let day = strategy.day_state(&ctx, date).expect("day state"); let (selected, _, _) = strategy @@ -16470,7 +16165,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = signal.to_string(); cfg.market_cap_lower_expr = "0".to_string(); cfg.market_cap_upper_expr = "100".to_string(); @@ -16941,7 +16635,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = "003008.SZ".to_string(); cfg.stock_filter_expr = "true".to_string(); cfg.benchmark_short_ma_days = 1; @@ -17314,7 +17007,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.exposure_expr = "0.5".to_string(); cfg.selection_limit_expr = "40".to_string(); @@ -17439,7 +17131,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.exposure_expr = "1.0".to_string(); cfg.selection_limit_expr = "40".to_string(); @@ -17553,7 +17244,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.exposure_expr = "1.0".to_string(); cfg.selection_limit_expr = "40".to_string(); @@ -17685,7 +17375,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.exposure_expr = "1.0".to_string(); cfg.selection_limit_expr = "40".to_string(); @@ -17813,7 +17502,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.rotation_enabled = false; cfg.daily_top_up_enabled = false; cfg.signal_symbol = symbol.to_string(); @@ -17968,7 +17656,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = "000001.SZ".to_string(); cfg.max_positions = 2; cfg.selection_limit_expr = "2".to_string(); @@ -18130,7 +17817,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.max_positions = 2; cfg.selection_limit_expr = "2".to_string(); @@ -18287,7 +17973,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.max_positions = 2; cfg.selection_limit_expr = "2".to_string(); @@ -18444,7 +18129,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.max_positions = 2; cfg.selection_limit_expr = "2".to_string(); @@ -18606,7 +18290,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = "000001.SZ".to_string(); cfg.max_positions = 3; cfg.selection_limit_expr = "3".to_string(); @@ -18769,7 +18452,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.exposure_expr = "0.5".to_string(); cfg.selection_limit_expr = "40".to_string(); @@ -18915,7 +18597,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.signal_symbol = symbol.to_string(); cfg.exposure_expr = "0.5".to_string(); cfg.selection_limit_expr = "40".to_string(); @@ -19086,7 +18767,6 @@ mod tests { cfg.stop_loss_expr.clear(); cfg.take_profit_expr = "1.1".to_string(); cfg.daily_top_up_enabled = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 15, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; @@ -19240,7 +18920,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.matching_type = MatchingType::MinuteLast; cfg.risk_config.trading_constraints.volume_limit_enabled = true; cfg.risk_config.trading_constraints.volume_percent = 0.25; @@ -19380,7 +19059,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(9, 33, 0).unwrap()); cfg.signal_symbol = symbol.to_string(); cfg.stock_filter_expr = "!at_lower_limit".to_string(); @@ -19530,12 +19208,11 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap()); let strategy = PlatformExprStrategy::new(cfg.clone()); assert_eq!( strategy - .aiquant_scheduled_quote(&ctx, date, symbol) + .scheduled_quote(&ctx, date, symbol) .map(|quote| quote.last_price), Some(10.0) ); @@ -19543,7 +19220,7 @@ mod tests { cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(14, 59, 0).unwrap()); let strategy = PlatformExprStrategy::new(cfg); let quote = strategy - .aiquant_scheduled_quote(&ctx, date, symbol) + .scheduled_quote(&ctx, date, symbol) .expect("latest known quote at or before 14:59"); assert_eq!(quote.timestamp, date.and_hms_opt(14, 58, 59).unwrap()); assert_eq!(quote.last_price, 19.8); @@ -19642,7 +19319,7 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; + cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap()); let strategy = PlatformExprStrategy::new(cfg); assert_eq!(strategy.marked_total_value(&ctx, date), 2_000.0); } @@ -19751,7 +19428,6 @@ mod tests { 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); @@ -20004,7 +19680,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(9, 33, 0).unwrap()); cfg.signal_symbol = signal.to_string(); cfg.max_positions = 1; @@ -20228,7 +19903,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(9, 33, 0).unwrap()); cfg.signal_symbol = signal.to_string(); cfg.max_positions = 1; @@ -20469,30 +20143,16 @@ mod tests { order_events: &[], fills: &[], }; - let mut default_cfg = PlatformExprStrategyConfig::microcap_rotation(); - default_cfg.universe_exclude.clear(); - let default_strategy = PlatformExprStrategy::new(default_cfg); - let stock = default_strategy - .stock_state(&ctx, date, symbol) - .expect("stock state"); - assert!( - default_strategy - .buy_rejection_reason(&ctx, date, symbol, &stock) - .expect("default rejection") - .is_some() - ); - - let mut aiquant_cfg = PlatformExprStrategyConfig::microcap_rotation(); - aiquant_cfg.universe_exclude.clear(); - aiquant_cfg.aiquant_transaction_cost = true; - let aiquant_strategy = PlatformExprStrategy::new(aiquant_cfg); - let stock = aiquant_strategy + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.universe_exclude.clear(); + let strategy = PlatformExprStrategy::new(cfg); + let stock = strategy .stock_state(&ctx, date, symbol) .expect("stock state"); - let rejection = aiquant_strategy + let rejection = strategy .buy_rejection_reason(&ctx, date, symbol, &stock) - .expect("aiquant rejection"); + .expect("buy rejection"); assert_eq!(rejection, None); } @@ -20587,7 +20247,6 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.universe_exclude.clear(); - cfg.aiquant_transaction_cost = true; let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state(&ctx, date, symbol) @@ -20791,7 +20450,6 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.universe_exclude.clear(); - cfg.aiquant_transaction_cost = true; let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state(&ctx, date, symbol) @@ -22698,9 +22356,7 @@ mod tests { order_events: &[], fills: &[], }; - let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; - let strategy = PlatformExprStrategy::new(cfg); + let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); let working_symbols = BTreeSet::from(["000001.SZ".to_string()]); let value_symbols = working_symbols.clone(); @@ -22885,7 +22541,6 @@ mod tests { cfg.stock_filter_expr = "close > 0".to_string(); cfg.exposure_expr = "0.5".to_string(); cfg.daily_top_up_enabled = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; @@ -23045,7 +22700,6 @@ mod tests { cfg.stop_loss_expr = "0.92".to_string(); cfg.take_profit_expr.clear(); cfg.daily_top_up_enabled = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; @@ -23201,7 +22855,6 @@ mod tests { cfg.stop_loss_expr = "0.92".to_string(); cfg.take_profit_expr.clear(); cfg.daily_top_up_enabled = true; - cfg.aiquant_transaction_cost = true; cfg.matching_type = MatchingType::NextBarOpen; let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; @@ -23374,7 +23027,6 @@ mod tests { cfg.stop_loss_expr = "0.92".to_string(); cfg.take_profit_expr.clear(); cfg.daily_top_up_enabled = true; - cfg.aiquant_transaction_cost = true; cfg.slippage_model = SlippageModel::PriceRatio(0.002); cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time")); let mut strategy = PlatformExprStrategy::new(cfg); @@ -23412,7 +23064,7 @@ mod tests { } #[test] - fn platform_refresh_rate_uses_stateful_aiquant_day_counter() { + fn platform_refresh_rate_uses_stateful_trading_day_counter() { let dates = [d(2025, 2, 5), d(2025, 2, 6), d(2025, 2, 7)]; let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"]; let data = DataSet::from_components( @@ -23609,7 +23261,7 @@ mod tests { signal_dates_decision .order_intents .iter() - .any(|intent| matches!(intent, OrderIntent::Value { reason, .. } if reason == "periodic_rebalance_buy")), + .any(|intent| matches!(intent, OrderIntent::TargetValue { reason, .. } if reason == "periodic_rebalance_target_adjust")), "{:?}", signal_dates_decision.order_intents ); @@ -24318,7 +23970,6 @@ mod tests { cfg.take_profit_expr = "1.1".to_string(); cfg.daily_top_up_enabled = true; cfg.release_slot_on_exit_signal = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; @@ -24538,7 +24189,6 @@ mod tests { cfg.stop_take_reference_price_mode = PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose; cfg.release_slot_on_exit_signal = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; @@ -24777,7 +24427,6 @@ mod tests { cfg.take_profit_expr = "1.1".to_string(); cfg.daily_top_up_enabled = true; cfg.release_slot_on_exit_signal = true; - cfg.aiquant_transaction_cost = true; cfg.risk_config.trading_constraints.volume_limit_enabled = true; cfg.risk_config.trading_constraints.volume_percent = 1.0; cfg.risk_config.trading_constraints.liquidity_limit_enabled = false; @@ -24988,7 +24637,6 @@ mod tests { cfg.take_profit_expr = "1.1".to_string(); cfg.daily_top_up_enabled = true; cfg.release_slot_on_exit_signal = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; @@ -25573,7 +25221,6 @@ mod tests { cfg.take_profit_expr = "false".to_string(); cfg.daily_top_up_enabled = true; cfg.release_slot_on_exit_signal = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); cfg.delayed_limit_open_exit_enabled = true; cfg.delayed_limit_open_exit_time = Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()); @@ -25788,7 +25435,6 @@ mod tests { cfg.take_profit_expr = "false".to_string(); cfg.daily_top_up_enabled = true; cfg.release_slot_on_exit_signal = true; - cfg.aiquant_transaction_cost = true; cfg.risk_config.trading_constraints.volume_limit_enabled = true; cfg.risk_config.trading_constraints.volume_percent = 0.25; cfg.risk_config.trading_constraints.liquidity_limit_enabled = false; @@ -26017,7 +25663,6 @@ mod tests { cfg.take_profit_expr = "1.1".to_string(); cfg.daily_top_up_enabled = true; cfg.release_slot_on_exit_signal = false; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); cfg.delayed_limit_open_exit_enabled = true; cfg.delayed_limit_open_exit_time = Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()); @@ -26259,7 +25904,6 @@ mod tests { cfg.take_profit_expr = "1.1".to_string(); cfg.daily_top_up_enabled = true; cfg.release_slot_on_exit_signal = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); cfg.delayed_limit_open_exit_enabled = true; cfg.delayed_limit_open_exit_time = Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()); @@ -26926,7 +26570,8 @@ mod tests { .filter(|intent| { matches!( intent, - OrderIntent::Value { reason, .. } if reason == "periodic_rebalance_buy" + OrderIntent::TargetValue { target_value, reason, .. } + if *target_value > 0.0 && reason == "periodic_rebalance_buy" ) }) .count(); @@ -27034,15 +26679,13 @@ mod tests { order_events: &[], fills: &[], }; - let mut aiquant_cfg = base_cfg; - aiquant_cfg.aiquant_transaction_cost = true; - let mut aiquant_strategy = PlatformExprStrategy::new(aiquant_cfg); - aiquant_strategy.rebalance_day_counter = 20; + let mut canonical_strategy = PlatformExprStrategy::new(base_cfg); + canonical_strategy.rebalance_day_counter = 20; - let aiquant_decision = aiquant_strategy + let canonical_decision = canonical_strategy .on_day(&aiquant_ctx) - .expect("aiquant platform decision"); - let aiquant_periodic_buys = aiquant_decision + .expect("canonical platform decision"); + let canonical_periodic_buys = canonical_decision .order_intents .iter() .filter(|intent| { @@ -27058,9 +26701,9 @@ mod tests { .count(); assert_eq!( - aiquant_periodic_buys, 2, + canonical_periodic_buys, 2, "{:?}", - aiquant_decision.order_intents + canonical_decision.order_intents ); } @@ -27206,7 +26849,6 @@ mod tests { cfg.stop_loss_expr = "0.9".to_string(); cfg.take_profit_expr.clear(); cfg.daily_top_up_enabled = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 20; @@ -27415,7 +27057,6 @@ mod tests { cfg.stop_loss_expr.clear(); cfg.daily_top_up_enabled = true; cfg.release_slot_on_exit_signal = true; - cfg.aiquant_transaction_cost = true; cfg.strict_value_budget = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); cfg.risk_config.trading_constraints.volume_limit_enabled = false; @@ -27620,7 +27261,6 @@ mod tests { cfg.stock_filter_expr = "close > 0".to_string(); cfg.take_profit_expr.clear(); cfg.stop_loss_expr.clear(); - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(14, 59, 0).unwrap()); let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 20; @@ -27773,13 +27413,13 @@ mod tests { ); assert!(matches!( &decision.order_intents[0], - OrderIntent::Value { + OrderIntent::TargetValue { symbol: intent_symbol, - value, + target_value, reason, } if intent_symbol == symbol - && reason == "periodic_rebalance_buy" - && (*value - 499_000.0).abs() < 1e-6 + && reason == "periodic_rebalance_target_adjust" + && (*target_value - 500_000.0).abs() < 1e-6 )); let mut target_value_cfg = PlatformExprStrategyConfig::microcap_rotation(); @@ -27794,7 +27434,6 @@ mod tests { target_value_cfg.stock_filter_expr = "close > 0".to_string(); target_value_cfg.take_profit_expr.clear(); target_value_cfg.stop_loss_expr.clear(); - target_value_cfg.aiquant_transaction_cost = true; target_value_cfg.rebalance_existing_positions = true; target_value_cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); @@ -27971,9 +27610,7 @@ mod tests { order_events: &[], fills: &[], }; - let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; - let strategy = PlatformExprStrategy::new(cfg); + let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); let mut projected = portfolio.clone(); let mut execution_state = super::ProjectedExecutionState::default(); @@ -28108,9 +27745,7 @@ mod tests { order_events: &[], fills: &[], }; - let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; - let strategy = PlatformExprStrategy::new(cfg); + let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); let mut projected = portfolio.clone(); let mut execution_state = super::ProjectedExecutionState::default(); @@ -28250,9 +27885,7 @@ mod tests { order_events: &[], fills: &[], }; - let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; - let strategy = PlatformExprStrategy::new(cfg); + let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); let mut projected = portfolio.clone(); let mut execution_state = super::ProjectedExecutionState::default(); @@ -28377,9 +28010,7 @@ mod tests { order_events: &[], fills: &[], }; - let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; - let strategy = PlatformExprStrategy::new(cfg); + let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); let mut projected = portfolio.clone(); let mut execution_state = super::ProjectedExecutionState::default(); @@ -28597,7 +28228,6 @@ mod tests { fills: &[], }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let strategy = PlatformExprStrategy::new(cfg); let mut projected = portfolio.clone(); @@ -28779,7 +28409,7 @@ mod tests { assert!( matches!( decision.order_intents.first(), - Some(crate::strategy::OrderIntent::Value { symbol, .. }) if symbol == "300002.SZ" + Some(crate::strategy::OrderIntent::TargetValue { symbol, .. }) if symbol == "300002.SZ" ), "intents={:?} diagnostics={:?}", decision.order_intents, @@ -28961,7 +28591,7 @@ mod tests { assert!( matches!( decision.order_intents.first(), - Some(crate::strategy::OrderIntent::Value { symbol, .. }) if symbol == "300001.SZ" + Some(crate::strategy::OrderIntent::TargetValue { symbol, .. }) if symbol == "300001.SZ" ), "intents={:?} diagnostics={:?}", decision.order_intents, @@ -29932,7 +29562,6 @@ mod tests { let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = "000001.SZ".to_string(); cfg.rotation_enabled = false; - cfg.aiquant_transaction_cost = true; cfg.matching_type = MatchingType::NextBarOpen; cfg.stop_loss_expr = "0.9".to_string(); cfg.take_profit_expr.clear(); @@ -31794,7 +31423,6 @@ mod tests { "prev_close > 5.0 && !at_upper_limit && !at_lower_limit".to_string(); cfg.rank_by = "model_score".to_string(); cfg.rank_desc = true; - cfg.aiquant_transaction_cost = true; cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); let strategy = PlatformExprStrategy::new(cfg); diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index 9977c78..25bfdc5 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -119,8 +119,6 @@ pub struct StrategyEngineConfig { pub template_id: Option, #[serde(default, alias = "profile_name")] pub profile_name: Option, - #[serde(default, alias = "compatibility_profile")] - pub compatibility_profile: Option, #[serde(default, alias = "benchmark_symbol")] pub benchmark_symbol: Option, #[serde(default, alias = "signal_symbol")] @@ -811,6 +809,7 @@ pub fn platform_expr_config_from_value( return platform_expr_config_from_spec(strategy_id, signal_symbol, None) .map_err(platform_config_error); } + reject_removed_compatibility_fields(value).map_err(platform_config_error)?; let mut value = value.clone(); normalize_risk_policy_aliases_in_value(&mut value).map_err(platform_config_error)?; let spec = serde_json::from_value::(value)?; @@ -818,6 +817,30 @@ pub fn platform_expr_config_from_value( .map_err(platform_config_error) } +fn reject_removed_compatibility_fields(value: &Value) -> Result<(), String> { + const SECTION_NAMES: [&str; 3] = ["engineConfig", "engine_config", "execution"]; + const FIELD_NAMES: [&str; 4] = [ + "compatibilityProfile", + "compatibility_profile", + "compatProfile", + "compat_profile", + ]; + for section_name in SECTION_NAMES { + let Some(section) = value.get(section_name).and_then(Value::as_object) else { + continue; + }; + if let Some(field_name) = FIELD_NAMES + .iter() + .find(|field_name| section.contains_key(**field_name)) + { + return Err(format!( + "{section_name}.{field_name} has been removed; configure matching, risk, fees and scheduling explicitly" + )); + } + } + Ok(()) +} + fn platform_config_error(message: String) -> serde_json::Error { serde_json::Error::io(std::io::Error::new( std::io::ErrorKind::InvalidData, @@ -849,12 +872,6 @@ fn parse_policy_date(value: Option<&str>) -> Option { .ok() } -fn is_aiquant_profile(value: Option<&str>) -> bool { - value - .map(|item| item.trim().to_ascii_lowercase().replace('-', "_")) - .is_some_and(|item| item == "aiquant" || item == "aiquant_rqalpha" || item == "rqalpha") -} - fn parse_stop_take_reference_price_mode( value: &str, ) -> Result { @@ -1917,52 +1934,10 @@ pub fn platform_expr_config_from_spec( if !cfg.benchmark_symbol.trim().is_empty() { cfg.benchmark_symbol = normalize_symbol(&cfg.benchmark_symbol, None); } - let aiquant_profile = spec.engine_config.as_ref().is_some_and(|engine| { - is_aiquant_profile(engine.profile_name.as_deref()) - || is_aiquant_profile(engine.compatibility_profile.as_deref()) - }); - if aiquant_profile { - cfg.aiquant_transaction_cost = true; - cfg.strict_value_budget = true; - if !cfg.universe_exclude.iter().any(|item| item == "bjse") { - cfg.universe_exclude.push("bjse".to_string()); - } - let trading = spec - .runtime_expressions - .as_ref() - .and_then(|runtime_expr| runtime_expr.trading.as_ref()); - if trading.and_then(|item| item.daily_top_up).is_none() { - cfg.daily_top_up_enabled = true; - } - if trading - .and_then(|item| item.retry_empty_rebalance) - .is_none() - { - cfg.retry_empty_rebalance = true; - } - } let trade_times = spec_trade_times(spec); if let Some(main_trade_time) = trade_times.last().copied() { cfg.intraday_execution_time = Some(main_trade_time); } - let delayed_limit_open_exit_explicit = spec - .runtime_expressions - .as_ref() - .and_then(|runtime_expr| runtime_expr.trading.as_ref()) - .and_then(|trading| trading.delayed_limit_open_exit) - .is_some(); - if aiquant_profile && !delayed_limit_open_exit_explicit && trade_times.len() > 1 { - let delayed_time = trade_times[0]; - if trade_times - .last() - .copied() - .map(|main_time| main_time != delayed_time) - .unwrap_or(true) - { - cfg.delayed_limit_open_exit_enabled = true; - cfg.delayed_limit_open_exit_time = Some(delayed_time); - } - } if let Some(execution) = spec.execution.as_ref() { apply_cost_overrides( &mut cfg, @@ -1994,13 +1969,6 @@ pub fn platform_expr_config_from_spec( )?; sync_quote_quantity_limit(&mut cfg); } - if cfg.aiquant_transaction_cost - && cfg - .minimum_commission - .is_some_and(|value| value.is_finite() && value <= 0.0) - { - cfg.minimum_commission = None; - } cfg.strict_value_budget = true; Ok(cfg) @@ -2485,16 +2453,12 @@ mod tests { assert_eq!(cfg.signal_symbol, "000852.SH"); assert_eq!(cfg.selection_limit_expr, "stocknum"); assert_eq!(cfg.refresh_rate_expr, "year >= 2024 ? 5 : 20"); - assert_eq!( - cfg.universe_exclude, - ["paused", "st", "kcb", "one_yuan", "bjse"] - ); + assert_eq!(cfg.universe_exclude, ["paused", "st", "kcb", "one_yuan"]); assert!(!cfg.rotation_enabled); assert!(cfg.daily_top_up_enabled); assert!(cfg.retry_empty_rebalance); assert_eq!(cfg.weak_market_shrink_overweight_threshold, Some(1.1)); assert!(!cfg.calendar_rebalance_interval); - assert!(cfg.aiquant_transaction_cost); assert_eq!(cfg.explicit_actions.len(), 1); assert_eq!( cfg.explicit_action_stage, @@ -2622,7 +2586,6 @@ mod tests { let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); - assert!(cfg.aiquant_transaction_cost); assert_eq!(cfg.commission_rate, Some(0.0003)); assert_eq!(cfg.minimum_commission, Some(5.0)); assert_eq!(cfg.stamp_tax_rate_before_change, Some(0.0005)); @@ -3084,7 +3047,7 @@ mod tests { } #[test] - fn aiquant_profile_defaults_to_daily_top_up_and_empty_retry() { + fn profile_name_does_not_inject_trading_behaviors() { let spec = serde_json::json!({ "engineConfig": { "profileName": "aiquant" @@ -3093,9 +3056,8 @@ mod tests { let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); - assert!(cfg.aiquant_transaction_cost); - assert!(cfg.daily_top_up_enabled); - assert!(cfg.retry_empty_rebalance); + assert!(!cfg.daily_top_up_enabled); + assert!(!cfg.retry_empty_rebalance); assert!(cfg.strict_value_budget); let explicit_off = serde_json::json!({ @@ -3125,7 +3087,7 @@ mod tests { } #[test] - fn engine_config_profile_name_enables_aiquant_semantics() { + fn engine_config_profile_name_is_metadata_only() { let spec = serde_json::json!({ "engineConfig": { "profileName": "aiquant" @@ -3133,12 +3095,12 @@ mod tests { }); let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); - - assert!(cfg.aiquant_transaction_cost); + assert!(!cfg.daily_top_up_enabled); + assert!(!cfg.retry_empty_rebalance); } #[test] - fn engine_config_profile_name_accepts_aiquant_rqalpha_alias() { + fn legacy_profile_name_does_not_inject_hidden_defaults() { let spec = serde_json::json!({ "engineConfig": { "profileName": "aiquant_rqalpha" @@ -3147,9 +3109,8 @@ mod tests { let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); - assert!(cfg.aiquant_transaction_cost); - assert!(cfg.daily_top_up_enabled); - assert!(cfg.retry_empty_rebalance); + assert!(!cfg.daily_top_up_enabled); + assert!(!cfg.retry_empty_rebalance); assert!(cfg.strict_value_budget); } @@ -3243,7 +3204,7 @@ mod tests { } #[test] - fn parses_daily_schedule_time_for_aiquant_execution_quotes() { + fn parses_daily_schedule_time_for_execution_quotes() { let spec = serde_json::json!({ "engineConfig": { "profileName": "aiquant" }, "runtimeExpressions": { @@ -3259,11 +3220,10 @@ mod tests { Some(NaiveTime::from_hms_opt(9, 33, 0).unwrap()) ); assert!(!cfg.calendar_rebalance_interval); - assert!(cfg.aiquant_transaction_cost); } #[test] - fn parses_aiquant_rebalance_trade_times_for_delayed_limit_exit() { + fn multiple_trade_times_do_not_imply_delayed_limit_exit() { let spec = serde_json::json!({ "engineConfig": { "profileName": "aiquant" }, "rebalance": { "tradeTimes": ["10:31", "10:40"] }, @@ -3278,15 +3238,12 @@ mod tests { cfg.intraday_execution_time, Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap()) ); - assert!(cfg.delayed_limit_open_exit_enabled); - assert_eq!( - cfg.delayed_limit_open_exit_time, - Some(NaiveTime::from_hms_opt(10, 31, 0).unwrap()) - ); + assert!(!cfg.delayed_limit_open_exit_enabled); + assert_eq!(cfg.delayed_limit_open_exit_time, None); } #[test] - fn parses_aiquant_compatibility_profile_for_delayed_limit_exit() { + fn rejects_removed_compatibility_profile() { let spec = serde_json::json!({ "engineConfig": { "profileName": "cn_a_microcap_v1", @@ -3298,16 +3255,11 @@ mod tests { } }); - let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); - - assert_eq!( - cfg.intraday_execution_time, - Some(NaiveTime::from_hms_opt(10, 15, 0).unwrap()) - ); - assert!(cfg.delayed_limit_open_exit_enabled); - assert_eq!( - cfg.delayed_limit_open_exit_time, - Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()) + let error = platform_expr_config_from_value("", "", &spec).expect_err("removed field"); + assert!( + error + .to_string() + .contains("compatibilityProfile has been removed") ); } diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 361c2c4..1732f40 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -1655,6 +1655,7 @@ pub struct OmniMicroCapConfig { pub stock_long_ma_days: usize, pub stock_volume_short_ma_days: usize, pub stock_volume_long_ma_days: usize, + pub stock_volume_filter_enabled: bool, pub rsi_rate: f64, pub trade_rate: f64, pub stop_loss_ratio: f64, @@ -1684,6 +1685,7 @@ impl OmniMicroCapConfig { stock_long_ma_days: 20, stock_volume_short_ma_days: 5, stock_volume_long_ma_days: 60, + stock_volume_filter_enabled: true, rsi_rate: 1.0001, trade_rate: 0.5, stop_loss_ratio: 0.93, @@ -1695,35 +1697,6 @@ impl OmniMicroCapConfig { } } - pub fn aiquant_v104() -> Self { - Self { - strategy_name: "aiquant-v1.0.4".to_string(), - refresh_rate: 120, - stocknum: 5, - xs: 4.0 / 500.0, - base_index_level: 2000.0, - base_cap_floor: 7.0, - cap_span: 10.0, - padding_ratio: 1.2, - min_padding: 29.5, - max_padding: 50.0, - benchmark_signal_symbol: "000852.SH".to_string(), - benchmark_short_ma_days: 5, - benchmark_long_ma_days: 20, - stock_short_ma_days: 5, - stock_mid_ma_days: 10, - stock_long_ma_days: 30, - stock_volume_short_ma_days: 5, - stock_volume_long_ma_days: 60, - rsi_rate: 1.0001, - trade_rate: 0.5, - stop_loss_ratio: 0.92, - take_profit_ratio: 1.16, - skip_month_day_ranges: Vec::new(), - risk_config: FidcRiskControlConfig::default(), - } - } - fn in_skip_window(&self, date: NaiveDate) -> bool { let year = date.year() as u32; let month = date.month(); @@ -2433,10 +2406,7 @@ impl OmniMicroCapStrategy { return false; } - if self.config.strategy_name.contains("aiquant") - || self.config.strategy_name.contains("AiQuant") - || self.config.strategy_name.contains("omni") - { + if self.config.stock_volume_filter_enabled { let Some(volume_ma5) = ctx.data.market_decision_volume_moving_average( date, symbol, @@ -3018,6 +2988,7 @@ mod tests { default_cfg.stock_short_ma_days = 1; default_cfg.stock_mid_ma_days = 2; default_cfg.stock_long_ma_days = 3; + default_cfg.stock_volume_filter_enabled = false; let default_strategy = OmniMicroCapStrategy::new(default_cfg.clone()); let (default_selected, _) = default_strategy .select_symbols(&ctx, dates[2], 0.0, 100.0) diff --git a/crates/fidc-core/tests/core_rules.rs b/crates/fidc-core/tests/core_rules.rs index 0178446..0d7e08e 100644 --- a/crates/fidc-core/tests/core_rules.rs +++ b/crates/fidc-core/tests/core_rules.rs @@ -63,13 +63,20 @@ fn china_cost_model_applies_minimum_commission_and_stamp_tax() { assert_eq!(buy.stamp_tax, 0.0); let sell = model.calculate(d(2023, 8, 25), OrderSide::Sell, 100_000.0); - assert!((sell.commission - 80.0).abs() < 1e-9); + assert!((sell.commission - 30.0).abs() < 1e-9); assert!((sell.stamp_tax - 100.0).abs() < 1e-9); } #[test] -fn aiquant_cost_model_matches_alv_run_options() { - let model = ChinaAShareCostModel::aiquant_default(); +fn configured_cost_model_matches_declared_run_options() { + let model = + ChinaAShareCostModel::from_trading_constraints(fidc_core::TradingConstraintConfig { + commission_rate: 0.0003, + minimum_commission: 5.0, + stamp_tax_rate_before_change: 0.0005, + stamp_tax_rate_after_change: 0.0005, + ..fidc_core::TradingConstraintConfig::default() + }); let buy = model.calculate(d(2026, 5, 19), OrderSide::Buy, 49_978.84); assert!((buy.commission - 14.993652).abs() < 1e-9); @@ -130,7 +137,7 @@ fn china_cost_model_tracks_minimum_commission_per_order_id() { assert!((first.commission - 5.0).abs() < 1e-9); assert!(second.commission.abs() < 1e-9); - assert!((third.commission - 12.6).abs() < 1e-9); + assert!((third.commission - 1.6).abs() < 1e-9); assert!((another_order.commission - 5.0).abs() < 1e-9); } diff --git a/crates/fidc-core/tests/corporate_actions.rs b/crates/fidc-core/tests/corporate_actions.rs index a6fc29d..643a3f9 100644 --- a/crates/fidc-core/tests/corporate_actions.rs +++ b/crates/fidc-core/tests/corporate_actions.rs @@ -368,7 +368,11 @@ fn engine_reinvests_dividend_receivable_in_round_lots() { first_date: buy_date, }, BrokerSimulator::new_with_execution_price( - ChinaAShareCostModel::default(), + ChinaAShareCostModel { + commission_rate: 0.0008, + minimum_commission: 0.0, + ..ChinaAShareCostModel::default() + }, ChinaEquityRuleHooks::default(), PriceField::Open, ), diff --git a/crates/fidc-core/tests/explicit_order_flow.rs b/crates/fidc-core/tests/explicit_order_flow.rs index 38f911e..7336ba8 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -3321,7 +3321,8 @@ fn rebalance_optimizer_skips_unfunded_buy_when_existing_position_cannot_sell() { .order_events .iter() .all(|event| !(event.symbol == "000002.SZ" && event.side == fidc_core::OrderSide::Buy)), - "optimizer should skip unfunded rebalance buy when locked holding cannot be sold" + "optimizer should skip unfunded rebalance buy when locked holding cannot be sold: {:#?}", + report ); assert_eq!( portfolio