修复FIDC选股阶段风控语义

This commit is contained in:
boris
2026-07-05 14:51:54 +08:00
parent d3c986e1f2
commit 00c9042c15
8 changed files with 307 additions and 88 deletions
+146 -7
View File
@@ -2426,18 +2426,26 @@ where
} else { } else {
ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field) ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field)
}; };
let mut risk_config = self.risk_config.clone(); let adjusted_candidate;
if self.aiquant_execution_rules { let candidate_for_check = if self.aiquant_execution_rules
risk_config.static_rules.respect_allow_buy_sell = false; && self.aiquant_sell_allow_flag_is_stale_lower_limit(candidate, snapshot, check_price)
} {
adjusted_candidate = crate::data::CandidateEligibility {
allow_sell: true,
..candidate.clone()
};
&adjusted_candidate
} else {
candidate
};
if let Some(reason) = ChinaAShareRiskControl::sell_rejection_reason_with_config( if let Some(reason) = ChinaAShareRiskControl::sell_rejection_reason_with_config(
date, date,
candidate, candidate_for_check,
snapshot, snapshot,
instrument, instrument,
Some(position), Some(position),
check_price, check_price,
&risk_config, &self.risk_config,
) { ) {
return RuleCheck::reject(reason); return RuleCheck::reject(reason);
} }
@@ -2453,6 +2461,21 @@ where
RuleCheck::allow() RuleCheck::allow()
} }
fn aiquant_sell_allow_flag_is_stale_lower_limit(
&self,
candidate: &crate::data::CandidateEligibility,
snapshot: &crate::data::DailyMarketSnapshot,
check_price: f64,
) -> bool {
if candidate.allow_sell || candidate.is_paused || snapshot.paused {
return false;
}
let snapshot_check_price =
ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field);
snapshot.is_at_lower_limit_price(snapshot_check_price)
&& !snapshot.is_at_lower_limit_price(check_price)
}
fn minimum_target_quantity( fn minimum_target_quantity(
&self, &self,
date: NaiveDate, date: NaiveDate,
@@ -5283,7 +5306,7 @@ where
} }
if self.volume_limit { if self.volume_limit {
let raw_limit = ((available_market_volume as f64) * self.volume_percent).round() as i64 let raw_limit = ((available_market_volume as f64) * self.volume_percent).floor() as i64
- consumed_turnover as i64; - consumed_turnover as i64;
if raw_limit <= 0 { if raw_limit <= 0 {
return Err(volume_limit_reason.to_string()); return Err(volume_limit_reason.to_string());
@@ -6537,6 +6560,27 @@ mod tests {
assert_eq!(fillable, Ok(5_000)); assert_eq!(fillable, Ok(5_000));
} }
#[test]
fn volume_limit_uses_floor_for_odd_lot_sell() {
let mut snapshot = limit_test_snapshot();
snapshot.minute_volume = 0;
snapshot.volume = 3;
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Close,
)
.with_matching_type(MatchingType::CurrentBarClose)
.with_volume_limit(true)
.with_volume_percent(0.5)
.with_liquidity_limit(false);
let fillable =
broker.market_fillable_quantity(&snapshot, OrderSide::Sell, 10, 100, 100, 0, true);
assert_eq!(fillable, Ok(1));
}
#[test] #[test]
fn current_bar_close_volume_limit_rejects_daily_zero_volume() { fn current_bar_close_volume_limit_rejects_daily_zero_volume() {
let mut snapshot = limit_test_snapshot(); let mut snapshot = limit_test_snapshot();
@@ -7427,4 +7471,99 @@ mod tests {
report.order_events report.order_events
); );
} }
#[test]
fn aiquant_sell_rule_still_blocks_real_allow_sell_false() {
let date = chrono::NaiveDate::from_ymd_opt(2024, 4, 17).expect("valid date");
let prev_date = chrono::NaiveDate::from_ymd_opt(2024, 4, 16).expect("valid date");
let symbol = "000001.SZ";
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_aiquant_execution_rules(true)
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
.with_volume_limit(false)
.with_liquidity_limit(false)
.with_inactive_limit(false)
.with_slippage_model(SlippageModel::None);
let mut snapshot = limit_test_snapshot();
snapshot.date = date;
snapshot.timestamp = Some("2024-04-17 10:18:00".to_string());
snapshot.day_open = 10.0;
snapshot.open = 10.0;
snapshot.high = 10.2;
snapshot.low = 9.8;
snapshot.close = 10.0;
snapshot.last_price = 10.0;
snapshot.bid1 = 10.0;
snapshot.ask1 = 10.0;
snapshot.prev_close = 10.0;
snapshot.upper_limit = 11.0;
snapshot.lower_limit = 9.0;
let mut candidate = limit_test_candidate(true, false);
candidate.date = date;
let mut quote = limit_test_quote(10.0, 9.99, 10.0);
quote.date = date;
quote.timestamp = date.and_hms_opt(10, 18, 0).unwrap();
quote.last_price = 10.0;
quote.bid1 = 9.99;
quote.ask1 = 10.0;
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![snapshot],
Vec::new(),
vec![candidate],
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1000.0,
prev_close: 1000.0,
volume: 1_000_000,
}],
Vec::new(),
vec![quote],
)
.expect("valid dataset");
let mut portfolio = PortfolioState::new(100.0);
portfolio.position_mut(symbol).buy(prev_date, 7_200, 8.48);
let mut report = BrokerExecutionReport::default();
broker
.process_target_value(
date,
&mut portfolio,
&data,
symbol,
0.0,
"stop_loss_exit",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("target sell processed");
assert!(report.fill_events.is_empty());
assert_eq!(
portfolio.position(symbol).map(|position| position.quantity),
Some(7_200)
);
let rejected_order = report
.order_events
.iter()
.find(|event| event.side == OrderSide::Sell)
.expect("sell order event");
assert_eq!(rejected_order.status, OrderStatus::Canceled);
assert!(
rejected_order.reason.contains("sell_disabled"),
"{}",
rejected_order.reason
);
}
} }
+6 -1
View File
@@ -3516,8 +3516,13 @@ mod tests {
) )
.expect("dataset"); .expect("dataset");
assert!(data.eligible_universe_on(date).is_empty()); assert_eq!(data.eligible_universe_on(date).len(), 1);
let mut risk_config = FidcRiskControlConfig::default(); let mut risk_config = FidcRiskControlConfig::default();
risk_config.static_rules.reject_kcb_selection = true;
assert!(
data.eligible_universe_on_with_risk_config(date, &risk_config)
.is_empty()
);
risk_config.static_rules.reject_kcb_selection = false; risk_config.static_rules.reject_kcb_selection = false;
let rows = data.eligible_universe_on_with_risk_config(date, &risk_config); let rows = data.eligible_universe_on_with_risk_config(date, &risk_config);
+16 -3
View File
@@ -5002,9 +5002,22 @@ mod tests {
.map(|snapshot| snapshot.close), .map(|snapshot| snapshot.close),
Some(11.5) Some(11.5)
); );
assert_eq!(
dataset
.eligible_universe_on(first)
.iter()
.map(|row| row.symbol.clone())
.collect::<Vec<_>>(),
vec![SYMBOL.to_string()],
"raw DataSet helper should not apply default selection risk"
);
let mut selection_risk_config = FidcRiskControlConfig::default();
selection_risk_config.static_rules.reject_paused_selection = true;
assert!( assert!(
dataset.eligible_universe_on(first).is_empty(), dataset
"raw DataSet helper remains a risk-filtered universe" .eligible_universe_on_with_risk_config(first, &selection_risk_config)
.is_empty(),
"explicit raw DataSet selection risk can still filter the universe"
); );
assert_eq!( assert_eq!(
manual_ctx manual_ctx
@@ -5017,7 +5030,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
manual_ctx manual_ctx
.eligible_universe_on_with_risk_config(first, &FidcRiskControlConfig::default()) .eligible_universe_on_with_risk_config(first, &selection_risk_config)
.into_iter() .into_iter()
.map(|row| row.symbol) .map(|row| row.symbol)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
+69 -27
View File
@@ -6019,7 +6019,7 @@ impl PlatformExprStrategy {
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
date: NaiveDate, date: NaiveDate,
factor_date: NaiveDate, factor_date: NaiveDate,
defer_limit_state_selection_risk: bool, defer_all_selection_risk: bool,
) -> Vec<EligibleUniverseSnapshot> { ) -> Vec<EligibleUniverseSnapshot> {
let mut rows = Vec::new(); let mut rows = Vec::new();
for factor in ctx.data.factor_snapshots_on(factor_date) { for factor in ctx.data.factor_snapshots_on(factor_date) {
@@ -6035,12 +6035,10 @@ impl PlatformExprStrategy {
let Some(market) = ctx.data.market(date, &factor.symbol) else { let Some(market) = ctx.data.market(date, &factor.symbol) else {
continue; continue;
}; };
if let Some(reason) = if let Some(_reason) =
self.selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market) self.selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market)
{ {
if !(defer_limit_state_selection_risk if !defer_all_selection_risk {
&& matches!(reason, "upper_limit" | "lower_limit"))
{
continue; continue;
} }
} }
@@ -6081,7 +6079,7 @@ impl PlatformExprStrategy {
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
date: NaiveDate, date: NaiveDate,
factor_date: NaiveDate, factor_date: NaiveDate,
defer_limit_state_selection_risk: bool, _defer_all_selection_risk: bool,
) -> Vec<FidcRiskDecisionAudit> { ) -> Vec<FidcRiskDecisionAudit> {
let mut decisions = Vec::new(); let mut decisions = Vec::new();
for factor in ctx.data.factor_snapshots_on(factor_date) { for factor in ctx.data.factor_snapshots_on(factor_date) {
@@ -6101,11 +6099,6 @@ impl PlatformExprStrategy {
ctx.data.instrument(&factor.symbol), ctx.data.instrument(&factor.symbol),
&self.config.risk_config, &self.config.risk_config,
) { ) {
if defer_limit_state_selection_risk
&& matches!(decision.rule_code.as_str(), "upper_limit" | "lower_limit")
{
continue;
}
decisions.push(decision); decisions.push(decision);
} }
} }
@@ -6464,13 +6457,13 @@ impl PlatformExprStrategy {
band_high: f64, band_high: f64,
limit: usize, limit: usize,
) -> Result<(Vec<String>, Vec<String>, Vec<FidcRiskDecisionAudit>), BacktestError> { ) -> Result<(Vec<String>, Vec<String>, Vec<FidcRiskDecisionAudit>), BacktestError> {
let defer_limit_state_selection_risk = ctx.is_lagged_execution(); let defer_all_selection_risk = ctx.is_lagged_execution();
let universe = if defer_limit_state_selection_risk { let universe = if defer_all_selection_risk {
self.selectable_universe_on_with_options(ctx, date, universe_factor_date, true) self.selectable_universe_on_with_options(ctx, date, universe_factor_date, true)
} else { } else {
self.selectable_universe_on(ctx, date, universe_factor_date) self.selectable_universe_on(ctx, date, universe_factor_date)
}; };
let risk_decisions = if defer_limit_state_selection_risk { let risk_decisions = if defer_all_selection_risk {
self.selection_risk_decisions_with_options(ctx, date, universe_factor_date, true) self.selection_risk_decisions_with_options(ctx, date, universe_factor_date, true)
} else { } else {
self.selection_risk_decisions(ctx, date, universe_factor_date) self.selection_risk_decisions(ctx, date, universe_factor_date)
@@ -8950,8 +8943,22 @@ mod tests {
let default_strategy = let default_strategy =
PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
assert_eq!(
default_strategy
.selectable_universe_on(&ctx, date, date)
.len(),
1
);
assert!( assert!(
default_strategy default_strategy
.selection_risk_decisions(&ctx, date, date)
.is_empty()
);
let mut selection_cfg = PlatformExprStrategyConfig::microcap_rotation();
selection_cfg.risk_config.static_rules.reject_kcb_selection = true;
let selection_strategy = PlatformExprStrategy::new(selection_cfg);
assert!(
selection_strategy
.selectable_universe_on(&ctx, date, date) .selectable_universe_on(&ctx, date, date)
.is_empty() .is_empty()
); );
@@ -8962,11 +8969,13 @@ mod tests {
date, date,
5, 5,
); );
assert!( assert!(risk_diagnostics.is_empty(), "{risk_diagnostics:?}");
risk_diagnostics let risk_decisions = selection_strategy.selection_risk_decisions(&ctx, date, date);
.iter() let risk_diagnostics = PlatformExprStrategy::selection_risk_decision_diagnostics(
.any(|item| item.contains("risk_decisions selection_total=1 by_rule=kcb:1")), &risk_decisions,
"{risk_diagnostics:?}" date,
date,
5,
); );
assert!( assert!(
risk_diagnostics risk_diagnostics
@@ -9118,12 +9127,41 @@ mod tests {
let default_strategy = let default_strategy =
PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
assert_eq!(
default_strategy
.selectable_universe_on(&ctx, date, date)
.len(),
symbols.len()
);
assert!( assert!(
default_strategy default_strategy
.selection_risk_decisions(&ctx, date, date)
.is_empty()
);
let mut strict_cfg = PlatformExprStrategyConfig::microcap_rotation();
strict_cfg
.risk_config
.static_rules
.reject_upper_limit_selection = true;
strict_cfg
.risk_config
.static_rules
.reject_lower_limit_selection = true;
strict_cfg
.risk_config
.static_rules
.reject_new_listing_selection = true;
strict_cfg
.risk_config
.static_rules
.reject_inactive_selection = true;
let strict_strategy = PlatformExprStrategy::new(strict_cfg);
assert!(
strict_strategy
.selectable_universe_on(&ctx, date, date) .selectable_universe_on(&ctx, date, date)
.is_empty() .is_empty()
); );
let rejected_rules = default_strategy let rejected_rules = strict_strategy
.selection_risk_decisions(&ctx, date, date) .selection_risk_decisions(&ctx, date, date)
.into_iter() .into_iter()
.map(|decision| (decision.symbol, decision.rule_code)) .map(|decision| (decision.symbol, decision.rule_code))
@@ -9254,10 +9292,11 @@ mod tests {
let default_strategy = let default_strategy =
PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
assert!( assert_eq!(
default_strategy default_strategy
.selectable_universe_on(&ctx, date, date) .selectable_universe_on(&ctx, date, date)
.is_empty() .len(),
1
); );
let default_stock = default_strategy let default_stock = default_strategy
.stock_state(&ctx, date, symbol) .stock_state(&ctx, date, symbol)
@@ -15239,6 +15278,8 @@ mod tests {
cfg.selection_limit_expr = "1".to_string(); cfg.selection_limit_expr = "1".to_string();
cfg.stock_filter_expr = "close > 0".to_string(); cfg.stock_filter_expr = "close > 0".to_string();
cfg.exposure_expr = "1.0".to_string(); cfg.exposure_expr = "1.0".to_string();
cfg.risk_config.static_rules.reject_st_selection = true;
cfg.risk_config.static_rules.reject_paused_selection = true;
let mut strategy = PlatformExprStrategy::new(cfg); let mut strategy = PlatformExprStrategy::new(cfg);
let decision = strategy.on_day(&ctx).expect("platform decision"); let decision = strategy.on_day(&ctx).expect("platform decision");
@@ -15247,7 +15288,7 @@ mod tests {
decision decision
.diagnostics .diagnostics
.iter() .iter()
.any(|item| item == "selected_symbols="), .any(|item| item == "selected_symbols=600462.SH"),
"{:?}", "{:?}",
decision.diagnostics decision.diagnostics
); );
@@ -15260,11 +15301,11 @@ mod tests {
decision.diagnostics decision.diagnostics
); );
assert!( assert!(
decision.order_intents.iter().all(|intent| !matches!( decision.order_intents.iter().any(|intent| matches!(
intent, intent,
OrderIntent::Shares { symbol, quantity, .. } OrderIntent::Shares { symbol, quantity, .. }
if symbol == candidate_symbol && *quantity > 0 if symbol == candidate_symbol && *quantity > 0
) && !matches!( ) || matches!(
intent, intent,
OrderIntent::Value { symbol, value, .. } OrderIntent::Value { symbol, value, .. }
if symbol == candidate_symbol && *value > 0.0 if symbol == candidate_symbol && *value > 0.0
@@ -15695,7 +15736,7 @@ mod tests {
} }
#[test] #[test]
fn platform_strategy_baseline_risk_filter_cannot_be_disabled_by_empty_exclude() { fn platform_strategy_empty_exclude_keeps_buy_risk_without_selection_filtering() {
let date = d(2024, 12, 31); let date = d(2024, 12, 31);
let symbol = "002494.SZ"; let symbol = "002494.SZ";
let data = DataSet::from_components( let data = DataSet::from_components(
@@ -15796,7 +15837,8 @@ mod tests {
let selected = strategy.selectable_universe_on(&ctx, date, date); let selected = strategy.selectable_universe_on(&ctx, date, date);
assert_eq!(rejection.as_deref(), Some("paused")); assert_eq!(rejection.as_deref(), Some("paused"));
assert!(selected.is_empty()); assert_eq!(selected.len(), 1);
assert_eq!(selected[0].symbol, symbol);
} }
fn sample_calendar() -> TradingCalendar { fn sample_calendar() -> TradingCalendar {
+45 -26
View File
@@ -45,27 +45,27 @@ pub struct StaticRiskRuleConfig {
impl Default for StaticRiskRuleConfig { impl Default for StaticRiskRuleConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
reject_st_selection: true, reject_st_selection: false,
reject_st_buy: true, reject_st_buy: true,
reject_star_st_selection: true, reject_star_st_selection: false,
reject_star_st_buy: true, reject_star_st_buy: true,
reject_paused_selection: true, reject_paused_selection: false,
reject_paused_buy: true, reject_paused_buy: true,
reject_paused_sell: true, reject_paused_sell: true,
reject_inactive_selection: true, reject_inactive_selection: false,
reject_inactive_buy: true, reject_inactive_buy: true,
reject_inactive_sell: true, reject_inactive_sell: true,
reject_new_listing_selection: true, reject_new_listing_selection: false,
reject_new_listing_buy: true, reject_new_listing_buy: true,
reject_kcb_selection: true, reject_kcb_selection: false,
reject_kcb_buy: true, reject_kcb_buy: true,
reject_bjse_selection: true, reject_bjse_selection: false,
reject_bjse_buy: true, reject_bjse_buy: true,
reject_one_yuan_selection: true, reject_one_yuan_selection: false,
reject_one_yuan_buy: true, reject_one_yuan_buy: true,
respect_allow_buy_sell: true, respect_allow_buy_sell: true,
reject_upper_limit_selection: true, reject_upper_limit_selection: false,
reject_lower_limit_selection: true, reject_lower_limit_selection: false,
reject_upper_limit_buy: true, reject_upper_limit_buy: true,
reject_lower_limit_sell: true, reject_lower_limit_sell: true,
forbid_same_day_rebuy_after_sell: true, forbid_same_day_rebuy_after_sell: true,
@@ -255,11 +255,6 @@ impl ChinaAShareRiskControl {
) { ) {
return Some(reason); return Some(reason);
} }
if config.static_rules.respect_allow_buy_sell
&& (!candidate.allow_buy || !candidate.allow_sell)
{
return Some("trade_disabled");
}
let selection_price = market.price(PriceField::Last); let selection_price = market.price(PriceField::Last);
if config.static_rules.reject_upper_limit_selection if config.static_rules.reject_upper_limit_selection
&& market.is_at_upper_limit_price(selection_price) && market.is_at_upper_limit_price(selection_price)
@@ -719,7 +714,6 @@ fn missing_single_field_rejected(
|| config.static_rules.reject_one_yuan_selection || config.static_rules.reject_one_yuan_selection
|| config.static_rules.reject_upper_limit_selection || config.static_rules.reject_upper_limit_selection
|| config.static_rules.reject_lower_limit_selection || config.static_rules.reject_lower_limit_selection
|| config.static_rules.respect_allow_buy_sell
} }
RiskCheckScope::Buy => { RiskCheckScope::Buy => {
config.static_rules.reject_st_buy config.static_rules.reject_st_buy
@@ -893,13 +887,19 @@ mod tests {
let default_reason = let default_reason =
ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None); ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None);
let mut config = FidcRiskControlConfig::default(); let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_kcb_selection = true;
let enabled_selection_reason =
ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config,
);
config.static_rules.reject_kcb_selection = false; config.static_rules.reject_kcb_selection = false;
config.static_rules.reject_kcb_buy = false; config.static_rules.reject_kcb_buy = false;
let configured_reason = ChinaAShareRiskControl::buy_rejection_reason_with_config( let configured_reason = ChinaAShareRiskControl::buy_rejection_reason_with_config(
date, &candidate, &market, None, 6.27, &config, date, &candidate, &market, None, 6.27, &config,
); );
assert_eq!(default_reason, Some("kcb")); assert_eq!(default_reason, None);
assert_eq!(enabled_selection_reason, Some("kcb"));
assert_eq!(configured_reason, None); assert_eq!(configured_reason, None);
} }
@@ -1008,6 +1008,10 @@ mod tests {
let default_buy = let default_buy =
ChinaAShareRiskControl::buy_rejection_reason(date, &candidate, &market, None, 6.27); ChinaAShareRiskControl::buy_rejection_reason(date, &candidate, &market, None, 6.27);
let mut config = FidcRiskControlConfig::default(); let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_bjse_selection = true;
let enabled_selection = ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config,
);
config.static_rules.reject_bjse_selection = false; config.static_rules.reject_bjse_selection = false;
config.static_rules.reject_bjse_buy = false; config.static_rules.reject_bjse_buy = false;
let configured_selection = ChinaAShareRiskControl::selection_rejection_reason_with_config( let configured_selection = ChinaAShareRiskControl::selection_rejection_reason_with_config(
@@ -1017,8 +1021,9 @@ mod tests {
date, &candidate, &market, None, 6.27, &config, date, &candidate, &market, None, 6.27, &config,
); );
assert_eq!(default_selection, Some("bjse")); assert_eq!(default_selection, None);
assert_eq!(default_buy, Some("bjse")); assert_eq!(default_buy, Some("bjse"));
assert_eq!(enabled_selection, Some("bjse"));
assert_eq!(configured_selection, None); assert_eq!(configured_selection, None);
assert_eq!(configured_buy, None); assert_eq!(configured_buy, None);
} }
@@ -1030,13 +1035,11 @@ mod tests {
candidate.symbol = "688506.SH".to_string(); candidate.symbol = "688506.SH".to_string();
candidate.risk_level_code = Some("missing_risk_state".to_string()); candidate.risk_level_code = Some("missing_risk_state".to_string());
let market = market(date, 6.27, 5.63); let market = market(date, 6.27, 5.63);
let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_kcb_selection = true;
let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config( let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config(
date, date, &candidate, &market, None, &config,
&candidate,
&market,
None,
&FidcRiskControlConfig::default(),
) )
.expect("kcb selection rejection"); .expect("kcb selection rejection");
@@ -1051,9 +1054,13 @@ mod tests {
candidate.allow_sell = true; candidate.allow_sell = true;
candidate.risk_level_code = Some("inactive_or_delisted".to_string()); candidate.risk_level_code = Some("inactive_or_delisted".to_string());
let market = market(date, 6.27, 5.63); let market = market(date, 6.27, 5.63);
let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_inactive_selection = true;
assert_eq!( assert_eq!(
ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None), ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config
),
Some("inactive_or_delisted") Some("inactive_or_delisted")
); );
assert_eq!( assert_eq!(
@@ -1189,6 +1196,8 @@ mod tests {
Some("missing_risk_state:upper_limit_price,lower_limit_price".to_string()); Some("missing_risk_state:upper_limit_price,lower_limit_price".to_string());
let market = market(date, 6.27, 5.63); let market = market(date, 6.27, 5.63);
let mut config = FidcRiskControlConfig::default(); let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_upper_limit_selection = true;
config.static_rules.reject_lower_limit_selection = true;
config.static_rules.reject_upper_limit_selection = false; config.static_rules.reject_upper_limit_selection = false;
let upper_disabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( let upper_disabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
@@ -1282,12 +1291,17 @@ mod tests {
let default_reason = let default_reason =
ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None); ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None);
let mut config = FidcRiskControlConfig::default(); let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_upper_limit_selection = true;
let enabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config,
);
config.static_rules.reject_upper_limit_selection = false; config.static_rules.reject_upper_limit_selection = false;
let configured_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( let configured_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config, date, &candidate, &market, None, &config,
); );
assert_eq!(default_reason, Some("upper_limit")); assert_eq!(default_reason, None);
assert_eq!(enabled_reason, Some("upper_limit"));
assert_eq!(configured_reason, None); assert_eq!(configured_reason, None);
} }
@@ -1300,12 +1314,17 @@ mod tests {
let default_reason = let default_reason =
ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None); ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None);
let mut config = FidcRiskControlConfig::default(); let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_lower_limit_selection = true;
let enabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config,
);
config.static_rules.reject_lower_limit_selection = false; config.static_rules.reject_lower_limit_selection = false;
let configured_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( let configured_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config, date, &candidate, &market, None, &config,
); );
assert_eq!(default_reason, Some("lower_limit")); assert_eq!(default_reason, None);
assert_eq!(enabled_reason, Some("lower_limit"));
assert_eq!(configured_reason, None); assert_eq!(configured_reason, None);
} }
} }
+15 -20
View File
@@ -2978,30 +2978,25 @@ mod tests {
let (default_selected, _) = default_strategy let (default_selected, _) = default_strategy
.select_symbols(&ctx, dates[2], 0.0, 100.0) .select_symbols(&ctx, dates[2], 0.0, 100.0)
.expect("default selection"); .expect("default selection");
assert!(default_selected.is_empty()); assert_eq!(default_selected, vec![symbol.to_string()]);
let default_risk_decisions = default_strategy.selection_risk_decisions(&ctx, dates[2]); let default_risk_decisions = default_strategy.selection_risk_decisions(&ctx, dates[2]);
assert_eq!(default_risk_decisions.len(), 1); assert!(default_risk_decisions.is_empty());
assert_eq!(default_risk_decisions[0].symbol, symbol);
assert_eq!(default_risk_decisions[0].rule_code, "kcb"); let mut cfg = default_cfg;
cfg.risk_config.static_rules.reject_kcb_selection = true;
let strict_strategy = OmniMicroCapStrategy::new(cfg);
let (selected, _) = strict_strategy
.select_symbols(&ctx, dates[2], 0.0, 100.0)
.expect("strict selection");
assert!(selected.is_empty());
let strict_risk_decisions = strict_strategy.selection_risk_decisions(&ctx, dates[2]);
assert_eq!(strict_risk_decisions.len(), 1);
assert_eq!(strict_risk_decisions[0].symbol, symbol);
assert_eq!(strict_risk_decisions[0].rule_code, "kcb");
assert!( assert!(
default_risk_decisions[0] strict_risk_decisions[0]
.diagnostic_line() .diagnostic_line()
.starts_with("risk_decision=") .starts_with("risk_decision=")
); );
let mut cfg = default_cfg;
cfg.risk_config.static_rules.reject_kcb_selection = false;
cfg.risk_config.static_rules.reject_kcb_buy = false;
let configured_strategy = OmniMicroCapStrategy::new(cfg);
let (selected, _) = configured_strategy
.select_symbols(&ctx, dates[2], 0.0, 100.0)
.expect("configured selection");
assert!(
configured_strategy
.selection_risk_decisions(&ctx, dates[2])
.is_empty()
);
assert_eq!(selected, vec![symbol.to_string()]);
} }
} }
+2 -2
View File
@@ -119,8 +119,8 @@ pub struct StrategyAiOptimizeRequest {
} }
const DEFAULT_THREE_YEAR_RETURN_TARGET_PROMPT: &str = "默认收益目标:用户没有明确指定更高收益阈值时,三年回测区间策略总收益 >= 150% 即视为满足收益目标;达到该阈值后可以继续优化夏普、回撤、换手和稳定性,但不得把已达标策略判为失败或为了追更高收益破坏无未来数据、持仓数量和同条件对账合同。"; const DEFAULT_THREE_YEAR_RETURN_TARGET_PROMPT: &str = "默认收益目标:用户没有明确指定更高收益阈值时,三年回测区间策略总收益 >= 150% 即视为满足收益目标;达到该阈值后可以继续优化夏普、回撤、换手和稳定性,但不得把已达标策略判为失败或为了追更高收益破坏无未来数据、持仓数量和同条件对账合同。";
const DEFAULT_RISK_POLICY_DSL_PROMPT: &str = "reject_st_selection=true、reject_st_buy=true、reject_star_st_selection=true、reject_star_st_buy=true、reject_paused_selection=true、reject_paused_buy=true、reject_paused_sell=true、reject_inactive_selection=true、reject_inactive_buy=true、reject_inactive_sell=true、reject_new_listing_selection=true、reject_new_listing_buy=true、reject_kcb_selection=true、reject_kcb_buy=true、reject_bjse_selection=true、reject_bjse_buy=true、reject_one_yuan_selection=true、reject_one_yuan_buy=true、respect_allow_buy_sell=true、reject_upper_limit_selection=true、reject_lower_limit_selection=true、reject_upper_limit_buy=true、reject_lower_limit_sell=true、forbid_same_day_rebuy_after_sell=true、blacklist_enabled=true、volume_limit_enabled=true、liquidity_limit_enabled=true、volume_percent=0.25、commission_rate=0.0003、minimum_commission=5、stamp_tax_rate_before_change=0.001、stamp_tax_rate_after_change=0.0005、stamp_tax_change_date=\"2023-08-28\""; const DEFAULT_RISK_POLICY_DSL_PROMPT: &str = "reject_st_selection=false、reject_st_buy=true、reject_star_st_selection=false、reject_star_st_buy=true、reject_paused_selection=false、reject_paused_buy=true、reject_paused_sell=true、reject_inactive_selection=false、reject_inactive_buy=true、reject_inactive_sell=true、reject_new_listing_selection=false、reject_new_listing_buy=true、reject_kcb_selection=false、reject_kcb_buy=true、reject_bjse_selection=false、reject_bjse_buy=true、reject_one_yuan_selection=false、reject_one_yuan_buy=true、respect_allow_buy_sell=true、reject_upper_limit_selection=false、reject_lower_limit_selection=false、reject_upper_limit_buy=true、reject_lower_limit_sell=true、forbid_same_day_rebuy_after_sell=true、blacklist_enabled=true、volume_limit_enabled=true、liquidity_limit_enabled=true、volume_percent=0.25、commission_rate=0.0003、minimum_commission=5、stamp_tax_rate_before_change=0.001、stamp_tax_rate_after_change=0.0005、stamp_tax_change_date=\"2023-08-28\"";
const DEFAULT_RISK_POLICY_DSL_CODE: &str = "reject_st_selection=true, reject_st_buy=true, reject_star_st_selection=true, reject_star_st_buy=true, reject_paused_selection=true, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=true, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=true, reject_new_listing_buy=true, reject_kcb_selection=true, reject_kcb_buy=true, reject_bjse_selection=true, reject_bjse_buy=true, reject_one_yuan_selection=true, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=true, reject_lower_limit_selection=true, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=true, volume_limit_enabled=true, liquidity_limit_enabled=true, volume_percent=0.25, commission_rate=0.0003, minimum_commission=5, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\""; const DEFAULT_RISK_POLICY_DSL_CODE: &str = "reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=true, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=true, volume_limit_enabled=true, liquidity_limit_enabled=true, volume_percent=0.25, commission_rate=0.0003, minimum_commission=5, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\"";
pub fn built_in_strategy_manual() -> StrategyAiManual { pub fn built_in_strategy_manual() -> StrategyAiManual {
StrategyAiManual { StrategyAiManual {
+8 -2
View File
@@ -396,13 +396,16 @@ mod tests {
) )
.unwrap(); .unwrap();
let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0); let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0);
let mut risk_config = FidcRiskControlConfig::default();
risk_config.static_rules.reject_st_selection = true;
risk_config.static_rules.reject_kcb_selection = true;
let (_selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext { let (_selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext {
decision_date: d(), decision_date: d(),
benchmark: &benchmark(), benchmark: &benchmark(),
reference_level: 2000.0, reference_level: 2000.0,
data: &data, data: &data,
dynamic_universe: None, dynamic_universe: None,
risk_config: Some(&FidcRiskControlConfig::default()), risk_config: Some(&risk_config),
}); });
let rules = diagnostics let rules = diagnostics
@@ -450,13 +453,16 @@ mod tests {
) )
.unwrap(); .unwrap();
let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0); let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0);
let mut risk_config = FidcRiskControlConfig::default();
risk_config.static_rules.reject_st_selection = true;
risk_config.static_rules.reject_kcb_selection = true;
let (selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext { let (selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext {
decision_date: d(), decision_date: d(),
benchmark: &benchmark(), benchmark: &benchmark(),
reference_level: 2000.0, reference_level: 2000.0,
data: &data, data: &data,
dynamic_universe: None, dynamic_universe: None,
risk_config: Some(&FidcRiskControlConfig::default()), risk_config: Some(&risk_config),
}); });
let selected_symbols = selected let selected_symbols = selected