Support buy scaling from platform factors
This commit is contained in:
@@ -24,6 +24,7 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub market_cap_upper_expr: String,
|
pub market_cap_upper_expr: String,
|
||||||
pub selection_limit_expr: String,
|
pub selection_limit_expr: String,
|
||||||
pub stock_filter_expr: String,
|
pub stock_filter_expr: String,
|
||||||
|
pub buy_scale_expr: String,
|
||||||
pub exposure_expr: String,
|
pub exposure_expr: String,
|
||||||
pub stop_loss_expr: String,
|
pub stop_loss_expr: String,
|
||||||
pub take_profit_expr: String,
|
pub take_profit_expr: String,
|
||||||
@@ -67,6 +68,7 @@ fn band_low(index_close) {
|
|||||||
stock_filter_expr:
|
stock_filter_expr:
|
||||||
"stock_ma_short > stock_ma_mid * ma_ratio && stock_ma_mid > stock_ma_long"
|
"stock_ma_short > stock_ma_mid * ma_ratio && stock_ma_mid > stock_ma_long"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
buy_scale_expr: "1.0".to_string(),
|
||||||
exposure_expr:
|
exposure_expr:
|
||||||
"benchmark_ma_short < benchmark_ma_long * ma_ratio ? 0.5 : 1.0".to_string(),
|
"benchmark_ma_short < benchmark_ma_long * ma_ratio ? 0.5 : 1.0".to_string(),
|
||||||
stop_loss_expr: "0.93".to_string(),
|
stop_loss_expr: "0.93".to_string(),
|
||||||
@@ -166,6 +168,8 @@ struct StockExpressionState {
|
|||||||
is_new_listing: bool,
|
is_new_listing: bool,
|
||||||
allow_buy: bool,
|
allow_buy: bool,
|
||||||
allow_sell: bool,
|
allow_sell: bool,
|
||||||
|
touched_upper_limit: bool,
|
||||||
|
touched_lower_limit: bool,
|
||||||
listed_days: i64,
|
listed_days: i64,
|
||||||
stock_ma_short: f64,
|
stock_ma_short: f64,
|
||||||
stock_ma_mid: f64,
|
stock_ma_mid: f64,
|
||||||
@@ -300,6 +304,10 @@ impl PlatformExprStrategy {
|
|||||||
"is_new_listing",
|
"is_new_listing",
|
||||||
"allow_buy",
|
"allow_buy",
|
||||||
"allow_sell",
|
"allow_sell",
|
||||||
|
"touched_upper_limit",
|
||||||
|
"touched_lower_limit",
|
||||||
|
"hit_upper_limit",
|
||||||
|
"hit_lower_limit",
|
||||||
"listed_days",
|
"listed_days",
|
||||||
"stock_ma_short",
|
"stock_ma_short",
|
||||||
"stock_ma_mid",
|
"stock_ma_mid",
|
||||||
@@ -794,6 +802,20 @@ impl PlatformExprStrategy {
|
|||||||
.data
|
.data
|
||||||
.market_decision_volume_moving_average(date, symbol, 60)
|
.market_decision_volume_moving_average(date, symbol, 60)
|
||||||
.unwrap_or(stock_volume_ma20);
|
.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 {
|
Ok(StockExpressionState {
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
@@ -821,6 +843,8 @@ impl PlatformExprStrategy {
|
|||||||
is_new_listing: candidate.is_new_listing,
|
is_new_listing: candidate.is_new_listing,
|
||||||
allow_buy: candidate.allow_buy,
|
allow_buy: candidate.allow_buy,
|
||||||
allow_sell: candidate.allow_sell,
|
allow_sell: candidate.allow_sell,
|
||||||
|
touched_upper_limit,
|
||||||
|
touched_lower_limit,
|
||||||
listed_days: if candidate.is_new_listing { 0 } else { 365 },
|
listed_days: if candidate.is_new_listing { 0 } else { 365 },
|
||||||
stock_ma_short,
|
stock_ma_short,
|
||||||
stock_ma_mid,
|
stock_ma_mid,
|
||||||
@@ -932,6 +956,10 @@ impl PlatformExprStrategy {
|
|||||||
scope.push("is_new_listing", stock.is_new_listing);
|
scope.push("is_new_listing", stock.is_new_listing);
|
||||||
scope.push("allow_buy", stock.allow_buy);
|
scope.push("allow_buy", stock.allow_buy);
|
||||||
scope.push("allow_sell", stock.allow_sell);
|
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("listed_days", stock.listed_days);
|
||||||
scope.push("at_upper_limit", at_upper_limit);
|
scope.push("at_upper_limit", at_upper_limit);
|
||||||
scope.push("at_lower_limit", at_lower_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("is_new_listing".into(), Dynamic::from(stock.is_new_listing));
|
||||||
factors.insert("allow_buy".into(), Dynamic::from(stock.allow_buy));
|
factors.insert("allow_buy".into(), Dynamic::from(stock.allow_buy));
|
||||||
factors.insert("allow_sell".into(), Dynamic::from(stock.allow_sell));
|
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("listed_days".into(), Dynamic::from(stock.listed_days));
|
||||||
factors.insert("at_upper_limit".into(), Dynamic::from(at_upper_limit));
|
factors.insert("at_upper_limit".into(), Dynamic::from(at_upper_limit));
|
||||||
factors.insert("at_lower_limit".into(), Dynamic::from(at_lower_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)
|
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(
|
fn stock_passes_expr(
|
||||||
&self,
|
&self,
|
||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
@@ -1611,6 +1662,12 @@ impl PlatformExprStrategy {
|
|||||||
"stock_volume_ma60" | "volume_ma60" => Some(stock.stock_volume_ma60),
|
"stock_volume_ma60" | "volume_ma60" => Some(stock.stock_volume_ma60),
|
||||||
"allow_buy" => Some(if stock.allow_buy { 1.0 } else { 0.0 }),
|
"allow_buy" => Some(if stock.allow_buy { 1.0 } else { 0.0 }),
|
||||||
"allow_sell" => Some(if stock.allow_sell { 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 }),
|
"paused" => Some(if stock.paused { 1.0 } else { 0.0 }),
|
||||||
"is_st" => Some(if stock.is_st { 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 }),
|
"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)? {
|
if !self.stock_passes_expr(ctx, &day, &stock)? {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let replacement_cash =
|
||||||
|
replacement_cash * self.buy_scale(ctx, &day, &stock)?;
|
||||||
|
if replacement_cash <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
order_intents.push(OrderIntent::Value {
|
order_intents.push(OrderIntent::Value {
|
||||||
symbol: symbol.clone(),
|
symbol: symbol.clone(),
|
||||||
value: replacement_cash,
|
value: replacement_cash,
|
||||||
@@ -1977,9 +2039,13 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
if !self.stock_passes_expr(ctx, &day, &stock)? {
|
if !self.stock_passes_expr(ctx, &day, &stock)? {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let buy_cash = fixed_buy_cash * self.buy_scale(ctx, &day, &stock)?;
|
||||||
|
if buy_cash <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
order_intents.push(OrderIntent::Value {
|
order_intents.push(OrderIntent::Value {
|
||||||
symbol: symbol.clone(),
|
symbol: symbol.clone(),
|
||||||
value: fixed_buy_cash,
|
value: buy_cash,
|
||||||
reason: "periodic_rebalance_buy".to_string(),
|
reason: "periodic_rebalance_buy".to_string(),
|
||||||
});
|
});
|
||||||
self.project_order_value(
|
self.project_order_value(
|
||||||
@@ -1987,7 +2053,7 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
&mut projected,
|
&mut projected,
|
||||||
date,
|
date,
|
||||||
symbol,
|
symbol,
|
||||||
fixed_buy_cash,
|
buy_cash,
|
||||||
&mut projected_execution_state,
|
&mut projected_execution_state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ strategy("microcap_volume_trend_000852") {
|
|||||||
|
|
||||||
risk.take_profit(close_rate)
|
risk.take_profit(close_rate)
|
||||||
risk.stop_loss(loss_rate)
|
risk.stop_loss(loss_rate)
|
||||||
|
allocation.buy_scale(touched_upper_limit ? 1.0 : trade_rate)
|
||||||
|
|
||||||
ordering.rank_by("market_cap", "asc")
|
ordering.rank_by("market_cap", "asc")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@
|
|||||||
"stopLossExpr": "loss_rate",
|
"stopLossExpr": "loss_rate",
|
||||||
"takeProfitExpr": "close_rate"
|
"takeProfitExpr": "close_rate"
|
||||||
},
|
},
|
||||||
|
"allocation": {
|
||||||
|
"buyScaleExpr": "touched_upper_limit ? 1.0 : trade_rate"
|
||||||
|
},
|
||||||
"ordering": {
|
"ordering": {
|
||||||
"rankBy": "market_cap",
|
"rankBy": "market_cap",
|
||||||
"rankExpr": "",
|
"rankExpr": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user