From 30a4071ee00c733c9d558d5b836cc737c49c5676 Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 12 Jul 2026 00:31:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90=E6=A8=A1=E5=9E=8B=E8=BD=AE?= =?UTF-8?q?=E5=8A=A8=E7=9B=AE=E6=A0=87=E8=B0=83=E4=BB=93=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 83 +++++++++++++++++++ .../fidc-core/src/platform_strategy_spec.rs | 14 ++++ 2 files changed, 97 insertions(+) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 45b3326..c972b34 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -229,6 +229,8 @@ pub struct PlatformExprStrategyConfig { pub signal_rebalance_dates: BTreeSet, pub rotation_enabled: bool, pub daily_top_up_enabled: bool, + pub daily_position_target_adjust_enabled: bool, + pub rebalance_existing_positions: bool, pub retry_empty_rebalance: bool, pub calendar_rebalance_interval: bool, pub max_holding_days: Option, @@ -299,6 +301,8 @@ fn band_low(index_close) { signal_rebalance_dates: BTreeSet::new(), rotation_enabled: true, daily_top_up_enabled: false, + daily_position_target_adjust_enabled: true, + rebalance_existing_positions: false, retry_empty_rebalance: false, calendar_rebalance_interval: false, max_holding_days: None, @@ -8881,6 +8885,7 @@ impl Strategy for PlatformExprStrategy { if self.config.aiquant_transaction_cost && self.config.rotation_enabled + && self.config.daily_position_target_adjust_enabled && trading_ratio > 0.0 && trading_ratio < 1.0 && selection_limit > 0 @@ -9370,7 +9375,44 @@ impl Strategy for PlatformExprStrategy { && !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 + && !self.config.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; + 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) @@ -25690,6 +25732,47 @@ mod tests { && reason == "periodic_rebalance_buy" && (*value - 499_000.0).abs() < 1e-6 )); + + let mut target_value_cfg = PlatformExprStrategyConfig::microcap_rotation(); + target_value_cfg.signal_symbol = symbol.to_string(); + target_value_cfg.refresh_rate = 20; + target_value_cfg.max_positions = 1; + target_value_cfg.benchmark_short_ma_days = 1; + target_value_cfg.benchmark_long_ma_days = 1; + target_value_cfg.market_cap_lower_expr = "0".to_string(); + target_value_cfg.market_cap_upper_expr = "100".to_string(); + target_value_cfg.selection_limit_expr = "1".to_string(); + 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()); + let mut target_value_strategy = PlatformExprStrategy::new(target_value_cfg); + target_value_strategy.rebalance_day_counter = 20; + + let target_value_decision = target_value_strategy + .on_day(&ctx) + .expect("target-value platform decision"); + + assert!( + target_value_decision + .order_intents + .iter() + .any(|intent| matches!( + intent, + OrderIntent::TargetValue { + symbol: intent_symbol, + target_value, + reason, + } if intent_symbol == symbol + && reason == "periodic_rebalance_target_adjust" + && (*target_value - 500_000.0).abs() < 1e-6 + )), + "{:?}", + target_value_decision.order_intents + ); } #[test] diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index 082e47d..2fbd6d1 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -691,6 +691,10 @@ pub struct StrategyExpressionTradingConfig { #[serde(default)] pub daily_top_up: Option, #[serde(default)] + pub daily_position_target_adjust: Option, + #[serde(default)] + pub rebalance_existing_positions: Option, + #[serde(default)] pub retry_empty_rebalance: Option, #[serde(default)] pub weak_market_shrink_overweight_threshold: Option, @@ -1628,6 +1632,12 @@ pub fn platform_expr_config_from_spec( if let Some(enabled) = trading.daily_top_up { cfg.daily_top_up_enabled = enabled; } + if let Some(enabled) = trading.daily_position_target_adjust { + cfg.daily_position_target_adjust_enabled = enabled; + } + if let Some(enabled) = trading.rebalance_existing_positions { + cfg.rebalance_existing_positions = enabled; + } if let Some(enabled) = trading.retry_empty_rebalance { cfg.retry_empty_rebalance = enabled; } @@ -2945,6 +2955,8 @@ mod tests { "runtimeExpressions": { "trading": { "dailyTopUp": false, + "dailyPositionTargetAdjust": false, + "rebalanceExistingPositions": true, "retryEmptyRebalance": false } } @@ -2953,6 +2965,8 @@ mod tests { let cfg = platform_expr_config_from_value("", "", &explicit_off).expect("config"); assert!(!cfg.daily_top_up_enabled); + assert!(!cfg.daily_position_target_adjust_enabled); + assert!(cfg.rebalance_existing_positions); assert!(!cfg.retry_empty_rebalance); }