恢复Source Lake滚动因子运行模式
This commit is contained in:
@@ -9,8 +9,8 @@ use rhai::{AST, Dynamic, Engine, Map, Scope};
|
|||||||
use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel};
|
use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel};
|
||||||
use crate::cost::ChinaAShareCostModel;
|
use crate::cost::ChinaAShareCostModel;
|
||||||
use crate::data::{
|
use crate::data::{
|
||||||
DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn,
|
DailyMarketSnapshot, EligibleUniverseSnapshot, NumericFactorMap, PriceField,
|
||||||
decision_market_cap_bn,
|
decision_free_float_cap_bn, decision_market_cap_bn,
|
||||||
};
|
};
|
||||||
use crate::engine::BacktestError;
|
use crate::engine::BacktestError;
|
||||||
use crate::events::OrderSide;
|
use crate::events::OrderSide;
|
||||||
@@ -424,6 +424,11 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub matching_type: MatchingType,
|
pub matching_type: MatchingType,
|
||||||
pub quote_quantity_limit: bool,
|
pub quote_quantity_limit: bool,
|
||||||
pub current_day_precomputed_factors: bool,
|
pub current_day_precomputed_factors: bool,
|
||||||
|
/// Use audited Strategy Factory Source Lake rolling fields when the
|
||||||
|
/// runtime intentionally loads only the latest decision rows. This is
|
||||||
|
/// opt-in; ordinary backtests always recompute rolling values from the
|
||||||
|
/// canonical market series.
|
||||||
|
pub prefer_precomputed_rolling_factors: bool,
|
||||||
pub intraday_execution_time: Option<NaiveTime>,
|
pub intraday_execution_time: Option<NaiveTime>,
|
||||||
pub delayed_limit_open_exit_enabled: bool,
|
pub delayed_limit_open_exit_enabled: bool,
|
||||||
pub delayed_limit_open_exit_time: Option<NaiveTime>,
|
pub delayed_limit_open_exit_time: Option<NaiveTime>,
|
||||||
@@ -495,6 +500,7 @@ impl PlatformExprStrategyConfig {
|
|||||||
matching_type: MatchingType::CurrentBarClose,
|
matching_type: MatchingType::CurrentBarClose,
|
||||||
quote_quantity_limit: true,
|
quote_quantity_limit: true,
|
||||||
current_day_precomputed_factors: false,
|
current_day_precomputed_factors: false,
|
||||||
|
prefer_precomputed_rolling_factors: false,
|
||||||
intraday_execution_time: None,
|
intraday_execution_time: None,
|
||||||
delayed_limit_open_exit_enabled: false,
|
delayed_limit_open_exit_enabled: false,
|
||||||
delayed_limit_open_exit_time: None,
|
delayed_limit_open_exit_time: None,
|
||||||
@@ -802,6 +808,42 @@ fn framework_stock_rolling_factor_requirement(key: &str) -> Option<(&'static str
|
|||||||
.map(|window| (field, window))
|
.map(|window| (field, window))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn precomputed_stock_rolling_mean<F>(get: F, field: &str, lookback: usize) -> Option<f64>
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> Option<f64>,
|
||||||
|
{
|
||||||
|
if lookback == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value_for = |key: String| get(&key).filter(|value| value.is_finite());
|
||||||
|
match field.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"close" | "prev_close" | "stock_close" | "price" => {
|
||||||
|
value_for(format!("ma{lookback}_prev_close"))
|
||||||
|
.or_else(|| value_for(format!("ma{lookback}")))
|
||||||
|
}
|
||||||
|
"volume" | "stock_volume" => value_for(format!("avg_volume{lookback}"))
|
||||||
|
.or_else(|| value_for(format!("vma{lookback}"))),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn precomputed_stock_current_rolling_mean<F>(get: F, field: &str, lookback: usize) -> Option<f64>
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> Option<f64>,
|
||||||
|
{
|
||||||
|
if lookback == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value_for = |key: String| get(&key).filter(|value| value.is_finite());
|
||||||
|
match field.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"close" | "prev_close" | "stock_close" | "price" => {
|
||||||
|
value_for(format!("ma{lookback}_current_back_adjusted_close"))
|
||||||
|
}
|
||||||
|
"volume" | "stock_volume" => value_for(format!("avg_volume{lookback}_current")),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SelectiveExpressionScope<'a> {
|
struct SelectiveExpressionScope<'a> {
|
||||||
inner: Scope<'static>,
|
inner: Scope<'static>,
|
||||||
required_identifiers: &'a AHashSet<String>,
|
required_identifiers: &'a AHashSet<String>,
|
||||||
@@ -3775,13 +3817,24 @@ impl PlatformExprStrategy {
|
|||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
symbol_id: u32,
|
symbol_id: u32,
|
||||||
symbol: &str,
|
symbol: &str,
|
||||||
|
extra_factors: Option<&NumericFactorMap>,
|
||||||
field: &str,
|
field: &str,
|
||||||
lookback: usize,
|
lookback: usize,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
ctx.data
|
let computed = || {
|
||||||
.market_decision_numeric_moving_average_by_symbol_id(
|
ctx.data
|
||||||
date, symbol_id, symbol, field, lookback,
|
.market_decision_numeric_moving_average_by_symbol_id(
|
||||||
)
|
date, symbol_id, symbol, field, lookback,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let precomputed = extra_factors.and_then(|factors| {
|
||||||
|
precomputed_stock_rolling_mean(|key| factors.get(key).copied(), field, lookback)
|
||||||
|
});
|
||||||
|
if self.config.prefer_precomputed_rolling_factors {
|
||||||
|
precomputed.or_else(computed)
|
||||||
|
} else {
|
||||||
|
computed().or(precomputed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stock_current_rolling_mean(
|
fn stock_current_rolling_mean(
|
||||||
@@ -3790,12 +3843,23 @@ impl PlatformExprStrategy {
|
|||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
symbol_id: u32,
|
symbol_id: u32,
|
||||||
symbol: &str,
|
symbol: &str,
|
||||||
|
extra_factors: Option<&NumericFactorMap>,
|
||||||
field: &str,
|
field: &str,
|
||||||
lookback: usize,
|
lookback: usize,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
ctx.data.market_current_numeric_moving_average_by_symbol_id(
|
let computed = || {
|
||||||
date, symbol_id, symbol, field, lookback,
|
ctx.data.market_current_numeric_moving_average_by_symbol_id(
|
||||||
)
|
date, symbol_id, symbol, field, lookback,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let precomputed = extra_factors.and_then(|factors| {
|
||||||
|
precomputed_stock_current_rolling_mean(|key| factors.get(key).copied(), field, lookback)
|
||||||
|
});
|
||||||
|
if self.config.prefer_precomputed_rolling_factors {
|
||||||
|
precomputed.or_else(computed)
|
||||||
|
} else {
|
||||||
|
computed().or(precomputed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stock_state_at_time(
|
fn stock_state_at_time(
|
||||||
@@ -3920,8 +3984,16 @@ impl PlatformExprStrategy {
|
|||||||
if !self.stock_rolling_requirements.requires(field, lookback) {
|
if !self.stock_rolling_requirements.requires(field, lookback) {
|
||||||
return f64::NAN;
|
return f64::NAN;
|
||||||
}
|
}
|
||||||
self.stock_decision_rolling_mean(ctx, date, symbol_id, symbol, field, lookback)
|
self.stock_decision_rolling_mean(
|
||||||
.unwrap_or(f64::NAN)
|
ctx,
|
||||||
|
date,
|
||||||
|
symbol_id,
|
||||||
|
symbol,
|
||||||
|
Some(&factor.extra_factors),
|
||||||
|
field,
|
||||||
|
lookback,
|
||||||
|
)
|
||||||
|
.unwrap_or(f64::NAN)
|
||||||
};
|
};
|
||||||
let stock_ma_short = rolling("close", self.config.stock_short_ma_days);
|
let stock_ma_short = rolling("close", self.config.stock_short_ma_days);
|
||||||
let stock_ma_mid = rolling("close", self.config.stock_mid_ma_days);
|
let stock_ma_mid = rolling("close", self.config.stock_mid_ma_days);
|
||||||
@@ -6037,6 +6109,9 @@ impl PlatformExprStrategy {
|
|||||||
day.date,
|
day.date,
|
||||||
stock.symbol_id,
|
stock.symbol_id,
|
||||||
&stock.symbol,
|
&stock.symbol,
|
||||||
|
ctx.data
|
||||||
|
.factor_by_symbol_id(day.date, stock.symbol_id)
|
||||||
|
.map(|factor| &factor.extra_factors),
|
||||||
field,
|
field,
|
||||||
lookback,
|
lookback,
|
||||||
)
|
)
|
||||||
@@ -6692,6 +6767,9 @@ impl PlatformExprStrategy {
|
|||||||
day.date,
|
day.date,
|
||||||
stock.symbol_id,
|
stock.symbol_id,
|
||||||
&stock.symbol,
|
&stock.symbol,
|
||||||
|
ctx.data
|
||||||
|
.factor_by_symbol_id(day.date, stock.symbol_id)
|
||||||
|
.map(|factor| &factor.extra_factors),
|
||||||
other,
|
other,
|
||||||
lookback,
|
lookback,
|
||||||
)
|
)
|
||||||
@@ -6741,6 +6819,9 @@ impl PlatformExprStrategy {
|
|||||||
day.date,
|
day.date,
|
||||||
stock.symbol_id,
|
stock.symbol_id,
|
||||||
&stock.symbol,
|
&stock.symbol,
|
||||||
|
ctx.data
|
||||||
|
.factor_by_symbol_id(day.date, stock.symbol_id)
|
||||||
|
.map(|factor| &factor.extra_factors),
|
||||||
other,
|
other,
|
||||||
lookback,
|
lookback,
|
||||||
)
|
)
|
||||||
@@ -12092,6 +12173,7 @@ mod tests {
|
|||||||
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode,
|
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode,
|
||||||
PlatformTradeAction, PlatformUniverseActionKind, RuntimeHelperResolution,
|
PlatformTradeAction, PlatformUniverseActionKind, RuntimeHelperResolution,
|
||||||
SelectionRiskDeferral, StockFilterQuoteUsage, framework_stock_rolling_factor_requirement,
|
SelectionRiskDeferral, StockFilterQuoteUsage, framework_stock_rolling_factor_requirement,
|
||||||
|
precomputed_stock_current_rolling_mean, precomputed_stock_rolling_mean,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
|
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
|
||||||
@@ -12159,6 +12241,39 @@ mod tests {
|
|||||||
assert_eq!(framework_stock_rolling_factor_requirement("alpha001"), None);
|
assert_eq!(framework_stock_rolling_factor_requirement("alpha001"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_lake_precomputed_rolling_aliases_use_only_canonical_fields() {
|
||||||
|
let factors = BTreeMap::from([
|
||||||
|
("ma5_prev_close".to_string(), 11.0),
|
||||||
|
("ma5".to_string(), 12.0),
|
||||||
|
("ma5_current_close".to_string(), 99.0),
|
||||||
|
("ma5_current_back_adjusted_close".to_string(), 13.0),
|
||||||
|
("avg_volume5".to_string(), 100.0),
|
||||||
|
("avg_volume5_current".to_string(), 120.0),
|
||||||
|
]);
|
||||||
|
let get = |key: &str| factors.get(key).copied();
|
||||||
|
assert_eq!(precomputed_stock_rolling_mean(get, "close", 5), Some(11.0));
|
||||||
|
assert_eq!(
|
||||||
|
precomputed_stock_current_rolling_mean(get, "close", 5),
|
||||||
|
Some(13.0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
precomputed_stock_rolling_mean(get, "volume", 5),
|
||||||
|
Some(100.0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
precomputed_stock_current_rolling_mean(get, "volume", 5),
|
||||||
|
Some(120.0)
|
||||||
|
);
|
||||||
|
// A source row that only has a raw current-close alias must not be
|
||||||
|
// mistaken for the adjusted rolling value.
|
||||||
|
let raw_only = BTreeMap::from([("ma5_current_close".to_string(), 99.0)]);
|
||||||
|
assert_eq!(
|
||||||
|
precomputed_stock_current_rolling_mean(|key| raw_only.get(key).copied(), "close", 5,),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn typed_runtime_numbers_preserve_legacy_rhai_formatting() {
|
fn typed_runtime_numbers_preserve_legacy_rhai_formatting() {
|
||||||
let RuntimeHelperResolution::Number(value) =
|
let RuntimeHelperResolution::Number(value) =
|
||||||
|
|||||||
Reference in New Issue
Block a user