Average Sentiment Oscillator (ASO)
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 > 0andperiod ≤ len; elseAsoError::InvalidPeriod.mode ∈ {0,1,2}; elseAsoError::InvalidMode.- All OHLC arrays must have equal length; else
AsoError::MissingData. - Empty input →
AsoError::EmptyInputData; all-NaN close series →AsoError::AllValuesNaN. - Requires at least
periodvalid points after the first finite close; elseAsoError::NotEnoughValidData. - Warmup: indices
[0 .. first + period − 1)areNaNfor 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
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.