修复恒定小数价格累加误差产生虚假均线信号
This commit is contained in:
@@ -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}));
|
||||
}
|
||||
|
||||
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.
|
||||
pub fn evaluate(
|
||||
spec: &PatternSpec,
|
||||
@@ -233,16 +247,8 @@ pub fn evaluate(
|
||||
let mut score = None;
|
||||
match spec.template.as_str() {
|
||||
"strength" => {
|
||||
let fast = prices[len - spec.n("fast_window")..]
|
||||
.iter()
|
||||
.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 fast = mean(prices[len - spec.n("fast_window")..].iter().map(|b| b.3))?;
|
||||
let slow = mean(prices[len - spec.n("slow_window")..].iter().map(|b| b.3))?;
|
||||
let momentum = c / prices[len - 1 - spec.n("momentum_window")].3 - 1.0;
|
||||
score = Some(momentum);
|
||||
result.values["momentum"] = json!(momentum);
|
||||
@@ -256,11 +262,11 @@ pub fn evaluate(
|
||||
.iter()
|
||||
.map(|b| b.1)
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let avg = prices[len - 1 - spec.n("volume_window")..len - 1]
|
||||
.iter()
|
||||
.map(|b| b.4)
|
||||
.sum::<f64>()
|
||||
/ spec.n("volume_window") as f64;
|
||||
let avg = mean(
|
||||
prices[len - 1 - spec.n("volume_window")..len - 1]
|
||||
.iter()
|
||||
.map(|b| b.4),
|
||||
)?;
|
||||
if avg <= 0.0 {
|
||||
return Err(format!(
|
||||
"pattern_input_invalid: symbol={}, reason=zero_reference_volume",
|
||||
@@ -324,11 +330,7 @@ pub fn evaluate(
|
||||
);
|
||||
}
|
||||
"ma_below" => {
|
||||
let avg = prices[len - spec.n("ma_window")..]
|
||||
.iter()
|
||||
.map(|b| b.3)
|
||||
.sum::<f64>()
|
||||
/ spec.n("ma_window") as f64;
|
||||
let avg = mean(prices[len - spec.n("ma_window")..].iter().map(|b| b.3))?;
|
||||
score = Some(avg / c - 1.0);
|
||||
result.values["ma"] = json!(avg);
|
||||
check(&mut result.checks, "收盘低于均线", c, "<", avg);
|
||||
@@ -562,6 +564,27 @@ mod tests {
|
||||
assert_eq!(a.score, b.score);
|
||||
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]
|
||||
fn daily_patterns_no_missing_data_fallback() {
|
||||
let (spec, days, mut series) = fixture("strength");
|
||||
|
||||
Reference in New Issue
Block a user