Support buy scaling from platform factors

This commit is contained in:
boris
2026-04-22 04:04:13 -07:00
parent 7f54309d53
commit 1a12c46589
3 changed files with 72 additions and 2 deletions

View File

@@ -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<f64, BacktestError> {
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,
);
}

View File

@@ -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")
}

View File

@@ -29,6 +29,9 @@
"stopLossExpr": "loss_rate",
"takeProfitExpr": "close_rate"
},
"allocation": {
"buyScaleExpr": "touched_upper_limit ? 1.0 : trade_rate"
},
"ordering": {
"rankBy": "market_cap",
"rankExpr": "",