Skip to content
GetProfitable
Search

Project: a pullback-to-EMA strategy with ATR stops

Lesson 23 · about 13 min

The second project is a full strategy: trend filter, pullback entry, ATR-based stop and target, risk-based sizing, commission and slippage, a test window for in-sample and out-of-sample runs, and order-fill alert messages. It is built the way modules 6 and 7 recommend, so its tester output is at least measuring something you could trade. Whether the numbers are any good on your market is the question the script lets you ask.

The script

//@version=6
strategy("Pullback to EMA", shorttitle="PB-EMA", overlay=true,
     initial_capital=10000,
     default_qty_type=strategy.fixed, default_qty_value=1,
     commission_type=strategy.commission.percent, commission_value=0.05,
     slippage=1, pyramiding=0)

g1 = "Trend"
trendLen = input.int(50, "Trend EMA", minval=5, group=g1)
pullLen  = input.int(20, "Pullback EMA", minval=2, group=g1)
g2 = "Risk"
riskPct  = input.float(1.0, "Risk per trade %", minval=0.1, step=0.1, group=g2)
atrLen   = input.int(14, "ATR length", minval=1, group=g2)
atrMult  = input.float(2.0, "ATR stop multiple", minval=0.5, step=0.25, group=g2)
rr       = input.float(2.0, "Reward to risk", minval=0.5, step=0.25, group=g2)
g3 = "Test window"
startTime = input.time(timestamp("2020-01-01T00:00:00"), "Start", group=g3)
endTime   = input.time(timestamp("2030-01-01T00:00:00"), "End", group=g3)

trendEma = ta.ema(close, trendLen)
pullEma  = ta.ema(close, pullLen)
atr      = ta.atr(atrLen)
inWindow = time >= startTime and time <= endTime

uptrend = close > trendEma and pullEma > trendEma
touched = low <= pullEma
resumed = close > pullEma and close > open
setup   = uptrend and touched and resumed and inWindow

if setup and strategy.position_size == 0
    stopDist    = atr * atrMult
    riskDollars = strategy.equity * riskPct / 100
    qty         = math.floor(riskDollars / stopDist)
    stopPrice   = math.round_to_mintick(close - stopDist)
    targetPrice = math.round_to_mintick(close + stopDist * rr)
    if qty > 0
        strategy.entry("Long", strategy.long, qty=qty,
             alert_message="PB-EMA buy " + syminfo.ticker + " qty " + str.tostring(qty))
        strategy.exit("Exit", "Long", stop=stopPrice, limit=targetPrice,
             alert_message="PB-EMA exit " + syminfo.ticker)

if strategy.position_size > 0 and close < trendEma
    strategy.close("Long", comment="Trend lost", alert_message="PB-EMA trend exit " + syminfo.ticker)

plot(trendEma, "Trend EMA", color=color.blue, linewidth=2)
plot(pullEma,  "Pullback EMA", color=color.orange)
plotshape(setup, "Setup", style=shape.triangleup, location=location.belowbar, color=color.green, size=size.tiny)
bgcolor(inWindow ? na : color.new(color.gray, 90))

Walkthrough

Declaration (module 6). Fixed quantity of 1 is a placeholder; every entry passes its own qty. Commission is 0.05% per fill and slippage one tick. Change both to match your broker before reading any result.

Rules. The trend is up when close is above the 50 EMA and the 20 EMA is above the 50. A pullback is a bar whose low touched the 20 EMA. Resumption is that same bar closing back above the 20 EMA and closing up. All three on one bar make the setup. It is deliberately simple; the point of the project is the machinery around the rules.

Sizing (module 6). Dollars at risk is riskPct of current equity; quantity is that divided by the stop distance, floored to whole units. For futures, divide by stopDist * syminfo.pointvalue; for forex or crypto, remove the floor. The if qty > 0 guard skips trades where the stop is too wide for the risk budget, which is a feature.

Orders (module 6). Entry is a market order filled at the next open. The exit is placed in the same block, in prices rounded to the tick, and activates when the entry fills. A second exit path closes at market if the close falls below the trend EMA, using strategy.close. Each order carries an alert_message so an order-fill alert produces a readable line in a journal or a bot.

Test window (module 7). Two input.time values bound the trades; the gray background shows bars outside the window. Tune with the window set to your in-sample period, then move it to the out-of-sample period once.

Reading the result

Open the Strategy Tester and go through module 7's list. Properties first: are commission and slippage what you set? Then closed trades: below 100, keep going before concluding anything. Then average trade against the round-trip cost, then the largest trade's share of net profit, then percent profitable with the win/loss ratio (this system will have a win rate somewhere around the breakeven for its reward-to-risk, and that is fine if the ratio holds). Then max drawdown, scaled by your intended risk.

Then the checks that matter more than the numbers:

  • Toggle use_bar_magnifier if your plan has it. A 2R target and a 2-ATR stop are far enough apart that both rarely fall in one daily bar, but on a 5-minute chart they will, and the results will move.
  • Run parameter neighbours: trend EMA 40, 50, 60; ATR multiple 1.5, 2, 2.5. Plateau or spike?
  • Run it on three or four related symbols. Does the sign of the expectancy hold?
  • Read the List of Trades for a handful of entries and confirm the fill is the next bar's open and the stop is where the code said.

Extensions

  • A short side, mirrored, with its own statistics in the long/short split.
  • A trailing stop that ratchets to close - atr * atrMult while in the trade, replacing the fixed target.
  • A time stop using strategy.opentrades.entry_bar_index(0).
  • A higher-timeframe trend filter with the [1] plus lookahead_on idiom from module 3.

Each one is a change, so each one is a return to the in-sample window and a fresh out-of-sample check.

Key idea: A testable strategy is rules plus risk-based sizing, realistic costs, tick-rounded stop and target attached at entry, a bounded test window, and alert messages on every order. The rules are the smallest part.

Try it: Run the strategy on a daily chart of a liquid index ETF with the window set to end two years ago. Record trades, avg trade, profit factor and max drawdown. Move the window to the last two years, run once, record the same four numbers next to the first set. Write one sentence about what changed.

Recap

  • Trend, pullback and resumption conditions on one bar make the setup; keep rules simple while building machinery.
  • Size by risk from strategy.equity, floor to whole units, skip trades where quantity rounds to zero.
  • Attach strategy.exit in prices with the entry; add a trend-loss strategy.close as a second exit path.
  • alert_message on every order feeds order-fill alerts to a journal or bot.
  • Bound the test window with input.time, tune in-sample, check out-of-sample once, then test neighbours and other symbols.

See it drawn

Original diagrams for the ideas on this page. Illustrative, not real market data.

Slippage on a market orderA buy order clears four price levels, so the average price paid is worse than the price first quoted.Buy 1,000 shares at marketpricesell orders resting (bar length = size)20.04300 shares20.03200 shares20.01200 shares20.00300 sharesnothing resting at 20.02order sweeps up the bookaverage fill 20.02SLIPPAGE0.02 a share$20.00 in totalintended 20.00Each level fills at its own price; the average is what you really paid.
Slippage on a market order. You click at 20.00, but only 300 shares are resting there, so the rest of the order fills at 20.01, 20.03 and 20.04. The average price paid is 20.02, and that two-cent gap is slippage.
How a position size is worked outAccount size, risk per trade and stop distance feed into one box giving the number of shares.ACCOUNT SIZE$25,000your capitalRISK PER TRADE1%of the accountSTOP DISTANCE$0.50entry to stopPOSITION SIZE500 sharesrisk budget: $25,000 × 1% = $250position size: $250 ÷ $0.50 = 500 shares
Working out a position size. Three numbers decide how big a trade is: the account, the share of it put at risk, and the distance from entry to stop. One percent of $25,000 is a $250 budget, and a $0.50 stop divides into that 500 times.
A trailing stop held two ATRs under a rising priceA rising price line with a stepped line below it that climbs whenever price climbs and holds its level whenever price falls, until price drops onto it.PRICE AND A TRAILING ATR STOP2 × ATRstop hittrailing stoppriceIllustrative prices. The stop follows price up and never moves back down.
A trailing stop set by ATR. Average true range measures how far a market typically travels in a session, so a stop placed a multiple of ATR under price leaves room for ordinary swings. The step line only ever ratchets up, and the circle marks where price falls onto it.