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 vectorta::indicators::ui::{ui, UiInput, UiParams};
use vectorta::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 vectorta::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 vectorta 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 vectorta 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 vectorta 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 support for UI is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.

# Coming soon: CUDA-accelerated UI calculations
# (batch optimization and many-series variants)

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);
}

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