feat(frama): add Fractal Adaptive Moving Average

Ehlers' FRAMA adapts its smoothing constant to the fractal dimension of
the recent window: tight tracking in trends, heavy smoothing in chop.
Uses the close-only variant where max/min over each window half drive
the dimension estimate. Period must be even (default 16).

Reference: Ehlers, Fractal Adaptive Moving Average, 2005.

Touchpoints:
- crates/wickra-core: frama.rs + mod.rs + lib.rs re-export
- bindings/python: PyFrama + __init__.py + test_new_indicators +
  test_known_values reference (constant series + uptrend tracking)
- bindings/node: FramaNode (scalar macro) + index.d.ts/index.js +
  indicators.test.js factory + reference value
- bindings/wasm: wasm_scalar_indicator! macro
- fuzz: indicator_update target covers Frama(16)
- crates/wickra/benches: bench_scalar entry
- README + CHANGELOG: Moving Averages row + Unreleased entry
This commit is contained in:
kingchenc
2026-05-24 12:31:34 +02:00
parent 3287146f44
commit d37fbd10f6
14 changed files with 307 additions and 13 deletions
@@ -39,6 +39,7 @@ const scalarFactories = {
KAMA: () => new wickra.KAMA(10, 2, 30),
ALMA: () => new wickra.ALMA(9, 0.85, 6.0),
McGinleyDynamic: () => new wickra.McGinleyDynamic(10),
FRAMA: () => new wickra.FRAMA(16),
SMMA: () => new wickra.SMMA(14),
TRIMA: () => new wickra.TRIMA(20),
ZLEMA: () => new wickra.ZLEMA(14),
@@ -261,6 +262,11 @@ test('LinRegAngle of a unit-slope series is 45 degrees', () => {
assert.ok(Math.abs(out[4] - 45) < 1e-9);
});
test('FRAMA pure uptrend hugs the latest close', () => {
const out = new wickra.FRAMA(4).batch([1, 2, 3, 4, 5, 6, 7, 8]);
assert.ok(Math.abs(out[out.length - 1] - 8) < 0.05);
});
test('McGinleyDynamic(3) seeds with SMA and recurses on the next price', () => {
// Seed = SMA([10, 20, 30]) = 20. On 40: ratio = 2, divisor = 0.6*3*16 = 28.8.
const out = new wickra.McGinleyDynamic(3).batch([10, 20, 30, 40]);