Average Sentiment Oscillator (ASO)

Parameters: period = 10 | mode = 0 (0–2)

Overview

ASO quantifies market sentiment by analyzing where price closes within individual bars and across rolling windows, producing bull and bear readings that always sum to 100 after initialization. The indicator calculates sentiment from two angles: intrabar positioning measures how close price finished relative to its range, while group analysis compares current levels to recent history over the specified period. Mode 0 averages both perspectives for balanced sentiment, mode 1 uses only intrabar data for immediate reactions, and mode 2 relies solely on group context for smoother signals. When bulls exceed 60, buying pressure dominates the market; conversely, bears above 60 indicate selling control. Traders use ASO to confirm breakouts when sentiment aligns with price direction, identify exhaustion when sentiment diverges from price trends, and time reversals when sentiment shifts from extreme readings back toward equilibrium at 50.

Implementation Examples

Compute Bulls/Bears from OHLC slices or Candles:

use vectorta::indicators::aso::{aso, AsoInput, AsoParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using with OHLC slices
let (open, high, low, close) = (
    vec![100.0, 102.0, 101.0, 103.0],
    vec![101.0, 103.0, 102.0, 104.0],
    vec![ 99.0, 100.5,  99.5, 101.5],
    vec![100.5, 102.5, 101.8, 103.5],
);
let params = AsoParams { period: Some(10), mode: Some(0) };
let input = AsoInput::from_slices(&open, &high, &low, &close, params);
let out = aso(&input)?;
for (b, e) in out.bulls.iter().zip(out.bears.iter()) {
    println!("bulls={}, bears={}", b, e);
}

// Using with Candles (defaults: period=10, mode=0; source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = AsoInput::with_default_candles(&candles);
let out = aso(&input)?;

API Reference

Input Methods
// From candles with custom source (close used for AsRef)
AsoInput::from_candles(&Candles, &str, AsoParams) -> AsoInput

// From OHLC slices
AsoInput::from_slices(&[f64], &[f64], &[f64], &[f64], AsoParams) -> AsoInput

// From candles with default params (period=10, mode=0; source="close")
AsoInput::with_default_candles(&Candles) -> AsoInput
Parameters Structure
pub struct AsoParams {
    pub period: Option<usize>, // Default: 10
    pub mode: Option<usize>,   // Default: 0 (0/1/2)
}
Output Structure
pub struct AsoOutput {
    pub bulls: Vec<f64>, // sentiment in [0, 100]
    pub bears: Vec<f64>, // sentiment in [0, 100]
}
// Property: for non-NaN outputs, bulls + bears == 100
Validation, Warmup & NaNs
  • period > 0 and period ≤ len; else AsoError::InvalidPeriod.
  • mode ∈ {0,1,2}; else AsoError::InvalidMode.
  • All OHLC arrays must have equal length; else AsoError::MissingData.
  • Empty input → AsoError::EmptyInputData; all-NaN close series → AsoError::AllValuesNaN.
  • Requires at least period valid points after the first finite close; else AsoError::NotEnoughValidData.
  • Warmup: indices [0 .. first + period − 1) are NaN for both outputs.
Error Handling
use vectorta::indicators::aso::{aso, AsoError};

match aso(&input) {
    Ok(output) => consume(output.bulls, output.bears),
    Err(AsoError::EmptyInputData) => eprintln!("ASO: input is empty"),
    Err(AsoError::AllValuesNaN) => eprintln!("ASO: all values are NaN"),
    Err(AsoError::InvalidPeriod { period, data_len }) =>
        eprintln!("ASO: invalid period {period} for length {data_len}"),
    Err(AsoError::NotEnoughValidData { needed, valid }) =>
        eprintln!("ASO: need {needed} valid points, have {valid}"),
    Err(AsoError::InvalidMode { mode }) =>
        eprintln!("ASO: invalid mode {mode}, must be 0, 1, or 2"),
    Err(AsoError::MissingData) => eprintln!("ASO: OHLC mismatch or missing"),
}

Python Bindings

Basic Usage

Work with NumPy arrays for open, high, low, close:

import numpy as np
from vectorta import aso

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

bulls, bears = aso(open_, high, low, close, period=10, mode=0, kernel="auto")
print(bulls.shape, bears.shape)
Streaming Updates
from vectorta import AsoStream

stream = AsoStream(period=10, mode=0)
for o,h,l,c in ohlc_stream():
    val = stream.update(o, h, l, c)
    if val is not None:
        bulls, bears = val
        use(bulls, bears)
Batch Sweeps
from vectorta import aso_batch

result = aso_batch(
    open_, high, low, close,
    period_range=(5, 20, 5),
    mode_range=(0, 2, 1),
    kernel="auto",
)

# result is a dict with reshaped NumPy arrays
bulls = result["bulls"]   # shape: (rows, len)
bears = result["bears"]   # shape: (rows, len)
periods = result["periods"]
modes = result["modes"]
CUDA Acceleration

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

# Coming soon: CUDA-accelerated ASO calculations
# from vectorta import aso_cuda_batch, aso_cuda_many_series_one_param
Return Structure
# aso(...)
np.ndarray, np.ndarray  # bulls, bears, each shape: (len,)

# aso_batch(...)
{
    "bulls": np.ndarray,   # shape: (rows, len)
    "bears": np.ndarray,   # shape: (rows, len)
    "periods": np.ndarray, # row-wise period
    "modes": np.ndarray,   # row-wise mode
}

JavaScript / WASM Bindings

Quick Usage

Call ASO from TypeScript/JavaScript:

import { aso_js } from 'vectorta-wasm';

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

// Returns { values: Float64Array, rows: 2, cols: len }
const res = aso_js(open, high, low, close, 10, 0);
const { values, rows, cols } = res;
const bulls = values.slice(0, cols);
const bears = values.slice(cols, 2 * cols);
Memory-Efficient Operations

Use zero-copy buffers for large datasets:

import { aso_alloc, aso_free, aso_into, memory } from 'vectorta-wasm';

const len = close.length;
const oPtr = aso_alloc(len), hPtr = aso_alloc(len);
const lPtr = aso_alloc(len), cPtr = aso_alloc(len);
const bullsPtr = aso_alloc(len), bearsPtr = aso_alloc(len);

// Copy input data into WASM memory
new Float64Array(memory.buffer, oPtr, len).set(open);
new Float64Array(memory.buffer, hPtr, len).set(high);
new Float64Array(memory.buffer, lPtr, len).set(low);
new Float64Array(memory.buffer, cPtr, len).set(close);

// Compute directly into allocated memory
// Args: open_ptr, high_ptr, low_ptr, close_ptr, bulls_ptr, bears_ptr, len, period, mode
aso_into(oPtr, hPtr, lPtr, cPtr, bullsPtr, bearsPtr, len, 10, 0);

// Read out
const bulls = new Float64Array(memory.buffer, bullsPtr, len).slice();
const bears = new Float64Array(memory.buffer, bearsPtr, len).slice();

// Free
aso_free(oPtr, len); aso_free(hPtr, len); aso_free(lPtr, len); aso_free(cPtr, len);
aso_free(bullsPtr, len); aso_free(bearsPtr, len);
Batch Processing

Test multiple parameter combinations efficiently:

import { aso_batch } from 'vectorta-wasm';

const cfg = { period_range: [5, 20, 5], mode_range: [0, 2, 1] };
const result = aso_batch(open, high, low, close, cfg);

// result: { values, combos, rows, cols }
// rows = 2 * num_combinations (bulls rows first, then bears rows)
const { values, combos, rows, cols } = result;
const numCombos = combos.length;

// Extract the first combo's bulls/bears rows
const bulls0 = values.slice(0 * cols, 1 * cols);
const bears0 = values.slice(numCombos * cols, (numCombos + 1) * cols);

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