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_magnifierif 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 * atrMultwhile 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]pluslookahead_onidiom 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.exitin prices with the entry; add a trend-lossstrategy.closeas a second exit path. alert_messageon 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.