Forecast Oscillator (FOSC)

Parameters: period = 5

Overview

The Forecast Oscillator reveals whether prices trade above or below their statistically projected values by calculating the percentage deviation between actual price and a linear regression forecast. Using least squares regression over a rolling window, the indicator projects where price should theoretically reach based on recent trend, then measures how reality diverges from this mathematical expectation. When FOSC reads positive, price exceeds its forecast, suggesting bullish momentum pushing beyond statistical norms, while negative readings indicate price falling short of projected levels, signaling bearish pressure. Traders monitor zero line crossings as momentum shifts, with moves from negative to positive marking acceleration phases where price begins outperforming its trend forecast. The oscillator excels at identifying overbought and oversold conditions through extreme readings that suggest unsustainable deviations from the regression trend. Unlike traditional oscillators that compare price to its own history, FOSC uniquely compares price to its mathematical forecast, providing a forward looking perspective on whether current movement aligns with or diverges from statistical expectations.

Implementation Examples

Compute FOSC from a price slice or candles:

use vectorta::indicators::fosc::{fosc, FoscInput, FoscParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// From a price slice
let prices = vec![100.0, 101.0, 102.5, 101.8, 103.2, 104.0];
let params = FoscParams { period: Some(5) }; // Default period = 5
let input = FoscInput::from_slice(&prices, params);
let result = fosc(&input)?;

// From candles with defaults (source = "close", period = 5)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = FoscInput::with_default_candles(&candles);
let result = fosc(&input)?;

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

API Reference

Input Methods
// From price slice
FoscInput::from_slice(&[f64], FoscParams) -> FoscInput

// From candles with custom source
FoscInput::from_candles(&Candles, &str, FoscParams) -> FoscInput

// From candles with default params (close, period=5)
FoscInput::with_default_candles(&Candles) -> FoscInput
Parameters Structure
#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
pub struct FoscParams {
    pub period: Option<usize>, // Default: 5
}
Output Structure
pub struct FoscOutput {
    pub values: Vec<f64>, // FOSC values (% difference vs. TSF)
}
Validation, Warmup & NaNs
  • period > 0 and period ≤ len; otherwise FoscError::InvalidPeriod.
  • Requires at least period valid points after the first finite value; otherwise FoscError::NotEnoughValidData.
  • Warmup: indices before first + period − 1 are NaN. First output appears at first + period − 1.
  • fosc_into_slice: destination slice length must equal input length; else FoscError::OutputSliceWrongLen.
  • Division by zero: if Pt == 0.0, output is NaN. Interior NaNs propagate.
Error Handling
use vectorta::indicators::fosc::{fosc, FoscError};

match fosc(&input) {
    Ok(output) => handle(output.values),
    Err(FoscError::EmptyInputData) => eprintln!("Input data is empty"),
    Err(FoscError::AllValuesNaN) => eprintln!("All values are NaN"),
    Err(FoscError::InvalidPeriod { period, data_len }) =>
        eprintln!("Invalid period {} for data length {}", period, data_len),
    Err(FoscError::NotEnoughValidData { needed, valid }) =>
        eprintln!("Need {} points, only {} valid after first finite", needed, valid),
    Err(FoscError::OutputSliceWrongLen { dst_len, data_len }) =>
        eprintln!("dst len {} must equal data len {}", dst_len, data_len),
    Err(FoscError::InvalidKernelForBatch) =>
        eprintln!("Invalid kernel used for batch operation"),
}

Python Bindings

Basic Usage

Work with NumPy arrays and optional kernel selection:

import numpy as np
from vectorta import fosc

prices = np.asarray([100.0, 101.0, 102.5, 101.8, 103.2], dtype=np.float64)

values = fosc(prices, period=5)
values = fosc(prices, period=5, kernel="scalar")  # or "auto", "avx2", "avx512" (if available)
Streaming Updates

Maintain rolling FOSC values via the PyO3-backed stream:

from vectorta import FoscStream

stream = FoscStream(period=5)

for p in price_feed():
    val = stream.update(p)
    if val is not None:
        handle(val)
Batch Sweeps

Evaluate multiple periods without leaving Python:

from vectorta import fosc_batch
import numpy as np

prices = np.asarray(load_prices(), dtype=np.float64)

result = fosc_batch(prices, period_range=(5, 25, 5), kernel="auto")

values = result["values"]   # shape: [rows, len(prices)]
periods = result["periods"] # list of periods used
CUDA Acceleration

CUDA support for FOSC is currently under development. The API will mirror other CUDA-enabled indicators once released.

# Coming soon: CUDA-accelerated FOSC calculations
# from vectorta import fosc_cuda_batch
# results = fosc_cuda_batch(prices, period_range=(5, 50, 1), device_id=0)

JavaScript / WASM

Basic Usage

Call FOSC directly from TypeScript or JavaScript with Float64Array:

import { fosc_js } from 'vectorta-wasm';

const prices = new Float64Array([100, 101, 102.5, 101.8, 103.2]);
const values = fosc_js(prices, 5);
console.log('FOSC values', Array.from(values));
Memory-Efficient Operations

Reuse WASM buffers and compute in place:

import { fosc_alloc, fosc_free, fosc_into, memory } from 'vectorta-wasm';

const prices = new Float64Array(loadPrices());
const len = prices.length;

const inPtr = fosc_alloc(len);
const outPtr = fosc_alloc(len);
new Float64Array(memory.buffer, inPtr, len).set(prices);

// args: in_ptr, out_ptr, len, period
fosc_into(inPtr, outPtr, len, 5);

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

fosc_free(inPtr, len);
fosc_free(outPtr, len);
Batch Processing

Sweep period ranges and get a structured result:

import { fosc_batch_js } from 'vectorta-wasm';

const prices = new Float64Array(loadPrices());
const out = fosc_batch_js(prices, { period_range: [5, 25, 5] });

// out has: { values: Float64Array, combos: [{ period }...], rows, cols }
const rows = out.rows;
const cols = out.cols;
const matrix = [] as number[][];
for (let r = 0; r < rows; r++) {
  const start = r * cols;
  matrix.push(Array.from(out.values.slice(start, start + cols)));
}

// Access metadata
console.log(out.combos.map(c => c.period));

Performance Analysis

Comparison:
View:

Across sizes, Rust CPU runs about 1.21× faster than Tulip C in this benchmark.

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