Dual Ulcer Index
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.
periodmust be greater than zero and no larger than the input length.thresholdmust be finite and greater than or equal to zero.- The longest contiguous valid run must be at least
2 * period - 1bars. - Warmup lasts for
2 * period - 2bars, which is also what the streaming API reports throughget_warmup_period(). - Streaming resets on non-finite or non-positive input and returns
Noneuntil 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
Across sizes, Rust CPU runs about 1.14× faster than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU)