Jayadev Rana Get a custom script
Free Pine Script Indicators / Expectancy & Kelly Lab
Free indicator · Pine Script v6

Expectancy & Kelly Lab

A deliberately simple moving-average signal that keeps its own trade list, then measures its expectancy in R, the full Kelly fraction implied by its own win rate and payoff, and the growth rate you would get at Kelly and at twice Kelly.

IndicatorPine Script v6OverlayFree

Get the codeDownload .pine.txt

Snapshots

The lab table after 209 trades: 35.4% win rate, a 2R target against a 1R stop, expectancy 0.062R per trade, full Kelly 3.1%, and the half-Kelly bet it recommends instead.
The lab table after 209 trades: 35.4% win rate, a 2R target against a 1R stop, expectancy 0.062R per trade, full Kelly 3.1%, and the half-Kelly bet it recommends instead.
The trade accounting kept by hand in plain variables, so every number in the table can be traced to a closed trade rather than to a strategy engine.
The trade accounting kept by hand in plain variables, so every number in the table can be traced to a closed trade rather than to a strategy engine.

How it works

  1. Trades are tracked in ordinary variables: one open trade at a time, a loser is -1R and a winner is the reward multiple, by construction.
  2. Expectancy is the mean of that R list; the Kelly fraction follows from the win rate and the payoff ratio, both measured, never assumed.
  3. The table also shows the growth rate at twice Kelly, which is where a winning edge still ends in ruin.

Settings

SettingDefaultWhat it does
Fast average20Fast moving-average length
Slow average50Slow moving-average length
Stop = ATR x2.0Stop distance, in ATRs
Target = R x2.0Target distance, as a multiple of the risk
Kelly fraction to actually use0.5Fraction of full Kelly to bet, because full Kelly is violent

Source code

In TradingView: open the Pine Editor, create a new indicator, paste the code, then click "Add to chart".

expectancy-kelly.pine.txtGitHubDownload
//@version=6
indicator("Expectancy Lab", overlay = true)

// 1. A deliberately simple edge, so the maths is the subject, not the signal
fastLen = input.int(20, "Fast average")
slowLen = input.int(50, "Slow average")
atrMult = input.float(2.0, "Stop = ATR x", step = 0.5)
rr      = input.float(2.0, "Target = R x", step = 0.5)
kellyCap = input.float(0.5, "Kelly fraction to actually use", step = 0.25)

// 2. The signal. ta.* calls live at the top level, on every bar, always.
atr  = ta.atr(14)
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
long = ta.crossover(fast, slow)
plot(fast, "Fast", color.aqua)
plot(slow, "Slow", color.orange)

// 3. The trade list, built by hand. One open trade at a time, so a "trade"
// is something you can point at instead of something an engine decided.
// A winner is +rr R and a loser is -1 R, by construction.
var float entry = na
var float stop  = na
var float targ  = na
var array<float> rs = array.new<float>()
opened  = false
hitStop = false
hitTarg = false
if na(entry)
    if long and atr > 0
        entry := close
        stop  := close - atrMult * atr
        targ  := close + atrMult * atr * rr
        opened := true
else
    if low <= stop
        array.push(rs, -1.0)
        entry := na
        hitStop := true
    else if high >= targ
        array.push(rs, rr)
        entry := na
        hitTarg := true
plotshape(opened, "Entry", shape.triangleup, location.belowbar, color.teal)
plotshape(hitTarg, "Target", shape.triangledown, location.abovebar, color.green)
plotshape(hitStop, "Stop", shape.xcross, location.abovebar, color.red)

// 4. The two numbers that decide everything
winRate() =>
    n = array.size(rs)
    w = 0
    for i = 0 to math.max(n - 1, 0)
        if n > 0 and array.get(rs, i) > 0
            w += 1
    n > 0 ? w / n : 0.0

avgOf(cond) =>
    n = array.size(rs)
    s = 0.0
    c = 0
    for i = 0 to math.max(n - 1, 0)
        if n > 0
            v = array.get(rs, i)
            if (cond and v > 0) or (not cond and v <= 0)
                s += v
                c += 1
    c > 0 ? s / c : 0.0

// 5. Long-run growth per trade if you bet a fraction f of the account
growth(f, p, q, b) =>
    f <= 0 or f >= 1 ? 0.0 : p * math.log(1 + f * b) + q * math.log(1 - f)

// 6. Expectancy, Kelly, and what over-betting does
var table t = table.new(position.top_right, 2, 9, border_width = 1)
row(r, a, b, bg) =>
    table.cell(t, 0, r, a, text_color = color.white, text_size = size.normal,
      bgcolor = color.new(color.gray, 20), text_halign = text.align_left)
    table.cell(t, 1, r, b, text_color = color.white, bgcolor = bg,
      text_size = size.normal)

if barstate.islast and array.size(rs) > 5
    n  = array.size(rs)
    p  = winRate()
    q  = 1 - p
    aw = avgOf(true)          // average win, in R
    al = math.abs(avgOf(false))   // average loss, in R
    b  = al > 0 ? aw / al : 0.0   // payoff ratio
    exp = p * aw - q * al         // expectancy per trade, in R
    kelly = b > 0 ? (p * b - q) / b : 0.0   // the full Kelly fraction
    used  = kelly * kellyCap
    // growth per trade at different bet sizes, from the same p and b
    gKelly  = growth(math.min(math.max(kelly, 0), 0.99), p, q, b)
    gDouble = growth(math.min(math.max(kelly * 2, 0), 0.99), p, q, b)
    row(0, "EXPECTANCY LAB", str.tostring(n) + " trades",
      color.new(color.blue, 20))
    row(1, "win rate  p", str.tostring(p * 100, "#.0") + "%",
      color.new(color.gray, 30))
    row(2, "avg win / avg loss", str.tostring(aw, "#.00") + "R / " +
      str.tostring(al, "#.00") + "R", color.new(color.gray, 30))
    row(3, "payoff  b", str.tostring(b, "#.00"), color.new(color.gray, 30))
    row(4, "expectancy per trade", str.tostring(exp, "#.000") + "R",
      exp > 0 ? color.new(color.green, 25) : color.new(color.red, 25))
    row(5, "full Kelly  f*", str.tostring(kelly * 100, "#.0") + "%",
      kelly > 0 ? color.new(color.green, 25) : color.new(color.red, 25))
    row(6, "what you should bet", str.tostring(used * 100, "#.0") + "%  (" +
      str.tostring(kellyCap, "#.##") + " Kelly)", color.new(color.teal, 25))
    row(7, "log growth at f*", str.tostring(gKelly, "#.0000"),
      color.new(color.gray, 30))
    worse = gDouble < gKelly
    row(8, "at 2x Kelly", str.tostring(gDouble, "#.0000") +
      (gDouble <= 0 ? "  RUIN" : ""),
      worse ? color.new(color.red, 25) : color.new(color.gray, 30))

Watch it built

This script is written and explained step by step in the video lesson.

Need a custom indicator or strategy?

Non-repainting Pine Script v6, backtested with real costs, alert and webhook ready. Fixed quote within 24 hours.

Free and open source under the Mozilla Public License 2.0. Educational content only, not financial advice. Backtest results are historical and include the costs stated; past performance does not predict future results. © Jayadev Rana · Privacy · Terms