feat(cfo): add Chande Forecast Oscillator
100 * (close - LinReg(close, period)) / close. Positive when close overshoots the linear forecast, negative when it undershoots. Holds the previous value if the close is zero (percentage form undefined). Single param period (default 14). Touchpoints: cfo.rs + mod.rs + lib.rs re-export, PyCfo + __init__.py + test_new_indicators SCALAR + test_known_values linear reference, CfoNode + index.d.ts/index.js + indicators.test.js factory + reference, WasmCfo via scalar macro, scalar-fuzz target, README + CHANGELOG.
This commit is contained in:
@@ -58,6 +58,7 @@ from ._wickra import (
|
||||
UltimateOscillator,
|
||||
APO,
|
||||
AwesomeOscillatorHistogram,
|
||||
CFO,
|
||||
PPO,
|
||||
DPO,
|
||||
Coppock,
|
||||
@@ -141,6 +142,7 @@ __all__ = [
|
||||
"UltimateOscillator",
|
||||
"APO",
|
||||
"AwesomeOscillatorHistogram",
|
||||
"CFO",
|
||||
"PPO",
|
||||
"DPO",
|
||||
"Coppock",
|
||||
|
||||
@@ -874,6 +874,54 @@ impl PyAoHist {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== CFO ==============================
|
||||
|
||||
#[pyclass(name = "CFO", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyCfo {
|
||||
inner: wc::Cfo,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCfo {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Cfo::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("CFO(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== APO ==============================
|
||||
|
||||
#[pyclass(name = "APO", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -4603,6 +4651,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyKama>()?;
|
||||
m.add_class::<PyApo>()?;
|
||||
m.add_class::<PyAoHist>()?;
|
||||
m.add_class::<PyCfo>()?;
|
||||
m.add_class::<PyCci>()?;
|
||||
m.add_class::<PyRoc>()?;
|
||||
m.add_class::<PyWilliamsR>()?;
|
||||
|
||||
@@ -76,6 +76,12 @@ def test_awesome_oscillator_histogram_flat_series_converges_to_zero():
|
||||
np.testing.assert_allclose(out[6:], 0.0, atol=1e-12)
|
||||
|
||||
|
||||
def test_cfo_perfect_linear_series_yields_zero():
|
||||
# LinReg of a perfectly linear series fits exactly, so CFO = 0 after warmup.
|
||||
out = ta.CFO(5).batch(np.arange(1.0, 21.0, dtype=np.float64) * 2.0)
|
||||
np.testing.assert_allclose(out[4:], 0.0, atol=1e-9)
|
||||
|
||||
|
||||
def test_apo_constant_series_converges_to_zero():
|
||||
# Both EMAs reproduce a constant exactly, so APO = 0 after warmup.
|
||||
out = ta.APO(3, 5).batch(np.full(30, 42.0, dtype=np.float64))
|
||||
|
||||
@@ -52,6 +52,7 @@ SCALAR = [
|
||||
(ta.StochRSI, (14, 14)),
|
||||
(ta.PPO, (12, 26)),
|
||||
(ta.APO, (12, 26)),
|
||||
(ta.CFO, (14,)),
|
||||
(ta.DPO, (20,)),
|
||||
(ta.Coppock, (14, 11, 10)),
|
||||
(ta.StdDev, (20,)),
|
||||
|
||||
Reference in New Issue
Block a user