修复恒定小数价格累加误差产生虚假均线信号
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}));
|
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(
|
||||||
.iter()
|
prices[len - 1 - spec.n("volume_window")..len - 1]
|
||||||
.map(|b| b.4)
|
.iter()
|
||||||
.sum::<f64>()
|
.map(|b| b.4),
|
||||||
/ 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");
|
||||||
|
|||||||
Reference in New Issue
Block a user