修复FIDC选股阶段风控语义
This commit is contained in:
@@ -2426,18 +2426,26 @@ where
|
||||
} else {
|
||||
ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field)
|
||||
};
|
||||
let mut risk_config = self.risk_config.clone();
|
||||
if self.aiquant_execution_rules {
|
||||
risk_config.static_rules.respect_allow_buy_sell = false;
|
||||
}
|
||||
let adjusted_candidate;
|
||||
let candidate_for_check = if self.aiquant_execution_rules
|
||||
&& 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(
|
||||
date,
|
||||
candidate,
|
||||
candidate_for_check,
|
||||
snapshot,
|
||||
instrument,
|
||||
Some(position),
|
||||
check_price,
|
||||
&risk_config,
|
||||
&self.risk_config,
|
||||
) {
|
||||
return RuleCheck::reject(reason);
|
||||
}
|
||||
@@ -2453,6 +2461,21 @@ where
|
||||
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(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -5283,7 +5306,7 @@ where
|
||||
}
|
||||
|
||||
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;
|
||||
if raw_limit <= 0 {
|
||||
return Err(volume_limit_reason.to_string());
|
||||
@@ -6537,6 +6560,27 @@ mod tests {
|
||||
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]
|
||||
fn current_bar_close_volume_limit_rejects_daily_zero_volume() {
|
||||
let mut snapshot = limit_test_snapshot();
|
||||
@@ -7427,4 +7471,99 @@ mod tests {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3516,8 +3516,13 @@ mod tests {
|
||||
)
|
||||
.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();
|
||||
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;
|
||||
let rows = data.eligible_universe_on_with_risk_config(date, &risk_config);
|
||||
|
||||
|
||||
@@ -5002,9 +5002,22 @@ mod tests {
|
||||
.map(|snapshot| snapshot.close),
|
||||
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!(
|
||||
dataset.eligible_universe_on(first).is_empty(),
|
||||
"raw DataSet helper remains a risk-filtered universe"
|
||||
dataset
|
||||
.eligible_universe_on_with_risk_config(first, &selection_risk_config)
|
||||
.is_empty(),
|
||||
"explicit raw DataSet selection risk can still filter the universe"
|
||||
);
|
||||
assert_eq!(
|
||||
manual_ctx
|
||||
@@ -5017,7 +5030,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
manual_ctx
|
||||
.eligible_universe_on_with_risk_config(first, &FidcRiskControlConfig::default())
|
||||
.eligible_universe_on_with_risk_config(first, &selection_risk_config)
|
||||
.into_iter()
|
||||
.map(|row| row.symbol)
|
||||
.collect::<Vec<_>>(),
|
||||
|
||||
@@ -6019,7 +6019,7 @@ impl PlatformExprStrategy {
|
||||
ctx: &StrategyContext<'_>,
|
||||
date: NaiveDate,
|
||||
factor_date: NaiveDate,
|
||||
defer_limit_state_selection_risk: bool,
|
||||
defer_all_selection_risk: bool,
|
||||
) -> Vec<EligibleUniverseSnapshot> {
|
||||
let mut rows = Vec::new();
|
||||
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 {
|
||||
continue;
|
||||
};
|
||||
if let Some(reason) =
|
||||
if let Some(_reason) =
|
||||
self.selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market)
|
||||
{
|
||||
if !(defer_limit_state_selection_risk
|
||||
&& matches!(reason, "upper_limit" | "lower_limit"))
|
||||
{
|
||||
if !defer_all_selection_risk {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -6081,7 +6079,7 @@ impl PlatformExprStrategy {
|
||||
ctx: &StrategyContext<'_>,
|
||||
date: NaiveDate,
|
||||
factor_date: NaiveDate,
|
||||
defer_limit_state_selection_risk: bool,
|
||||
_defer_all_selection_risk: bool,
|
||||
) -> Vec<FidcRiskDecisionAudit> {
|
||||
let mut decisions = Vec::new();
|
||||
for factor in ctx.data.factor_snapshots_on(factor_date) {
|
||||
@@ -6101,11 +6099,6 @@ impl PlatformExprStrategy {
|
||||
ctx.data.instrument(&factor.symbol),
|
||||
&self.config.risk_config,
|
||||
) {
|
||||
if defer_limit_state_selection_risk
|
||||
&& matches!(decision.rule_code.as_str(), "upper_limit" | "lower_limit")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
decisions.push(decision);
|
||||
}
|
||||
}
|
||||
@@ -6464,13 +6457,13 @@ impl PlatformExprStrategy {
|
||||
band_high: f64,
|
||||
limit: usize,
|
||||
) -> Result<(Vec<String>, Vec<String>, Vec<FidcRiskDecisionAudit>), BacktestError> {
|
||||
let defer_limit_state_selection_risk = ctx.is_lagged_execution();
|
||||
let universe = if defer_limit_state_selection_risk {
|
||||
let defer_all_selection_risk = ctx.is_lagged_execution();
|
||||
let universe = if defer_all_selection_risk {
|
||||
self.selectable_universe_on_with_options(ctx, date, universe_factor_date, true)
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
self.selection_risk_decisions(ctx, date, universe_factor_date)
|
||||
@@ -8950,8 +8943,22 @@ mod tests {
|
||||
|
||||
let default_strategy =
|
||||
PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
|
||||
assert_eq!(
|
||||
default_strategy
|
||||
.selectable_universe_on(&ctx, date, date)
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
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)
|
||||
.is_empty()
|
||||
);
|
||||
@@ -8962,11 +8969,13 @@ mod tests {
|
||||
date,
|
||||
5,
|
||||
);
|
||||
assert!(
|
||||
risk_diagnostics
|
||||
.iter()
|
||||
.any(|item| item.contains("risk_decisions selection_total=1 by_rule=kcb:1")),
|
||||
"{risk_diagnostics:?}"
|
||||
assert!(risk_diagnostics.is_empty(), "{risk_diagnostics:?}");
|
||||
let risk_decisions = selection_strategy.selection_risk_decisions(&ctx, date, date);
|
||||
let risk_diagnostics = PlatformExprStrategy::selection_risk_decision_diagnostics(
|
||||
&risk_decisions,
|
||||
date,
|
||||
date,
|
||||
5,
|
||||
);
|
||||
assert!(
|
||||
risk_diagnostics
|
||||
@@ -9118,12 +9127,41 @@ mod tests {
|
||||
|
||||
let default_strategy =
|
||||
PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
|
||||
assert_eq!(
|
||||
default_strategy
|
||||
.selectable_universe_on(&ctx, date, date)
|
||||
.len(),
|
||||
symbols.len()
|
||||
);
|
||||
assert!(
|
||||
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)
|
||||
.is_empty()
|
||||
);
|
||||
let rejected_rules = default_strategy
|
||||
let rejected_rules = strict_strategy
|
||||
.selection_risk_decisions(&ctx, date, date)
|
||||
.into_iter()
|
||||
.map(|decision| (decision.symbol, decision.rule_code))
|
||||
@@ -9254,10 +9292,11 @@ mod tests {
|
||||
|
||||
let default_strategy =
|
||||
PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
|
||||
assert!(
|
||||
assert_eq!(
|
||||
default_strategy
|
||||
.selectable_universe_on(&ctx, date, date)
|
||||
.is_empty()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
let default_stock = default_strategy
|
||||
.stock_state(&ctx, date, symbol)
|
||||
@@ -15239,6 +15278,8 @@ mod tests {
|
||||
cfg.selection_limit_expr = "1".to_string();
|
||||
cfg.stock_filter_expr = "close > 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 decision = strategy.on_day(&ctx).expect("platform decision");
|
||||
@@ -15247,7 +15288,7 @@ mod tests {
|
||||
decision
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|item| item == "selected_symbols="),
|
||||
.any(|item| item == "selected_symbols=600462.SH"),
|
||||
"{:?}",
|
||||
decision.diagnostics
|
||||
);
|
||||
@@ -15260,11 +15301,11 @@ mod tests {
|
||||
decision.diagnostics
|
||||
);
|
||||
assert!(
|
||||
decision.order_intents.iter().all(|intent| !matches!(
|
||||
decision.order_intents.iter().any(|intent| matches!(
|
||||
intent,
|
||||
OrderIntent::Shares { symbol, quantity, .. }
|
||||
if symbol == candidate_symbol && *quantity > 0
|
||||
) && !matches!(
|
||||
) || matches!(
|
||||
intent,
|
||||
OrderIntent::Value { symbol, value, .. }
|
||||
if symbol == candidate_symbol && *value > 0.0
|
||||
@@ -15695,7 +15736,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 symbol = "002494.SZ";
|
||||
let data = DataSet::from_components(
|
||||
@@ -15796,7 +15837,8 @@ mod tests {
|
||||
let selected = strategy.selectable_universe_on(&ctx, date, date);
|
||||
|
||||
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 {
|
||||
|
||||
@@ -45,27 +45,27 @@ pub struct StaticRiskRuleConfig {
|
||||
impl Default for StaticRiskRuleConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
reject_st_selection: true,
|
||||
reject_st_selection: false,
|
||||
reject_st_buy: true,
|
||||
reject_star_st_selection: true,
|
||||
reject_star_st_selection: false,
|
||||
reject_star_st_buy: true,
|
||||
reject_paused_selection: true,
|
||||
reject_paused_selection: false,
|
||||
reject_paused_buy: true,
|
||||
reject_paused_sell: true,
|
||||
reject_inactive_selection: true,
|
||||
reject_inactive_selection: false,
|
||||
reject_inactive_buy: true,
|
||||
reject_inactive_sell: true,
|
||||
reject_new_listing_selection: true,
|
||||
reject_new_listing_selection: false,
|
||||
reject_new_listing_buy: true,
|
||||
reject_kcb_selection: true,
|
||||
reject_kcb_selection: false,
|
||||
reject_kcb_buy: true,
|
||||
reject_bjse_selection: true,
|
||||
reject_bjse_selection: false,
|
||||
reject_bjse_buy: true,
|
||||
reject_one_yuan_selection: true,
|
||||
reject_one_yuan_selection: false,
|
||||
reject_one_yuan_buy: true,
|
||||
respect_allow_buy_sell: true,
|
||||
reject_upper_limit_selection: true,
|
||||
reject_lower_limit_selection: 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,
|
||||
@@ -255,11 +255,6 @@ impl ChinaAShareRiskControl {
|
||||
) {
|
||||
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);
|
||||
if config.static_rules.reject_upper_limit_selection
|
||||
&& 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_upper_limit_selection
|
||||
|| config.static_rules.reject_lower_limit_selection
|
||||
|| config.static_rules.respect_allow_buy_sell
|
||||
}
|
||||
RiskCheckScope::Buy => {
|
||||
config.static_rules.reject_st_buy
|
||||
@@ -893,13 +887,19 @@ mod tests {
|
||||
let default_reason =
|
||||
ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None);
|
||||
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_buy = false;
|
||||
let configured_reason = ChinaAShareRiskControl::buy_rejection_reason_with_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);
|
||||
}
|
||||
|
||||
@@ -1008,6 +1008,10 @@ mod tests {
|
||||
let default_buy =
|
||||
ChinaAShareRiskControl::buy_rejection_reason(date, &candidate, &market, None, 6.27);
|
||||
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_buy = false;
|
||||
let configured_selection = ChinaAShareRiskControl::selection_rejection_reason_with_config(
|
||||
@@ -1017,8 +1021,9 @@ mod tests {
|
||||
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!(enabled_selection, Some("bjse"));
|
||||
assert_eq!(configured_selection, None);
|
||||
assert_eq!(configured_buy, None);
|
||||
}
|
||||
@@ -1030,13 +1035,11 @@ mod tests {
|
||||
candidate.symbol = "688506.SH".to_string();
|
||||
candidate.risk_level_code = Some("missing_risk_state".to_string());
|
||||
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(
|
||||
date,
|
||||
&candidate,
|
||||
&market,
|
||||
None,
|
||||
&FidcRiskControlConfig::default(),
|
||||
date, &candidate, &market, None, &config,
|
||||
)
|
||||
.expect("kcb selection rejection");
|
||||
|
||||
@@ -1051,9 +1054,13 @@ mod tests {
|
||||
candidate.allow_sell = true;
|
||||
candidate.risk_level_code = Some("inactive_or_delisted".to_string());
|
||||
let market = market(date, 6.27, 5.63);
|
||||
let mut config = FidcRiskControlConfig::default();
|
||||
config.static_rules.reject_inactive_selection = true;
|
||||
|
||||
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")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1189,6 +1196,8 @@ mod tests {
|
||||
Some("missing_risk_state:upper_limit_price,lower_limit_price".to_string());
|
||||
let market = market(date, 6.27, 5.63);
|
||||
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;
|
||||
|
||||
let upper_disabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
|
||||
@@ -1282,12 +1291,17 @@ mod tests {
|
||||
let default_reason =
|
||||
ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None);
|
||||
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;
|
||||
let configured_reason = ChinaAShareRiskControl::selection_rejection_reason_with_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);
|
||||
}
|
||||
|
||||
@@ -1300,12 +1314,17 @@ mod tests {
|
||||
let default_reason =
|
||||
ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None);
|
||||
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;
|
||||
let configured_reason = ChinaAShareRiskControl::selection_rejection_reason_with_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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2978,30 +2978,25 @@ mod tests {
|
||||
let (default_selected, _) = default_strategy
|
||||
.select_symbols(&ctx, dates[2], 0.0, 100.0)
|
||||
.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]);
|
||||
assert_eq!(default_risk_decisions.len(), 1);
|
||||
assert_eq!(default_risk_decisions[0].symbol, symbol);
|
||||
assert_eq!(default_risk_decisions[0].rule_code, "kcb");
|
||||
assert!(default_risk_decisions.is_empty());
|
||||
|
||||
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!(
|
||||
default_risk_decisions[0]
|
||||
strict_risk_decisions[0]
|
||||
.diagnostic_line()
|
||||
.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()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ pub struct StrategyAiOptimizeRequest {
|
||||
}
|
||||
|
||||
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_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_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=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 {
|
||||
StrategyAiManual {
|
||||
|
||||
@@ -396,13 +396,16 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
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 {
|
||||
decision_date: d(),
|
||||
benchmark: &benchmark(),
|
||||
reference_level: 2000.0,
|
||||
data: &data,
|
||||
dynamic_universe: None,
|
||||
risk_config: Some(&FidcRiskControlConfig::default()),
|
||||
risk_config: Some(&risk_config),
|
||||
});
|
||||
|
||||
let rules = diagnostics
|
||||
@@ -450,13 +453,16 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
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 {
|
||||
decision_date: d(),
|
||||
benchmark: &benchmark(),
|
||||
reference_level: 2000.0,
|
||||
data: &data,
|
||||
dynamic_universe: None,
|
||||
risk_config: Some(&FidcRiskControlConfig::default()),
|
||||
risk_config: Some(&risk_config),
|
||||
});
|
||||
|
||||
let selected_symbols = selected
|
||||
|
||||
Reference in New Issue
Block a user