Ulcer Index on TradingView: formula and limits
The Ulcer Index indicator measures the root mean square of percentage drawdowns. This guide implements a rolling version: each close is compared with its own N-bar highest close, and the latest N squared drawdowns are averaged before taking the square root. Use the worked table and Pine Script to check the values on your chart. A low reading does not establish that a trade is safe.
Define the peak before calculating UI
For each bar t, define H(t) as the highest close from t−N+1 through t, including the current bar. Set D(t) = 100 × (Close(t) / H(t) − 1). Then calculate UI(t) as the square root of the mean of D² from t−N+1 through t. Each historical drawdown keeps the peak calculated at its own bar; do not apply today's peak retrospectively to every close.
H(t) = max(Close[t−N+1 … t])
D(t) = 100 × (Close(t) / H(t) − 1)
UI(t) = √((D(t)² + … + D(t−N+1)²) / N)
This rolling formula follows StockCharts' calculation. Peter G. Martin's original algorithm keeps the highest value reached since the start of the measurement period. Those peak definitions can give different results. State which one you use before comparing a chart indicator with a portfolio report.
Squaring gives a 10% drawdown four times the contribution of a 5% drawdown: 100 versus 25. Taking the square root restores the percentage scale. UI still aggregates a path; it is neither the current drawdown nor a forecast of a future loss.
Worked example: the old peak leaves the window
These are invented closing prices for a calculation check, not market data or a backtest. Use N = 3 for both windows. The script waits for a full peak window, followed by three valid drawdowns.
| Bar | Close | 3-bar peak | Drawdown % | Squared drawdown | UI % |
|---|---|---|---|---|---|
| 1 | 100 | Not ready | Not ready | Not ready | Not ready |
| 2 | 100 | Not ready | Not ready | Not ready | Not ready |
| 3 | 100 | 100 | 0 | 0 | Not ready |
| 4 | 90 | 100 | -10 | 100 | Not ready |
| 5 | 90 | 100 | -10 | 100 | 8.1650 |
| 6 | 90 | 90 | 0 | 0 | 8.1650 |
| 7 | 90 | 90 | 0 | 0 | 5.7735 |
| 8 | 90 | 90 | 0 | 0 | 0.0000 |
At bar 5, UI = √((0 + 100 + 100) / 3) = 8.1650. At bar 6, the peak becomes 90 and the new drawdown is zero, but the average still contains two squared drawdowns of 100. At bar 7 only one remains: √(100 / 3) = 5.7735. By bar 8 UI is zero although price is still 10% below the old 100 peak. A falling rolling UI can reflect an expiring window without a price recovery.
Pine Script v6 with explicit confirmation
Paste this indicator into TradingView's Pine Editor and add it to a chart. The Data Window exposes the peak and drawdown for comparison. The default N = 14 is an example setting; the editable reference line does not affect the calculation. The script creates no orders.
//@version=6
indicator("Ulcer Index: rolling close", overlay=false, precision=4)
length = input.int(14, "Window (chart bars)", minval=1)
reference = input.float(5.0, "Reference line (not a trade rule)", minval=0.0)
confirmedOnly = input.bool(true, "Show only confirmed bars")
// Require N consecutive positive closes for each rolling peak.
validCloses = math.sum(not na(close) and close > 0 ? 1.0 : 0.0, length)
peak = ta.highest(close, length)
drawdown = validCloses == length ? 100.0 * (close / peak - 1.0) : na
// Require N consecutive valid drawdowns; do not bridge missing values.
validDrawdowns = math.sum(not na(drawdown) ? 1.0 : 0.0, length)
squaredSum = math.sum(nz(drawdown * drawdown, 0.0), length)
ui = validDrawdowns == length ? math.sqrt(squaredSum / length) : na
visibleUI = not confirmedOnly or barstate.isconfirmed ? ui : na
plot(visibleUI, "Ulcer Index (%)", color=color.blue, linewidth=2, style=plot.style_linebr)
hline(reference, "User reference", color=color.gray)
plot(peak, "Rolling highest close", display=display.data_window)
plot(drawdown, "Drawdown (%)", display=display.data_window)
crossedAbove = ta.crossover(ui, reference)
alertcondition(barstate.isconfirmed and crossedAbove, "UI crossed reference at close", "Ulcer Index crossed above the user reference on a closed bar.")With positive, available closes, the first full UI needs 2N−1 bars: 27 bars when N = 14, or 5 when N = 3. An invalid close interrupts initialization. Both rolling sums run on every bar, including the unfinished bar; the confirmation setting controls visibility afterward.
Window units and when a reading is final
| Setting | Meaning in this script | Check before comparing |
|---|---|---|
| Window N | N chart bars in each of the two windows | Daily, weekly and hourly bars cover different histories. |
| Reference line | Visual level and alert comparison only | There is no universal risk cutoff of 3, 5 or 8. |
| Confirmed bars | Hide the open bar until its closing update | Turning this off displays provisional values. |
| Price series | Positive chart closes | Match symbol, adjustment settings, session and chart type. |
TradingView indicators recalculate during an open realtime bar. Its close, rolling peak and drawdown can change. This example uses barstate.isconfirmed to publish the current reading at bar close and to gate the reference-crossing alert. Create the alert with the script's named condition and choose Once Per Bar Close. This behavior is documented in TradingView bar states and the execution model.
Using ui[1] instead would show the previous bar's value, with one bar of delay. The code uses the chart timeframe and does not request higher-timeframe data. Adding such a request requires a separate confirmation design; this example does not establish its behavior.
Ulcer Index, standard deviation and maximum drawdown
| Measure | Calculation question | Boundary |
|---|---|---|
| Rolling Ulcer Index | How large are recent percentage drawdowns, after squaring and averaging? | Depends on rolling peak and averaging windows; retains no permanent high-water mark. |
| Standard deviation of returns | How dispersed are returns around their mean? | Includes deviations in both directions. Specify the return interval and annualization. |
| Maximum drawdown | What is the largest peak-to-later-trough percentage decline in the chosen period? | Summarizes the worst decline, without averaging all underwater observations. It can also update on a rolling basis. |
A price-based UI describes the asset's closes. A strategy equity curve also depends on exposure, entries, exits and costs. To compare strategy risk, first align the equity sampling and peak convention. UI alone provides no return target or evidence that an entry will work.
Check the script before adding trade rules
- Fix the symbol, chart type, timeframe, session and price adjustments. Record N and the number of available bars.
- At a closed bar, compare the Data Window peak with the highest close in that bar's own N-bar window.
- Recalculate each of the last N drawdowns using its own peak. Square, average and take the square root; compare with the plotted UI.
- Inspect a new high and a bar where an old high expires. Use the worked example to distinguish price recovery from window expiry.
- Check the open bar with confirmed mode on and off. Define exits, position size, commission and slippage separately before turning the indicator into a strategy.
Describe the exact implementation to Pineify
Ask the Pine Script AI Coding Agent to preserve the rolling peak, positive-close checks, full-window initialization and bar-close confirmation. Request the intermediate values so you can compare the result bar by bar before adding trading logic.
Build a Pine v6 Ulcer Index using each bar's N-bar highest close. Average N squared percentage drawdowns and take the square root. Wait for full valid windows. Plot only confirmed bars by default. Show peak and drawdown in the Data Window. Do not add entry or exit rules.Open Pine Script AI Coding Agent
Educational calculation example. The indicator and reference alert do not establish suitability, profitability or a safe level of exposure.
Ulcer Index questions
Sources and related reading
Sources checked October 17, 2026. Calculation table is an original hypothetical example.