Trading Dev AcademyFree quant education

Free lesson · Charts & patterns

Indicators as arithmetic: ATR, moving averages, RSI and bands

Open interactive lessonPractice calculationsExplore labs

Start with the idea

Begin with one operation: an average or a range. Then see how smoothing, differencing and normalising produce familiar indicators. Matching the seed and denominator is essential when comparing hand work, Python and charting software.

Symbols, units & horizon
  • H,L,C: bar high, low and close in price units
  • TR: true range including the gap from prior close
  • ATR: Wilder-smoothed true range
  • n: positive window length
  • SMA,EMA: simple and exponential moving averages
  • a: EMA update fraction
  • Gₜ,Dₜ: nonnegative gain and loss magnitude of a close change
  • Ḡ,D̄: Wilder-smoothed gains and losses, not simple averages after seeding
  • RS: ratio of smoothed gains to losses
  • RSI: ratio scaled to 0–100
  • sₜ: rolling sample price SD
  • k: band width in SD units
  • ±: plus for upper band, minus for lower
  • pⱼ,vⱼ: transaction price and size
  • VWAP: transaction volume-weighted price

When and why to use this

Use ATR for volatility-scaled distances, averages for trend definitions, RSI for a momentum-state feature, bands for relative location and VWAP for an execution benchmark. Each use still needs a matching decision horizon.

An indicator transforms observations; it does not add an independent source of information to the same OHLCV data. A moving average smooths noise while introducing lag. ATR measures the size of price movement, including gaps. RSI compares smoothed gains and losses. Bands express price relative to a chosen centre and scale. Use these quantities for explicit hypotheses, normalisation or risk rather than stacking similar indicators as if they were independent confirmations.

TRt=max⁡(Ht−Lt,|Ht−Ct−1|,|Lt−Ct−1|),ATRt=(n−1)ATRt−1+TRtn
Algebra and arithmetic

Include gaps, then update an average

  1. True range is the largest of the intrabar span and the two distances from the previous close. With previous close 100, H=105,L=103, the candidates are 2,5,3, so TR=5.
  2. Keep n−1 parts of the old Wilder average and add one part of the new TR, then divide by n. Equivalently ATRₜ=ATRₜ₋₁+(TRₜ−ATRₜ₋₁)/n.
Work it by hand

With old ATR=2,n=14,TR=5, new ATR=(26+5)/14=2.214286.

SMAt=1n∑j=0n−1Ct−j,EMAt=aCt+(1−a)EMAt−1,a=2n+1
Algebra and arithmetic

Give recent prices explicit weights

  1. An n-bar simple average adds the latest n closes and divides by n. For 100,102,104 with n=3, SMA=102.
  2. EMAₜ−EMAₜ₋₁=a(Cₜ−EMAₜ₋₁). For n=3, a=2/4=.5. Old EMA=100 and new close=104 gives EMA=102. The seed affects the early series.
Work it by hand

The weights a and 1−a sum to 1, so an EMA update is a weighted average, not a forecast equation.

Gt=max⁡(Ct−Ct−1,0),Dt=max⁡(Ct−1−Ct,0),RS=G‾D‾,RSI=100G‾G‾+D‾
Algebra and arithmetic

Simplify the RSI ratio

  1. For positive average loss, start from 100−100/(1+RS). Combine the terms: 100RS/(1+RS).
  2. Substitute RS=Ḡ/D̄ and multiply numerator and denominator by D̄ to obtain 100Ḡ/(Ḡ+D̄).
Work it by hand

Average gain=2, average loss=1 gives RSI=200/3=66.67. This normalisation does not say there is a 66.67% chance of an up move.

bandst=SMAt±kst,VWAP=∑jpjvj∑jvj
Algebra and arithmetic

Construct bands and a volume-weighted average

  1. With mean 100, sample SD 2 and k=2, add and subtract 4 to get [96,104]. Solving an upper-band observation for scale gives s=(upper−SMA)/k when k>0.
  2. VWAP is total price×volume divided by total volume. For 100 shares at 10 and 300 at 12: (1000+3600)/400=11.5.
Work it by hand

The unweighted mean price would be 11. Larger volume at 12 pulls VWAP toward 12.

Wilder smoothing gives each new observation weight 1/n; the conventional EMA uses 2/(n+1), so the two are not interchangeable. The functions seed Wilder averages with the first n observations and seed EMA with the first value. Platforms may seed differently. ATR is in price units, not a percentage. Dividing by price or using return volatility changes the interpretation.

For RSI, smooth positive and negative close changes separately using the same rule. When average loss is zero but gains are positive, RSI is 100; when gains are zero but losses positive, it is 0. With both zero, the ratio is undefined; our teaching convention reports 50 and states that convention. “Overbought” does not by itself mean the next return is negative.

Bollinger-style bands use a moving average plus or minus a chosen multiple of rolling SD. The example uses sample SD with n−1; some platforms use population SD. A ±2 SD band around recent prices is not automatically a 95% forecast interval for the next price. Transaction-level VWAP weights actual trade prices by size. A typical-price OHLCV proxy approximates that average and must be labelled as a proxy.

Python implementation

Self-contained teaching example. Python 3.10+; dependencies and input conventions are shown in the code and notation. Run in your own Python environment.

from statistics import mean, stdev

def wilder(values, n):
    if n<1: raise ValueError("Positive period required")
    out=[None]*len(values)
    if len(values)<n: return out
    out[n-1]=mean(values[:n])
    for i in range(n,len(values)):
        out[i]=((n-1)*out[i-1]+values[i])/n
    return out

def atr(bars,n=14):
    # First bar uses H-L because no prior close is supplied.
    tr=[max(b['h']-b['l'],abs(b['h']-bars[i-1]['c']),abs(b['l']-bars[i-1]['c']))
        if i else b['h']-b['l'] for i,b in enumerate(bars)]
    return wilder(tr,n)

def averages(closes,n):
    if n<1: raise ValueError("Positive period required")
    sma=[None if i<n-1 else mean(closes[i-n+1:i+1]) for i in range(len(closes))]
    ema=[]
    a=2/(n+1)
    for c in closes: ema.append(c if not ema else a*c+(1-a)*ema[-1])
    return sma,ema

def rsi(closes,n=14):
    changes=[b-a for a,b in zip(closes,closes[1:])]
    gains=wilder([max(x,0) for x in changes],n)
    losses=wilder([max(-x,0) for x in changes],n)
    values=[None if g is None else (50 if g+l==0 else 100*g/(g+l))
            for g,l in zip(gains,losses)]
    return [None]+values if closes else []

def bands(window,k=2):
    centre,sd=mean(window),stdev(window)  # n-1 sample denominator
    return centre-k*sd,centre,centre+k*sd

def vwap(transactions):
    volume=sum(v for p,v in transactions)
    if volume<=0 or any(v<0 for p,v in transactions): raise ValueError("Invalid volume")
    return sum(p*v for p,v in transactions)/volume

print(bands([98,100,102]), vwap([(10,100),(12,300)]))

Continue learning

Candles, Structures & Pattern Research — all lessons
  1. First principles: what a candle actually records
  2. Doji, hammer, shooting star and long-body bars
  3. Engulfing, inside bars and multi-candle sequences
  4. Trends, ranges, breakouts and chart structures
  5. Indicators as arithmetic: ATR, moving averages, RSI and bands
  6. Known strategy families: from chart idea to complete rules
  7. Pattern recognition: rules, features, shapelets and image models
  8. Quantitative recognition I: build a causal candle feature table
  9. Quantitative recognition II: shapelets and constrained dynamic time warping
  10. Quantitative recognition III: causal encoders and contrastive learning
  11. Quantitative recognition IV: GAF images, CNNs, transformers and visual-model audits
  12. Quantitative recognition V: calibrate, abstain and test the complete strategy
  13. Research review: what the evidence does and does not establish

Quantitative finance and development glossary · Python resources and libraries · Research sources and limitations