修复恒定小数价格累加误差产生虚假均线信号

This commit is contained in:
boris
2026-09-09 07:03:55 +08:00
parent 3fe2da3ee0
commit 3b5a7cd318
+42 -19
View File
@@ -126,6 +126,20 @@ fn check(checks: &mut Vec<Value>, label: &str, actual: f64, operator: &str, thre
checks.push(json!({"label":label,"actual":actual,"operator":operator,"threshold":threshold,"passed":passed})); checks.push(json!({"label":label,"actual":actual,"operator":operator,"threshold":threshold,"passed":passed}));
} }
fn mean(mut values: impl ExactSizeIterator<Item = f64>) -> Result<f64, String> {
let count = values.len();
let first = values.next().ok_or("pattern_mean_empty")?;
// Center before summation so an unchanged decimal price stays exactly unchanged.
let result = first
+ values
.map(|value| (value - first) / count as f64)
.sum::<f64>();
if !result.is_finite() {
return Err("pattern_mean_nonfinite".into());
}
Ok(result)
}
/// No calendar compression, fill-forward prices or numerical substitutes. /// No calendar compression, fill-forward prices or numerical substitutes.
pub fn evaluate( pub fn evaluate(
spec: &PatternSpec, spec: &PatternSpec,
@@ -233,16 +247,8 @@ pub fn evaluate(
let mut score = None; let mut score = None;
match spec.template.as_str() { match spec.template.as_str() {
"strength" => { "strength" => {
let fast = prices[len - spec.n("fast_window")..] let fast = mean(prices[len - spec.n("fast_window")..].iter().map(|b| b.3))?;
.iter() let slow = mean(prices[len - spec.n("slow_window")..].iter().map(|b| b.3))?;
.map(|b| b.3)
.sum::<f64>()
/ spec.n("fast_window") as f64;
let slow = prices[len - spec.n("slow_window")..]
.iter()
.map(|b| b.3)
.sum::<f64>()
/ spec.n("slow_window") as f64;
let momentum = c / prices[len - 1 - spec.n("momentum_window")].3 - 1.0; let momentum = c / prices[len - 1 - spec.n("momentum_window")].3 - 1.0;
score = Some(momentum); score = Some(momentum);
result.values["momentum"] = json!(momentum); result.values["momentum"] = json!(momentum);
@@ -256,11 +262,11 @@ pub fn evaluate(
.iter() .iter()
.map(|b| b.1) .map(|b| b.1)
.fold(f64::NEG_INFINITY, f64::max); .fold(f64::NEG_INFINITY, f64::max);
let avg = prices[len - 1 - spec.n("volume_window")..len - 1] let avg = mean(
prices[len - 1 - spec.n("volume_window")..len - 1]
.iter() .iter()
.map(|b| b.4) .map(|b| b.4),
.sum::<f64>() )?;
/ spec.n("volume_window") as f64;
if avg <= 0.0 { if avg <= 0.0 {
return Err(format!( return Err(format!(
"pattern_input_invalid: symbol={}, reason=zero_reference_volume", "pattern_input_invalid: symbol={}, reason=zero_reference_volume",
@@ -324,11 +330,7 @@ pub fn evaluate(
); );
} }
"ma_below" => { "ma_below" => {
let avg = prices[len - spec.n("ma_window")..] let avg = mean(prices[len - spec.n("ma_window")..].iter().map(|b| b.3))?;
.iter()
.map(|b| b.3)
.sum::<f64>()
/ spec.n("ma_window") as f64;
score = Some(avg / c - 1.0); score = Some(avg / c - 1.0);
result.values["ma"] = json!(avg); result.values["ma"] = json!(avg);
check(&mut result.checks, "收盘低于均线", c, "<", avg); check(&mut result.checks, "收盘低于均线", c, "<", avg);
@@ -562,6 +564,27 @@ mod tests {
assert_eq!(a.score, b.score); assert_eq!(a.score, b.score);
assert_eq!(a.checks, b.checks); assert_eq!(a.checks, b.checks);
} }
#[test]
fn daily_patterns_flat_decimal_prices_do_not_create_a_sell_signal() {
let (mut spec, _, mut series) = fixture("strength");
spec.template = "ma_below".into();
spec.parameters = BTreeMap::from([("ma_window".into(), json!(60))]);
for bar in &mut series.bars {
bar.open = Some(10.1);
bar.high = Some(10.1);
bar.low = Some(10.1);
bar.close = Some(10.1);
}
let days = series.bars.iter().map(|bar| bar.date).collect::<Vec<_>>();
let result = evaluate(&spec, &days, &series).unwrap();
assert!(
!result.matched,
"unchanged decimal prices must not trigger a below-MA sell: {:?}",
result.checks
);
assert_eq!(result.values["ma"], 10.1);
}
#[test] #[test]
fn daily_patterns_no_missing_data_fallback() { fn daily_patterns_no_missing_data_fallback() {
let (spec, days, mut series) = fixture("strength"); let (spec, days, mut series) = fixture("strength");