修正平台表达式回测口径
This commit is contained in:
@@ -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!({
|
||||
|
||||
Reference in New Issue
Block a user