Compare commits
2 Commits
v2026.5.10
...
v2026.5.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a402f2048 | ||
|
|
bbe60537ff |
@@ -100,6 +100,57 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
||||||
engine.run()?
|
engine.run()?
|
||||||
}
|
}
|
||||||
|
"aiquant-v104" => {
|
||||||
|
let mut strategy_cfg = OmniMicroCapConfig::aiquant_v104();
|
||||||
|
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
||||||
|
if !signal_symbol.trim().is_empty() {
|
||||||
|
strategy_cfg.benchmark_signal_symbol = signal_symbol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(date) = debug_date {
|
||||||
|
let eligible = data.eligible_universe_on(date);
|
||||||
|
eprintln!(
|
||||||
|
"DEBUG eligible_universe_on {} count={}",
|
||||||
|
date,
|
||||||
|
eligible.len()
|
||||||
|
);
|
||||||
|
for row in eligible.iter().take(20) {
|
||||||
|
eprintln!(" {} {:.6}", row.symbol, row.market_cap_bn);
|
||||||
|
}
|
||||||
|
let mut debug_strategy = OmniMicroCapStrategy::new(strategy_cfg.clone());
|
||||||
|
let debug_subscriptions = BTreeSet::new();
|
||||||
|
let decision = debug_strategy.on_day(&StrategyContext {
|
||||||
|
execution_date: date,
|
||||||
|
decision_date: date,
|
||||||
|
decision_index: 1,
|
||||||
|
data: &data,
|
||||||
|
portfolio: &PortfolioState::new(20_000.0),
|
||||||
|
futures_account: None,
|
||||||
|
open_orders: &[],
|
||||||
|
dynamic_universe: None,
|
||||||
|
subscriptions: &debug_subscriptions,
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
|
})?;
|
||||||
|
eprintln!("DEBUG notes={:?}", decision.notes);
|
||||||
|
eprintln!("DEBUG diagnostics={:?}", decision.diagnostics);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
config.decision_lag_trading_days = decision_lag.unwrap_or(1);
|
||||||
|
config.execution_price_field = execution_price.unwrap_or(PriceField::Close);
|
||||||
|
config.initial_cash = initial_cash.unwrap_or(20_000.0);
|
||||||
|
let strategy = OmniMicroCapStrategy::new(strategy_cfg);
|
||||||
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks::default(),
|
||||||
|
config.execution_price_field,
|
||||||
|
);
|
||||||
|
let mut engine = BacktestEngine::new(data, strategy, broker, config);
|
||||||
|
engine.run()?
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let mut strategy_cfg = OmniMicroCapConfig::omni_microcap();
|
let mut strategy_cfg = OmniMicroCapConfig::omni_microcap();
|
||||||
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
if let Ok(signal_symbol) = std::env::var("FIDC_BT_SIGNAL_SYMBOL") {
|
||||||
|
|||||||
@@ -114,6 +114,15 @@ pub struct DynamicRangeConfig {
|
|||||||
pub cap_span: Option<f64>,
|
pub cap_span: Option<f64>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub xs: Option<f64>,
|
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)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
@@ -636,8 +645,8 @@ pub fn platform_expr_config_from_spec(
|
|||||||
if let Some(stock_ma_filter) = engine.stock_ma_filter.as_ref() {
|
if let Some(stock_ma_filter) = engine.stock_ma_filter.as_ref() {
|
||||||
let ratio = stock_ma_filter.rsi_rate.unwrap_or(1.0001);
|
let ratio = stock_ma_filter.rsi_rate.unwrap_or(1.0001);
|
||||||
cfg.stock_filter_expr = format!(
|
cfg.stock_filter_expr = format!(
|
||||||
"stock_ma_short > stock_ma_mid * {} && stock_ma_mid > stock_ma_long",
|
"stock_ma_short > stock_ma_mid * {} && stock_ma_mid * {} > stock_ma_long",
|
||||||
ratio
|
ratio, ratio
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(index_throttle) = engine.index_throttle.as_ref() {
|
if let Some(index_throttle) = engine.index_throttle.as_ref() {
|
||||||
|
|||||||
@@ -1090,6 +1090,9 @@ pub struct CnSmallCapRotationConfig {
|
|||||||
pub base_index_level: f64,
|
pub base_index_level: f64,
|
||||||
pub base_cap_floor: f64,
|
pub base_cap_floor: f64,
|
||||||
pub cap_span: f64,
|
pub cap_span: f64,
|
||||||
|
pub padding_ratio: f64,
|
||||||
|
pub min_padding: f64,
|
||||||
|
pub max_padding: f64,
|
||||||
pub short_ma_days: usize,
|
pub short_ma_days: usize,
|
||||||
pub long_ma_days: usize,
|
pub long_ma_days: usize,
|
||||||
pub stock_short_ma_days: usize,
|
pub stock_short_ma_days: usize,
|
||||||
@@ -1114,6 +1117,9 @@ impl CnSmallCapRotationConfig {
|
|||||||
base_index_level: 2000.0,
|
base_index_level: 2000.0,
|
||||||
base_cap_floor: 7.0,
|
base_cap_floor: 7.0,
|
||||||
cap_span: 10.0,
|
cap_span: 10.0,
|
||||||
|
padding_ratio: 0.5,
|
||||||
|
min_padding: 8.0,
|
||||||
|
max_padding: 20.0,
|
||||||
short_ma_days: 3,
|
short_ma_days: 3,
|
||||||
long_ma_days: 5,
|
long_ma_days: 5,
|
||||||
stock_short_ma_days: 3,
|
stock_short_ma_days: 3,
|
||||||
@@ -1138,6 +1144,9 @@ impl CnSmallCapRotationConfig {
|
|||||||
base_index_level: 2000.0,
|
base_index_level: 2000.0,
|
||||||
base_cap_floor: 7.0,
|
base_cap_floor: 7.0,
|
||||||
cap_span: 10.0,
|
cap_span: 10.0,
|
||||||
|
padding_ratio: 0.5,
|
||||||
|
min_padding: 8.0,
|
||||||
|
max_padding: 20.0,
|
||||||
short_ma_days: 5,
|
short_ma_days: 5,
|
||||||
long_ma_days: 10,
|
long_ma_days: 10,
|
||||||
stock_short_ma_days: 5,
|
stock_short_ma_days: 5,
|
||||||
@@ -1185,6 +1194,9 @@ impl CnSmallCapRotationStrategy {
|
|||||||
config.cap_span,
|
config.cap_span,
|
||||||
config.xs,
|
config.xs,
|
||||||
config.stocknum,
|
config.stocknum,
|
||||||
|
config.padding_ratio,
|
||||||
|
config.min_padding,
|
||||||
|
config.max_padding,
|
||||||
),
|
),
|
||||||
config,
|
config,
|
||||||
last_gross_exposure: None,
|
last_gross_exposure: None,
|
||||||
@@ -1508,6 +1520,9 @@ pub struct OmniMicroCapConfig {
|
|||||||
pub base_index_level: f64,
|
pub base_index_level: f64,
|
||||||
pub base_cap_floor: f64,
|
pub base_cap_floor: f64,
|
||||||
pub cap_span: f64,
|
pub cap_span: f64,
|
||||||
|
pub padding_ratio: f64,
|
||||||
|
pub min_padding: f64,
|
||||||
|
pub max_padding: f64,
|
||||||
pub benchmark_signal_symbol: String,
|
pub benchmark_signal_symbol: String,
|
||||||
pub benchmark_short_ma_days: usize,
|
pub benchmark_short_ma_days: usize,
|
||||||
pub benchmark_long_ma_days: usize,
|
pub benchmark_long_ma_days: usize,
|
||||||
@@ -1531,6 +1546,9 @@ impl OmniMicroCapConfig {
|
|||||||
base_index_level: 2000.0,
|
base_index_level: 2000.0,
|
||||||
base_cap_floor: 7.0,
|
base_cap_floor: 7.0,
|
||||||
cap_span: 10.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_signal_symbol: "000001.SH".to_string(),
|
||||||
benchmark_short_ma_days: 5,
|
benchmark_short_ma_days: 5,
|
||||||
benchmark_long_ma_days: 10,
|
benchmark_long_ma_days: 10,
|
||||||
@@ -1547,6 +1565,32 @@ impl OmniMicroCapConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn aiquant_v104() -> Self {
|
||||||
|
Self {
|
||||||
|
strategy_name: "aiquant-v1.0.4".to_string(),
|
||||||
|
refresh_rate: 120,
|
||||||
|
stocknum: 5,
|
||||||
|
xs: 3.0 / 500.0,
|
||||||
|
base_index_level: 2000.0,
|
||||||
|
base_cap_floor: 7.0,
|
||||||
|
cap_span: 25.0,
|
||||||
|
padding_ratio: 0.5,
|
||||||
|
min_padding: 12.5,
|
||||||
|
max_padding: 30.0,
|
||||||
|
benchmark_signal_symbol: "000852.SH".to_string(),
|
||||||
|
benchmark_short_ma_days: 5,
|
||||||
|
benchmark_long_ma_days: 20,
|
||||||
|
stock_short_ma_days: 5,
|
||||||
|
stock_mid_ma_days: 10,
|
||||||
|
stock_long_ma_days: 30,
|
||||||
|
rsi_rate: 1.0001,
|
||||||
|
trade_rate: 0.5,
|
||||||
|
stop_loss_ratio: 0.92,
|
||||||
|
take_profit_ratio: 1.16,
|
||||||
|
skip_month_day_ranges: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn in_skip_window(&self, date: NaiveDate) -> bool {
|
fn in_skip_window(&self, date: NaiveDate) -> bool {
|
||||||
let month = date.month();
|
let month = date.month();
|
||||||
let day = date.day();
|
let day = date.day();
|
||||||
@@ -2144,7 +2188,18 @@ impl OmniMicroCapStrategy {
|
|||||||
let y = (index_level - self.config.base_index_level) * self.config.xs
|
let y = (index_level - self.config.base_index_level) * self.config.xs
|
||||||
+ self.config.base_cap_floor;
|
+ self.config.base_cap_floor;
|
||||||
let start = y.round();
|
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(
|
fn stock_passes_ma_filter(
|
||||||
@@ -2175,7 +2230,46 @@ impl OmniMicroCapStrategy {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
ma_short > ma_mid * self.config.rsi_rate && ma_mid > ma_long
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ma_pass {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume filter: V5 < V60 (applied for omni_microcap strategies)
|
||||||
|
if self.config.strategy_name.contains("aiquant") || self.config.strategy_name.contains("AiQuant") || self.config.strategy_name.contains("omni") {
|
||||||
|
let Some(volume_ma5) = ctx.data.market_decision_volume_moving_average(
|
||||||
|
date,
|
||||||
|
symbol,
|
||||||
|
5,
|
||||||
|
) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(volume_ma60) = ctx.data.market_decision_volume_moving_average(
|
||||||
|
date,
|
||||||
|
symbol,
|
||||||
|
60,
|
||||||
|
) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if volume_ma5 >= volume_ma60 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn special_name(&self, ctx: &StrategyContext<'_>, symbol: &str) -> bool {
|
fn special_name(&self, ctx: &StrategyContext<'_>, symbol: &str) -> bool {
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ pub struct DynamicMarketCapBandSelector {
|
|||||||
pub cap_span: f64,
|
pub cap_span: f64,
|
||||||
pub xs: f64,
|
pub xs: f64,
|
||||||
pub top_n: usize,
|
pub top_n: usize,
|
||||||
|
pub padding_ratio: f64,
|
||||||
|
pub min_padding: f64,
|
||||||
|
pub max_padding: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DynamicMarketCapBandSelector {
|
impl DynamicMarketCapBandSelector {
|
||||||
@@ -87,6 +90,9 @@ impl DynamicMarketCapBandSelector {
|
|||||||
cap_span: f64,
|
cap_span: f64,
|
||||||
xs: f64,
|
xs: f64,
|
||||||
top_n: usize,
|
top_n: usize,
|
||||||
|
padding_ratio: f64,
|
||||||
|
min_padding: f64,
|
||||||
|
max_padding: f64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base_index_level,
|
base_index_level,
|
||||||
@@ -94,11 +100,14 @@ impl DynamicMarketCapBandSelector {
|
|||||||
cap_span,
|
cap_span,
|
||||||
xs,
|
xs,
|
||||||
top_n,
|
top_n,
|
||||||
|
padding_ratio,
|
||||||
|
min_padding,
|
||||||
|
max_padding,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn demo(top_n: usize) -> Self {
|
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 {
|
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) {
|
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 start = ((benchmark_level - self.base_index_level) * self.xs) + self.base_cap_floor;
|
||||||
let low = start.round();
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user