统一回测策略风控字段校验
This commit is contained in:
@@ -2388,6 +2388,19 @@ where
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn reserved_open_buy_quantity(&self, symbol: &str, exclude_order_id: Option<u64>) -> u32 {
|
||||
self.open_orders
|
||||
.borrow()
|
||||
.iter()
|
||||
.filter(|order| {
|
||||
order.side == OrderSide::Buy
|
||||
&& order.symbol == symbol
|
||||
&& exclude_order_id.is_none_or(|order_id| order.order_id != order_id)
|
||||
})
|
||||
.map(|order| order.remaining_quantity)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn process_open_orders(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -4251,6 +4264,49 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let size_check_price = limit_price.unwrap_or_else(|| {
|
||||
self.execution_order_limit_check_price(
|
||||
date,
|
||||
data,
|
||||
symbol,
|
||||
snapshot,
|
||||
OrderSide::Sell,
|
||||
algo_request,
|
||||
)
|
||||
});
|
||||
if let Some(rule_reason) = ChinaAShareRiskControl::order_size_rejection_reason_with_config(
|
||||
OrderSide::Sell,
|
||||
requested_qty,
|
||||
position.quantity,
|
||||
size_check_price,
|
||||
&self.risk_config,
|
||||
) {
|
||||
report.order_events.push(OrderEvent {
|
||||
date,
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
order_id: Some(order_id),
|
||||
symbol: symbol.to_string(),
|
||||
side: OrderSide::Sell,
|
||||
requested_quantity: requested_qty,
|
||||
filled_quantity: 0,
|
||||
status: OrderStatus::Rejected,
|
||||
reason: format!("{reason}: {rule_reason}"),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
Self::creation_reject_kind(emit_creation_events),
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
format!("status=Rejected reason={rule_reason}"),
|
||||
);
|
||||
self.clear_open_order(order_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if emit_creation_events {
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
@@ -5984,6 +6040,54 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let current_position_quantity = portfolio
|
||||
.position(symbol)
|
||||
.map(|position| position.quantity)
|
||||
.unwrap_or(0)
|
||||
.saturating_add(self.reserved_open_buy_quantity(symbol, Some(order_id)));
|
||||
let size_check_price = limit_price.unwrap_or_else(|| {
|
||||
self.execution_order_limit_check_price(
|
||||
date,
|
||||
data,
|
||||
symbol,
|
||||
snapshot,
|
||||
OrderSide::Buy,
|
||||
algo_request,
|
||||
)
|
||||
});
|
||||
if let Some(rule_reason) = ChinaAShareRiskControl::order_size_rejection_reason_with_config(
|
||||
OrderSide::Buy,
|
||||
requested_qty,
|
||||
current_position_quantity,
|
||||
size_check_price,
|
||||
&self.risk_config,
|
||||
) {
|
||||
report.order_events.push(OrderEvent {
|
||||
date,
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
order_id: Some(order_id),
|
||||
symbol: symbol.to_string(),
|
||||
side: OrderSide::Buy,
|
||||
requested_quantity: requested_qty,
|
||||
filled_quantity: 0,
|
||||
status: OrderStatus::Rejected,
|
||||
reason: format!("{reason}: {rule_reason}"),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
Self::creation_reject_kind(emit_creation_events),
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
format!("status=Rejected reason={rule_reason}"),
|
||||
);
|
||||
self.clear_open_order(order_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if emit_creation_events {
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
|
||||
@@ -79,6 +79,7 @@ pub use platform_strategy_spec::{
|
||||
StrategyRebalanceSpec, StrategyRiskPolicySpec, StrategyRuntimeEnvironment,
|
||||
StrategyRuntimeExpressions, StrategyRuntimeSpec, StrategyUniverseSpec,
|
||||
platform_expr_config_from_spec, platform_expr_config_from_value,
|
||||
validate_strategy_risk_policy_fields,
|
||||
};
|
||||
pub use portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position};
|
||||
pub use risk_control::{
|
||||
|
||||
@@ -268,6 +268,12 @@ pub struct StrategyEngineConfig {
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StrategyRiskPolicySpec {
|
||||
#[serde(default, alias = "max_order_quantity", alias = "maxOrderQuantity")]
|
||||
pub max_order_quantity: Option<f64>,
|
||||
#[serde(default, alias = "max_order_notional", alias = "maxOrderNotional")]
|
||||
pub max_order_notional: Option<f64>,
|
||||
#[serde(default, alias = "max_symbol_position", alias = "maxSymbolPosition")]
|
||||
pub max_symbol_position: Option<f64>,
|
||||
#[serde(default, alias = "reject_st_selection", alias = "rejectStSelection")]
|
||||
pub reject_st_selection: Option<bool>,
|
||||
#[serde(default, alias = "reject_st_buy", alias = "rejectStBuy")]
|
||||
@@ -412,6 +418,9 @@ const RISK_POLICY_BOOL_ALIAS_GROUPS: &[(&str, &[&str])] = &[
|
||||
];
|
||||
|
||||
const RISK_POLICY_VALUE_ALIAS_GROUPS: &[(&str, &[&str])] = &[
|
||||
("maxOrderQuantity", &["max_order_quantity"]),
|
||||
("maxOrderNotional", &["max_order_notional"]),
|
||||
("maxSymbolPosition", &["max_symbol_position"]),
|
||||
("volumePercent", &["volume_percent"]),
|
||||
("commissionRate", &["commission_rate"]),
|
||||
(
|
||||
@@ -1042,6 +1051,7 @@ pub fn platform_expr_config_from_value(
|
||||
.map_err(platform_config_error);
|
||||
}
|
||||
reject_removed_compatibility_fields(value).map_err(platform_config_error)?;
|
||||
validate_strategy_risk_policy_fields(value).map_err(platform_config_error)?;
|
||||
let mut value = value.clone();
|
||||
normalize_strategy_aliases_in_value(&mut value).map_err(platform_config_error)?;
|
||||
normalize_risk_policy_aliases_in_value(&mut value).map_err(platform_config_error)?;
|
||||
@@ -1050,6 +1060,69 @@ pub fn platform_expr_config_from_value(
|
||||
.map_err(platform_config_error)
|
||||
}
|
||||
|
||||
/// Reject misspelled or unsupported fields inside a strategy risk policy.
|
||||
/// Generic JSON deserialization otherwise ignores unknown keys and makes a
|
||||
/// strategy appear protected while silently using the process default.
|
||||
pub fn validate_strategy_risk_policy_fields(value: &Value) -> Result<(), String> {
|
||||
let mut allowed = BTreeSet::<&str>::new();
|
||||
for (canonical, aliases) in RISK_POLICY_BOOL_ALIAS_GROUPS
|
||||
.iter()
|
||||
.chain(RISK_POLICY_VALUE_ALIAS_GROUPS.iter())
|
||||
{
|
||||
allowed.insert(*canonical);
|
||||
for key in *aliases {
|
||||
allowed.insert(*key);
|
||||
}
|
||||
}
|
||||
for key in [
|
||||
"blacklistedSymbols",
|
||||
"blacklisted_symbols",
|
||||
"blacklistedInstruments",
|
||||
"blacklisted_instruments",
|
||||
"blacklist",
|
||||
// Legacy execution aliases are accepted by StrategyExecutionSpec and
|
||||
// normalized into the same shared switches.
|
||||
"volumeLimit",
|
||||
"volume_limit",
|
||||
"liquidityLimit",
|
||||
"liquidity_limit",
|
||||
] {
|
||||
allowed.insert(key);
|
||||
}
|
||||
|
||||
fn walk(value: &Value, allowed: &BTreeSet<&str>, path: &str) -> Result<(), String> {
|
||||
let Some(object) = value.as_object() else {
|
||||
return Ok(());
|
||||
};
|
||||
for (key, child) in object {
|
||||
let child_path = if path.is_empty() {
|
||||
key.clone()
|
||||
} else {
|
||||
format!("{path}.{key}")
|
||||
};
|
||||
if matches!(key.as_str(), "riskPolicy" | "risk_policy") {
|
||||
if child.is_null() {
|
||||
// Typed specs serialize an omitted Option policy as null;
|
||||
// that is equivalent to an absent strategy policy.
|
||||
continue;
|
||||
}
|
||||
let Some(policy) = child.as_object() else {
|
||||
return Err(format!("{child_path} must be a JSON object"));
|
||||
};
|
||||
for policy_key in policy.keys() {
|
||||
if !allowed.contains(policy_key.as_str()) {
|
||||
return Err(format!("unsupported riskPolicy field: {policy_key}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(child, allowed, &child_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
walk(value, &allowed, "")
|
||||
}
|
||||
|
||||
fn reject_removed_compatibility_fields(value: &Value) -> Result<(), String> {
|
||||
const SECTION_NAMES: [&str; 3] = ["engineConfig", "engine_config", "execution"];
|
||||
const FIELD_NAMES: [&str; 4] = [
|
||||
@@ -1095,6 +1168,14 @@ fn valid_non_negative(value: Option<f64>) -> Option<f64> {
|
||||
value.filter(|item| item.is_finite() && *item >= 0.0)
|
||||
}
|
||||
|
||||
fn valid_positive_limit(value: Option<f64>, field_name: &str) -> Result<Option<f64>, String> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(item) if item.is_finite() && item > 0.0 => Ok(Some(item)),
|
||||
Some(_) => Err(format!("{field_name} must be a positive finite number")),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_percent_ratio(value: f64, field_name: &str) -> Result<f64, String> {
|
||||
if !value.is_finite() || value <= 0.0 {
|
||||
return Err(format!("{field_name} must be a positive finite number"));
|
||||
@@ -1214,6 +1295,27 @@ fn apply_risk_policy_overrides(
|
||||
let Some(policy) = policy else {
|
||||
return Ok(());
|
||||
};
|
||||
let max_order_quantity = valid_positive_limit(
|
||||
policy.max_order_quantity,
|
||||
"riskPolicy.maxOrderQuantity",
|
||||
)?;
|
||||
let max_order_notional = valid_positive_limit(
|
||||
policy.max_order_notional,
|
||||
"riskPolicy.maxOrderNotional",
|
||||
)?;
|
||||
let max_symbol_position = valid_positive_limit(
|
||||
policy.max_symbol_position,
|
||||
"riskPolicy.maxSymbolPosition",
|
||||
)?;
|
||||
if let Some(value) = max_order_quantity {
|
||||
cfg.risk_config.trading_constraints.max_order_quantity = value;
|
||||
}
|
||||
if let Some(value) = max_order_notional {
|
||||
cfg.risk_config.trading_constraints.max_order_notional = value;
|
||||
}
|
||||
if let Some(value) = max_symbol_position {
|
||||
cfg.risk_config.trading_constraints.max_symbol_position = value;
|
||||
}
|
||||
let static_rules = &mut cfg.risk_config.static_rules;
|
||||
if let Some(value) = policy.reject_st_selection {
|
||||
static_rules.reject_st_selection = value;
|
||||
@@ -3287,6 +3389,9 @@ mod tests {
|
||||
"rejectUpperLimitBuy": false,
|
||||
"rejectLowerLimitSell": false,
|
||||
"forbidSameDayRebuyAfterSell": true,
|
||||
"maxOrderQuantity": 8000,
|
||||
"maxOrderNotional": 2000000,
|
||||
"maxSymbolPosition": 12000,
|
||||
"blacklist": [" 600000.SH ", ""],
|
||||
"volumeLimitEnabled": true,
|
||||
"volumePercent": 0.1,
|
||||
@@ -3328,6 +3433,9 @@ mod tests {
|
||||
);
|
||||
assert!(cfg.risk_config.trading_constraints.volume_limit_enabled);
|
||||
assert!(cfg.risk_config.trading_constraints.liquidity_limit_enabled);
|
||||
assert_eq!(cfg.risk_config.trading_constraints.max_order_quantity, 8000.0);
|
||||
assert_eq!(cfg.risk_config.trading_constraints.max_order_notional, 2_000_000.0);
|
||||
assert_eq!(cfg.risk_config.trading_constraints.max_symbol_position, 12_000.0);
|
||||
assert!((cfg.risk_config.trading_constraints.volume_percent - 0.25).abs() < 1e-12);
|
||||
assert_eq!(
|
||||
cfg.risk_config
|
||||
@@ -3350,6 +3458,22 @@ mod tests {
|
||||
assert!(cfg.quote_quantity_limit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_positive_shared_order_limits() {
|
||||
for (field, value) in [
|
||||
("maxOrderQuantity", 0.0),
|
||||
("maxOrderNotional", -1.0),
|
||||
("maxSymbolPosition", -0.5),
|
||||
] {
|
||||
let spec = serde_json::json!({
|
||||
"execution": { "riskPolicy": { field: value } }
|
||||
});
|
||||
let error = platform_expr_config_from_value("", "", &spec)
|
||||
.expect_err("invalid shared order limit must fail");
|
||||
assert!(error.to_string().contains("riskPolicy"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volume_limit_does_not_enable_quote_quantity_limit_for_minute_last() {
|
||||
let spec = serde_json::json!({
|
||||
@@ -3482,6 +3606,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_strategy_risk_policy_fields_before_deserialization() {
|
||||
let error = validate_strategy_risk_policy_fields(&serde_json::json!({
|
||||
"execution": {
|
||||
"riskPolicy": {"rejectStBuy": true, "typoRiskSwitch": false}
|
||||
}
|
||||
}))
|
||||
.expect_err("unsupported risk policy fields must fail closed");
|
||||
assert!(error.contains("typoRiskSwitch"), "{error}");
|
||||
validate_strategy_risk_policy_fields(&serde_json::json!({
|
||||
"execution": {"riskPolicy": {"maxOrderQuantity": 1000}}
|
||||
}))
|
||||
.expect("supported strategy risk fields should pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_equivalent_risk_policy_alias_values() {
|
||||
let spec = serde_json::json!({
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ pub struct StrategyAiOptimizeRequest {
|
||||
}
|
||||
|
||||
const PERFORMANCE_ACCEPTANCE_CONTRACT_PROMPT: &str = "收益验收合同:收益、回撤、年度收益、样本外区间及比较运算符只能来自用户目标、请求约束或不可变 candidate/promotion contract;不得注入 120% 或其他默认数值,也不得提高、降低或替换已经明确的门槛。没有明确数值合同时只做策略有效性、数据时序和风险审计,禁止声称收益已经达标;存在冻结合同时必须逐项按原运算符验证,不能只看总收益。";
|
||||
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、allow_market_orders=true、live_trading_enabled=false、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, allow_market_orders=true, live_trading_enabled=false, 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 = "max_order_quantity=1000000、max_order_notional=100000000、max_symbol_position=10000000、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、allow_market_orders=true、live_trading_enabled=false、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 = "max_order_quantity=1000000, max_order_notional=100000000, max_symbol_position=10000000, 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, allow_market_orders=true, live_trading_enabled=false, 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 {
|
||||
@@ -250,7 +250,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
},
|
||||
ManualSection {
|
||||
title: "risk.policy / risk.blacklist".to_string(),
|
||||
detail: "统一配置 FIDC 基础风控。risk.policy(...) 支持 reject_st_selection、reject_st_buy、reject_star_st_selection、reject_star_st_buy、reject_paused_selection、reject_paused_buy、reject_paused_sell、reject_inactive_selection、reject_inactive_buy、reject_inactive_sell、reject_new_listing_selection、reject_new_listing_buy、reject_kcb_selection、reject_kcb_buy、reject_bjse_selection、reject_bjse_buy、reject_one_yuan_selection、reject_one_yuan_buy、respect_allow_buy_sell、reject_upper_limit_selection、reject_lower_limit_selection、reject_upper_limit_buy、reject_lower_limit_sell、forbid_same_day_rebuy_after_sell、blacklist_enabled、allow_market_orders、live_trading_enabled、blacklisted_symbols、volume_limit_enabled、liquidity_limit_enabled、volume_percent、commission_rate、minimum_commission、stamp_tax_rate_before_change、stamp_tax_rate_after_change、stamp_tax_change_date 等命名参数;risk.blacklist([\"600000.SH\"]) 写策略级黑名单。框架默认的 ST、*ST、停牌、退市、科创、北交所、一元、涨跌停、同日卖出禁买、黑名单、成交量和费用等基础风控必须走 risk.policy 或运行态 RiskLimits,不能被转换器隐式写进 universe.exclude 或 filter.stock_expr;但源策略明确写出的业务选股排除属于策略本身,必须原样保留在 filter.stock_expr,并且不能反向修改冻结的 reject_*_selection 开关。PG/Source Lake 是真相源,Redis 只可做当日锁、热配置缓存和配置变更通知。".to_string(),
|
||||
detail: "统一配置 FIDC 基础风控。risk.policy(...) 支持 max_order_quantity、max_order_notional、max_symbol_position,以及 ST/*ST、停牌、退市、新股、科创、北交所、一元、涨跌停、同日卖出禁买、黑名单、成交量、流动性和交易成本等命名参数;risk.blacklist([\"600000.SH\"]) 写策略级黑名单。框架默认基础风控必须走 risk.policy 或运行态 RiskLimits,不能被转换器隐式写进 universe.exclude 或 filter.stock_expr;源策略明确写出的业务选股排除属于策略本身,必须原样保留,不能反向修改冻结的 reject_*_selection 开关;冻结的 `reject_*_selection` 值不得改变。PG/Source Lake 是真相源,Redis 只可做当日锁、热配置缓存和配置变更通知。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "corporate_actions.dividend_reinvestment".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user