Expose signal open to platform strategies
This commit is contained in:
@@ -485,7 +485,9 @@ impl SymbolPriceSeries {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct BenchmarkPriceSeries {
|
struct BenchmarkPriceSeries {
|
||||||
dates: Vec<NaiveDate>,
|
dates: Vec<NaiveDate>,
|
||||||
|
opens: Vec<f64>,
|
||||||
closes: Vec<f64>,
|
closes: Vec<f64>,
|
||||||
|
open_prefix: Vec<f64>,
|
||||||
close_prefix: Vec<f64>,
|
close_prefix: Vec<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,16 +496,24 @@ impl BenchmarkPriceSeries {
|
|||||||
let mut sorted = rows.to_vec();
|
let mut sorted = rows.to_vec();
|
||||||
sorted.sort_by_key(|row| row.date);
|
sorted.sort_by_key(|row| row.date);
|
||||||
let dates = sorted.iter().map(|row| row.date).collect::<Vec<_>>();
|
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 closes = sorted.iter().map(|row| row.close).collect::<Vec<_>>();
|
||||||
|
let open_prefix = prefix_sums(&opens);
|
||||||
let close_prefix = prefix_sums(&closes);
|
let close_prefix = prefix_sums(&closes);
|
||||||
Self {
|
Self {
|
||||||
dates,
|
dates,
|
||||||
|
opens,
|
||||||
closes,
|
closes,
|
||||||
|
open_prefix,
|
||||||
close_prefix,
|
close_prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> {
|
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 {
|
if lookback == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -516,7 +526,11 @@ impl BenchmarkPriceSeries {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let start = end - lookback;
|
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)
|
Some(sum / lookback as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -942,6 +956,20 @@ impl DataSet {
|
|||||||
self.benchmark_series_cache.moving_average(date, lookback)
|
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] {
|
pub fn eligible_universe_on(&self, date: NaiveDate) -> &[EligibleUniverseSnapshot] {
|
||||||
self.eligible_universe_by_date
|
self.eligible_universe_by_date
|
||||||
.get(&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"
|
"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(),
|
buy_scale_expr: "1.0".to_string(),
|
||||||
exposure_expr:
|
exposure_expr: "1.0".to_string(),
|
||||||
"benchmark_ma_short < benchmark_ma_long * ma_ratio ? 0.5 : 1.0".to_string(),
|
stop_loss_expr: String::new(),
|
||||||
stop_loss_expr: "0.93".to_string(),
|
take_profit_expr: String::new(),
|
||||||
take_profit_expr: "1.07".to_string(),
|
|
||||||
rank_by: "market_cap".to_string(),
|
rank_by: "market_cap".to_string(),
|
||||||
rank_expr: String::new(),
|
rank_expr: String::new(),
|
||||||
rank_desc: false,
|
rank_desc: false,
|
||||||
@@ -110,7 +109,9 @@ struct ProjectedExecutionFill {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct DayExpressionState {
|
struct DayExpressionState {
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
|
signal_open: f64,
|
||||||
signal_close: f64,
|
signal_close: f64,
|
||||||
|
benchmark_open: f64,
|
||||||
benchmark_close: f64,
|
benchmark_close: f64,
|
||||||
benchmark_ma_short: f64,
|
benchmark_ma_short: f64,
|
||||||
benchmark_ma_long: f64,
|
benchmark_ma_long: f64,
|
||||||
@@ -647,6 +648,15 @@ impl PlatformExprStrategy {
|
|||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
) -> Result<DayExpressionState, BacktestError> {
|
) -> 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
|
let signal_close = ctx
|
||||||
.data
|
.data
|
||||||
.market_decision_close(date, &self.config.signal_symbol)
|
.market_decision_close(date, &self.config.signal_symbol)
|
||||||
@@ -655,11 +665,12 @@ impl PlatformExprStrategy {
|
|||||||
symbol: self.config.signal_symbol.clone(),
|
symbol: self.config.signal_symbol.clone(),
|
||||||
field: "decision_close",
|
field: "decision_close",
|
||||||
})?;
|
})?;
|
||||||
let benchmark_close = ctx
|
let benchmark = ctx
|
||||||
.data
|
.data
|
||||||
.benchmark(date)
|
.benchmark(date)
|
||||||
.ok_or(BacktestError::MissingBenchmark { date })?
|
.ok_or(BacktestError::MissingBenchmark { date })?;
|
||||||
.close;
|
let benchmark_open = benchmark.open;
|
||||||
|
let benchmark_close = benchmark.close;
|
||||||
let benchmark_ma_short = ctx
|
let benchmark_ma_short = ctx
|
||||||
.data
|
.data
|
||||||
.market_decision_close_moving_average(
|
.market_decision_close_moving_average(
|
||||||
@@ -711,7 +722,9 @@ impl PlatformExprStrategy {
|
|||||||
|
|
||||||
Ok(DayExpressionState {
|
Ok(DayExpressionState {
|
||||||
date,
|
date,
|
||||||
|
signal_open,
|
||||||
signal_close,
|
signal_close,
|
||||||
|
benchmark_open,
|
||||||
benchmark_close,
|
benchmark_close,
|
||||||
benchmark_ma_short,
|
benchmark_ma_short,
|
||||||
benchmark_ma_long,
|
benchmark_ma_long,
|
||||||
@@ -868,7 +881,9 @@ impl PlatformExprStrategy {
|
|||||||
position: Option<&PositionExpressionState>,
|
position: Option<&PositionExpressionState>,
|
||||||
) -> Scope<'static> {
|
) -> Scope<'static> {
|
||||||
let mut scope = Scope::new();
|
let mut scope = Scope::new();
|
||||||
|
scope.push("signal_open", day.signal_open);
|
||||||
scope.push("signal_close", day.signal_close);
|
scope.push("signal_close", day.signal_close);
|
||||||
|
scope.push("benchmark_open", day.benchmark_open);
|
||||||
scope.push("benchmark_close", day.benchmark_close);
|
scope.push("benchmark_close", day.benchmark_close);
|
||||||
scope.push("signal_ma5", day.signal_ma5);
|
scope.push("signal_ma5", day.signal_ma5);
|
||||||
scope.push("signal_ma10", day.signal_ma10);
|
scope.push("signal_ma10", day.signal_ma10);
|
||||||
@@ -898,7 +913,9 @@ impl PlatformExprStrategy {
|
|||||||
scope.push("is_month_end", day.is_month_end);
|
scope.push("is_month_end", day.is_month_end);
|
||||||
scope.push("signal_ma30", day.signal_ma30);
|
scope.push("signal_ma30", day.signal_ma30);
|
||||||
let mut day_factors = Map::new();
|
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("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("benchmark_close".into(), Dynamic::from(day.benchmark_close));
|
||||||
day_factors.insert("signal_ma5".into(), Dynamic::from(day.signal_ma5));
|
day_factors.insert("signal_ma5".into(), Dynamic::from(day.signal_ma5));
|
||||||
day_factors.insert("signal_ma10".into(), Dynamic::from(day.signal_ma10));
|
day_factors.insert("signal_ma10".into(), Dynamic::from(day.signal_ma10));
|
||||||
@@ -1244,7 +1261,11 @@ impl PlatformExprStrategy {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let value = match field {
|
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),
|
"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
|
"signal_close" => ctx
|
||||||
.data
|
.data
|
||||||
.market_decision_close_moving_average(day.date, &self.config.signal_symbol, lookback),
|
.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 {
|
let Some(position) = ctx.portfolio.position(symbol) else {
|
||||||
return Ok((false, false));
|
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 {
|
if position.quantity == 0 || position.average_cost <= 0.0 {
|
||||||
return Ok((false, false));
|
return Ok((false, false));
|
||||||
}
|
}
|
||||||
@@ -1855,27 +1879,35 @@ impl PlatformExprStrategy {
|
|||||||
quantity: position.quantity as i64,
|
quantity: position.quantity as i64,
|
||||||
sellable_qty: position.sellable_qty(date) 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 self.config.stop_loss_expr.trim().is_empty() {
|
||||||
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 {
|
|
||||||
false
|
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 self.config.take_profit_expr.trim().is_empty() {
|
||||||
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 {
|
|
||||||
false
|
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))
|
Ok((stop_hit, profit_hit))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
title: "日级字段".to_string(),
|
title: "日级字段".to_string(),
|
||||||
detail: "可直接在表达式中使用的日级与账户字段。".to_string(),
|
detail: "可直接在表达式中使用的日级与账户字段。".to_string(),
|
||||||
fields: vec![
|
fields: vec![
|
||||||
ManualField { name: "signal_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_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: "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: "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() },
|
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![
|
functions: vec![
|
||||||
ManualFunction { name: "factor".to_string(), signature: "factor(\"column_name\")".to_string(), detail: "读取当前股票的数据库因子列。".to_string() },
|
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: "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: "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: "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() },
|
ManualFunction { name: "safe_div".to_string(), signature: "safe_div(lhs, rhs, fallback)".to_string(), detail: "安全除法。".to_string() },
|
||||||
|
|||||||
Reference in New Issue
Block a user