Fractal Adaptive Moving Average (FRAMA)

Parameters: window = 10 | sc = 300 | fc = 1

Overview

The Fractal Adaptive Moving Average dynamically adjusts its smoothing factor by measuring the fractal dimension of price movement, becoming more responsive during trending periods and more stable during consolidations. FRAMA calculates the roughness of price action using fractal geometry concepts, where smooth trends exhibit lower fractal dimensions near 1 while choppy markets approach dimension 2, then uses this measurement to modify the exponential smoothing constant in real time. During strong directional moves, the fractal dimension drops and FRAMA tracks price closely like a fast moving average, capturing trend momentum without significant lag. Conversely, when markets enter sideways phases with high fractal dimension, FRAMA automatically increases smoothing to filter out noise, behaving more like a slow moving average to avoid whipsaws. Traders value FRAMA for its intelligent adaptation that eliminates the compromise between responsiveness and stability inherent in fixed period averages. The indicator particularly excels in volatile markets where it maintains trend following capabilities while automatically adjusting to reduce false signals during choppy consolidations that plague traditional moving averages.

Implementation Examples

Compute FRAMA from slices or candles:

use vectorta::indicators::moving_averages::frama::{frama, FramaInput, FramaParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using explicit high/low/close slices
let high = vec![/* ... */];
let low = vec![/* ... */];
let close = vec![/* ... */];
let params = FramaParams { window: Some(10), sc: Some(300), fc: Some(1) };
let input = FramaInput::from_slices(&high, &low, &close, params);
let result = frama(&input)?;

// Using Candles with default parameters (window=10, sc=300, fc=1)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = FramaInput::with_default_candles(&candles);
let result = frama(&input)?;

// Access the FRAMA values
for value in result.values {
    println!("FRAMA: {}", value);
}

API Reference

Input Methods
// From high/low/close slices
FramaInput::from_slices(&[f64], &[f64], &[f64], FramaParams) -> FramaInput

// From candles (uses high/low/close; close as seed source)
FramaInput::from_candles(&Candles, FramaParams) -> FramaInput

// Candles with default params (window=10, sc=300, fc=1)
FramaInput::with_default_candles(&Candles) -> FramaInput
Parameters Structure
pub struct FramaParams {
    pub window: Option<usize>, // Default: 10 (odd rounded up to even)
    pub sc: Option<usize>,     // Default: 300
    pub fc: Option<usize>,     // Default: 1
}
Output Structure
pub struct FramaOutput {
    pub values: Vec<f64>, // FRAMA values (seeded SMA then adaptive updates)
}
Validation, Warmup & NaNs
  • window > 0; odd window is evenized internally for computation.
  • Uses first index where high, low, and close are all finite; prior outputs are NaN.
  • Requires at least even(window) valid points after the first finite triple; else FramaError::NotEnoughValidData.
  • Seed at warm = first + even(window) - 1 is the SMA of close over the evenized window.
  • After warmup, if any of high/low/close at t is NaN, FRAMA holds previous value (no NaN propagation).
Error Handling
#[derive(Debug, Error)]
pub enum FramaError {
    #[error("frama: Input data slice is empty.")] EmptyInputData,
    #[error("frama: Mismatched slice lengths: high={high}, low={low}, close={close}")]
    MismatchedInputLength { high: usize, low: usize, close: usize },
    #[error("frama: All values are NaN.")] AllValuesNaN,
    #[error("frama: Invalid window: window = {window}, data length = {data_len}")]
    InvalidWindow { window: usize, data_len: usize },
    #[error("frama: Not enough valid data: needed = {needed}, valid = {valid}")]
    NotEnoughValidData { needed: usize, valid: usize },
}

// Example usage
match frama(&input) {
    Ok(out) => println!("{} values", out.values.len()),
    Err(FramaError::InvalidWindow { window, data_len }) =>
        eprintln!("Invalid window={} for len={}", window, data_len),
    Err(FramaError::NotEnoughValidData { needed, valid }) =>
        eprintln!("Need {} valid points, only {}", needed, valid),
    Err(e) => eprintln!("frama error: {}", e),
}

Python Bindings

Basic Usage

Compute FRAMA from NumPy arrays (H/L/C):

import numpy as np
from vectorta import frama

high = np.array([...], dtype=np.float64)
low = np.array([...], dtype=np.float64)
close = np.array([...], dtype=np.float64)

# Defaults: window=10, sc=300, fc=1
vals = frama(high, low, close, window=10, sc=300, fc=1, kernel=None)
print(vals)
Streaming Real-time Updates

Feed high/low/close triples per tick:

from vectorta import FramaStream

stream = FramaStream(window=10, sc=300, fc=1)
for h, l, c in realtime_hlc:
    v = stream.update(h, l, c)
    if v is not None:
        print('FRAMA:', v)
Batch Parameter Sweep

Sweep window/sc/fc combinations:

import numpy as np
from vectorta import frama_batch

high = np.array([...])
low = np.array([...])
close = np.array([...])

res = frama_batch(
    high, low, close,
    window_range=(10, 32, 2),
    sc_range=(300, 300, 0),
    fc_range=(1, 1, 0),
    kernel='auto'
)

# res contains: values (2D flat), windows, scs, fcs
print(res['values'].shape)
CUDA Acceleration
import numpy as np
from vectorta import frama_cuda_batch_dev, frama_cuda_many_series_one_param_dev

# One series, many params (F32 arrays expected)
h = np.asarray(high, dtype=np.float32)
l = np.asarray(low, dtype=np.float32)
c = np.asarray(close, dtype=np.float32)
(device_out, combos) = frama_cuda_batch_dev(
    h, l, c,
    window_range=(10, 32, 2),
    sc_range=(300, 300, 0),
    fc_range=(1, 1, 0),
    device_id=0
)

# Many series (time-major), one parameter set
tm_h, tm_l, tm_c = H_tm_f32, L_tm_f32, C_tm_f32  # shape [T, N]
dev_out = frama_cuda_many_series_one_param_dev(
    tm_h, tm_l, tm_c,
    window=10, sc=300, fc=1,
    device_id=0
)

JavaScript/WASM Bindings

Basic Usage

Compute FRAMA in JavaScript/TypeScript:

import { frama_js } from 'vectorta-wasm';

const high = new Float64Array([/* ... */]);
const low = new Float64Array([/* ... */]);
const close = new Float64Array([/* ... */]);

const values = frama_js(high, low, close, 10, 300, 1);
console.log('FRAMA values:', values);
Memory-Efficient Operations

Use zero-copy operations for large arrays:

import { frama_alloc, frama_free, frama_into, memory } from 'vectorta-wasm';

const len = 10_000;
const h = new Float64Array(len);
const l = new Float64Array(len);
const c = new Float64Array(len);

const hPtr = frama_alloc(len);
const lPtr = frama_alloc(len);
const cPtr = frama_alloc(len);
const outPtr = frama_alloc(len);

new Float64Array(memory.buffer, hPtr, len).set(h);
new Float64Array(memory.buffer, lPtr, len).set(l);
new Float64Array(memory.buffer, cPtr, len).set(c);

// Args: high_ptr, low_ptr, close_ptr, out_ptr, len, window, sc, fc
frama_into(hPtr, lPtr, cPtr, outPtr, len, 10, 300, 1);

const out = new Float64Array(memory.buffer, outPtr, len).slice();

frama_free(hPtr, len);
frama_free(lPtr, len);
frama_free(cPtr, len);
frama_free(outPtr, len);
Batch Processing

Test multiple parameter combinations:

import { frama_batch_js, frama_batch_metadata_js } from 'vectorta-wasm';

const h = new Float64Array([/* ... */]);
const l = new Float64Array([/* ... */]);
const c = new Float64Array([/* ... */]);

// Define parameter sweep ranges
const w0=10, w1=32, ws=2;
const s0=300, s1=300, ss=0;
const f0=1, f1=1, fs=0;

// Get flattened [window, sc, fc, window, sc, fc, ...]
const meta = frama_batch_metadata_js(w0, w1, ws, s0, s1, ss, f0, f1, fs);
const combos = meta.length / 3;

// Compute all combos (returns flat values)
const flat = frama_batch_js(h, l, c, w0, w1, ws, s0, s1, ss, f0, f1, fs);

// Reshape into matrix [combos x len]
const len = c.length;
const rows = combos;
const matrix = [] as Float64Array[];
for (let i = 0; i < rows; i++) {
  const start = i * len;
  matrix.push(flat.slice(start, start + len));
}

Performance Analysis

Comparison:
View:
Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05

CUDA note

In our benchmark workload, the Rust CPU implementation is faster than CUDA for this indicator. Prefer the Rust/CPU path unless your workload differs.

Related Indicators