修正平台策略费用和表达式口径

This commit is contained in:
boris
2026-06-15 18:03:21 +08:00
parent 1c31fa80d2
commit 5181d0e403
2 changed files with 152 additions and 2 deletions
+145 -2
View File
@@ -920,7 +920,9 @@ impl PlatformExprStrategy {
if let Some(value) = self.config.commission_rate {
model.commission_rate = value;
}
if let Some(value) = self.config.minimum_commission {
if let Some(value) = self.config.minimum_commission
&& (value > 0.0 || !self.config.aiquant_transaction_cost)
{
model.minimum_commission = value;
}
if let Some(value) = self.config.stamp_tax_rate_before_change {
@@ -2893,7 +2895,9 @@ impl PlatformExprStrategy {
}
fn normalize_expr(expr: &str) -> String {
Self::rewrite_ternary(&Self::normalize_runtime_field_aliases(expr.trim()))
let expr = Self::normalize_runtime_field_aliases(expr.trim());
let expr = Self::normalize_python_numeric_division(&expr);
Self::rewrite_ternary(&expr)
}
fn compact_expr(expr: &str) -> String {
@@ -2994,9 +2998,125 @@ impl PlatformExprStrategy {
}
let rhs = Self::normalize_runtime_field_aliases(rhs);
let rhs = Self::normalize_prelude_runtime_helpers(&rhs);
let rhs = Self::normalize_python_numeric_division(&rhs);
format!("{lhs} {}{suffix}", Self::rewrite_ternary(&rhs))
}
fn normalize_python_numeric_division(expr: &str) -> String {
let bytes = expr.as_bytes();
let mut insertion_points = BTreeSet::<usize>::new();
let mut cursor = 0usize;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut escaped = false;
while cursor < bytes.len() {
let ch = bytes[cursor] as char;
if escaped {
escaped = false;
cursor += 1;
continue;
}
if ch == '\\' && (in_single_quote || in_double_quote) {
escaped = true;
cursor += 1;
continue;
}
if ch == '\'' && !in_double_quote {
in_single_quote = !in_single_quote;
cursor += 1;
continue;
}
if ch == '"' && !in_single_quote {
in_double_quote = !in_double_quote;
cursor += 1;
continue;
}
if ch == '/' && !in_single_quote && !in_double_quote {
if let (Some((_, lhs_end)), Some((_, rhs_end))) = (
Self::integer_literal_before(expr, cursor),
Self::integer_literal_after(expr, cursor),
) {
insertion_points.insert(lhs_end);
insertion_points.insert(rhs_end);
}
}
cursor += 1;
}
if insertion_points.is_empty() {
return expr.to_string();
}
let mut output = String::with_capacity(expr.len() + insertion_points.len() * 2);
for (idx, ch) in expr.char_indices() {
if insertion_points.contains(&idx) {
output.push_str(".0");
}
output.push(ch);
}
if insertion_points.contains(&expr.len()) {
output.push_str(".0");
}
output
}
fn integer_literal_before(expr: &str, slash_idx: usize) -> Option<(usize, usize)> {
let bytes = expr.as_bytes();
let mut end = slash_idx;
while end > 0 && bytes[end - 1].is_ascii_whitespace() {
end -= 1;
}
let mut start = end;
while start > 0 && bytes[start - 1].is_ascii_digit() {
start -= 1;
}
if start == end {
return None;
}
if start > 0 {
let prev = bytes[start - 1];
if prev == b'.' || prev == b'_' || prev.is_ascii_alphanumeric() {
return None;
}
}
if end < bytes.len() {
let next = bytes[end];
if next == b'.' || next == b'_' || next.is_ascii_alphanumeric() {
return None;
}
}
Some((start, end))
}
fn integer_literal_after(expr: &str, slash_idx: usize) -> Option<(usize, usize)> {
let bytes = expr.as_bytes();
let mut start = slash_idx + 1;
while start < bytes.len() && bytes[start].is_ascii_whitespace() {
start += 1;
}
let mut end = start;
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
if start == end {
return None;
}
if start > 0 {
let prev = bytes[start - 1];
if prev == b'.' || prev == b'_' || prev.is_ascii_alphanumeric() {
return None;
}
}
if end < bytes.len() {
let next = bytes[end];
if next == b'.' || next == b'_' || next.is_ascii_alphanumeric() {
return None;
}
}
Some((start, end))
}
fn normalize_runtime_field_aliases(expr: &str) -> String {
expr.replace("signal.close", "signal_close")
.replace("signal.open", "signal_open")
@@ -6870,6 +6990,17 @@ mod tests {
assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9);
}
#[test]
fn platform_expr_aiquant_cost_model_ignores_zero_minimum_commission_override() {
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.aiquant_transaction_cost = true;
cfg.commission_rate = Some(0.0003);
cfg.minimum_commission = Some(0.0);
let strategy = PlatformExprStrategy::new(cfg);
assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9);
}
#[test]
fn platform_expr_rewrites_prelude_assignment_ternary() {
let prelude = "let cap_low = sig_close <= 0 ? 7 : max(5, min(cap_low_raw, 70));";
@@ -6887,6 +7018,18 @@ mod tests {
assert!(rewritten.contains("let sig_close = signal_close;"));
}
#[test]
fn platform_expr_normalizes_python_style_integer_division() {
let prelude = "let xs = 4 / 500;\nlet label = \"4 / 500\";";
let rewritten = PlatformExprStrategy::normalize_prelude_for_eval(prelude);
assert!(rewritten.contains("let xs = 4.0 / 500.0;"));
assert!(rewritten.contains("let label = \"4 / 500\";"));
let expr =
PlatformExprStrategy::normalize_expr("round((signal_close - 2000) * 4 / 500 + 3)");
assert!(expr.contains("4.0 / 500.0"));
}
#[test]
fn platform_expr_expands_day_factor_in_prelude() {
let prelude = concat!(
@@ -992,6 +992,13 @@ pub fn platform_expr_config_from_spec(
execution.stamp_tax_rate_after_change,
);
}
if cfg.aiquant_transaction_cost
&& cfg
.minimum_commission
.is_some_and(|value| value.is_finite() && value <= 0.0)
{
cfg.minimum_commission = None;
}
cfg
}