Ulcer Index (UI)

Parameters: period = 14

Overview

The Ulcer Index quantifies downside risk by measuring the depth and duration of price drawdowns from recent highs. Unlike standard deviation which treats upward and downward volatility equally, the Ulcer Index focuses exclusively on the pain of declines, making it particularly relevant for risk averse investors. The calculation tracks how far price falls below its rolling maximum over a lookback period, squares these percentage drawdowns to emphasize larger declines, averages them, then takes the square root to return to percentage units. Higher readings indicate deeper or more persistent drawdowns, signaling elevated risk, while lower values suggest price is trading near recent highs with minimal pullback stress. The indicator proves especially valuable for portfolio management and position sizing, as it quantifies the actual losses traders experience during holding periods rather than theoretical two-sided volatility. Default parameters use a 14 period lookback with percentage scaling at 100, matching conventional applications in financial literature and providing meaningful risk metrics for most trading timeframes.

Implementation Examples

Get started with UI in just a few lines:

use vector_ta::indicators::ui::{ui, UiInput, UiParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using with price data slice
let prices = vec![100.0, 102.0, 101.5, 103.0, 105.0, 104.5];
let params = UiParams {
    period: Some(14),
    scalar: Some(100.0),
};
let input = UiInput::from_slice(&prices, params);
let result = ui(&input)?;

// Using with Candles data structure
// Quick and simple with default parameters (period=14, scalar=100; source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = UiInput::with_default_candles(&candles);
let result = ui(&input)?;

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

API Reference

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

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

// From candles with default params (close, period=14, scalar=100)
UiInput::with_default_candles(&Candles) -> UiInput
Parameters Structure
pub struct UiParams {
    pub period: Option<usize>, // Default: 14
    pub scalar: Option<f64>,   // Default: 100.0
}
Output Structure
pub struct UiOutput {
    pub values: Vec<f64>, // UI values (RMS of % drawdowns)
}
Validation, Warmup & NaNs
  • period > 0 and period ≤ len(data); otherwise UiError::InvalidPeriod.
  • scalar must be finite; otherwise UiError::InvalidScalar. Any finite sign is accepted (magnitude matters).
  • There must be at least period valid points after the first finite input; otherwise UiError::NotEnoughValidData.
  • Warmup: outputs are NaN up to first_valid + (period × 2 − 2); after warmup, windows lacking full validity yield NaN.
  • Numeric guard: tiny negative averages are clamped to 0 before sqrt to avoid FP artifacts.
Error Handling
use vector_ta::indicators::ui::{ui, UiError};

match ui(&input) {
    Ok(output) => process_results(output.values),
    Err(UiError::EmptyInput) =>
        println!("Input data is empty"),
    Err(UiError::AllValuesNaN) =>
        println!("All values are NaN"),
    Err(UiError::InvalidPeriod { period, data_len }) =>
        println!("Invalid period {} for data length {}", period, data_len),
    Err(UiError::NotEnoughValidData { needed, valid }) =>
        println!("Need {} valid points, got {}", needed, valid),
    Err(UiError::InvalidLength { expected, actual }) =>
        println!("Destination length {} must equal {}", actual, expected),
    Err(UiError::InvalidScalar { scalar }) =>
        println!("Scalar must be finite, got {}", scalar),
}

Python Bindings

Basic Usage

Calculate UI using NumPy arrays (defaults: period=14, scalar=100):

import numpy as np
from vector_ta import ui

# Prepare price data as NumPy array
prices = np.array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5])

# Calculate UI with defaults (period=14, scalar=100)
result = ui(prices, period=14, scalar=100.0)

# Or specify kernel for performance optimization ("auto", "avx2", ...)
result = ui(prices, period=14, scalar=100.0, kernel="auto")

# Result is a NumPy array matching input length
print(f"UI values: {result}")
Streaming Real-time Updates

Process real-time price updates efficiently:

from vector_ta import UiStream

# Initialize streaming UI calculator
stream = UiStream(period=14, scalar=100.0)

# Process real-time price updates
for price in price_feed:
    ui_value = stream.update(price)

    if ui_value is not None:
        # UI value is ready (None during warmup period)
        print(f"Current UI: {ui_value}")
Batch Parameter Optimization

Test multiple parameter combinations for optimization:

import numpy as np
from vector_ta import ui_batch

# Your price data
prices = np.array([...])  # Your historical prices

# Define parameter ranges to test
# (start, end, step) for each parameter
period_range = (5, 50, 5)     # 5, 10, 15, ..., 50
scalar_range = (100.0, 100.0, 0.0)  # keep scalar fixed at 100

# Run batch calculation
results = ui_batch(
    prices,
    period_range=period_range,
    scalar_range=scalar_range,
    kernel="auto"
)

# Access results
print(f"Values shape: {results['values'].shape}")  # (num_combinations, len(prices))
print(f"Periods tested: {results['periods']}")
print(f"Scalars tested: {results['scalars']}")
CUDA Acceleration

CUDA helpers are available when the Python package is built with CUDA support. Inputs must be float32; outputs are device arrays (DLPack / __cuda_array_interface__ compatible).

import numpy as np
from vector_ta import ui_cuda_batch_dev, ui_cuda_many_series_one_param_dev

# One series (float32)
data_f32 = np.asarray(load_data(), dtype=np.float32)

dev = ui_cuda_batch_dev(
    data_f32=data_f32,
    period_range=(5, 30, 5),
    scalar_range=(0.5, 2.0, 0.5),
    device_id=0,
)

# Many series (time-major)
data_tm_f32 = np.asarray(load_data_time_major_matrix(), dtype=np.float32)

dev_tm = ui_cuda_many_series_one_param_dev(
    data_tm_f32=data_tm_f32,
    period=14,
    scalar=1.0,
    device_id=0,
)

JavaScript/WASM Bindings

Basic Usage

Calculate UI in JavaScript/TypeScript:

import { ui_js } from 'vectorta-wasm';

// Price data as Float64Array or regular array
const prices = new Float64Array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5]);

// Calculate UI with specified parameters
const result = ui_js(prices, 14, 100.0);  // period=14, scalar=100

// Result is a Float64Array
console.log('UI values:', result);

// Use with async/await for better error handling
async function calculateUI(prices: Float64Array): Promise<Float64Array> {
  try {
    return ui_js(prices, 14, 100.0);
  } catch (error) {
    console.error('UI calculation failed:', error);
    throw error;
  }
}
Memory-Efficient Operations

Use zero-copy operations for better performance with large datasets:

import { ui_alloc, ui_free, ui_into, memory } from 'vectorta-wasm';

// Prepare your price data
const prices = new Float64Array([/* your data */]);
const length = prices.length;

// Allocate WASM memory for input and output
const inPtr = ui_alloc(length);
const outPtr = ui_alloc(length);

// Copy input data into WASM memory
new Float64Array(memory.buffer, inPtr, length).set(prices);

// Calculate UI directly into allocated memory
// Args: in_ptr, out_ptr, len, period, scalar
ui_into(inPtr, outPtr, length, 14, 100.0);

// Read results from WASM memory (slice() to copy out)
const uiValues = new Float64Array(memory.buffer, outPtr, length).slice();

// Free allocated memory when done
ui_free(inPtr, length);
ui_free(outPtr, length);

console.log('UI values:', uiValues);
Batch Processing

Test multiple parameter combinations efficiently:

import { ui_batch, ui_batch_into, ui_alloc, ui_free, memory } from 'vectorta-wasm';

// Your price data
const prices = new Float64Array([/* historical prices */]);

// Option A: High-level batch API with config object
const config = {
  period_range: [5, 50, 5],         // start, end, step
  scalar_range: [100.0, 100.0, 0.0] // keep scalar fixed
};
const batchOut = ui_batch(prices, config);
// batchOut: { values: Float64Array, combos: UiParams[], rows: number, cols: number }

// Option B: Fast pointer API (manual memory management)
// Compute number of combinations for allocation
const nPeriods = Math.floor((50 - 5) / 5) + 1;  // inclusive range: 5..50 step 5
const nScalars = 1;                              // scalar step = 0.0 => single value
const rows = nPeriods * nScalars;

// Allocate and copy input into WASM memory
const inPtr = ui_alloc(prices.length);
new Float64Array(memory.buffer, inPtr, prices.length).set(prices);

// Allocate output buffer for rows × cols
const outPtr = ui_alloc(rows * prices.length);
try {
  const writtenRows = ui_batch_into(
    inPtr, outPtr, prices.length,
    5, 50, 5,
    100.0, 100.0, 0.0
  );

  // Read back results (flat array: row-major [rows * cols])
  const out = new Float64Array(memory.buffer, outPtr, rows * prices.length).slice();
  console.log('rows written:', writtenRows, 'values length:', out.length);
} finally {
  ui_free(inPtr, prices.length);
  ui_free(outPtr, rows * prices.length);
}

CUDA Bindings (Rust)

use vector_ta::cuda::CudaUi;
use vector_ta::indicators::ui::UiBatchRange;

let cuda = CudaUi::new(0)?;

let prices: [f32] = /* ... */;
let sweep = UiBatchRange::default();

let out = cuda.ui_batch_dev(&prices, &sweep)?;
let _ = out;

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