修复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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user