Expose signal open to platform strategies
This commit is contained in:
@@ -485,7 +485,9 @@ impl SymbolPriceSeries {
|
||||
#[derive(Debug, Clone)]
|
||||
struct BenchmarkPriceSeries {
|
||||
dates: Vec<NaiveDate>,
|
||||
opens: Vec<f64>,
|
||||
closes: Vec<f64>,
|
||||
open_prefix: Vec<f64>,
|
||||
close_prefix: Vec<f64>,
|
||||
}
|
||||
|
||||
@@ -494,16 +496,24 @@ impl BenchmarkPriceSeries {
|
||||
let mut sorted = rows.to_vec();
|
||||
sorted.sort_by_key(|row| row.date);
|
||||
let dates = sorted.iter().map(|row| row.date).collect::<Vec<_>>();
|
||||
let opens = sorted.iter().map(|row| row.open).collect::<Vec<_>>();
|
||||
let closes = sorted.iter().map(|row| row.close).collect::<Vec<_>>();
|
||||
let open_prefix = prefix_sums(&opens);
|
||||
let close_prefix = prefix_sums(&closes);
|
||||
Self {
|
||||
dates,
|
||||
opens,
|
||||
closes,
|
||||
open_prefix,
|
||||
close_prefix,
|
||||
}
|
||||
}
|
||||
|
||||
fn moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> {
|
||||
self.moving_average_for(date, lookback, PriceField::Close)
|
||||
}
|
||||
|
||||
fn moving_average_for(&self, date: NaiveDate, lookback: usize, field: PriceField) -> Option<f64> {
|
||||
if lookback == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -516,7 +526,11 @@ impl BenchmarkPriceSeries {
|
||||
return None;
|
||||
}
|
||||
let start = end - lookback;
|
||||
let sum = self.close_prefix[end] - self.close_prefix[start];
|
||||
let prefix = match field {
|
||||
PriceField::Open => &self.open_prefix,
|
||||
PriceField::Close | PriceField::Last => &self.close_prefix,
|
||||
};
|
||||
let sum = prefix[end] - prefix[start];
|
||||
Some(sum / lookback as f64)
|
||||
}
|
||||
|
||||
@@ -942,6 +956,20 @@ impl DataSet {
|
||||
self.benchmark_series_cache.moving_average(date, lookback)
|
||||
}
|
||||
|
||||
pub fn benchmark_open_moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> {
|
||||
self.benchmark_series_cache
|
||||
.moving_average_for(date, lookback, PriceField::Open)
|
||||
}
|
||||
|
||||
pub fn market_open_moving_average(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
symbol: &str,
|
||||
lookback: usize,
|
||||
) -> Option<f64> {
|
||||
self.market_moving_average(date, symbol, lookback, PriceField::Open)
|
||||
}
|
||||
|
||||
pub fn eligible_universe_on(&self, date: NaiveDate) -> &[EligibleUniverseSnapshot] {
|
||||
self.eligible_universe_by_date
|
||||
.get(&date)
|
||||
|
||||
@@ -69,10 +69,9 @@ fn band_low(index_close) {
|
||||
"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(),
|
||||
take_profit_expr: "1.07".to_string(),
|
||||
exposure_expr: "1.0".to_string(),
|
||||
stop_loss_expr: String::new(),
|
||||
take_profit_expr: String::new(),
|
||||
rank_by: "market_cap".to_string(),
|
||||
rank_expr: String::new(),
|
||||
rank_desc: false,
|
||||
@@ -110,7 +109,9 @@ struct ProjectedExecutionFill {
|
||||
#[derive(Debug, Clone)]
|
||||
struct DayExpressionState {
|
||||
date: NaiveDate,
|
||||
signal_open: f64,
|
||||
signal_close: f64,
|
||||
benchmark_open: f64,
|
||||
benchmark_close: f64,
|
||||
benchmark_ma_short: f64,
|
||||
benchmark_ma_long: f64,
|
||||
@@ -647,6 +648,15 @@ impl PlatformExprStrategy {
|
||||
ctx: &StrategyContext<'_>,
|
||||
date: NaiveDate,
|
||||
) -> Result<DayExpressionState, BacktestError> {
|
||||
let signal_open = ctx
|
||||
.data
|
||||
.market(date, &self.config.signal_symbol)
|
||||
.ok_or_else(|| BacktestError::MissingPrice {
|
||||
date,
|
||||
symbol: self.config.signal_symbol.clone(),
|
||||
field: "open",
|
||||
})?
|
||||
.open;
|
||||
let signal_close = ctx
|
||||
.data
|
||||
.market_decision_close(date, &self.config.signal_symbol)
|
||||
@@ -655,11 +665,12 @@ impl PlatformExprStrategy {
|
||||
symbol: self.config.signal_symbol.clone(),
|
||||
field: "decision_close",
|
||||
})?;
|
||||
let benchmark_close = ctx
|
||||
let benchmark = ctx
|
||||
.data
|
||||
.benchmark(date)
|
||||
.ok_or(BacktestError::MissingBenchmark { date })?
|
||||
.close;
|
||||
.ok_or(BacktestError::MissingBenchmark { date })?;
|
||||
let benchmark_open = benchmark.open;
|
||||
let benchmark_close = benchmark.close;
|
||||
let benchmark_ma_short = ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(
|
||||
@@ -711,7 +722,9 @@ impl PlatformExprStrategy {
|
||||
|
||||
Ok(DayExpressionState {
|
||||
date,
|
||||
signal_open,
|
||||
signal_close,
|
||||
benchmark_open,
|
||||
benchmark_close,
|
||||
benchmark_ma_short,
|
||||
benchmark_ma_long,
|
||||
@@ -868,7 +881,9 @@ impl PlatformExprStrategy {
|
||||
position: Option<&PositionExpressionState>,
|
||||
) -> Scope<'static> {
|
||||
let mut scope = Scope::new();
|
||||
scope.push("signal_open", day.signal_open);
|
||||
scope.push("signal_close", day.signal_close);
|
||||
scope.push("benchmark_open", day.benchmark_open);
|
||||
scope.push("benchmark_close", day.benchmark_close);
|
||||
scope.push("signal_ma5", day.signal_ma5);
|
||||
scope.push("signal_ma10", day.signal_ma10);
|
||||
@@ -898,7 +913,9 @@ impl PlatformExprStrategy {
|
||||
scope.push("is_month_end", day.is_month_end);
|
||||
scope.push("signal_ma30", day.signal_ma30);
|
||||
let mut day_factors = Map::new();
|
||||
day_factors.insert("signal_open".into(), Dynamic::from(day.signal_open));
|
||||
day_factors.insert("signal_close".into(), Dynamic::from(day.signal_close));
|
||||
day_factors.insert("benchmark_open".into(), Dynamic::from(day.benchmark_open));
|
||||
day_factors.insert("benchmark_close".into(), Dynamic::from(day.benchmark_close));
|
||||
day_factors.insert("signal_ma5".into(), Dynamic::from(day.signal_ma5));
|
||||
day_factors.insert("signal_ma10".into(), Dynamic::from(day.signal_ma10));
|
||||
@@ -1244,7 +1261,11 @@ impl PlatformExprStrategy {
|
||||
));
|
||||
}
|
||||
let value = match field {
|
||||
"benchmark_open" => ctx.data.benchmark_open_moving_average(day.date, lookback),
|
||||
"benchmark_close" => ctx.data.benchmark_moving_average(day.date, lookback),
|
||||
"signal_open" => ctx
|
||||
.data
|
||||
.market_open_moving_average(day.date, &self.config.signal_symbol, lookback),
|
||||
"signal_close" => ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(day.date, &self.config.signal_symbol, lookback),
|
||||
@@ -1838,6 +1859,9 @@ impl PlatformExprStrategy {
|
||||
let Some(position) = ctx.portfolio.position(symbol) else {
|
||||
return Ok((false, false));
|
||||
};
|
||||
if self.config.stop_loss_expr.trim().is_empty() && self.config.take_profit_expr.trim().is_empty() {
|
||||
return Ok((false, false));
|
||||
}
|
||||
if position.quantity == 0 || position.average_cost <= 0.0 {
|
||||
return Ok((false, false));
|
||||
}
|
||||
@@ -1855,27 +1879,35 @@ impl PlatformExprStrategy {
|
||||
quantity: position.quantity as i64,
|
||||
sellable_qty: position.sellable_qty(date) as i64,
|
||||
};
|
||||
let stop_result = self.eval_dynamic(ctx, &self.config.stop_loss_expr, day, Some(&stock), Some(&position_state))?;
|
||||
let stop_hit = if let Some(boolean) = stop_result.clone().try_cast::<bool>() {
|
||||
boolean
|
||||
} else if let Some(multiplier) = stop_result.clone().try_cast::<f64>() {
|
||||
current_price <= position.average_cost * multiplier
|
||||
} else if let Some(multiplier) = stop_result.try_cast::<i64>() {
|
||||
current_price <= position.average_cost * multiplier as f64
|
||||
} else {
|
||||
let stop_hit = if self.config.stop_loss_expr.trim().is_empty() {
|
||||
false
|
||||
} else {
|
||||
let stop_result = self.eval_dynamic(ctx, &self.config.stop_loss_expr, day, Some(&stock), Some(&position_state))?;
|
||||
if let Some(boolean) = stop_result.clone().try_cast::<bool>() {
|
||||
boolean
|
||||
} else if let Some(multiplier) = stop_result.clone().try_cast::<f64>() {
|
||||
current_price <= position.average_cost * multiplier
|
||||
} else if let Some(multiplier) = stop_result.try_cast::<i64>() {
|
||||
current_price <= position.average_cost * multiplier as f64
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
let take_result = self.eval_dynamic(ctx, &self.config.take_profit_expr, day, Some(&stock), Some(&position_state))?;
|
||||
let profit_hit = if let Some(boolean) = take_result.clone().try_cast::<bool>() {
|
||||
boolean
|
||||
} else if let Some(multiplier) = take_result.clone().try_cast::<f64>() {
|
||||
!ctx.data.require_market(date, symbol)?.is_at_upper_limit_price(current_price)
|
||||
&& current_price / position.average_cost > multiplier
|
||||
} else if let Some(multiplier) = take_result.try_cast::<i64>() {
|
||||
!ctx.data.require_market(date, symbol)?.is_at_upper_limit_price(current_price)
|
||||
&& current_price / position.average_cost > multiplier as f64
|
||||
} else {
|
||||
let profit_hit = if self.config.take_profit_expr.trim().is_empty() {
|
||||
false
|
||||
} else {
|
||||
let take_result = self.eval_dynamic(ctx, &self.config.take_profit_expr, day, Some(&stock), Some(&position_state))?;
|
||||
if let Some(boolean) = take_result.clone().try_cast::<bool>() {
|
||||
boolean
|
||||
} else if let Some(multiplier) = take_result.clone().try_cast::<f64>() {
|
||||
!ctx.data.require_market(date, symbol)?.is_at_upper_limit_price(current_price)
|
||||
&& current_price / position.average_cost > multiplier
|
||||
} else if let Some(multiplier) = take_result.try_cast::<i64>() {
|
||||
!ctx.data.require_market(date, symbol)?.is_at_upper_limit_price(current_price)
|
||||
&& current_price / position.average_cost > multiplier as f64
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
Ok((stop_hit, profit_hit))
|
||||
}
|
||||
|
||||
@@ -116,8 +116,8 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
title: "日级字段".to_string(),
|
||||
detail: "可直接在表达式中使用的日级与账户字段。".to_string(),
|
||||
fields: vec![
|
||||
ManualField { name: "signal_close".to_string(), field_type: "float".to_string(), detail: "信号指数前一日收盘价。".to_string() },
|
||||
ManualField { name: "benchmark_close".to_string(), field_type: "float".to_string(), detail: "基准前一日收盘价。".to_string() },
|
||||
ManualField { name: "signal_open/signal_close".to_string(), field_type: "float".to_string(), detail: "信号指数当日开盘价与前一日收盘价。".to_string() },
|
||||
ManualField { name: "benchmark_open/benchmark_close".to_string(), field_type: "float".to_string(), detail: "基准当日开盘价与前一日收盘价。".to_string() },
|
||||
ManualField { name: "signal_ma5/signal_ma10/signal_ma20/signal_ma30".to_string(), field_type: "float".to_string(), detail: "信号指数滚动均线。".to_string() },
|
||||
ManualField { name: "benchmark_ma5/benchmark_ma10/benchmark_ma20/benchmark_ma30".to_string(), field_type: "float".to_string(), detail: "基准指数滚动均线。".to_string() },
|
||||
ManualField { name: "cash/available_cash/market_value/total_equity".to_string(), field_type: "float".to_string(), detail: "账户资金与总资产。".to_string() },
|
||||
@@ -158,7 +158,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
functions: vec![
|
||||
ManualFunction { name: "factor".to_string(), signature: "factor(\"column_name\")".to_string(), detail: "读取当前股票的数据库因子列。".to_string() },
|
||||
ManualFunction { name: "day_factor".to_string(), signature: "day_factor(\"field_name\")".to_string(), detail: "读取日级/指数级字段映射。".to_string() },
|
||||
ManualFunction { name: "rolling_mean".to_string(), signature: "rolling_mean(\"field\", lookback)".to_string(), detail: "任意字段滚动均值,支持 volume/amount/turnover_ratio 等。".to_string() },
|
||||
ManualFunction { name: "rolling_mean".to_string(), signature: "rolling_mean(\"field\", lookback)".to_string(), detail: "任意字段滚动均值,支持 volume/amount/turnover_ratio、signal_open/signal_close、benchmark_open/benchmark_close 等。".to_string() },
|
||||
ManualFunction { name: "sma".to_string(), signature: "sma(\"field\", lookback)".to_string(), detail: "rolling_mean 的别名。".to_string() },
|
||||
ManualFunction { name: "round/floor/ceil/abs/min/max/clamp".to_string(), signature: "round(x)".to_string(), detail: "常用数值函数。".to_string() },
|
||||
ManualFunction { name: "safe_div".to_string(), signature: "safe_div(lhs, rhs, fallback)".to_string(), detail: "安全除法。".to_string() },
|
||||
|
||||
Reference in New Issue
Block a user