Compare commits

...

27 Commits

Author SHA1 Message Date
boris d84fad721d Merge remote-tracking branch 'origin/main'
# Conflicts:
#	crates/fidc-core/src/events.rs
#	crates/fidc-core/src/lib.rs
2026-08-27 09:07:51 +08:00
boris ce4d17c293 规范化策略规格别名避免运行时重复字段 2026-08-27 09:06:37 +08:00
boris 801a27dace 让显式动作继承运行调度 2026-08-27 08:37:33 +08:00
boris 9b00a0777a 增加类型化开放订单改单能力 2026-08-27 08:08:39 +08:00
boris 6ee1835ca5 记录分钟成交精确时间 2026-08-27 02:47:19 +08:00
boris 8d7bb60c30 共享回测分钟报价索引 2026-08-27 01:38:56 +08:00
boris 01d1e5073d 修正跨调度撮合流动性重复消费 2026-08-27 00:57:34 +08:00
boris 78c5b72ed3 完善统一策略规格元数据字段 2026-08-26 23:39:11 +08:00
boris 50690540cd 导出统一基准调仓规格类型 2026-08-26 23:29:18 +08:00
boris 7f66bcfff7 统一基准与调仓规格字段 2026-08-26 23:26:24 +08:00
boris 68bff3f661 统一引擎执行规格字段 2026-08-26 23:21:16 +08:00
boris e867aea3b1 统一执行参数规格字段 2026-08-26 23:08:09 +08:00
boris 723ce93623 导出统一风控策略规格类型 2026-08-26 23:02:04 +08:00
boris 48acd66c30 修正风控别名测试断言 2026-08-26 22:59:52 +08:00
boris 3926ac2985 统一风控策略执行别名 2026-08-26 22:56:50 +08:00
boris 255fc2b878 限定订单有效期运行模式能力 2026-08-26 22:14:18 +08:00
boris 8e238f9131 实现类型化订单有效期合同 2026-08-26 21:00:22 +08:00
boris bc0f5f6089 修正回测部分成交终态合同 2026-08-26 18:28:42 +08:00
boris 8303a6477b 允许运行态追加结算交易日历 2026-08-26 14:28:20 +08:00
boris 8fcf34b3a9 保留延迟资金到账表达式 2026-08-26 13:59:59 +08:00
boris 9399a61b46 增加回测结束边界状态审计 2026-08-26 13:37:36 +08:00
boris 82604481b6 下推分钟报价订阅过滤 2026-08-26 07:58:55 +08:00
Boris d3bacffd8b 减少每日诊断文本临时分配 2026-08-26 06:43:59 +08:00
Boris 5929fedf91 跳过零管理费无效集合复制 2026-08-26 06:39:13 +08:00
Boris 0867655d85 减少文本因子读取临时分配 2026-08-26 05:08:34 +08:00
boris 77622e164c 减少日频数据读取临时分配 2026-08-26 04:48:22 +08:00
boris bf2e3af4eb 优化数值表达式helper执行路径 2026-08-26 03:27:00 +08:00
2 changed files with 175 additions and 1 deletions
-1
View File
@@ -516,7 +516,6 @@ mod tests {
"2025-01-02 10:18:03"
);
}
#[test]
fn process_event_business_lifecycle_filter_keeps_audit_events_only() {
assert!(ProcessEventKind::OrderUpdateReject.is_business_lifecycle());
@@ -615,6 +615,142 @@ fn normalize_risk_policy_aliases_in_value(value: &mut Value) -> Result<(), Strin
Ok(())
}
/// Serde aliases are intentionally strict: a JSON object containing both a
/// camelCase field and its snake_case alias is reported as a duplicate field.
/// Runtime payloads are assembled by several services, so the same strategy
/// contract can legitimately arrive with both spellings. Canonicalise those
/// pairs once at the boundary, while rejecting conflicting values instead of
/// silently choosing one.
fn normalize_strategy_aliases_in_value(value: &mut Value) -> Result<(), String> {
normalize_strategy_aliases_in_value_inner(value, false)
}
fn normalize_strategy_aliases_in_value_inner(
value: &mut Value,
in_risk_policy: bool,
) -> Result<(), String> {
match value {
Value::Object(object) => {
if !in_risk_policy {
normalize_strategy_object_aliases(object)?;
}
for (key, child) in object.iter_mut() {
normalize_strategy_aliases_in_value_inner(
child,
in_risk_policy || matches!(key.as_str(), "riskPolicy" | "risk_policy"),
)?;
}
}
Value::Array(items) => {
for item in items {
normalize_strategy_aliases_in_value_inner(item, in_risk_policy)?;
}
}
_ => {}
}
Ok(())
}
const STRATEGY_ALIAS_GROUPS: &[(&str, &[&str])] = &[
("strategyId", &["strategy_id"]),
("tradeTimes", &["trade_times"]),
("signalSymbol", &["signal_symbol"]),
("engineConfig", &["engine_config"]),
("runtimeExpressions", &["runtime_expressions"]),
("rebalanceSchedule", &["rebalance_schedule"]),
("skipWindows", &["skip_windows"]),
("dynamicRange", &["dynamic_range"]),
("stockMaFilter", &["stock_ma_filter"]),
("indexThrottle", &["index_throttle"]),
("benchmarkSymbol", &["benchmark_symbol"]),
("matchingType", &["matching_type"]),
("slippageModel", &["slippage_model"]),
("slippageValue", &["slippage_value"]),
("slippageImpactCoefficient", &["slippage_impact_coefficient"]),
("slippageVolatilityCoefficient", &["slippage_volatility_coefficient"]),
(
"slippageMaxValue",
&["slippage_max_value", "slippage_max_rate"],
),
("commissionRate", &["commission_rate"]),
(
"minimumCommission",
&["minimum_commission", "min_commission", "minCommission"],
),
("transferFeeRate", &["transfer_fee_rate", "transferFeeRate"]),
("stampTaxRate", &["stamp_tax_rate"]),
("stampTaxRateBeforeChange", &["stamp_tax_rate_before_change"]),
("stampTaxRateAfterChange", &["stamp_tax_rate_after_change"]),
("stampTaxChangeDate", &["stamp_tax_change_date"]),
("volumeLimit", &["volume_limit"]),
("volumeLimitEnabled", &["volume_limit_enabled"]),
("liquidityLimit", &["liquidity_limit"]),
("liquidityLimitEnabled", &["liquidity_limit_enabled"]),
("volumePercent", &["volume_percent"]),
("riskPolicy", &["risk_policy"]),
("strictValueBudget", &["strict_value_budget"]),
("rebalanceCashMode", &["rebalance_cash_mode"]),
(
"sellThenBuyDelaySlippageRate",
&["sell_then_buy_delay_slippage_rate"],
),
(
"maxHoldingDays",
&["max_hold_days", "max_holding_days", "maxHoldDays"],
),
(
"referencePriceMode",
&["reference_price_mode", "stop_take_reference_price_mode"],
),
];
fn strategy_alias_values_semantically_equal(left: &Value, right: &Value) -> bool {
if left == right {
return true;
}
match (left, right) {
(Value::String(left), Value::String(right)) => left.trim() == right.trim(),
(Value::String(left), Value::Number(right))
| (Value::Number(right), Value::String(left)) => left
.trim()
.parse::<f64>()
.ok()
.zip(right.as_f64())
.is_some_and(|(left, right)| (left - right).abs() < 1e-12),
_ => false,
}
}
fn normalize_strategy_object_aliases(
object: &mut serde_json::Map<String, Value>,
) -> Result<(), String> {
for (canonical, aliases) in STRATEGY_ALIAS_GROUPS {
let mut values = Vec::new();
if let Some(value) = object.remove(*canonical).filter(|value| !value.is_null()) {
values.push(value);
}
for alias in *aliases {
if let Some(value) = object.remove(*alias).filter(|value| !value.is_null()) {
values.push(value);
}
}
let Some(first) = values.first().cloned() else {
continue;
};
if values
.iter()
.skip(1)
.any(|value| !strategy_alias_values_semantically_equal(&first, value))
{
return Err(format!(
"strategy field {canonical} has conflicting alias values"
));
}
object.insert((*canonical).to_string(), first);
}
Ok(())
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DynamicRangeConfig {
@@ -907,6 +1043,7 @@ pub fn platform_expr_config_from_value(
}
reject_removed_compatibility_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)?;
let spec = serde_json::from_value::<StrategyRuntimeSpec>(value)?;
platform_expr_config_from_spec(strategy_id, signal_symbol, Some(&spec))
@@ -3384,6 +3521,44 @@ mod tests {
);
}
#[test]
fn canonicalizes_duplicate_execution_aliases_without_changing_strategy_intent() {
let spec = serde_json::json!({
"execution": {
"matchingType": "next_bar_open",
"matching_type": "next_bar_open",
"slippageModel": "price_ratio",
"slippage_model": "price_ratio",
"slippageValue": 0.001,
"slippage_value": "0.001"
}
});
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert_eq!(cfg.matching_type, MatchingType::NextBarOpen);
assert_eq!(cfg.slippage_model, SlippageModel::PriceRatio(0.001));
}
#[test]
fn rejects_conflicting_non_policy_strategy_aliases() {
let spec = serde_json::json!({
"execution": {
"matchingType": "next_bar_open",
"matching_type": "current_bar_close"
}
});
let error = platform_expr_config_from_value("", "", &spec)
.expect_err("conflicting execution aliases must fail");
assert!(
error
.to_string()
.contains("strategy field matchingType has conflicting alias values")
);
}
#[test]
fn parses_execution_slippage_overrides_into_platform_config() {
let spec = serde_json::json!({