实现FIDC配置化风控与交易成本

This commit is contained in:
boris
2026-07-02 07:16:47 +08:00
parent 754fc91376
commit 7db0e8da1d
17 changed files with 2689 additions and 249 deletions
+65 -9
View File
@@ -12,7 +12,7 @@ use crate::events::{
};
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
use crate::risk_control::ChinaAShareRiskControl;
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig};
use crate::rules::{EquityRuleHooks, RuleCheck};
use crate::strategy::{
AlgoOrderStyle, OpenOrderView, OrderIntent, StrategyDecision, TargetPortfolioOrderPricing,
@@ -171,6 +171,8 @@ pub struct BrokerSimulator<C, R> {
strict_value_budget: bool,
aiquant_execution_rules: bool,
same_day_buy_close_mark_at_fill: bool,
risk_config: FidcRiskControlConfig,
same_day_sold_symbols: RefCell<BTreeMap<NaiveDate, BTreeSet<String>>>,
intraday_execution_start_time: Option<NaiveTime>,
runtime_intraday_start_time: Cell<Option<NaiveTime>>,
runtime_intraday_end_time: Cell<Option<NaiveTime>>,
@@ -194,6 +196,8 @@ impl<C, R> BrokerSimulator<C, R> {
strict_value_budget: false,
aiquant_execution_rules: false,
same_day_buy_close_mark_at_fill: false,
risk_config: FidcRiskControlConfig::default(),
same_day_sold_symbols: RefCell::new(BTreeMap::new()),
intraday_execution_start_time: None,
runtime_intraday_start_time: Cell::new(None),
runtime_intraday_end_time: Cell::new(None),
@@ -221,6 +225,8 @@ impl<C, R> BrokerSimulator<C, R> {
strict_value_budget: false,
aiquant_execution_rules: false,
same_day_buy_close_mark_at_fill: false,
risk_config: FidcRiskControlConfig::default(),
same_day_sold_symbols: RefCell::new(BTreeMap::new()),
intraday_execution_start_time: None,
runtime_intraday_start_time: Cell::new(None),
runtime_intraday_end_time: Cell::new(None),
@@ -259,6 +265,14 @@ impl<C, R> BrokerSimulator<C, R> {
self
}
pub fn with_risk_config(mut self, config: FidcRiskControlConfig) -> Self {
self.volume_limit = config.trading_constraints.volume_limit_enabled;
self.volume_percent = config.trading_constraints.volume_percent;
self.liquidity_limit = config.trading_constraints.liquidity_limit_enabled;
self.risk_config = config;
self
}
pub fn same_day_buy_close_mark_at_fill(&self) -> bool {
self.same_day_buy_close_mark_at_fill
}
@@ -1381,6 +1395,34 @@ where
.retain(|existing| existing.order_id != order_id);
}
fn mark_same_day_sold(&self, date: NaiveDate, symbol: &str) {
self.same_day_sold_symbols
.borrow_mut()
.entry(date)
.or_default()
.insert(symbol.to_string());
}
fn same_day_rebuy_rejection_reason(
&self,
date: NaiveDate,
symbol: &str,
) -> Option<&'static str> {
if self
.risk_config
.static_rules
.forbid_same_day_rebuy_after_sell
&& self
.same_day_sold_symbols
.borrow()
.get(&date)
.is_some_and(|symbols| symbols.contains(symbol))
{
return Some("same_day_rebuy_forbidden");
}
None
}
fn extend_report(into: &mut BrokerExecutionReport, mut other: BrokerExecutionReport) {
into.order_events.append(&mut other.order_events);
into.fill_events.append(&mut other.fill_events);
@@ -2181,16 +2223,17 @@ where
} else {
ChinaAShareRiskControl::buy_check_price(snapshot, self.execution_price_field)
};
if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason(
if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason_with_config(
date,
candidate,
snapshot,
instrument,
check_price,
&self.risk_config,
) {
return RuleCheck::reject(reason);
}
if !self.aiquant_execution_rules {
if !self.aiquant_execution_rules && !self.rules.duplicates_standard_china_risk() {
return self
.rules
.can_buy(date, snapshot, candidate, self.execution_price_field);
@@ -2220,16 +2263,20 @@ where
} else {
ChinaAShareRiskControl::buy_check_price(snapshot, self.execution_price_field)
};
if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason(
if let Some(reason) = self.same_day_rebuy_rejection_reason(date, symbol) {
return RuleCheck::reject(reason);
}
if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason_with_config(
date,
candidate,
snapshot,
instrument,
check_price,
&self.risk_config,
) {
return RuleCheck::reject(reason);
}
if !self.aiquant_execution_rules {
if !self.aiquant_execution_rules && !self.rules.duplicates_standard_china_risk() {
return self
.rules
.can_buy(date, snapshot, candidate, self.execution_price_field);
@@ -2263,17 +2310,18 @@ where
} else {
ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field)
};
if let Some(reason) = ChinaAShareRiskControl::sell_rejection_reason(
if let Some(reason) = ChinaAShareRiskControl::sell_rejection_reason_with_config(
date,
candidate,
snapshot,
instrument,
Some(position),
check_price,
&self.risk_config,
) {
return RuleCheck::reject(reason);
}
if !self.aiquant_execution_rules {
if !self.aiquant_execution_rules && !self.rules.duplicates_standard_china_risk() {
return self.rules.can_sell(
date,
snapshot,
@@ -2929,6 +2977,7 @@ where
net_cash_flow: net_cash,
reason: reason.to_string(),
});
self.mark_same_day_sold(date, symbol);
Self::emit_order_process_event(
report,
date,
@@ -5065,10 +5114,16 @@ where
return None;
}
match side {
OrderSide::Buy if snapshot.is_at_upper_limit_price(execution_price) => {
OrderSide::Buy
if self.risk_config.static_rules.reject_upper_limit_buy
&& snapshot.is_at_upper_limit_price(execution_price) =>
{
Some("open at or above upper limit")
}
OrderSide::Sell if snapshot.is_at_lower_limit_price(execution_price) => {
OrderSide::Sell
if self.risk_config.static_rules.reject_lower_limit_sell
&& snapshot.is_at_lower_limit_price(execution_price) =>
{
Some("open at or below lower limit")
}
_ => None,
@@ -5991,6 +6046,7 @@ mod tests {
stamp_tax_rate_before_change: 0.0005,
stamp_tax_rate_after_change: 0.0005,
minimum_commission: 5.0,
..ChinaAShareCostModel::default()
},
ChinaEquityRuleHooks,
PriceField::Last,