扩展指标

This commit is contained in:
2026-07-09 05:08:16 +08:00
commit 308c46ab9a
537 changed files with 152299 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
[package]
name = "ferro_ta_fuzz"
version = "0.0.1"
edition = "2021"
publish = false
# Exclude from the root workspace so cargo doesn't reject it as an unlisted member
[workspace]
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
ferro_ta_core = { path = "../crates/ferro_ta_core" }
[[bin]]
name = "fuzz_sma"
path = "fuzz_targets/fuzz_sma.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_rsi"
path = "fuzz_targets/fuzz_rsi.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_ema"
path = "fuzz_targets/fuzz_ema.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_bbands"
path = "fuzz_targets/fuzz_bbands.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_macd"
path = "fuzz_targets/fuzz_macd.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_atr"
path = "fuzz_targets/fuzz_atr.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_stoch"
path = "fuzz_targets/fuzz_stoch.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_mfi"
path = "fuzz_targets/fuzz_mfi.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_wma"
path = "fuzz_targets/fuzz_wma.rs"
test = false
doc = false
bench = false
[profile.release]
debug = 1
@@ -0,0 +1,48 @@
/*!
Fuzz target for `ferro_ta_core::volatility::atr`.
Verifies that ATR never panics, output length matches input, and all
finite values are non-negative (ATR is always >= 0).
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::volatility;
fuzz_target!(|data: &[u8]| {
if data.len() < 2 {
return;
}
let timeperiod = ((data[0] as usize) % 64) + 1;
// Need 3 f64s per bar (high, low, close)
let float_bytes = &data[1..];
let n_floats = float_bytes.len() / 8;
let n_bars = n_floats / 3;
if n_bars == 0 {
return;
}
let all_floats: Vec<f64> = (0..n_bars * 3)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
let high = &all_floats[..n_bars];
let low = &all_floats[n_bars..n_bars * 2];
let close = &all_floats[n_bars * 2..n_bars * 3];
let result = volatility::atr(high, low, close, timeperiod);
assert_eq!(result.len(), high.len(), "ATR output length mismatch");
// ATR values should be non-negative when finite
for (i, &v) in result.iter().enumerate() {
if v.is_finite() {
assert!(v >= 0.0, "ATR result[{i}] = {v} is negative");
}
}
});
@@ -0,0 +1,60 @@
/*!
Fuzz target for `ferro_ta_core::overlap::bbands`.
Verifies that BBANDS never panics and that the three output vectors
(upper, middle, lower) always have the same length as the input.
When finite, upper >= middle >= lower must hold.
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::overlap;
fuzz_target!(|data: &[u8]| {
if data.len() < 3 {
return;
}
let timeperiod = ((data[0] as usize) % 64) + 1;
// Use second byte for deviation multipliers (1.0 - 4.0 range)
let nbdevup = 1.0 + (data[1] as f64 / 255.0) * 3.0;
let nbdevdn = 1.0 + (data[2] as f64 / 255.0) * 3.0;
let float_bytes = &data[3..];
let n_floats = float_bytes.len() / 8;
if n_floats == 0 {
return;
}
let close: Vec<f64> = (0..n_floats)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
let (upper, middle, lower) = overlap::bbands(&close, timeperiod, nbdevup, nbdevdn);
assert_eq!(upper.len(), close.len(), "BBANDS upper length mismatch");
assert_eq!(middle.len(), close.len(), "BBANDS middle length mismatch");
assert_eq!(lower.len(), close.len(), "BBANDS lower length mismatch");
// When all three are finite, upper >= middle >= lower
for i in 0..close.len() {
if upper[i].is_finite() && middle[i].is_finite() && lower[i].is_finite() {
assert!(
upper[i] >= middle[i],
"BBANDS upper[{i}] ({}) < middle[{i}] ({})",
upper[i],
middle[i]
);
assert!(
middle[i] >= lower[i],
"BBANDS middle[{i}] ({}) < lower[{i}] ({})",
middle[i],
lower[i]
);
}
}
});
@@ -0,0 +1,35 @@
/*!
Fuzz target for `ferro_ta_core::overlap::ema`.
Verifies that EMA never panics for any input and that the output length
always matches the input length.
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::overlap;
fuzz_target!(|data: &[u8]| {
if data.len() < 2 {
return;
}
let timeperiod = ((data[0] as usize) % 64) + 1;
let float_bytes = &data[1..];
let n_floats = float_bytes.len() / 8;
if n_floats == 0 {
return;
}
let close: Vec<f64> = (0..n_floats)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
let result = overlap::ema(&close, timeperiod);
assert_eq!(result.len(), close.len(), "EMA output length mismatch");
});
@@ -0,0 +1,41 @@
/*!
Fuzz target for `ferro_ta_core::overlap::macd`.
Verifies that MACD never panics and that all three output vectors
(macd, signal, histogram) match the input length.
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::overlap;
fuzz_target!(|data: &[u8]| {
if data.len() < 4 {
return;
}
// Extract periods from first 3 bytes (1-64 range each)
let fastperiod = ((data[0] as usize) % 32) + 1;
let slowperiod = ((data[1] as usize) % 32) + fastperiod + 1; // slow > fast
let signalperiod = ((data[2] as usize) % 32) + 1;
let float_bytes = &data[3..];
let n_floats = float_bytes.len() / 8;
if n_floats == 0 {
return;
}
let close: Vec<f64> = (0..n_floats)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
let (macd, signal, hist) = overlap::macd(&close, fastperiod, slowperiod, signalperiod);
assert_eq!(macd.len(), close.len(), "MACD line length mismatch");
assert_eq!(signal.len(), close.len(), "MACD signal length mismatch");
assert_eq!(hist.len(), close.len(), "MACD histogram length mismatch");
});
@@ -0,0 +1,51 @@
/*!
Fuzz target for `ferro_ta_core::volume::mfi`.
Verifies that MFI never panics, output length matches input, and finite
values lie in [0, 100].
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::volume;
fuzz_target!(|data: &[u8]| {
if data.len() < 2 {
return;
}
let timeperiod = ((data[0] as usize) % 64) + 1;
// Need 4 f64s per bar (high, low, close, volume)
let float_bytes = &data[1..];
let n_floats = float_bytes.len() / 8;
let n_bars = n_floats / 4;
if n_bars == 0 {
return;
}
let all_floats: Vec<f64> = (0..n_bars * 4)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
let high = &all_floats[..n_bars];
let low = &all_floats[n_bars..n_bars * 2];
let close = &all_floats[n_bars * 2..n_bars * 3];
let vol = &all_floats[n_bars * 3..n_bars * 4];
let result = volume::mfi(high, low, close, vol, timeperiod);
assert_eq!(result.len(), high.len(), "MFI output length mismatch");
for (i, &v) in result.iter().enumerate() {
if v.is_finite() {
assert!(
v >= 0.0 && v <= 100.0,
"MFI result[{i}] = {v} is out of [0, 100]"
);
}
}
});
@@ -0,0 +1,52 @@
/*!
Fuzz target for `ferro_ta_core::momentum::rsi`.
Generates arbitrary f64 slices (via raw bytes) and arbitrary timeperiods,
verifying that RSI never panics and that all finite output values lie in
the range [0, 100].
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::momentum;
fuzz_target!(|data: &[u8]| {
// Need at least 1 byte for timeperiod + 8 bytes for one f64
if data.len() < 2 {
return;
}
// Extract timeperiod from first byte (1-64)
let timeperiod = ((data[0] as usize) % 64) + 1;
// Interpret remaining bytes as f64 values
let float_bytes = &data[1..];
let n_floats = float_bytes.len() / 8;
if n_floats == 0 {
return;
}
let close: Vec<f64> = (0..n_floats)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
// Must not panic
let result = momentum::rsi(&close, timeperiod);
// Result length must match input
assert_eq!(result.len(), close.len(), "RSI output length mismatch");
// All finite output values must be in [0, 100]
for (i, &v) in result.iter().enumerate() {
if v.is_finite() {
assert!(
v >= 0.0 && v <= 100.0,
"RSI result[{i}] = {v} is out of [0, 100]"
);
}
}
});
@@ -0,0 +1,51 @@
/*!
Fuzz target for `ferro_ta_core::overlap::sma`.
The fuzzer generates arbitrary byte sequences and interprets them as
`f64` values plus a `timeperiod`. The invariant under test is that the
function **never panics** for any input — it may return `NaN`, `Inf`, or
an all-NaN slice, but it must not crash.
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::overlap;
fuzz_target!(|data: &[u8]| {
// Need at least 1 byte for timeperiod + 8 bytes for one f64
if data.len() < 2 {
return;
}
// Extract timeperiod from first byte (1-64 to keep runs fast)
let timeperiod = ((data[0] as usize) % 64) + 1;
// Interpret remaining bytes as f64 values (skip incomplete trailing bytes)
let float_bytes = &data[1..];
let n_floats = float_bytes.len() / 8;
if n_floats == 0 {
return;
}
let close: Vec<f64> = (0..n_floats)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
// Must not panic for any input
let result = overlap::sma(&close, timeperiod);
// Result length must match input length
assert_eq!(result.len(), close.len(), "SMA output length mismatch");
// The first (timeperiod - 1) values must be NaN
for i in 0..(timeperiod.min(close.len()).saturating_sub(1)) {
assert!(
result[i].is_nan(),
"SMA result[{i}] should be NaN (warm-up period)"
);
}
});
@@ -0,0 +1,63 @@
/*!
Fuzz target for `ferro_ta_core::momentum::stoch`.
Verifies that STOCH never panics, output lengths match, and finite
values lie in [0, 100].
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::momentum;
fuzz_target!(|data: &[u8]| {
if data.len() < 4 {
return;
}
let fastk_period = ((data[0] as usize) % 32) + 1;
let slowk_period = ((data[1] as usize) % 16) + 1;
let slowd_period = ((data[2] as usize) % 16) + 1;
// Need 3 f64s per bar (high, low, close)
let float_bytes = &data[3..];
let n_floats = float_bytes.len() / 8;
let n_bars = n_floats / 3;
if n_bars == 0 {
return;
}
let all_floats: Vec<f64> = (0..n_bars * 3)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
let high = &all_floats[..n_bars];
let low = &all_floats[n_bars..n_bars * 2];
let close = &all_floats[n_bars * 2..n_bars * 3];
let (slowk, slowd) = momentum::stoch(high, low, close, fastk_period, slowk_period, slowd_period);
assert_eq!(slowk.len(), high.len(), "STOCH slowk length mismatch");
assert_eq!(slowd.len(), high.len(), "STOCH slowd length mismatch");
// Finite values should be in [0, 100]
for (i, &v) in slowk.iter().enumerate() {
if v.is_finite() {
assert!(
v >= 0.0 && v <= 100.0,
"STOCH slowk[{i}] = {v} is out of [0, 100]"
);
}
}
for (i, &v) in slowd.iter().enumerate() {
if v.is_finite() {
assert!(
v >= 0.0 && v <= 100.0,
"STOCH slowd[{i}] = {v} is out of [0, 100]"
);
}
}
});
@@ -0,0 +1,35 @@
/*!
Fuzz target for `ferro_ta_core::overlap::wma`.
Verifies that WMA never panics and that the output length always
matches the input length.
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use ferro_ta_core::overlap;
fuzz_target!(|data: &[u8]| {
if data.len() < 2 {
return;
}
let timeperiod = ((data[0] as usize) % 64) + 1;
let float_bytes = &data[1..];
let n_floats = float_bytes.len() / 8;
if n_floats == 0 {
return;
}
let close: Vec<f64> = (0..n_floats)
.map(|i| {
let chunk: [u8; 8] = float_bytes[i * 8..(i + 1) * 8].try_into().unwrap();
f64::from_le_bytes(chunk)
})
.collect();
let result = overlap::wma(&close, timeperiod);
assert_eq!(result.len(), close.len(), "WMA output length mismatch");
});