2 Commits

Author SHA1 Message Date
boris
2165831708 使用前一交易日指数价格计算市值区间,模拟实盘场景
- 修改trading_ratio()返回5个值,包含prev_level
- 使用prev_level计算市值区间,符合实盘决策逻辑
- 调整默认参数对齐AiQuant实际运行版本(xs=0.008, cap_span=10)
- 增强MA过滤调试日志,输出首个决策日所有股票的过滤详情
- 添加市值区间计算调试日志
2026-05-12 18:03:56 +08:00
boris
1a402f2048 实现市值区间padding机制
- 添加padding_ratio、min_padding、max_padding配置参数
- 在市值区间计算中应用padding扩大选股范围
- 更新OmniMicroCapConfig、CnSmallCapRotationConfig和DynamicMarketCapBandSelector
- AiQuant V1.0.4默认padding: ratio=0.5, min=12.5, max=30.0
- 目标:增加候选股票数量,匹配AiQuant行为
2026-05-11 20:38:12 +08:00
3 changed files with 103 additions and 17 deletions

View File

@@ -114,6 +114,15 @@ pub struct DynamicRangeConfig {
pub cap_span: Option<f64>,
#[serde(default)]
pub xs: Option<f64>,
/// Padding ratio to expand the market cap range (e.g., 0.5 means 50% of span)
#[serde(default)]
pub padding_ratio: Option<f64>,
/// Minimum padding in billion yuan
#[serde(default)]
pub min_padding: Option<f64>,
/// Maximum padding in billion yuan
#[serde(default)]
pub max_padding: Option<f64>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]

View File

@@ -1090,6 +1090,9 @@ pub struct CnSmallCapRotationConfig {
pub base_index_level: f64,
pub base_cap_floor: f64,
pub cap_span: f64,
pub padding_ratio: f64,
pub min_padding: f64,
pub max_padding: f64,
pub short_ma_days: usize,
pub long_ma_days: usize,
pub stock_short_ma_days: usize,
@@ -1114,6 +1117,9 @@ impl CnSmallCapRotationConfig {
base_index_level: 2000.0,
base_cap_floor: 7.0,
cap_span: 10.0,
padding_ratio: 0.5,
min_padding: 8.0,
max_padding: 20.0,
short_ma_days: 3,
long_ma_days: 5,
stock_short_ma_days: 3,
@@ -1138,6 +1144,9 @@ impl CnSmallCapRotationConfig {
base_index_level: 2000.0,
base_cap_floor: 7.0,
cap_span: 10.0,
padding_ratio: 0.5,
min_padding: 8.0,
max_padding: 20.0,
short_ma_days: 5,
long_ma_days: 10,
stock_short_ma_days: 5,
@@ -1185,6 +1194,9 @@ impl CnSmallCapRotationStrategy {
config.cap_span,
config.xs,
config.stocknum,
config.padding_ratio,
config.min_padding,
config.max_padding,
),
config,
last_gross_exposure: None,
@@ -1508,6 +1520,9 @@ pub struct OmniMicroCapConfig {
pub base_index_level: f64,
pub base_cap_floor: f64,
pub cap_span: f64,
pub padding_ratio: f64,
pub min_padding: f64,
pub max_padding: f64,
pub benchmark_signal_symbol: String,
pub benchmark_short_ma_days: usize,
pub benchmark_long_ma_days: usize,
@@ -1531,6 +1546,9 @@ impl OmniMicroCapConfig {
base_index_level: 2000.0,
base_cap_floor: 7.0,
cap_span: 10.0,
padding_ratio: 0.5,
min_padding: 8.0,
max_padding: 20.0,
benchmark_signal_symbol: "000001.SH".to_string(),
benchmark_short_ma_days: 5,
benchmark_long_ma_days: 10,
@@ -1552,10 +1570,13 @@ impl OmniMicroCapConfig {
strategy_name: "aiquant-v1.0.4".to_string(),
refresh_rate: 120,
stocknum: 5,
xs: 3.0 / 500.0,
xs: 4.0 / 500.0,
base_index_level: 2000.0,
base_cap_floor: 7.0,
cap_span: 25.0,
cap_span: 10.0,
padding_ratio: 1.2,
min_padding: 29.5,
max_padding: 50.0,
benchmark_signal_symbol: "000852.SH".to_string(),
benchmark_short_ma_days: 5,
benchmark_long_ma_days: 20,
@@ -2120,7 +2141,8 @@ impl OmniMicroCapStrategy {
&self,
ctx: &StrategyContext<'_>,
date: NaiveDate,
) -> Result<(f64, f64, f64, f64), BacktestError> {
) -> Result<(f64, f64, f64, f64, f64), BacktestError> {
// 当前交易日的指数价格用于MA计算和仓位控制
let current_level = ctx
.data
.market_decision_close(date, &self.config.benchmark_signal_symbol)
@@ -2129,6 +2151,16 @@ impl OmniMicroCapStrategy {
symbol: self.config.benchmark_signal_symbol.clone(),
field: "decision_close",
})?;
// 前一交易日的指数价格(用于市值区间计算,模拟实盘场景)
let prev_level = if let Some(prev_date) = ctx.data.previous_trading_date(date, 1) {
ctx.data
.market_decision_close(prev_date, &self.config.benchmark_signal_symbol)
.unwrap_or(current_level)
} else {
current_level
};
let ma_short = ctx
.data
.market_decision_close_moving_average(
@@ -2160,14 +2192,25 @@ impl OmniMicroCapStrategy {
} else {
1.0
};
Ok((current_level, ma_short, ma_long, trading_ratio))
Ok((current_level, prev_level, ma_short, ma_long, trading_ratio))
}
fn market_cap_band(&self, index_level: f64) -> (f64, f64) {
let y = (index_level - self.config.base_index_level) * self.config.xs
+ self.config.base_cap_floor;
let start = y.round();
(start, start + self.config.cap_span)
let end = start + self.config.cap_span;
// Apply padding to expand the range
let span = end - start;
let padding = (span * self.config.padding_ratio)
.max(self.config.min_padding)
.min(self.config.max_padding);
let lower_bound = (start - padding).max(0.0);
let upper_bound = end + padding;
(lower_bound, upper_bound)
}
fn stock_passes_ma_filter(
@@ -2201,14 +2244,25 @@ impl OmniMicroCapStrategy {
// MA filter: ma_short > ma_mid * rsi_rate && ma_mid * rsi_rate > ma_long
let ma_pass = ma_short > ma_mid * self.config.rsi_rate && ma_mid * self.config.rsi_rate > ma_long;
// Debug logging for first few stocks
static DEBUG_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let count = DEBUG_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if count < 10 {
eprintln!("[DEBUG MA] {} date={} ma5={:.4} ma10={:.4} ma30={:.4} rsi_rate={:.6} pass={} (ma5 > ma10*rsi={:.4}? {} && ma10*rsi > ma30={:.4}? {})",
symbol, date, ma_short, ma_mid, ma_long, self.config.rsi_rate, ma_pass,
ma_mid * self.config.rsi_rate, ma_short > ma_mid * self.config.rsi_rate,
ma_long, ma_mid * self.config.rsi_rate > ma_long);
// Debug logging for ALL stocks on first decision date
static DEBUG_DATE: std::sync::Mutex<Option<NaiveDate>> = std::sync::Mutex::new(None);
let mut debug_date = DEBUG_DATE.lock().unwrap();
let should_debug = if let Some(d) = *debug_date {
d == date
} else {
*debug_date = Some(date);
true
};
if should_debug {
eprintln!("[MA_FILTER] {} cap={:.2} ma5={:.4} ma10={:.4} ma30={:.4} ma10*rsi={:.4} pass={} ({}>{:.4}? {} && {:.4}>{}? {})",
symbol,
ctx.data.market_decision_close(date, symbol).unwrap_or(0.0),
ma_short, ma_mid, ma_long,
ma_mid * self.config.rsi_rate,
ma_pass,
ma_short, ma_mid * self.config.rsi_rate, ma_short > ma_mid * self.config.rsi_rate,
ma_mid * self.config.rsi_rate, ma_long, ma_mid * self.config.rsi_rate > ma_long);
}
if !ma_pass {
@@ -2608,7 +2662,7 @@ impl Strategy for OmniMicroCapStrategy {
});
}
let (index_level, ma_short, ma_long, trading_ratio) = match self.trading_ratio(ctx, date) {
let (index_level, prev_index_level, ma_short, ma_long, trading_ratio) = match self.trading_ratio(ctx, date) {
Ok(value) => value,
Err(BacktestError::Execution(message))
if message.contains("insufficient benchmark") =>
@@ -2626,7 +2680,10 @@ impl Strategy for OmniMicroCapStrategy {
}
Err(err) => return Err(err),
};
let (band_low, band_high) = self.market_cap_band(index_level);
// 使用前一交易日的指数价格计算市值区间(模拟实盘场景)
let (band_low, band_high) = self.market_cap_band(prev_index_level);
eprintln!("[DEBUG] date={} current_index={:.2} prev_index={:.2} band=[{:.0}, {:.0}]",
date, index_level, prev_index_level, band_low, band_high);
let (stock_list, selection_notes) = self.select_symbols(ctx, date, band_low, band_high)?;
let periodic_rebalance = ctx.decision_index % self.config.refresh_rate == 0;
let mut projected = ctx.portfolio.clone();

View File

@@ -78,6 +78,9 @@ pub struct DynamicMarketCapBandSelector {
pub cap_span: f64,
pub xs: f64,
pub top_n: usize,
pub padding_ratio: f64,
pub min_padding: f64,
pub max_padding: f64,
}
impl DynamicMarketCapBandSelector {
@@ -87,6 +90,9 @@ impl DynamicMarketCapBandSelector {
cap_span: f64,
xs: f64,
top_n: usize,
padding_ratio: f64,
min_padding: f64,
max_padding: f64,
) -> Self {
Self {
base_index_level,
@@ -94,11 +100,14 @@ impl DynamicMarketCapBandSelector {
cap_span,
xs,
top_n,
padding_ratio,
min_padding,
max_padding,
}
}
pub fn demo(top_n: usize) -> Self {
Self::new(2000.0, 7.0, 10.0, 4.0 / 500.0, top_n)
Self::new(2000.0, 7.0, 10.0, 4.0 / 500.0, top_n, 0.5, 8.0, 20.0)
}
pub fn regime(&self, benchmark_level: f64) -> BandRegime {
@@ -114,7 +123,18 @@ impl DynamicMarketCapBandSelector {
pub fn band_for_level(&self, benchmark_level: f64) -> (f64, f64) {
let start = ((benchmark_level - self.base_index_level) * self.xs) + self.base_cap_floor;
let low = start.round();
(low, low + self.cap_span)
let high = low + self.cap_span;
// Apply padding to expand the range
let span = high - low;
let padding = (span * self.padding_ratio)
.max(self.min_padding)
.min(self.max_padding);
let lower_bound = (low - padding).max(0.0);
let upper_bound = high + padding;
(lower_bound, upper_bound)
}
}