统一回测策略风控字段校验
This commit is contained in:
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField};
|
||||
use crate::instrument::Instrument;
|
||||
use crate::portfolio::Position;
|
||||
use crate::OrderSide;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ChinaAShareRiskControl;
|
||||
@@ -77,6 +78,13 @@ impl Default for StaticRiskRuleConfig {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TradingConstraintConfig {
|
||||
/// Shared execution limits. These fields intentionally use the same
|
||||
/// names and defaults as the FIDC trading-core RiskLimits contract so a
|
||||
/// strategy cannot appear protected in paper/live while being unlimited
|
||||
/// in a backtest.
|
||||
pub max_order_quantity: f64,
|
||||
pub max_order_notional: f64,
|
||||
pub max_symbol_position: f64,
|
||||
pub volume_limit_enabled: bool,
|
||||
pub volume_percent: f64,
|
||||
pub liquidity_limit_enabled: bool,
|
||||
@@ -91,6 +99,9 @@ pub struct TradingConstraintConfig {
|
||||
impl Default for TradingConstraintConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_order_quantity: 1_000_000.0,
|
||||
max_order_notional: 100_000_000.0,
|
||||
max_symbol_position: 10_000_000.0,
|
||||
volume_limit_enabled: true,
|
||||
volume_percent: 0.25,
|
||||
liquidity_limit_enabled: true,
|
||||
@@ -479,6 +490,36 @@ impl ChinaAShareRiskControl {
|
||||
None
|
||||
}
|
||||
|
||||
/// Apply the shared quantity/notional/position limits at the same stage
|
||||
/// as paper/live `RiskLimits`. Static instrument rules remain in the
|
||||
/// side-specific methods above; this helper only checks order sizing and
|
||||
/// never changes selection semantics.
|
||||
pub fn order_size_rejection_reason_with_config(
|
||||
side: OrderSide,
|
||||
requested_quantity: u32,
|
||||
current_position_quantity: u32,
|
||||
check_price: f64,
|
||||
config: &FidcRiskControlConfig,
|
||||
) -> Option<&'static str> {
|
||||
let limits = &config.trading_constraints;
|
||||
if (requested_quantity as f64) > limits.max_order_quantity {
|
||||
return Some("quantity exceeds max_order_quantity");
|
||||
}
|
||||
if check_price.is_finite()
|
||||
&& check_price > 0.0
|
||||
&& (requested_quantity as f64) * check_price > limits.max_order_notional
|
||||
{
|
||||
return Some("notional exceeds max_order_notional");
|
||||
}
|
||||
if side == OrderSide::Buy
|
||||
&& (current_position_quantity as f64) + (requested_quantity as f64)
|
||||
> limits.max_symbol_position
|
||||
{
|
||||
return Some("position exceeds max_symbol_position");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn sell_rejection_reason(
|
||||
date: NaiveDate,
|
||||
candidate: &CandidateEligibility,
|
||||
@@ -1426,4 +1467,53 @@ mod tests {
|
||||
assert_eq!(enabled_reason, Some("lower_limit"));
|
||||
assert_eq!(configured_reason, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_order_size_limits_apply_to_both_sides_and_buy_position() {
|
||||
let mut config = FidcRiskControlConfig::default();
|
||||
config.trading_constraints.max_order_quantity = 500.0;
|
||||
config.trading_constraints.max_order_notional = 5_000.0;
|
||||
config.trading_constraints.max_symbol_position = 800.0;
|
||||
|
||||
assert_eq!(
|
||||
ChinaAShareRiskControl::order_size_rejection_reason_with_config(
|
||||
OrderSide::Buy,
|
||||
600,
|
||||
0,
|
||||
5.0,
|
||||
&config,
|
||||
),
|
||||
Some("quantity exceeds max_order_quantity")
|
||||
);
|
||||
assert_eq!(
|
||||
ChinaAShareRiskControl::order_size_rejection_reason_with_config(
|
||||
OrderSide::Sell,
|
||||
400,
|
||||
10_000,
|
||||
20.0,
|
||||
&config,
|
||||
),
|
||||
Some("notional exceeds max_order_notional")
|
||||
);
|
||||
assert_eq!(
|
||||
ChinaAShareRiskControl::order_size_rejection_reason_with_config(
|
||||
OrderSide::Buy,
|
||||
300,
|
||||
600,
|
||||
5.0,
|
||||
&config,
|
||||
),
|
||||
Some("position exceeds max_symbol_position")
|
||||
);
|
||||
assert_eq!(
|
||||
ChinaAShareRiskControl::order_size_rejection_reason_with_config(
|
||||
OrderSide::Sell,
|
||||
200,
|
||||
10_000,
|
||||
5.0,
|
||||
&config,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user