修复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 {
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
);
}
}