From 1a12c46589e0ce5f1c76abb79d8020a65eea8438 Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 22 Apr 2026 04:04:13 -0700 Subject: [PATCH] Support buy scaling from platform factors --- .../fidc-core/src/platform_expr_strategy.rs | 70 ++++++++++++++++++- .../000852量价微盘平台策略.engine-script.txt | 1 + .../000852量价微盘平台策略.strategy_spec.json | 3 + 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 13b910a..3d3fa76 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -24,6 +24,7 @@ pub struct PlatformExprStrategyConfig { pub market_cap_upper_expr: String, pub selection_limit_expr: String, pub stock_filter_expr: String, + pub buy_scale_expr: String, pub exposure_expr: String, pub stop_loss_expr: String, pub take_profit_expr: String, @@ -67,6 +68,7 @@ fn band_low(index_close) { stock_filter_expr: "stock_ma_short > stock_ma_mid * ma_ratio && stock_ma_mid > stock_ma_long" .to_string(), + buy_scale_expr: "1.0".to_string(), exposure_expr: "benchmark_ma_short < benchmark_ma_long * ma_ratio ? 0.5 : 1.0".to_string(), stop_loss_expr: "0.93".to_string(), @@ -166,6 +168,8 @@ struct StockExpressionState { is_new_listing: bool, allow_buy: bool, allow_sell: bool, + touched_upper_limit: bool, + touched_lower_limit: bool, listed_days: i64, stock_ma_short: f64, stock_ma_mid: f64, @@ -300,6 +304,10 @@ impl PlatformExprStrategy { "is_new_listing", "allow_buy", "allow_sell", + "touched_upper_limit", + "touched_lower_limit", + "hit_upper_limit", + "hit_lower_limit", "listed_days", "stock_ma_short", "stock_ma_mid", @@ -794,6 +802,20 @@ impl PlatformExprStrategy { .data .market_decision_volume_moving_average(date, symbol, 60) .unwrap_or(stock_volume_ma20); + let touched_upper_limit = factor + .extra_factors + .get("touched_upper_limit") + .or_else(|| factor.extra_factors.get("hit_upper_limit")) + .copied() + .unwrap_or_default() + >= 0.5; + let touched_lower_limit = factor + .extra_factors + .get("touched_lower_limit") + .or_else(|| factor.extra_factors.get("hit_lower_limit")) + .copied() + .unwrap_or_default() + >= 0.5; Ok(StockExpressionState { symbol: symbol.to_string(), @@ -821,6 +843,8 @@ impl PlatformExprStrategy { is_new_listing: candidate.is_new_listing, allow_buy: candidate.allow_buy, allow_sell: candidate.allow_sell, + touched_upper_limit, + touched_lower_limit, listed_days: if candidate.is_new_listing { 0 } else { 365 }, stock_ma_short, stock_ma_mid, @@ -932,6 +956,10 @@ impl PlatformExprStrategy { scope.push("is_new_listing", stock.is_new_listing); scope.push("allow_buy", stock.allow_buy); scope.push("allow_sell", stock.allow_sell); + scope.push("touched_upper_limit", stock.touched_upper_limit); + scope.push("touched_lower_limit", stock.touched_lower_limit); + scope.push("hit_upper_limit", stock.touched_upper_limit); + scope.push("hit_lower_limit", stock.touched_lower_limit); scope.push("listed_days", stock.listed_days); scope.push("at_upper_limit", at_upper_limit); scope.push("at_lower_limit", at_lower_limit); @@ -983,6 +1011,16 @@ impl PlatformExprStrategy { factors.insert("is_new_listing".into(), Dynamic::from(stock.is_new_listing)); factors.insert("allow_buy".into(), Dynamic::from(stock.allow_buy)); factors.insert("allow_sell".into(), Dynamic::from(stock.allow_sell)); + factors.insert( + "touched_upper_limit".into(), + Dynamic::from(stock.touched_upper_limit), + ); + factors.insert( + "touched_lower_limit".into(), + Dynamic::from(stock.touched_lower_limit), + ); + factors.insert("hit_upper_limit".into(), Dynamic::from(stock.touched_upper_limit)); + factors.insert("hit_lower_limit".into(), Dynamic::from(stock.touched_lower_limit)); factors.insert("listed_days".into(), Dynamic::from(stock.listed_days)); factors.insert("at_upper_limit".into(), Dynamic::from(at_upper_limit)); factors.insert("at_lower_limit".into(), Dynamic::from(at_lower_limit)); @@ -1551,6 +1589,19 @@ impl PlatformExprStrategy { Ok(value.round().max(1.0) as usize) } + fn buy_scale( + &self, + ctx: &StrategyContext<'_>, + day: &DayExpressionState, + stock: &StockExpressionState, + ) -> Result { + if self.config.buy_scale_expr.trim().is_empty() { + return Ok(1.0); + } + self.eval_float(ctx, &self.config.buy_scale_expr, day, Some(stock), None) + .map(|value| value.clamp(0.0, 1.0)) + } + fn stock_passes_expr( &self, ctx: &StrategyContext<'_>, @@ -1611,6 +1662,12 @@ impl PlatformExprStrategy { "stock_volume_ma60" | "volume_ma60" => Some(stock.stock_volume_ma60), "allow_buy" => Some(if stock.allow_buy { 1.0 } else { 0.0 }), "allow_sell" => Some(if stock.allow_sell { 1.0 } else { 0.0 }), + "touched_upper_limit" | "hit_upper_limit" => { + Some(if stock.touched_upper_limit { 1.0 } else { 0.0 }) + } + "touched_lower_limit" | "hit_lower_limit" => { + Some(if stock.touched_lower_limit { 1.0 } else { 0.0 }) + } "paused" => Some(if stock.paused { 1.0 } else { 0.0 }), "is_st" => Some(if stock.is_st { 1.0 } else { 0.0 }), "is_kcb" => Some(if stock.is_kcb { 1.0 } else { 0.0 }), @@ -1913,6 +1970,11 @@ impl Strategy for PlatformExprStrategy { if !self.stock_passes_expr(ctx, &day, &stock)? { continue; } + let replacement_cash = + replacement_cash * self.buy_scale(ctx, &day, &stock)?; + if replacement_cash <= 0.0 { + continue; + } order_intents.push(OrderIntent::Value { symbol: symbol.clone(), value: replacement_cash, @@ -1977,9 +2039,13 @@ impl Strategy for PlatformExprStrategy { if !self.stock_passes_expr(ctx, &day, &stock)? { continue; } + let buy_cash = fixed_buy_cash * self.buy_scale(ctx, &day, &stock)?; + if buy_cash <= 0.0 { + continue; + } order_intents.push(OrderIntent::Value { symbol: symbol.clone(), - value: fixed_buy_cash, + value: buy_cash, reason: "periodic_rebalance_buy".to_string(), }); self.project_order_value( @@ -1987,7 +2053,7 @@ impl Strategy for PlatformExprStrategy { &mut projected, date, symbol, - fixed_buy_cash, + buy_cash, &mut projected_execution_state, ); } diff --git a/data/demo/000852量价微盘平台策略.engine-script.txt b/data/demo/000852量价微盘平台策略.engine-script.txt index 308e89b..5aa3e69 100644 --- a/data/demo/000852量价微盘平台策略.engine-script.txt +++ b/data/demo/000852量价微盘平台策略.engine-script.txt @@ -52,6 +52,7 @@ strategy("microcap_volume_trend_000852") { risk.take_profit(close_rate) risk.stop_loss(loss_rate) + allocation.buy_scale(touched_upper_limit ? 1.0 : trade_rate) ordering.rank_by("market_cap", "asc") } diff --git a/data/demo/000852量价微盘平台策略.strategy_spec.json b/data/demo/000852量价微盘平台策略.strategy_spec.json index 4f1ad02..370cca8 100644 --- a/data/demo/000852量价微盘平台策略.strategy_spec.json +++ b/data/demo/000852量价微盘平台策略.strategy_spec.json @@ -29,6 +29,9 @@ "stopLossExpr": "loss_rate", "takeProfitExpr": "close_rate" }, + "allocation": { + "buyScaleExpr": "touched_upper_limit ? 1.0 : trade_rate" + }, "ordering": { "rankBy": "market_cap", "rankExpr": "",