Dual Ulcer Index

Parameters: period = 5 | auto_threshold = true | threshold = 0.1

Overview

Dual Ulcer Index extends the usual ulcer-style drawdown idea into a two-sided stress measure. The long-side ulcer output measures how far the current close sits below the rolling highest close over the chosen window. The short-side ulcer output mirrors that logic in the other direction by measuring how far the current close sits above the rolling lowest close. Both sides are squared and averaged before taking the square root, so persistent stress matters more than a single one-bar spike.

The third output is a threshold line. In automatic mode it becomes the running average of the absolute spread between the long and short ulcer readings, which lets the threshold adapt to the stress environment already seen in the series. In manual mode it stays fixed at the user-supplied threshold. The result is a compact three-line structure that can show whether downside stress, upside squeeze stress, or the balance threshold is dominating.

Defaults: Dual Ulcer Index uses `period = 5`, `auto_threshold = true`, and `threshold = 0.1`.

Implementation Examples

Compute the long-side ulcer, short-side ulcer, and threshold lines from a close series.

use vector_ta::indicators::dual_ulcer_index::{
    dual_ulcer_index,
    DualUlcerIndexInput,
    DualUlcerIndexParams,
};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

let output = dual_ulcer_index(&DualUlcerIndexInput::from_slice(
    &close,
    DualUlcerIndexParams {
        period: Some(5),
        auto_threshold: Some(true),
        threshold: Some(0.1),
    },
))?;

let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let candle_output = dual_ulcer_index(
    &DualUlcerIndexInput::with_default_candles(&candles)
)?;

println!("long = {:?}", output.long_ulcer.last());
println!("short = {:?}", output.short_ulcer.last());
println!("threshold = {:?}", candle_output.threshold.last());

API Reference

Input Methods
// From candles
DualUlcerIndexInput::from_candles(
    &Candles,
    DualUlcerIndexParams,
) -> DualUlcerIndexInput

// From a raw close slice
DualUlcerIndexInput::from_slice(&[f64], DualUlcerIndexParams)
    -> DualUlcerIndexInput

// From candles with default parameters
DualUlcerIndexInput::with_default_candles(&Candles)
    -> DualUlcerIndexInput
Parameters Structure
pub struct DualUlcerIndexParams {
    pub period: Option<usize>,         // default 5
    pub auto_threshold: Option<bool>,  // default true
    pub threshold: Option<f64>,        // default 0.1
}
Output Structure
pub struct DualUlcerIndexOutput {
    pub long_ulcer: Vec<f64>,
    pub short_ulcer: Vec<f64>,
    pub threshold: Vec<f64>,
}
Validation, Warmup & NaNs
  • The input close series must be non-empty and contain finite positive prices.
  • period must be greater than zero and no larger than the input length.
  • threshold must be finite and greater than or equal to zero.
  • The longest contiguous valid run must be at least 2 * period - 1 bars.
  • Warmup lasts for 2 * period - 2 bars, which is also what the streaming API reports through get_warmup_period().
  • Streaming resets on non-finite or non-positive input and returns None until warmup completes again.
  • Automatic threshold mode is path-dependent because it averages the observed ulcer-gap magnitude over time.
  • Destination slices for in-place APIs must match the expected lengths exactly, and batch mode rejects invalid ranges or unsupported kernels.
Builder, Streaming & Batch APIs
// Builder
DualUlcerIndexBuilder::new()
    .period(usize)
    .auto_threshold(bool)
    .threshold(f64)
    .kernel(Kernel)
    .apply_slice(&[f64])

DualUlcerIndexBuilder::new()
    .apply(&Candles)

DualUlcerIndexBuilder::new()
    .into_stream()

// Stream
DualUlcerIndexStream::try_new(DualUlcerIndexParams)
DualUlcerIndexStream::update(f64) -> Option<(f64, f64, f64)>
DualUlcerIndexStream::get_warmup_period() -> usize

// Batch
DualUlcerIndexBatchBuilder::new()
    .period_range(start, end, step)
    .period_static(usize)
    .threshold_range(start, end, step)
    .threshold_static(f64)
    .auto_threshold(bool)
    .kernel(Kernel)
    .apply_slice(&[f64])

DualUlcerIndexBatchBuilder::new()
    .apply_candles(&Candles)
Error Handling
pub enum DualUlcerIndexError {
    EmptyInputData,
    AllValuesNaN,
    InvalidPeriod { period: usize, data_len: usize },
    NotEnoughValidData { needed: usize, valid: usize },
    InvalidThreshold { threshold: f64 },
    OutputLengthMismatch { expected: usize, got: usize },
    InvalidRange { start: String, end: String, step: String },
    InvalidKernelForBatch(Kernel),
    MismatchedOutputLen { dst_len: usize, expected_len: usize },
    InvalidInput { msg: String },
}

Python Bindings

Python exposes a three-array single-run function, a streaming class, and a batch function. The single-run binding returns a tuple-like three-output object covering long ulcer, short ulcer, and threshold. Batch returns three matrices plus the tested periods, threshold values, auto-threshold flags, and the rows and cols shape.

import numpy as np
from vector_ta import (
    dual_ulcer_index,
    dual_ulcer_index_batch,
    DualUlcerIndexStream,
)

data = np.asarray(close_values, dtype=np.float64)

long_ulcer, short_ulcer, threshold = dual_ulcer_index(
    data,
    period=5,
    auto_threshold=True,
    threshold=0.1,
    kernel="auto",
)

stream = DualUlcerIndexStream(
    period=5,
    auto_threshold=False,
    threshold=0.2,
)
print(stream.update(data[-1]))

batch = dual_ulcer_index_batch(
    data,
    period_range=(5, 15, 5),
    threshold_range=(0.1, 0.3, 0.1),
    auto_threshold=False,
    kernel="auto",
)

print(batch["periods"], batch["threshold_values"], batch["rows"], batch["cols"])

JavaScript/WASM Bindings

The WASM layer exposes an object-returning single-run wrapper, an object-returning batch wrapper, and lower-level allocation and in-place exports. Both high-level JavaScript wrappers return named properties for long_ulcer, short_ulcer, and threshold, while batch also returns rows, cols, and combos.

import init, {
  dual_ulcer_index_js,
  dual_ulcer_index_batch_js,
} from "/pkg/vector_ta.js";

await init();

const data = new Float64Array(closeValues);

const single = dual_ulcer_index_js(data, 5, true, 0.1);
console.log(single.long_ulcer, single.short_ulcer, single.threshold);

const batch = dual_ulcer_index_batch_js(data, {
  period_range: [5, 15, 5],
  threshold_range: [0.1, 0.3, 0.1],
  auto_threshold: false,
});

console.log(batch.long_ulcer, batch.short_ulcer, batch.threshold, batch.rows, batch.cols);

CUDA Bindings (Rust)

Additional details for the CUDA bindings can be found inside the VectorTA repository.

Performance Analysis

Comparison:
View:
Placeholder data (no recorded benchmarks for this indicator)

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

Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU)

Related Indicators