Completes the F7 family (Volatility) end to end: - Rust core: natr.rs (ATR as a percentage of close), std_dev.rs (rolling population standard deviation), ulcer_index.rs (RMS of trailing-high drawdowns — downside-only risk), historical_volatility.rs (annualised sample stddev of log returns). Each with a full Indicator impl, runnable doctest and reference / constant-series / warmup / reset / batch==streaming tests. - Python: PyNatr / PyStdDev / PyUlcerIndex / PyHistoricalVolatility PyO3 classes + module registration + .pyi stubs. - Node: StdDevNode / UlcerIndexNode via the scalar macro, explicit NatrNode and HistoricalVolatilityNode; index.d.ts and index.js updated. - WASM: WasmStdDev / WasmUlcerIndex / WasmHistoricalVolatility via the scalar macro, explicit WasmNatr. - Wiki: Indicator-Natr/StdDev/UlcerIndex/HistoricalVolatility.md plus rows in Indicators-Overview.md and entries in Home.md. cargo fmt + clippy (core/wickra/data/wasm/node) clean; 350 core tests, 25 data tests and 49 doctests green.
162 lines
4.8 KiB
Markdown
162 lines
4.8 KiB
Markdown
# UlcerIndex
|
||
|
||
> Ulcer Index — Peter Martin's downside-only risk measure: the
|
||
> root-mean-square of recent drawdowns.
|
||
|
||
## Quick reference
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| Family | Volatility |
|
||
| Sub-category | Downside risk |
|
||
| Input type | `f64` (single close) |
|
||
| Output type | `f64` |
|
||
| Output range | `[0, ∞)` (percent) |
|
||
| Default parameters | `period = 14` (Python) |
|
||
| Warmup period | `2·period − 1` |
|
||
| Interpretation | Depth and duration of drawdowns; `0` means no drawdown at all. |
|
||
|
||
## Formula
|
||
|
||
```
|
||
max_t = highest price over the trailing `period` bars
|
||
drawdown_t = 100 · (price_t − max_t) / max_t
|
||
UlcerIndex = √( mean( drawdown² over period ) )
|
||
```
|
||
|
||
Standard deviation treats an up-move and a down-move as equally
|
||
"volatile". The Ulcer Index measures only the **pain of being underwater**:
|
||
for each bar it takes the percentage drop from the trailing high, squares
|
||
it, and reports the root-mean-square. A market that only rises has no
|
||
drawdown and an Ulcer Index of `0`; the deeper and longer the drawdowns,
|
||
the higher the reading. It is the volatility term in the Martin ratio
|
||
(Ulcer Performance Index).
|
||
|
||
## Parameters
|
||
|
||
| Name | Type | Default | Valid range | Description |
|
||
|----------|---------|---------------|-------------|-------------|
|
||
| `period` | `usize` | `14` (Python) | `>= 1` | Look-back for both the trailing high and the RMS window. `0` errors with `Error::PeriodZero`. |
|
||
|
||
The Python binding defaults `period` to `14`.
|
||
|
||
## Inputs / Outputs
|
||
|
||
From `crates/wickra-core/src/indicators/ulcer_index.rs`:
|
||
|
||
```rust
|
||
impl Indicator for UlcerIndex {
|
||
type Input = f64;
|
||
type Output = f64;
|
||
// update(&mut self, input: f64) -> Option<f64>
|
||
}
|
||
```
|
||
|
||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||
`Array<number>` (NaN warmup).
|
||
|
||
## Warmup
|
||
|
||
`UlcerIndex::new(period).warmup_period() == 2·period − 1`. The first
|
||
`period` prices fill the trailing-maximum window; the per-bar squared
|
||
drawdown then needs another `period − 1` bars to fill the RMS window.
|
||
|
||
## Edge cases
|
||
|
||
- **Pure up-trend.** Price never trades below its own running high, so
|
||
every drawdown — and the Ulcer Index — is `0`
|
||
(`pure_uptrend_yields_zero` pins this).
|
||
- **Constant series.** A flat series has no drawdown; the output is `0.0`
|
||
(`constant_series_yields_zero` pins this).
|
||
- **Non-negative.** The Ulcer Index is an RMS of real numbers and is
|
||
never negative (`output_is_non_negative` pins this).
|
||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped.
|
||
- **Reset.** `ui.reset()` clears both rolling windows and the sum.
|
||
|
||
## Examples
|
||
|
||
### Rust
|
||
|
||
```rust
|
||
use wickra::{BatchExt, Indicator, UlcerIndex};
|
||
|
||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||
let mut ui = UlcerIndex::new(2)?;
|
||
let out: Vec<Option<f64>> = ui.batch(&[10.0, 8.0, 12.0, 9.0]);
|
||
println!("{:?}", out);
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
Output:
|
||
|
||
```
|
||
[None, None, Some(14.142135623730951), Some(17.67766952966369)]
|
||
```
|
||
|
||
`UlcerIndex(2)` warms up after `3` bars. At bar 3 the squared drawdowns in
|
||
the window are `[400, 0]`, so the index is `√(400/2) = √200`. At bar 4
|
||
they are `[0, 625]`, giving `√(625/2) = √312.5`. This matches the
|
||
`reference_values` test in
|
||
`crates/wickra-core/src/indicators/ulcer_index.rs`.
|
||
|
||
### Python
|
||
|
||
```python
|
||
import numpy as np
|
||
import wickra as ta
|
||
|
||
ui = ta.UlcerIndex(2)
|
||
print(ui.batch(np.array([10.0, 8.0, 12.0, 9.0])))
|
||
```
|
||
|
||
Output:
|
||
|
||
```
|
||
[ nan nan 14.1421356 17.6776695]
|
||
```
|
||
|
||
### Node
|
||
|
||
```javascript
|
||
const ta = require('wickra');
|
||
const ui = new ta.UlcerIndex(2);
|
||
console.log(ui.batch([10, 8, 12, 9]));
|
||
```
|
||
|
||
Output:
|
||
|
||
```
|
||
[ NaN, NaN, 14.142135623730951, 17.67766952966369 ]
|
||
```
|
||
|
||
## Interpretation
|
||
|
||
`UlcerIndex` answers "how uncomfortable has holding this been?" — a high
|
||
reading means deep or prolonged drawdowns, a low reading means a smooth
|
||
ride up. It is most useful for *comparing* instruments or strategies on a
|
||
downside-risk basis, and as the denominator of the Ulcer Performance
|
||
Index (`(return − risk-free) / UlcerIndex`), a Sharpe-ratio analogue that
|
||
penalises only downside volatility.
|
||
|
||
## Common pitfalls
|
||
|
||
- **Reading it as two-sided volatility.** The Ulcer Index ignores upside
|
||
entirely — a wildly choppy *up*-trend can still score near `0`. Use
|
||
[`StdDev`](Indicator-StdDev.md) for two-sided dispersion.
|
||
- **Forgetting the doubled warmup.** Warmup is `2·period − 1`, not
|
||
`period`.
|
||
|
||
## References
|
||
|
||
Peter Martin and Byron McCann, *The Investor's Guide to Fidelity Funds*
|
||
(1989); the index is also documented at StockCharts. The trailing-high
|
||
drawdown RMS here follows that definition.
|
||
|
||
## See also
|
||
|
||
- [Indicator-StdDev.md](Indicator-StdDev.md) — two-sided dispersion.
|
||
- [Indicator-Atr.md](Indicator-Atr.md) — per-bar range volatility.
|
||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|