修复执行价索引和平台表达式回退
This commit is contained in:
@@ -4694,7 +4694,11 @@ where
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if candidate_gross <= cash + 1e-6 {
|
||||
let candidate_cost = self
|
||||
.cost_model
|
||||
.calculate(snapshot.date, OrderSide::Buy, candidate_gross)
|
||||
.total();
|
||||
if candidate_gross + candidate_cost <= cash + 1e-6 {
|
||||
break;
|
||||
}
|
||||
budget_block_reason = Some("insufficient cash after fees");
|
||||
|
||||
@@ -734,6 +734,27 @@ impl BenchmarkPriceSeries {
|
||||
Some(sum / lookback as f64)
|
||||
}
|
||||
|
||||
fn decision_values_for(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
lookback: usize,
|
||||
field: PriceField,
|
||||
) -> Vec<f64> {
|
||||
if lookback == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let end = match self.dates.binary_search(&date) {
|
||||
Ok(idx) => idx,
|
||||
Err(0) => return Vec::new(),
|
||||
Err(idx) => idx,
|
||||
};
|
||||
let start = end.saturating_sub(lookback);
|
||||
match field {
|
||||
PriceField::DayOpen | PriceField::Open => self.opens[start..end].to_vec(),
|
||||
PriceField::Close | PriceField::Last => self.closes[start..end].to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn moving_average_for(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -791,7 +812,7 @@ pub struct DataSet {
|
||||
candidate_by_date: BTreeMap<NaiveDate, Vec<CandidateEligibility>>,
|
||||
candidate_index: HashMap<(NaiveDate, String), CandidateEligibility>,
|
||||
corporate_actions_by_date: BTreeMap<NaiveDate, Vec<CorporateAction>>,
|
||||
execution_quotes_index: HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>>,
|
||||
execution_quotes_by_date: HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>,
|
||||
order_book_depth_index: HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>,
|
||||
benchmark_by_date: BTreeMap<NaiveDate, BenchmarkSnapshot>,
|
||||
market_series_by_symbol: HashMap<String, SymbolPriceSeries>,
|
||||
@@ -1075,7 +1096,7 @@ impl DataSet {
|
||||
.map(|item| ((item.date, item.symbol.clone()), item))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let corporate_actions_by_date = group_by_date(corporate_actions, |item| item.date);
|
||||
let execution_quotes_index = build_execution_quote_index(execution_quotes);
|
||||
let execution_quotes_by_date = build_execution_quote_index(execution_quotes);
|
||||
let order_book_depth_index = build_order_book_depth_index(order_book_depth);
|
||||
|
||||
let benchmark_by_date = benchmarks
|
||||
@@ -1105,7 +1126,7 @@ impl DataSet {
|
||||
candidate_by_date,
|
||||
candidate_index,
|
||||
corporate_actions_by_date,
|
||||
execution_quotes_index,
|
||||
execution_quotes_by_date,
|
||||
order_book_depth_index,
|
||||
benchmark_by_date,
|
||||
market_series_by_symbol,
|
||||
@@ -1177,14 +1198,22 @@ impl DataSet {
|
||||
}
|
||||
|
||||
pub fn execution_quotes_on(&self, date: NaiveDate, symbol: &str) -> &[IntradayExecutionQuote] {
|
||||
self.execution_quotes_index
|
||||
.get(&(date, symbol.to_string()))
|
||||
self.execution_quotes_by_date
|
||||
.get(&date)
|
||||
.and_then(|rows_by_symbol| rows_by_symbol.get(symbol))
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
pub fn execution_quote_key_set(&self) -> HashSet<(NaiveDate, String)> {
|
||||
self.execution_quotes_index.keys().cloned().collect()
|
||||
self.execution_quotes_by_date
|
||||
.iter()
|
||||
.flat_map(|(date, rows_by_symbol)| {
|
||||
rows_by_symbol
|
||||
.keys()
|
||||
.map(move |symbol| (*date, symbol.clone()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn add_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
||||
@@ -1192,7 +1221,12 @@ impl DataSet {
|
||||
let mut touched = HashSet::<(NaiveDate, String)>::new();
|
||||
for quote in quotes {
|
||||
let key = (quote.date, quote.symbol.clone());
|
||||
let rows = self.execution_quotes_index.entry(key.clone()).or_default();
|
||||
let rows = self
|
||||
.execution_quotes_by_date
|
||||
.entry(quote.date)
|
||||
.or_default()
|
||||
.entry(quote.symbol.clone())
|
||||
.or_default();
|
||||
if rows.iter().any(|existing| {
|
||||
existing.timestamp == quote.timestamp && existing.symbol == quote.symbol
|
||||
}) {
|
||||
@@ -1202,8 +1236,12 @@ impl DataSet {
|
||||
touched.insert(key);
|
||||
added += 1;
|
||||
}
|
||||
for key in touched {
|
||||
if let Some(rows) = self.execution_quotes_index.get_mut(&key) {
|
||||
for (date, symbol) in touched {
|
||||
if let Some(rows) = self
|
||||
.execution_quotes_by_date
|
||||
.get_mut(&date)
|
||||
.and_then(|rows_by_symbol| rows_by_symbol.get_mut(&symbol))
|
||||
{
|
||||
rows.sort_by_key(|quote| quote.timestamp);
|
||||
}
|
||||
}
|
||||
@@ -1223,10 +1261,11 @@ impl DataSet {
|
||||
|
||||
pub fn execution_quotes_on_date(&self, date: NaiveDate) -> Vec<IntradayExecutionQuote> {
|
||||
let mut quotes = self
|
||||
.execution_quotes_index
|
||||
.iter()
|
||||
.filter(|((quote_date, _), _)| *quote_date == date)
|
||||
.flat_map(|(_, rows)| rows.iter().cloned())
|
||||
.execution_quotes_by_date
|
||||
.get(&date)
|
||||
.into_iter()
|
||||
.flat_map(|rows_by_symbol| rows_by_symbol.values())
|
||||
.flat_map(|rows| rows.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
quotes.sort_by(|left, right| {
|
||||
left.timestamp
|
||||
@@ -1346,10 +1385,10 @@ impl DataSet {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut quotes = self
|
||||
.execution_quotes_index
|
||||
.iter()
|
||||
.filter(|((_, quote_symbol), _)| quote_symbol == symbol)
|
||||
.flat_map(|(_, rows)| rows.iter())
|
||||
.execution_quotes_by_date
|
||||
.values()
|
||||
.filter_map(|rows_by_symbol| rows_by_symbol.get(symbol))
|
||||
.flat_map(|rows| rows.iter())
|
||||
.filter(|quote| intraday_quote_visible(quote, date, active_datetime, include_now))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1846,12 +1885,11 @@ impl DataSet {
|
||||
.collect(),
|
||||
Some("1m") | Some("tick") => {
|
||||
let mut bars = self
|
||||
.execution_quotes_index
|
||||
.execution_quotes_by_date
|
||||
.iter()
|
||||
.filter(|((date, quote_symbol), _)| {
|
||||
quote_symbol == symbol && *date >= start && *date <= end
|
||||
})
|
||||
.flat_map(|(_, rows)| rows.iter())
|
||||
.filter(|(date, _)| **date >= start && **date <= end)
|
||||
.filter_map(|(_, rows_by_symbol)| rows_by_symbol.get(symbol))
|
||||
.flat_map(|rows| rows.iter())
|
||||
.map(intraday_quote_price_bar)
|
||||
.collect::<Vec<_>>();
|
||||
bars.sort_by(|left, right| {
|
||||
@@ -2269,6 +2307,25 @@ impl DataSet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn benchmark_decision_numeric_values(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
field: &str,
|
||||
lookback: usize,
|
||||
) -> Vec<f64> {
|
||||
let field = normalize_field(field);
|
||||
match field.as_str() {
|
||||
"open" | "day_open" | "dayopen" | "benchmark_open" => self
|
||||
.benchmark_series_cache
|
||||
.trailing_values_for(date, lookback, PriceField::Open),
|
||||
_ => self.benchmark_series_cache.decision_values_for(
|
||||
date,
|
||||
lookback,
|
||||
PriceField::Close,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn market_open_moving_average(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -3279,17 +3336,21 @@ fn build_futures_params_index(
|
||||
|
||||
fn build_execution_quote_index(
|
||||
execution_quotes: Vec<IntradayExecutionQuote>,
|
||||
) -> HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>> {
|
||||
let mut grouped = HashMap::<(NaiveDate, String), Vec<IntradayExecutionQuote>>::new();
|
||||
) -> HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>> {
|
||||
let mut grouped = HashMap::<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>::new();
|
||||
for quote in execution_quotes {
|
||||
grouped
|
||||
.entry((quote.date, quote.symbol.clone()))
|
||||
.entry(quote.date)
|
||||
.or_default()
|
||||
.entry(quote.symbol.clone())
|
||||
.or_default()
|
||||
.push(quote);
|
||||
}
|
||||
|
||||
for quotes in grouped.values_mut() {
|
||||
quotes.sort_by_key(|quote| quote.timestamp);
|
||||
for rows_by_symbol in grouped.values_mut() {
|
||||
for quotes in rows_by_symbol.values_mut() {
|
||||
quotes.sort_by_key(|quote| quote.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
grouped
|
||||
|
||||
@@ -439,19 +439,27 @@ fn precomputed_stock_rolling_mean(
|
||||
field: &str,
|
||||
lookback: usize,
|
||||
) -> Option<f64> {
|
||||
let key = match (field.trim().to_ascii_lowercase().as_str(), lookback) {
|
||||
("close" | "prev_close" | "stock_close" | "price", 5) => "ma5_prev_close",
|
||||
("close" | "prev_close" | "stock_close" | "price", 10) => "ma10_prev_close",
|
||||
("close" | "prev_close" | "stock_close" | "price", 20) => "ma20_prev_close",
|
||||
("close" | "prev_close" | "stock_close" | "price", 30) => "ma30_prev_close",
|
||||
("volume" | "stock_volume", 5) => "avg_volume5",
|
||||
("volume" | "stock_volume", 20) => "avg_volume20",
|
||||
("volume" | "stock_volume", 60) => "avg_volume60",
|
||||
let keys: &[&str] = match (field.trim().to_ascii_lowercase().as_str(), lookback) {
|
||||
("close" | "prev_close" | "stock_close" | "price", 5) => {
|
||||
&["ma5_prev_close", "sf_jq_v104_ma5", "ma5"]
|
||||
}
|
||||
("close" | "prev_close" | "stock_close" | "price", 10) => {
|
||||
&["ma10_prev_close", "sf_jq_v104_ma10", "ma10"]
|
||||
}
|
||||
("close" | "prev_close" | "stock_close" | "price", 20) => {
|
||||
&["ma20_prev_close", "sf_jq_v104_ma20", "ma20"]
|
||||
}
|
||||
("close" | "prev_close" | "stock_close" | "price", 30) => {
|
||||
&["ma30_prev_close", "sf_jq_v104_ma30", "ma30"]
|
||||
}
|
||||
("volume" | "stock_volume", 5) => &["avg_volume5", "sf_jq_v104_v5", "vma5"],
|
||||
("volume" | "stock_volume", 20) => &["avg_volume20", "sf_jq_v104_v20", "vma20"],
|
||||
("volume" | "stock_volume", 60) => &["avg_volume60", "sf_jq_v104_v60", "vma60"],
|
||||
("volume" | "stock_volume", 100) => &["avg_volume100", "sf_jq_v104_v100", "vma100"],
|
||||
_ => return None,
|
||||
};
|
||||
extra_factors
|
||||
.get(key)
|
||||
.copied()
|
||||
keys.iter()
|
||||
.find_map(|key| extra_factors.get(*key).copied())
|
||||
.filter(|value| value.is_finite())
|
||||
}
|
||||
|
||||
@@ -1155,6 +1163,21 @@ impl PlatformExprStrategy {
|
||||
})
|
||||
}
|
||||
|
||||
fn has_execution_quote_at_or_after(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
date: NaiveDate,
|
||||
symbol: &str,
|
||||
execution_state: &ProjectedExecutionState,
|
||||
) -> bool {
|
||||
let start_cursor =
|
||||
self.projected_execution_start_cursor(ctx, date, symbol, execution_state);
|
||||
ctx.data
|
||||
.execution_quotes_on(date, symbol)
|
||||
.iter()
|
||||
.any(|quote| quote.timestamp >= start_cursor)
|
||||
}
|
||||
|
||||
fn project_target_zero(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
@@ -1190,7 +1213,7 @@ impl PlatformExprStrategy {
|
||||
execution_state,
|
||||
)
|
||||
.or_else(|| {
|
||||
if ctx.data.execution_quotes_on(date, symbol).is_empty() {
|
||||
if !self.has_execution_quote_at_or_after(ctx, date, symbol, execution_state) {
|
||||
Some(ProjectedExecutionFill {
|
||||
price: self.projected_execution_price(market, OrderSide::Sell),
|
||||
quantity,
|
||||
@@ -1391,7 +1414,7 @@ impl PlatformExprStrategy {
|
||||
execution_state,
|
||||
)
|
||||
.or_else(|| {
|
||||
if ctx.data.execution_quotes_on(date, symbol).is_empty() {
|
||||
if !self.has_execution_quote_at_or_after(ctx, date, symbol, execution_state) {
|
||||
Some(ProjectedExecutionFill {
|
||||
price: execution_price,
|
||||
quantity,
|
||||
@@ -1601,78 +1624,92 @@ impl PlatformExprStrategy {
|
||||
let factor = ctx.data.require_factor(factor_date, symbol)?;
|
||||
let candidate = ctx.data.require_candidate(date, symbol)?;
|
||||
let instrument = ctx.data.instrument(symbol);
|
||||
let stock_ma_short = ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(date, symbol, self.config.stock_short_ma_days)
|
||||
let stock_ma_short = precomputed_stock_rolling_mean(
|
||||
&factor.extra_factors,
|
||||
"close",
|
||||
self.config.stock_short_ma_days,
|
||||
)
|
||||
.or_else(|| {
|
||||
ctx.data.market_decision_close_moving_average(
|
||||
date,
|
||||
symbol,
|
||||
self.config.stock_short_ma_days,
|
||||
)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma_mid = precomputed_stock_rolling_mean(
|
||||
&factor.extra_factors,
|
||||
"close",
|
||||
self.config.stock_mid_ma_days,
|
||||
)
|
||||
.or_else(|| {
|
||||
ctx.data.market_decision_close_moving_average(
|
||||
date,
|
||||
symbol,
|
||||
self.config.stock_mid_ma_days,
|
||||
)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma_long = precomputed_stock_rolling_mean(
|
||||
&factor.extra_factors,
|
||||
"close",
|
||||
self.config.stock_long_ma_days,
|
||||
)
|
||||
.or_else(|| {
|
||||
ctx.data.market_decision_close_moving_average(
|
||||
date,
|
||||
symbol,
|
||||
self.config.stock_long_ma_days,
|
||||
)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma5 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5)
|
||||
.or_else(|| {
|
||||
precomputed_stock_rolling_mean(
|
||||
&factor.extra_factors,
|
||||
"close",
|
||||
self.config.stock_short_ma_days,
|
||||
)
|
||||
ctx.data
|
||||
.market_decision_close_moving_average(date, symbol, 5)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma_mid = ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(date, symbol, self.config.stock_mid_ma_days)
|
||||
let stock_ma10 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10)
|
||||
.or_else(|| {
|
||||
precomputed_stock_rolling_mean(
|
||||
&factor.extra_factors,
|
||||
"close",
|
||||
self.config.stock_mid_ma_days,
|
||||
)
|
||||
ctx.data
|
||||
.market_decision_close_moving_average(date, symbol, 10)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma_long = ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(date, symbol, self.config.stock_long_ma_days)
|
||||
let stock_ma20 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20)
|
||||
.or_else(|| {
|
||||
precomputed_stock_rolling_mean(
|
||||
&factor.extra_factors,
|
||||
"close",
|
||||
self.config.stock_long_ma_days,
|
||||
)
|
||||
ctx.data
|
||||
.market_decision_close_moving_average(date, symbol, 20)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma5 = ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(date, symbol, 5)
|
||||
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5))
|
||||
let stock_ma30 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30)
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.market_decision_close_moving_average(date, symbol, 30)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma10 = ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(date, symbol, 10)
|
||||
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10))
|
||||
let stock_volume_ma5 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 5)
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.market_decision_volume_moving_average(date, symbol, 5)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma20 = ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(date, symbol, 20)
|
||||
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20))
|
||||
let stock_volume_ma10 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 10)
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.market_decision_volume_moving_average(date, symbol, 10)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_ma30 = ctx
|
||||
.data
|
||||
.market_decision_close_moving_average(date, symbol, 30)
|
||||
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30))
|
||||
let stock_volume_ma20 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 20)
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.market_decision_volume_moving_average(date, symbol, 20)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_volume_ma5 = ctx
|
||||
.data
|
||||
.market_decision_volume_moving_average(date, symbol, 5)
|
||||
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 5))
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_volume_ma10 = ctx
|
||||
.data
|
||||
.market_decision_volume_moving_average(date, symbol, 10)
|
||||
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 10))
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_volume_ma20 = ctx
|
||||
.data
|
||||
.market_decision_volume_moving_average(date, symbol, 20)
|
||||
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 20))
|
||||
.unwrap_or(f64::NAN);
|
||||
let stock_volume_ma60 = ctx
|
||||
.data
|
||||
.market_decision_volume_moving_average(date, symbol, 60)
|
||||
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 60))
|
||||
let stock_volume_ma60 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 60)
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.market_decision_volume_moving_average(date, symbol, 60)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
let touched_upper_limit = !market.paused
|
||||
&& (market.is_at_upper_limit_price(market.close)
|
||||
@@ -1706,7 +1743,7 @@ impl PlatformExprStrategy {
|
||||
})
|
||||
.or_else(|| decision_quote.and_then(|quote| quote.buy_price()))
|
||||
.unwrap_or(market.last_price),
|
||||
prev_close: market.prev_close,
|
||||
prev_close: feature_market.prev_close,
|
||||
amount,
|
||||
upper_limit: market.upper_limit,
|
||||
lower_limit: market.lower_limit,
|
||||
@@ -2416,6 +2453,60 @@ impl PlatformExprStrategy {
|
||||
Self::rewrite_ternary(&Self::normalize_runtime_field_aliases(expr.trim()))
|
||||
}
|
||||
|
||||
fn compact_expr(expr: &str) -> String {
|
||||
let mut output = String::with_capacity(expr.len());
|
||||
let mut in_single_quote = false;
|
||||
let mut in_double_quote = false;
|
||||
let mut escaped = false;
|
||||
for ch in expr.chars() {
|
||||
if escaped {
|
||||
output.push(ch);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if ch == '\\' && (in_single_quote || in_double_quote) {
|
||||
output.push(ch);
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if ch == '\'' && !in_double_quote {
|
||||
in_single_quote = !in_single_quote;
|
||||
output.push(ch);
|
||||
continue;
|
||||
}
|
||||
if ch == '"' && !in_single_quote {
|
||||
in_double_quote = !in_double_quote;
|
||||
output.push(ch);
|
||||
continue;
|
||||
}
|
||||
if !in_single_quote && !in_double_quote && ch.is_whitespace() {
|
||||
continue;
|
||||
}
|
||||
output.push(ch);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn prelude_numeric_constant(&self, name: &str) -> Option<f64> {
|
||||
for line in Self::normalize_prelude_for_eval(&self.config.prelude).lines() {
|
||||
let trimmed = line.trim();
|
||||
let Some(body) = trimmed.strip_prefix("let ") else {
|
||||
continue;
|
||||
};
|
||||
let Some((lhs, rhs)) = body.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
if lhs.trim() != name {
|
||||
continue;
|
||||
}
|
||||
let rhs = rhs.trim().trim_end_matches(';').trim();
|
||||
if let Ok(value) = rhs.parse::<f64>() {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn normalize_prelude_for_eval(prelude: &str) -> String {
|
||||
prelude
|
||||
.lines()
|
||||
@@ -3269,7 +3360,9 @@ 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),
|
||||
"benchmark_close" => ctx
|
||||
.data
|
||||
.benchmark_decision_moving_average(day.date, lookback),
|
||||
"signal_open" => {
|
||||
ctx.data
|
||||
.market_open_moving_average(day.date, &self.config.signal_symbol, lookback)
|
||||
@@ -3290,16 +3383,16 @@ impl PlatformExprStrategy {
|
||||
"rolling_mean(\"{other}\", {lookback}) requires stock context"
|
||||
))
|
||||
})?;
|
||||
ctx.data
|
||||
.market_decision_numeric_moving_average(
|
||||
day.date,
|
||||
&stock.symbol,
|
||||
other,
|
||||
lookback,
|
||||
)
|
||||
.or_else(|| {
|
||||
precomputed_stock_rolling_mean(&stock.extra_factors, other, lookback)
|
||||
})
|
||||
precomputed_stock_rolling_mean(&stock.extra_factors, other, lookback).or_else(
|
||||
|| {
|
||||
ctx.data.market_decision_numeric_moving_average(
|
||||
day.date,
|
||||
&stock.symbol,
|
||||
other,
|
||||
lookback,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
value.ok_or_else(|| {
|
||||
@@ -3375,7 +3468,7 @@ impl PlatformExprStrategy {
|
||||
.benchmark_numeric_values(day.date, "open", lookback),
|
||||
"benchmark_close" => ctx
|
||||
.data
|
||||
.benchmark_numeric_values(day.date, "close", lookback),
|
||||
.benchmark_decision_numeric_values(day.date, "close", lookback),
|
||||
"signal_open" => ctx.data.market_decision_numeric_values(
|
||||
day.date,
|
||||
&self.config.signal_symbol,
|
||||
@@ -3867,10 +3960,13 @@ impl PlatformExprStrategy {
|
||||
|
||||
fn selection_dates(&self, ctx: &StrategyContext<'_>) -> (NaiveDate, NaiveDate) {
|
||||
let decision_date = ctx.decision_date;
|
||||
let factor_date = ctx
|
||||
.data
|
||||
.previous_trading_date(decision_date, 1)
|
||||
.unwrap_or(decision_date);
|
||||
let factor_date = if self.config.aiquant_transaction_cost {
|
||||
decision_date
|
||||
} else {
|
||||
ctx.data
|
||||
.previous_trading_date(decision_date, 1)
|
||||
.unwrap_or(decision_date)
|
||||
};
|
||||
let selection_date = if self.config.aiquant_transaction_cost
|
||||
&& self.config.intraday_execution_time.is_some()
|
||||
{
|
||||
@@ -4764,6 +4860,9 @@ impl PlatformExprStrategy {
|
||||
if self.config.stock_filter_expr.trim().is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
if let Some(value) = self.fast_stock_passes_expr(ctx, day, stock) {
|
||||
return Ok(value);
|
||||
}
|
||||
match self.eval_bool(ctx, &self.config.stock_filter_expr, day, Some(stock), None) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(error) if Self::is_missing_rolling_mean_error(&error) => Ok(false),
|
||||
@@ -4771,6 +4870,56 @@ impl PlatformExprStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
fn fast_stock_passes_expr(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
day: &DayExpressionState,
|
||||
stock: &StockExpressionState,
|
||||
) -> Option<bool> {
|
||||
let compact = Self::compact_expr(&Self::normalize_expr(&self.config.stock_filter_expr));
|
||||
let ma_ratio = self.prelude_numeric_constant("ma_ratio").unwrap_or(1.0);
|
||||
if compact == "stock_ma_short>stock_ma_mid*ma_ratio&&stock_ma_mid>stock_ma_long" {
|
||||
return Some(
|
||||
stock.stock_ma_short.is_finite()
|
||||
&& stock.stock_ma_mid.is_finite()
|
||||
&& stock.stock_ma_long.is_finite()
|
||||
&& stock.stock_ma_short > stock.stock_ma_mid * ma_ratio
|
||||
&& stock.stock_ma_mid > stock.stock_ma_long,
|
||||
);
|
||||
}
|
||||
|
||||
if compact
|
||||
!= "listed_days>=min_listed_days&&rolling_mean(\"close\",5)>rolling_mean(\"close\",10)*ma_ratio&&rolling_mean(\"close\",10)>rolling_mean(\"close\",30)*ma_ratio&&rolling_mean(\"volume\",5)<rolling_mean(\"volume\",100)*max_volume_ratio"
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let min_listed_days = self
|
||||
.prelude_numeric_constant("min_listed_days")
|
||||
.unwrap_or(0.0);
|
||||
let max_volume_ratio = self
|
||||
.prelude_numeric_constant("max_volume_ratio")
|
||||
.unwrap_or(1.0);
|
||||
let volume_ma100 = precomputed_stock_rolling_mean(&stock.extra_factors, "volume", 100)
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.market_decision_volume_moving_average(day.date, &stock.symbol, 100)
|
||||
})
|
||||
.unwrap_or(f64::NAN);
|
||||
|
||||
Some(
|
||||
(stock.listed_days as f64) >= min_listed_days
|
||||
&& stock.stock_ma5.is_finite()
|
||||
&& stock.stock_ma10.is_finite()
|
||||
&& stock.stock_ma30.is_finite()
|
||||
&& stock.stock_volume_ma5.is_finite()
|
||||
&& volume_ma100.is_finite()
|
||||
&& stock.stock_ma5 > stock.stock_ma10 * ma_ratio
|
||||
&& stock.stock_ma10 > stock.stock_ma30 * ma_ratio
|
||||
&& stock.stock_volume_ma5 < volume_ma100 * max_volume_ratio,
|
||||
)
|
||||
}
|
||||
|
||||
fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 {
|
||||
match self.config.market_cap_field.as_str() {
|
||||
"free_float_cap" | "free_float_market_cap" => row.free_float_cap_bn,
|
||||
@@ -5130,6 +5279,8 @@ impl PlatformExprStrategy {
|
||||
| "touched_lower_limit"
|
||||
| "hit_upper_limit"
|
||||
| "hit_lower_limit"
|
||||
| "at_upper_limit"
|
||||
| "at_lower_limit"
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -6964,6 +7115,7 @@ mod tests {
|
||||
.expect("stock state");
|
||||
|
||||
assert_eq!(stock.close, 9.9);
|
||||
assert_eq!(stock.prev_close, 9.7);
|
||||
assert_eq!(stock.volume, 12_300);
|
||||
assert_eq!(stock.last, 19.06);
|
||||
assert_eq!(stock.market_cap, 8.0);
|
||||
@@ -7183,6 +7335,26 @@ mod tests {
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: decision_date,
|
||||
symbol: limit_symbol.to_string(),
|
||||
market_cap_bn: 1.0,
|
||||
free_float_cap_bn: 1.0,
|
||||
pe_ttm: 8.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: decision_date,
|
||||
symbol: fallback_symbol.to_string(),
|
||||
market_cap_bn: 2.0,
|
||||
free_float_cap_bn: 2.0,
|
||||
pe_ttm: 8.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
extra_factors: BTreeMap::new(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: execution_date,
|
||||
symbol: limit_symbol.to_string(),
|
||||
|
||||
@@ -43,7 +43,8 @@ impl Strategy for BuyThenHoldStrategy {
|
||||
#[test]
|
||||
fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
let date2 = d(2025, 1, 3);
|
||||
let delist_date = d(2025, 1, 3);
|
||||
let date2 = d(2025, 1, 6);
|
||||
let data = DataSet::from_components(
|
||||
vec![
|
||||
Instrument {
|
||||
@@ -52,8 +53,8 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: Some(date1),
|
||||
status: "delisted".to_string(),
|
||||
delisted_at: Some(delist_date),
|
||||
status: "active".to_string(),
|
||||
},
|
||||
Instrument {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
@@ -115,7 +116,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run()
|
||||
DailyMarketSnapshot {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
||||
day_open: 5.1,
|
||||
open: 5.1,
|
||||
high: 5.2,
|
||||
@@ -273,7 +274,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: Some(date2),
|
||||
status: "delisted".to_string(),
|
||||
status: "active".to_string(),
|
||||
},
|
||||
Instrument {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
|
||||
@@ -329,7 +329,7 @@ impl Strategy for AuctionOrderStrategy {
|
||||
exit_symbols: BTreeSet::new(),
|
||||
order_intents: vec![fidc_core::OrderIntent::Value {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
value: 1_000.0,
|
||||
value: 1_010.0,
|
||||
reason: "auction_buy".to_string(),
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
@@ -3734,7 +3734,7 @@ impl Strategy for BuyMissingRowThenHoldStrategy {
|
||||
exit_symbols: BTreeSet::new(),
|
||||
order_intents: vec![OrderIntent::Value {
|
||||
symbol: "601028.SH".to_string(),
|
||||
value: 1_000.0,
|
||||
value: 1_010.0,
|
||||
reason: "seed_position".to_string(),
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
|
||||
Reference in New Issue
Block a user