Merge remote-tracking branch 'refs/remotes/bundle/main'

This commit is contained in:
boris
2026-08-29 15:14:11 +08:00
5 changed files with 337 additions and 3 deletions
+104
View File
@@ -2388,6 +2388,19 @@ where
.sum() .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( fn process_open_orders(
&self, &self,
date: NaiveDate, date: NaiveDate,
@@ -4251,6 +4264,49 @@ where
return Ok(()); 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 { if emit_creation_events {
Self::emit_order_process_event( Self::emit_order_process_event(
report, report,
@@ -5984,6 +6040,54 @@ where
return Ok(()); 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 { if emit_creation_events {
Self::emit_order_process_event( Self::emit_order_process_event(
report, report,
+1
View File
@@ -79,6 +79,7 @@ pub use platform_strategy_spec::{
StrategyRebalanceSpec, StrategyRiskPolicySpec, StrategyRuntimeEnvironment, StrategyRebalanceSpec, StrategyRiskPolicySpec, StrategyRuntimeEnvironment,
StrategyRuntimeExpressions, StrategyRuntimeSpec, StrategyUniverseSpec, StrategyRuntimeExpressions, StrategyRuntimeSpec, StrategyUniverseSpec,
platform_expr_config_from_spec, platform_expr_config_from_value, 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 portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position};
pub use risk_control::{ pub use risk_control::{
@@ -268,6 +268,12 @@ pub struct StrategyEngineConfig {
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct StrategyRiskPolicySpec { 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")] #[serde(default, alias = "reject_st_selection", alias = "rejectStSelection")]
pub reject_st_selection: Option<bool>, pub reject_st_selection: Option<bool>,
#[serde(default, alias = "reject_st_buy", alias = "rejectStBuy")] #[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])] = &[ const RISK_POLICY_VALUE_ALIAS_GROUPS: &[(&str, &[&str])] = &[
("maxOrderQuantity", &["max_order_quantity"]),
("maxOrderNotional", &["max_order_notional"]),
("maxSymbolPosition", &["max_symbol_position"]),
("volumePercent", &["volume_percent"]), ("volumePercent", &["volume_percent"]),
("commissionRate", &["commission_rate"]), ("commissionRate", &["commission_rate"]),
( (
@@ -1042,6 +1051,7 @@ pub fn platform_expr_config_from_value(
.map_err(platform_config_error); .map_err(platform_config_error);
} }
reject_removed_compatibility_fields(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(); let mut value = value.clone();
normalize_strategy_aliases_in_value(&mut value).map_err(platform_config_error)?; 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)?; 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) .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> { fn reject_removed_compatibility_fields(value: &Value) -> Result<(), String> {
const SECTION_NAMES: [&str; 3] = ["engineConfig", "engine_config", "execution"]; const SECTION_NAMES: [&str; 3] = ["engineConfig", "engine_config", "execution"];
const FIELD_NAMES: [&str; 4] = [ 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) 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> { fn normalize_percent_ratio(value: f64, field_name: &str) -> Result<f64, String> {
if !value.is_finite() || value <= 0.0 { if !value.is_finite() || value <= 0.0 {
return Err(format!("{field_name} must be a positive finite number")); 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 { let Some(policy) = policy else {
return Ok(()); 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; let static_rules = &mut cfg.risk_config.static_rules;
if let Some(value) = policy.reject_st_selection { if let Some(value) = policy.reject_st_selection {
static_rules.reject_st_selection = value; static_rules.reject_st_selection = value;
@@ -3287,6 +3389,9 @@ mod tests {
"rejectUpperLimitBuy": false, "rejectUpperLimitBuy": false,
"rejectLowerLimitSell": false, "rejectLowerLimitSell": false,
"forbidSameDayRebuyAfterSell": true, "forbidSameDayRebuyAfterSell": true,
"maxOrderQuantity": 8000,
"maxOrderNotional": 2000000,
"maxSymbolPosition": 12000,
"blacklist": [" 600000.SH ", ""], "blacklist": [" 600000.SH ", ""],
"volumeLimitEnabled": true, "volumeLimitEnabled": true,
"volumePercent": 0.1, "volumePercent": 0.1,
@@ -3328,6 +3433,9 @@ mod tests {
); );
assert!(cfg.risk_config.trading_constraints.volume_limit_enabled); assert!(cfg.risk_config.trading_constraints.volume_limit_enabled);
assert!(cfg.risk_config.trading_constraints.liquidity_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!((cfg.risk_config.trading_constraints.volume_percent - 0.25).abs() < 1e-12);
assert_eq!( assert_eq!(
cfg.risk_config cfg.risk_config
@@ -3350,6 +3458,22 @@ mod tests {
assert!(cfg.quote_quantity_limit); 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] #[test]
fn volume_limit_does_not_enable_quote_quantity_limit_for_minute_last() { fn volume_limit_does_not_enable_quote_quantity_limit_for_minute_last() {
let spec = serde_json::json!({ 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] #[test]
fn accepts_equivalent_risk_policy_alias_values() { fn accepts_equivalent_risk_policy_alias_values() {
let spec = serde_json::json!({ let spec = serde_json::json!({
+90
View File
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField}; use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField};
use crate::instrument::Instrument; use crate::instrument::Instrument;
use crate::portfolio::Position; use crate::portfolio::Position;
use crate::OrderSide;
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct ChinaAShareRiskControl; pub struct ChinaAShareRiskControl;
@@ -77,6 +78,13 @@ impl Default for StaticRiskRuleConfig {
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TradingConstraintConfig { 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_limit_enabled: bool,
pub volume_percent: f64, pub volume_percent: f64,
pub liquidity_limit_enabled: bool, pub liquidity_limit_enabled: bool,
@@ -91,6 +99,9 @@ pub struct TradingConstraintConfig {
impl Default for TradingConstraintConfig { impl Default for TradingConstraintConfig {
fn default() -> Self { fn default() -> Self {
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_limit_enabled: true,
volume_percent: 0.25, volume_percent: 0.25,
liquidity_limit_enabled: true, liquidity_limit_enabled: true,
@@ -479,6 +490,36 @@ impl ChinaAShareRiskControl {
None 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( pub fn sell_rejection_reason(
date: NaiveDate, date: NaiveDate,
candidate: &CandidateEligibility, candidate: &CandidateEligibility,
@@ -1426,4 +1467,53 @@ mod tests {
assert_eq!(enabled_reason, Some("lower_limit")); assert_eq!(enabled_reason, Some("lower_limit"));
assert_eq!(configured_reason, None); 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
);
}
} }
+3 -3
View File
@@ -119,8 +119,8 @@ pub struct StrategyAiOptimizeRequest {
} }
const PERFORMANCE_ACCEPTANCE_CONTRACT_PROMPT: &str = "收益验收合同:收益、回撤、年度收益、样本外区间及比较运算符只能来自用户目标、请求约束或不可变 candidate/promotion contract;不得注入 120% 或其他默认数值,也不得提高、降低或替换已经明确的门槛。没有明确数值合同时只做策略有效性、数据时序和风险审计,禁止声称收益已经达标;存在冻结合同时必须逐项按原运算符验证,不能只看总收益。"; 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_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 = "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 { pub fn built_in_strategy_manual() -> StrategyAiManual {
StrategyAiManual { StrategyAiManual {
@@ -250,7 +250,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
}, },
ManualSection { ManualSection {
title: "risk.policy / risk.blacklist".to_string(), 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 { ManualSection {
title: "corporate_actions.dividend_reinvestment".to_string(), title: "corporate_actions.dividend_reinvestment".to_string(),