215 lines
6.7 KiB
Markdown
215 lines
6.7 KiB
Markdown
# RSI
|
||||
|
|
|
|||
|
|
> Relative Strength Index — Wilder's bounded momentum oscillator that maps
|
|||
|
|
> the ratio of average gains to average losses onto the `[0, 100]` range.
|
|||
|
|
|
|||
|
|
## Quick reference
|
|||
|
|
|
|||
|
|
| Field | Value |
|
|||
|
|
|-------|-------|
|
|||
|
|
| Family | Momentum |
|
|||
|
|
| Sub-category | bounded oscillator |
|
|||
|
|
| Input type | `f64` (close) |
|
|||
|
|
| Output type | `f64` |
|
|||
|
|
| Output range | `[0, 100]` |
|
|||
|
|
| Default parameters | `period = 14` (Python) |
|
|||
|
|
| Warmup period | `period + 1` (15 for `period = 14`) |
|
|||
|
|
| Interpretation | overbought above 70, oversold below 30 (Wilder's thresholds) |
|
|||
|
|
|
|||
|
|
## Formula
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
diff_t = close_t − close_{t-1}
|
|||
|
|
gain_t = max(diff_t, 0)
|
|||
|
|
loss_t = max(−diff_t, 0)
|
|||
|
|
|
|||
|
|
Seed (Wilder, at t = period):
|
|||
|
|
avg_gain_p = (gain_1 + … + gain_p) / p
|
|||
|
|
avg_loss_p = (loss_1 + … + loss_p) / p
|
|||
|
|
|
|||
|
|
Recursive smoothing (t > period), with α = 1 / period:
|
|||
|
|
avg_gain_t = (avg_gain_{t-1} · (period − 1) + gain_t) / period
|
|||
|
|
avg_loss_t = (avg_loss_{t-1} · (period − 1) + loss_t) / period
|
|||
|
|
|
|||
|
|
RS_t = avg_gain_t / avg_loss_t
|
|||
|
|
RSI_t = 100 − 100 / (1 + RS_t)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
When `avg_loss_t == 0` and `avg_gain_t > 0`, RSI is `100` directly; when both
|
|||
|
|
are zero (a perfectly flat series) the implementation returns the standard
|
|||
|
|
`50` convention.
|
|||
|
|
|
|||
|
|
## Parameters
|
|||
|
|
|
|||
|
|
| Name | Type | Default (Python) | Valid range | Description |
|
|||
|
|
|------|------|------------------|-------------|-------------|
|
|||
|
|
| `period` | `usize` | `14` | `>= 1` | Wilder smoothing length. `Rsi::new(0)` returns `Error::PeriodZero`. |
|
|||
|
|
|
|||
|
|
## Inputs / Outputs
|
|||
|
|
|
|||
|
|
From `impl Indicator for Rsi` in `crates/wickra-core/src/indicators/rsi.rs`:
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
type Input = f64;
|
|||
|
|
type Output = f64;
|
|||
|
|
fn update(&mut self, input: f64) -> Option<f64>;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The output is a scalar in `[0, 100]`. In Python `batch(prices)` returns a
|
|||
|
|
1-D `np.ndarray` of `float64`, with `NaN` in the warmup positions. In Node
|
|||
|
|
`batch(prices)` returns a flat `number[]`, also `NaN` during warmup.
|
|||
|
|
|
|||
|
|
## Warmup
|
|||
|
|
|
|||
|
|
`warmup_period()` returns `period + 1`. The reason is that RSI consumes
|
|||
|
|
*diffs*, not prices: with `period` prices you only have `period − 1` diffs,
|
|||
|
|
so you need exactly one extra price before Wilder's seed average is well
|
|||
|
|
defined. The Rust test `warmup_period_is_period_plus_one` pins this:
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
let rsi = Rsi::new(14).unwrap();
|
|||
|
|
assert_eq!(rsi.warmup_period(), 15);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
In streaming terms, the first `period` calls to `update()` return `None`;
|
|||
|
|
the `(period + 1)`-th call returns the first `Some(value)`.
|
|||
|
|
|
|||
|
|
## Edge cases
|
|||
|
|
|
|||
|
|
- **Flat input.** When every input price is identical, every `gain` and
|
|||
|
|
every `loss` is zero, so `avg_loss == avg_gain == 0`. The implementation
|
|||
|
|
returns `50.0` by convention (see `Rsi::rsi_from_avgs`). The unit test
|
|||
|
|
`flat_series_yields_rsi_50` pins this behaviour.
|
|||
|
|
- **Pure uptrend / pure downtrend.** `avg_loss == 0` with `avg_gain > 0`
|
|||
|
|
short-circuits to `100`; the mirror case returns `0`. Tests
|
|||
|
|
`pure_uptrend_yields_rsi_100` and `pure_downtrend_yields_rsi_0` cover
|
|||
|
|
this.
|
|||
|
|
- **Non-finite input.** `update()` returns the previously emitted value
|
|||
|
|
(or `None` if no value has been emitted yet) when the input is `NaN` or
|
|||
|
|
infinite — the internal state is *not* advanced.
|
|||
|
|
- **Reset.** `reset()` returns the indicator to the freshly-constructed
|
|||
|
|
state: `prev_close`, both seed buffers, both averages, and `last_value`
|
|||
|
|
are cleared.
|
|||
|
|
|
|||
|
|
## Examples
|
|||
|
|
|
|||
|
|
### Rust
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
use wickra::{BatchExt, Indicator, Rsi};
|
|||
|
|
|
|||
|
|
let prices = [
|
|||
|
|
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
|
|||
|
|
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
|
|||
|
|
46.03, 46.41, 46.22, 45.64,
|
|||
|
|
];
|
|||
|
|
let mut rsi = Rsi::new(14)?;
|
|||
|
|
let out = rsi.batch(&prices);
|
|||
|
|
println!("first = {}", out[14].unwrap());
|
|||
|
|
println!("last = {}", out[19].unwrap());
|
|||
|
|
# Ok::<(), wickra::Error>(())
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Verified output:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
first = 70.46413502109705
|
|||
|
|
last = 57.91502067008556
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Python
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import numpy as np
|
|||
|
|
import wickra as ta
|
|||
|
|
|
|||
|
|
prices = np.array([
|
|||
|
|
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
|
|||
|
|
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
|
|||
|
|
46.03, 46.41, 46.22, 45.64,
|
|||
|
|
], dtype=float)
|
|||
|
|
rsi = ta.RSI(14)
|
|||
|
|
v = rsi.batch(prices)
|
|||
|
|
print("warmup:", rsi.warmup_period())
|
|||
|
|
print("first :", float(v[14]))
|
|||
|
|
print("last :", float(v[-1]))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Verified output:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
warmup: 15
|
|||
|
|
first : 70.46413502109705
|
|||
|
|
last : 57.91502067008556
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Node
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
const wickra = require('wickra');
|
|||
|
|
|
|||
|
|
const rsi = new wickra.RSI(14);
|
|||
|
|
const prices = [
|
|||
|
|
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
|
|||
|
|
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
|
|||
|
|
46.03, 46.41, 46.22, 45.64,
|
|||
|
|
];
|
|||
|
|
const v = rsi.batch(prices);
|
|||
|
|
console.log('warmup:', rsi.warmupPeriod());
|
|||
|
|
console.log('first :', v[14]);
|
|||
|
|
console.log('last :', v[19]);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Verified output:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
warmup: 15
|
|||
|
|
first : 70.46413502109705
|
|||
|
|
last : 57.91502067008556
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Interpretation
|
|||
|
|
|
|||
|
|
- **Overbought / oversold zones.** Wilder's classic thresholds are `70`
|
|||
|
|
(overbought) and `30` (oversold). Many crypto and FX desks tighten them
|
|||
|
|
to `80 / 20` for trending markets and loosen to `60 / 40` for
|
|||
|
|
range-bound markets.
|
|||
|
|
- **Midline cross.** A move through `50` is sometimes used as a directional
|
|||
|
|
signal; above 50 means average gains exceed average losses over the
|
|||
|
|
smoothing window.
|
|||
|
|
- **Divergence.** A higher price high paired with a lower RSI high (bearish
|
|||
|
|
divergence) is a classic Wilder signal; the symmetric pattern at lows is
|
|||
|
|
bullish.
|
|||
|
|
|
|||
|
|
## Common pitfalls
|
|||
|
|
|
|||
|
|
- **RSI on flat input is `50`, not undefined.** The implementation returns
|
|||
|
|
`50.0` when both averages are zero. Do not interpret this as a neutral
|
|||
|
|
signal — it is a placeholder that means "the indicator has no opinion
|
|||
|
|
yet". Pair RSI with a volatility filter (e.g. ATR) if your strategy is
|
|||
|
|
sensitive to ranging markets.
|
|||
|
|
- **`period + 1` warmup, not `period`.** A common bug is sizing the result
|
|||
|
|
array against `period` and indexing into the warmup region. The first
|
|||
|
|
`Some` arrives at the *(period + 1)*-th `update`; in batch form, indices
|
|||
|
|
`0..period` are `None`/`NaN`. See [Warmup Periods](../../Warmup-Periods.md).
|
|||
|
|
- **Non-finite inputs are absorbed silently.** `update(f64::NAN)` does not
|
|||
|
|
advance the state and returns the previous value. If you depend on a 1:1
|
|||
|
|
input-to-output mapping, pre-validate your data before feeding it in.
|
|||
|
|
|
|||
|
|
## References
|
|||
|
|
|
|||
|
|
- J. Welles Wilder, *New Concepts in Technical Trading Systems*, Trend
|
|||
|
|
Research, 1978. The original publication that defines both RSI and the
|
|||
|
|
Wilder smoothing scheme used internally.
|
|||
|
|
|
|||
|
|
## See also
|
|||
|
|
|
|||
|
|
- [Indicator: MacdIndicator](Indicator-MacdIndicator.md) — also momentum,
|
|||
|
|
but trend-following and unbounded.
|
|||
|
|
- [Indicator: Stochastic](Indicator-Stochastic.md) — sibling bounded
|
|||
|
|
oscillator, faster and noisier than RSI.
|
|||
|
|
- [Warmup Periods](../../Warmup-Periods.md) — the canonical `period + 1`
|
|||
|
|
off-by-one explained.
|
|||
|
|
- [Quickstart: Python](../../Quickstart-Python.md) — full RSI batch / streaming
|
|||
|
|
walk-through.
|