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