修正平台表达式回测口径

This commit is contained in:
boris
2026-06-13 20:01:24 +08:00
parent 0dca8e0eff
commit e1d36fc0c7
2 changed files with 160 additions and 20 deletions
+79 -20
View File
@@ -208,8 +208,13 @@ pub struct PlatformExprStrategyConfig {
pub retry_empty_rebalance: bool,
pub calendar_rebalance_interval: bool,
pub aiquant_transaction_cost: bool,
pub commission_rate: Option<f64>,
pub minimum_commission: Option<f64>,
pub stamp_tax_rate_before_change: Option<f64>,
pub stamp_tax_rate_after_change: Option<f64>,
pub strict_value_budget: bool,
pub quote_quantity_limit: bool,
pub current_day_precomputed_factors: bool,
pub intraday_execution_time: Option<NaiveTime>,
pub explicit_action_stage: PlatformExplicitActionStage,
pub explicit_action_schedule: Option<PlatformRebalanceSchedule>,
@@ -266,8 +271,13 @@ fn band_low(index_close) {
retry_empty_rebalance: false,
calendar_rebalance_interval: false,
aiquant_transaction_cost: false,
commission_rate: None,
minimum_commission: None,
stamp_tax_rate_before_change: None,
stamp_tax_rate_after_change: None,
strict_value_budget: false,
quote_quantity_limit: true,
current_day_precomputed_factors: false,
intraday_execution_time: None,
explicit_action_stage: PlatformExplicitActionStage::OnDay,
explicit_action_schedule: None,
@@ -879,11 +889,24 @@ impl PlatformExprStrategy {
}
fn cost_model(&self) -> ChinaAShareCostModel {
if self.config.aiquant_transaction_cost {
let mut model = if self.config.aiquant_transaction_cost {
ChinaAShareCostModel::aiquant_rqalpha_default()
} else {
ChinaAShareCostModel::default()
};
if let Some(value) = self.config.commission_rate {
model.commission_rate = value;
}
if let Some(value) = self.config.minimum_commission {
model.minimum_commission = value;
}
if let Some(value) = self.config.stamp_tax_rate_before_change {
model.stamp_tax_rate_before_change = value;
}
if let Some(value) = self.config.stamp_tax_rate_after_change {
model.stamp_tax_rate_after_change = value;
}
model
}
fn marked_total_value(&self, ctx: &StrategyContext<'_>, date: NaiveDate) -> f64 {
@@ -4007,14 +4030,16 @@ impl PlatformExprStrategy {
Ok(value.round().max(1.0) as usize)
}
fn selection_dates(&self, ctx: &StrategyContext<'_>) -> (NaiveDate, NaiveDate) {
fn selection_dates(&self, ctx: &StrategyContext<'_>) -> (NaiveDate, NaiveDate, NaiveDate) {
let decision_date = ctx.decision_date;
let factor_date = if self.config.aiquant_transaction_cost {
let previous_factor_date = ctx
.data
.previous_trading_date(decision_date, 1)
.unwrap_or(decision_date);
let stock_factor_date = if self.config.current_day_precomputed_factors {
decision_date
} else {
ctx.data
.previous_trading_date(decision_date, 1)
.unwrap_or(decision_date)
previous_factor_date
};
let selection_date = if self.config.aiquant_transaction_cost
&& self.config.intraday_execution_time.is_some()
@@ -4023,7 +4048,7 @@ impl PlatformExprStrategy {
} else {
decision_date
};
(selection_date, factor_date)
(selection_date, previous_factor_date, stock_factor_date)
}
fn buy_scale(
@@ -5138,6 +5163,13 @@ impl PlatformExprStrategy {
candidate: &EligibleUniverseSnapshot,
stock: &StockExpressionState,
) -> f64 {
match self.config.market_cap_field.as_str() {
"market_cap" | "market_cap_bn" => return candidate.market_cap_bn,
"free_float_cap" | "free_float_market_cap" | "free_float_cap_bn" => {
return candidate.free_float_cap_bn;
}
_ => {}
}
self.stock_numeric_field_value(candidate, stock, self.config.market_cap_field.as_str())
.unwrap_or_else(|| self.field_value(candidate))
}
@@ -5236,18 +5268,19 @@ impl PlatformExprStrategy {
&self,
ctx: &StrategyContext<'_>,
date: NaiveDate,
factor_date: NaiveDate,
universe_factor_date: NaiveDate,
stock_factor_date: NaiveDate,
day: &DayExpressionState,
band_low: f64,
band_high: f64,
limit: usize,
) -> Result<(Vec<String>, Vec<String>), BacktestError> {
let universe = self.selectable_universe_on(ctx, date, factor_date);
let universe = self.selectable_universe_on(ctx, date, universe_factor_date);
let mut diagnostics = Vec::new();
let mut candidates = Vec::new();
for candidate in universe {
let stock =
self.stock_state_with_factor_date(ctx, date, factor_date, &candidate.symbol)?;
self.stock_state_with_factor_date(ctx, date, stock_factor_date, &candidate.symbol)?;
let field_value = self.selection_field_value(&candidate, &stock);
if !field_value.is_finite() {
if diagnostics.len() < 12 {
@@ -5341,19 +5374,20 @@ impl PlatformExprStrategy {
&self,
ctx: &StrategyContext<'_>,
date: NaiveDate,
factor_date: NaiveDate,
universe_factor_date: NaiveDate,
stock_factor_date: NaiveDate,
day: &DayExpressionState,
band_low: f64,
band_high: f64,
selection_limit: usize,
) -> Result<(Vec<String>, Vec<String>, Vec<String>), BacktestError> {
let universe = self.selectable_universe_on(ctx, date, factor_date);
let universe = self.selectable_universe_on(ctx, date, universe_factor_date);
let mut diagnostics = Vec::new();
let mut candidates = Vec::new();
let apply_stock_filter = !self.stock_filter_uses_intraday_quote_fields();
for candidate in universe {
let stock =
self.stock_state_with_factor_date(ctx, date, factor_date, &candidate.symbol)?;
self.stock_state_with_factor_date(ctx, date, stock_factor_date, &candidate.symbol)?;
let field_value = self.selection_field_value(&candidate, &stock);
if !field_value.is_finite() {
if diagnostics.len() < 12 {
@@ -5406,7 +5440,8 @@ impl PlatformExprStrategy {
processed_symbols.push(symbol.clone());
}
for (symbol, _) in &candidates[cursor..end] {
let stock = self.stock_state_with_factor_date(ctx, date, factor_date, symbol)?;
let stock =
self.stock_state_with_factor_date(ctx, date, stock_factor_date, symbol)?;
if let Some(reason) = self.buy_rejection_reason(ctx, date, symbol, &stock)? {
if diagnostics.len() < 12 {
diagnostics.push(format!("{symbol} quote_plan rejected by {reason}"));
@@ -5453,7 +5488,7 @@ impl PlatformExprStrategy {
}
let day = self.day_state(ctx, ctx.decision_date)?;
let (selection_date, factor_date) = self.selection_dates(ctx);
let (selection_date, universe_factor_date, stock_factor_date) = self.selection_dates(ctx);
let requires_intraday_selection_quotes = self.stock_filter_uses_intraday_quote_fields();
let (band_low, band_high) = self.market_cap_band(ctx, &day)?;
let selection_limit = self
@@ -5462,7 +5497,8 @@ impl PlatformExprStrategy {
let (candidate_symbols, order_symbols, diagnostics) = self.select_quote_plan_symbols(
ctx,
selection_date,
factor_date,
universe_factor_date,
stock_factor_date,
&day,
band_low,
band_high,
@@ -5472,7 +5508,7 @@ impl PlatformExprStrategy {
execution_date: ctx.execution_date,
decision_date: ctx.decision_date,
selection_date,
factor_date,
factor_date: stock_factor_date,
requires_intraday_selection_quotes,
band_low,
band_high,
@@ -5658,7 +5694,8 @@ impl Strategy for PlatformExprStrategy {
}
let day = self.day_state(ctx, decision_date)?;
let (selection_market_date, selection_factor_date) = self.selection_dates(ctx);
let (selection_market_date, selection_universe_factor_date, selection_factor_date) =
self.selection_dates(ctx);
let (explicit_action_intents, explicit_action_diagnostics) =
if self.config.explicit_action_stage == PlatformExplicitActionStage::OnDay
&& self.explicit_actions_active(ctx.data.calendar(), execution_date)
@@ -5696,6 +5733,7 @@ impl Strategy for PlatformExprStrategy {
let (stock_list, notes) = self.select_symbols(
ctx,
selection_market_date,
selection_universe_factor_date,
selection_factor_date,
&day,
band_low,
@@ -6207,7 +6245,7 @@ impl Strategy for PlatformExprStrategy {
)
},
format!(
"selected={} periodic_rebalance={} exits={} projected_positions={} intents={} limit={} decision_date={} selection_market_date={} selection_factor_date={} execution_date={} budget_total={:.2} marked_total={:.2} day_total={:.2}",
"selected={} periodic_rebalance={} exits={} projected_positions={} intents={} limit={} decision_date={} selection_market_date={} selection_universe_factor_date={} selection_factor_date={} execution_date={} budget_total={:.2} marked_total={:.2} day_total={:.2}",
stock_list.len(),
periodic_rebalance,
exit_symbols.len(),
@@ -6216,6 +6254,7 @@ impl Strategy for PlatformExprStrategy {
selection_limit,
decision_date,
selection_market_date,
selection_universe_factor_date,
selection_factor_date,
execution_date,
aiquant_total_value,
@@ -6331,6 +6370,18 @@ mod tests {
));
}
#[test]
fn platform_expr_cost_model_uses_commission_override() {
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.aiquant_transaction_cost = true;
cfg.commission_rate = Some(0.0003);
cfg.minimum_commission = Some(5.0);
let strategy = PlatformExprStrategy::new(cfg);
assert!((strategy.buy_commission(100_000.0) - 30.0).abs() < 1e-9);
assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9);
}
#[test]
fn platform_expr_rewrites_prelude_assignment_ternary() {
let prelude = "let cap_low = sig_close <= 0 ? 7 : max(5, min(cap_low_raw, 70));";
@@ -7404,7 +7455,7 @@ mod tests {
let day = strategy.day_state(&ctx, date).expect("day state");
let (selected, _) = strategy
.select_symbols(&ctx, date, date, &day, 10.0, 20.0, 1)
.select_symbols(&ctx, date, date, date, &day, 10.0, 20.0, 1)
.expect("selection");
assert!(selected.is_empty());
}
@@ -7908,6 +7959,14 @@ mod tests {
"{:?}",
decision.diagnostics
);
assert!(
decision
.diagnostics
.iter()
.any(|item| item.contains("selection_factor_date=2023-11-10")),
"{:?}",
decision.diagnostics
);
assert!(
decision
.diagnostics
@@ -67,6 +67,14 @@ pub struct StrategyExecutionSpec {
#[serde(default)]
pub slippage_max_value: Option<f64>,
#[serde(default)]
pub commission_rate: Option<f64>,
#[serde(default)]
pub minimum_commission: Option<f64>,
#[serde(default)]
pub stamp_tax_rate_before_change: Option<f64>,
#[serde(default)]
pub stamp_tax_rate_after_change: Option<f64>,
#[serde(default)]
pub strict_value_budget: Option<bool>,
}
@@ -108,6 +116,14 @@ pub struct StrategyEngineConfig {
#[serde(default)]
pub slippage_max_value: Option<f64>,
#[serde(default)]
pub commission_rate: Option<f64>,
#[serde(default)]
pub minimum_commission: Option<f64>,
#[serde(default)]
pub stamp_tax_rate_before_change: Option<f64>,
#[serde(default)]
pub stamp_tax_rate_after_change: Option<f64>,
#[serde(default)]
pub strict_value_budget: Option<bool>,
#[serde(default)]
pub dividend_reinvestment: Option<bool>,
@@ -343,6 +359,31 @@ pub fn platform_expr_config_from_value(
))
}
fn valid_non_negative(value: Option<f64>) -> Option<f64> {
value.filter(|item| item.is_finite() && *item >= 0.0)
}
fn apply_cost_overrides(
cfg: &mut PlatformExprStrategyConfig,
commission_rate: Option<f64>,
minimum_commission: Option<f64>,
stamp_tax_rate_before_change: Option<f64>,
stamp_tax_rate_after_change: Option<f64>,
) {
if let Some(value) = valid_non_negative(commission_rate) {
cfg.commission_rate = Some(value);
}
if let Some(value) = valid_non_negative(minimum_commission) {
cfg.minimum_commission = Some(value);
}
if let Some(value) = valid_non_negative(stamp_tax_rate_before_change) {
cfg.stamp_tax_rate_before_change = Some(value);
}
if let Some(value) = valid_non_negative(stamp_tax_rate_after_change) {
cfg.stamp_tax_rate_after_change = Some(value);
}
}
pub fn platform_expr_config_from_spec(
strategy_id: &str,
signal_symbol: &str,
@@ -440,6 +481,13 @@ pub fn platform_expr_config_from_spec(
{
cfg.benchmark_symbol = spec_benchmark_symbol.clone();
}
apply_cost_overrides(
&mut cfg,
engine.commission_rate,
engine.minimum_commission,
engine.stamp_tax_rate_before_change,
engine.stamp_tax_rate_after_change,
);
}
if let Some(spec_signal_symbol) = spec
@@ -741,6 +789,15 @@ pub fn platform_expr_config_from_spec(
{
cfg.aiquant_transaction_cost = true;
}
if let Some(execution) = spec.execution.as_ref() {
apply_cost_overrides(
&mut cfg,
execution.commission_rate,
execution.minimum_commission,
execution.stamp_tax_rate_before_change,
execution.stamp_tax_rate_after_change,
);
}
cfg
}
@@ -1169,6 +1226,30 @@ mod tests {
);
}
#[test]
fn parses_execution_cost_overrides_into_platform_config() {
let spec = serde_json::json!({
"execution": {
"compatibilityProfile": "aiquant_rqalpha",
"commissionRate": 0.0003,
"minimumCommission": 5.0,
"stampTaxRateBeforeChange": 0.0005,
"stampTaxRateAfterChange": 0.0005
},
"engineConfig": {
"commissionRate": 0.0008
}
});
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert!(cfg.aiquant_transaction_cost);
assert_eq!(cfg.commission_rate, Some(0.0003));
assert_eq!(cfg.minimum_commission, Some(5.0));
assert_eq!(cfg.stamp_tax_rate_before_change, Some(0.0005));
assert_eq!(cfg.stamp_tax_rate_after_change, Some(0.0005));
}
#[test]
fn parses_daily_schedule_time_for_aiquant_execution_quotes() {
let spec = serde_json::json!({