From 9b4462f88022bf4104b7e043ca561c15d3202214 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 28 May 2026 18:40:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E7=AD=96=E7=95=A5=E6=AD=A2?= =?UTF-8?q?=E7=9B=88=E6=AD=A2=E6=8D=9F=E5=92=8C=E8=A1=A5=E4=BB=93=E6=8A=95?= =?UTF-8?q?=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 136 +++++++++++++++++- 1 file changed, 129 insertions(+), 7 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 77272bb..3cbc4d6 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -5373,7 +5373,11 @@ impl PlatformExprStrategy { if let Some(boolean) = stop_result.clone().try_cast::() { boolean } else if let Some(multiplier) = stop_result.clone().try_cast::() { - current_price <= avg_price * multiplier + if multiplier > 0.0 && multiplier < 0.5 { + holding_return <= -multiplier + } else { + current_price <= avg_price * multiplier + } } else if let Some(multiplier) = stop_result.try_cast::() { current_price <= avg_price * multiplier as f64 } else { @@ -5393,7 +5397,11 @@ impl PlatformExprStrategy { if let Some(boolean) = take_result.clone().try_cast::() { boolean } else if let Some(multiplier) = take_result.clone().try_cast::() { - current_price / avg_price > multiplier + if multiplier <= 1.0 { + holding_return > multiplier + } else { + current_price / avg_price > multiplier + } } else if let Some(multiplier) = take_result.try_cast::() { current_price / avg_price > multiplier as f64 } else { @@ -5625,8 +5633,14 @@ impl Strategy for PlatformExprStrategy { end_time: Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")), reason: "delayed_limit_open_sell".to_string(), }); - if self - .project_target_zero( + let projected_sold = if ctx + .data + .execution_quotes_on(execution_date, &symbol) + .is_empty() + { + false + } else { + self.project_target_zero( ctx, &mut projected, execution_date, @@ -5634,11 +5648,12 @@ impl Strategy for PlatformExprStrategy { &mut projected_execution_state, ) .is_some() - { + }; + if projected_sold { same_day_sold_symbols.insert(symbol.clone()); + delayed_sold_symbols.insert(symbol.clone()); + self.pending_highlimit_holdings.remove(&symbol); } - delayed_sold_symbols.insert(symbol.clone()); - self.pending_highlimit_holdings.remove(&symbol); } let mut aiquant_available_cash = if delayed_sold_symbols.is_empty() { @@ -6616,6 +6631,113 @@ mod tests { ); } + #[test] + fn platform_stop_take_fraction_values_are_return_thresholds() { + let prev_date = d(2025, 3, 13); + let date = d(2025, 3, 14); + let symbol = "600561.SH"; + let evaluate = |last_price: f64, stop_loss_expr: &str, take_profit_expr: &str| { + let data = DataSet::from_components( + vec![Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: "SH".to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + vec![DailyMarketSnapshot { + date, + symbol: symbol.to_string(), + timestamp: Some("2025-03-14 10:18:00".to_string()), + day_open: last_price, + open: last_price, + high: last_price, + low: last_price, + close: last_price, + last_price, + bid1: last_price, + ask1: last_price, + prev_close: 10.0, + volume: 1_000_000, + tick_volume: 1_000, + bid1_volume: 1_000, + ask1_volume: 1_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 11.0, + lower_limit: 9.0, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 3.2, + free_float_cap_bn: 2.1, + pe_ttm: 8.0, + turnover_ratio: Some(3.0), + effective_turnover_ratio: Some(3.0), + extra_factors: BTreeMap::new(), + }], + vec![CandidateEligibility { + date, + symbol: symbol.to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: false, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1002.0, + prev_close: 998.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + let mut portfolio = PortfolioState::new(1_000_000.0); + portfolio.position_mut(symbol).buy(prev_date, 10_000, 10.0); + let subscriptions = BTreeSet::new(); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 40, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: None, + order_events: &[], + fills: &[], + }; + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.rotation_enabled = false; + cfg.signal_symbol = symbol.to_string(); + cfg.stop_loss_expr = stop_loss_expr.to_string(); + cfg.take_profit_expr = take_profit_expr.to_string(); + let strategy = PlatformExprStrategy::new(cfg); + let day = strategy.day_state(&ctx, date).expect("day state"); + strategy + .stop_take_action(&ctx, date, date, &day, symbol) + .expect("stop take action") + }; + + assert_eq!(evaluate(10.5, "", "0.16"), (false, false)); + assert_eq!(evaluate(11.7, "", "0.16"), (false, true)); + assert_eq!(evaluate(9.3, "0.08", ""), (false, false)); + assert_eq!(evaluate(9.1, "0.08", ""), (true, false)); + } + #[test] fn platform_stock_state_uses_current_day_limit_status_for_buy_scale() { let date = d(2025, 3, 14);