Strategy · Pine Script v6 · Own pane

Strategy Template

by MoDarK-MK· code credited to {{SHORTTITLE}}

This strategy template provides a modular foundation for building TradingView v6 backtests.

View on GitHub

GitHub stars1on the repository
Pine versionv6current
Lines of code593
Settings40inputs
LicenceMPL-2.0
Last update7 weeks ago4 Sep 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new strategy, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart. The trades and results appear in the Strategy Tester tab; adjust the settings with the gear icon.

Source Code

593 lines of Pine Script v6 from assets/templates/strategy_template.pine, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

strategy_template.pine
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © {{SHORTTITLE}}

//@version=6
strategy("{{TITLE}}", shorttitle="{{SHORTTITLE}}", overlay={{OVERLAY}},
     default_qty_type=strategy.percent_of_equity, default_qty_value=10,
     initial_capital=10000, currency=currency.USD,
     commission_type=strategy.commission.percent, commission_value=0.05,
     slippage=1, margin_long=100, margin_short=100,
     pyramiding=1, process_orders_on_close=true, max_labels_count=500)

// Strategy skeleton with four risk modules wired together. The SIGNAL is a
// placeholder EMA cross — replace the longSignal/shortSignal block with your own
// idea and leave the rest of the machinery in place.
//
//   1. Position sizing  — risk-% of equity per trade, sized off the stop distance
//   2. Stops & targets  — ATR or percent stop, TP1/TP2 at R multiples
//   3. Breakeven+trail  — resolved into ONE ratcheting stop price
//   4. Filters          — direction, HTF trend, volatility, session, date window
//
// NOTE (backtest honesty): none of this makes a backtest predictive.
//   - process_orders_on_close=true fills at the signal bar's close. Realistic for
//     close-based signals, optimistic if your idea needs an intrabar fill.
//   - The trailing stop is computed in Pine, so it only moves on bar CLOSE, not
//     intrabar. A broker-side trailing stop would move sooner and exit elsewhere.
//   - Partial exits each record their own closed trade, so strategy.closedtrades
//     roughly doubles with partials on. The dashboard reports positions taken
//     separately — that is the number to check against the 100-trade rule.
//
// pyramiding is a const int, so pyramiding=input.int(...) is a compile error. To
// actually pyramid you must edit the literal above AND raise maxEntriesInput.
// See references/strategy-guide.md before changing any of the risk math.

// ————— Constants
string THEME_DARK  = "Dark"
string THEME_LIGHT = "Light"
color  BULL_DARK   = #26A69A
color  BEAR_DARK   = #EF5350
color  BULL_LIGHT  = #00796B
color  BEAR_LIGHT  = #C62828

string DIR_BOTH  = "Long & Short"
string DIR_LONG  = "Long Only"
string DIR_SHORT = "Short Only"
string STOP_ATR  = "ATR"
string STOP_PCT  = "Percent"
string SIZE_RISK = "Risk %"
string SIZE_EQ   = "Equity %"
string SIZE_FIX  = "Fixed Qty"
string OPP_HOLD  = "Ignore"
string OPP_CLOSE = "Close"
string OPP_FLIP  = "Reverse"

string ID_LONG  = "Long"
string ID_SHORT = "Short"
string ID_L_TP1 = "L TP1"
string ID_L_RUN = "L Runner"
string ID_S_TP1 = "S TP1"
string ID_S_RUN = "S Runner"

// ————— Inputs: Signal
int  fastLengthInput    = input.int(12, "Fast EMA Length", minval=1, group="Signal")
int  slowLengthInput    = input.int(26, "Slow EMA Length", minval=1, group="Signal")
bool confirmedOnlyInput = input.bool(true, "Act Only On Confirmed Bars", group="Signal")

// ————— Inputs: Direction & Regime
string tradeDirectionInput = input.string(
     DIR_BOTH, "Trade Direction",
     options=[DIR_BOTH, DIR_LONG, DIR_SHORT], group="Direction & Regime")
bool   useTrendFilterInput = input.bool(true, "Use HTF Trend Filter", group="Direction & Regime")
string trendTfInput        = input.timeframe("240", "Trend Timeframe", group="Direction & Regime")
int    trendMaLenInput     = input.int(200, "Trend MA Length", minval=1, group="Direction & Regime")
bool   useVolFilterInput   = input.bool(false, "Skip Low-Volatility Regimes", group="Direction & Regime")
float  minAtrPercentInput  = input.float(0.15, "Min ATR % Of Price", minval=0.0, step=0.05, group="Direction & Regime")

// ————— Inputs: Session & Date Window
bool   useSessionInput       = input.bool(false, "Restrict To Session", group="Session & Date Window")
string sessionInput          = input.session("0930-1600", "Session", group="Session & Date Window")
string sessionTzInput        = input.string(
     "America/New_York", "Session Timezone",
     options=["America/New_York", "Europe/London", "Asia/Tokyo", "UTC"], group="Session & Date Window")
bool   closeOutsideSessInput = input.bool(true, "Flatten At Session End", group="Session & Date Window")
bool   useDateWindowInput    = input.bool(false, "Restrict To Date Window", group="Session & Date Window")
int    startDateInput        = input.time(timestamp("01 Jan 2020 00:00"), "Start", group="Session & Date Window")
int    endDateInput          = input.time(timestamp("01 Jan 2030 00:00"), "End", group="Session & Date Window")

// ————— Inputs: Position Sizing
string sizingModeInput     = input.string(
     SIZE_RISK, "Sizing Mode",
     options=[SIZE_RISK, SIZE_EQ, SIZE_FIX], group="Position Sizing")
float  riskPercentInput    = input.float(
     1.0, "Risk % Of Equity Per Trade", minval=0.1, maxval=10.0, step=0.1, group="Position Sizing")
float  equityPercentInput  = input.float(
     10.0, "Equity % Per Trade", minval=0.1, maxval=100.0, step=1.0, group="Position Sizing")
float  fixedQtyInput       = input.float(1.0, "Fixed Quantity", minval=0.000001, group="Position Sizing")
float  maxNotionalPctInput = input.float(
     100.0, "Max Notional % Of Equity", minval=1.0, maxval=500.0, step=10.0, group="Position Sizing")
float  qtyStepInput        = input.float(
     0.0, "Quantity Step (0 = fractional ok)", minval=0.0, step=0.1, group="Position Sizing")

// ————— Inputs: Stops & Targets
string stopModeInput     = input.string(STOP_ATR, "Stop Model", options=[STOP_ATR, STOP_PCT], group="Stops & Targets")
int    atrLenInput       = input.int(14, "ATR Length", minval=1, group="Stops & Targets")
float  atrStopMultInput  = input.float(2.0, "ATR Stop Multiple", minval=0.1, step=0.1, group="Stops & Targets")
float  stopPercentInput  = input.float(2.0, "Percent Stop", minval=0.1, step=0.1, group="Stops & Targets")
int    minStopTicksInput = input.int(4, "Minimum Stop (ticks)", minval=1, group="Stops & Targets")
float  tp1RmultInput     = input.float(1.0, "TP1 (R multiple)", minval=0.1, step=0.1, group="Stops & Targets")
float  tp2RmultInput     = input.float(2.0, "TP2 (R multiple)", minval=0.1, step=0.1, group="Stops & Targets")
bool   usePartialsInput  = input.bool(true, "Take Partial At TP1", group="Stops & Targets")
int    tp1PercentInput   = input.int(
     50, "TP1 Size (% of position)", minval=1, maxval=99, group="Stops & Targets")
bool   useTp2Input       = input.bool(true, "Use TP2 On The Runner", group="Stops & Targets")

// ————— Inputs: Breakeven & Trailing
string BE_GROUP = "Breakeven & Trailing"
bool  useBreakevenInput    = input.bool(true, "Move Stop To Breakeven", group=BE_GROUP)
float beTriggerRmultInput  = input.float(1.0, "Breakeven Trigger (R)", minval=0.1, step=0.1, group=BE_GROUP)
float beOffsetRmultInput   = input.float(0.1, "Breakeven Offset (R)", minval=0.0, step=0.05, group=BE_GROUP)
bool  useTrailInput        = input.bool(true, "Use Trailing Stop", group=BE_GROUP)
float trailStartRmultInput = input.float(1.5, "Trail Starts At (R)", minval=0.1, step=0.1, group=BE_GROUP)
float trailAtrMultInput    = input.float(2.0, "Trail Distance (ATR)", minval=0.1, step=0.1, group=BE_GROUP)

// ————— Inputs: Trade Management
string oppositeModeInput = input.string(
     OPP_FLIP, "On Opposite Signal",
     options=[OPP_HOLD, OPP_CLOSE, OPP_FLIP], group="Trade Management")
int    cooldownBarsInput = input.int(0, "Cooldown After Exit (bars)", minval=0, group="Trade Management")
int    maxEntriesInput   = input.int(1, "Max Concurrent Entries", minval=1, group="Trade Management")
bool   reanchorInput     = input.bool(true, "Re-anchor Plan To Fill Price", group="Trade Management")

// ————— Inputs: Account Risk Guards
// These are engine-level circuit breakers, not per-trade stops. Per-trade risk
// is sized above; this is what stops a bad DAY or a bad month from compounding
// into a blown account, and it is the layer most published strategies omit.
//
// Each is disabled by its maximum value rather than by being skipped, so the
// calls stay unconditional at global scope — the engine expects them to be
// configured identically on every bar.
string RISK_GROUP = "Account Risk Guards"
float maxIntradayLossInput = input.float(
     100.0, "Halt After Daily Loss Of (% equity)", minval=1.0, maxval=100.0, step=0.5,
     group=RISK_GROUP,
     tooltip="Stops all trading for the rest of the day once the day's loss reaches " +
     "this share of equity. 100 disables it.")
float maxDrawdownInput = input.float(
     100.0, "Halt After Drawdown Of (% equity)", minval=1.0, maxval=100.0, step=1.0,
     group=RISK_GROUP,
     tooltip="Stops trading permanently once equity falls this far below its peak. " +
     "100 disables it.")
int maxConsLossDaysInput = input.int(
     1000, "Halt After Consecutive Losing Days", minval=1, maxval=1000, group=RISK_GROUP,
     tooltip="1000 disables it.")
int maxDailyOrdersInput = input.int(
     1000, "Max Filled Orders Per Day", minval=1, maxval=1000, group=RISK_GROUP,
     tooltip="A blunt guard against a signal that starts firing every bar. 1000 disables it.")

// ————— Inputs: Appearance
string themeInput         = input.string(THEME_DARK, "Theme", options=[THEME_DARK, THEME_LIGHT], group="Appearance")
bool   showDashboardInput = input.bool(true, "Show Dashboard", group="Appearance")
string tablePosInput      = input.string(
     "Top Right", "Dashboard Position",
     options=["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group="Appearance")
int    panelTranspInput   = input.int(
     15, "Dashboard Transparency", minval=0, maxval=90, group="Appearance")
bool   showLevelsInput    = input.bool(true, "Plot Stop / Target Levels", group="Appearance")
bool   testModeInput      = input.bool(false, "Test Mode", group="Debug/Test")

// ————— Function declarations
// @function Returns the bullish accent color for the selected theme.
getBullColor(string theme) =>
    switch theme
        THEME_DARK  => BULL_DARK
        THEME_LIGHT => BULL_LIGHT
        => BULL_DARK

// @function Returns the bearish accent color for the selected theme.
getBearColor(string theme) =>
    switch theme
        THEME_DARK  => BEAR_DARK
        THEME_LIGHT => BEAR_LIGHT
        => BEAR_DARK

// @function Primary text color — the value column reads first.
getTextColor(string theme) =>
    theme == THEME_LIGHT ? #131722 : #d1d4dc

// @function Secondary text color — deliberately lower contrast than the value.
getMutedColor(string theme) =>
    theme == THEME_LIGHT ? #4a4e58 : #b2b5be

// @function Panel background. Keep transparency low (10-25): at 80+ the candles
// behind the panel win and the numbers stop being readable.
getPanelColor(string theme, int transp) =>
    color.new(theme == THEME_LIGHT ? #ffffff : #131722, math.max(0, math.min(100, transp)))

// @function Maps a position-name string to the matching position.* constant.
tablePosFrom(string name) =>
    switch name
        "Top Left"     => position.top_left
        "Bottom Right" => position.bottom_right
        "Bottom Left"  => position.bottom_left
        => position.top_right

// @function One label/value dashboard row. Both colors are required arguments,
// which is what stops a cell shipping without text_color — Pine defaults cell
// text to BLACK, invisible on a dark panel. Labels align left and values align
// right so the digits form a column and don't jitter as the price ticks.
dashRow(table t, int row, string label, string value, color labelCol, color valueCol) =>
    table.cell(t, 0, row, label, text_color=labelCol, text_size=size.small,
         text_halign=text.align_left)
    table.cell(t, 1, row, value, text_color=valueCol, text_size=size.small,
         text_halign=text.align_right)

// @function Stop distance in PRICE units, floored at a minimum tick count so a
// tight-range bar can't produce a zero-width stop — that would divide by zero in
// the sizing math and request an unbounded position.
stopDistance(string mode, float atrValue, float px) =>
    float raw = switch mode
        STOP_ATR => atrValue * atrStopMultInput
        STOP_PCT => px * stopPercentInput / 100
        => atrValue * atrStopMultInput
    math.max(raw, syminfo.mintick * minStopTicksInput)

// @function Contracts/shares sized so a full stop-out costs about riskPercentInput
// of equity. syminfo.pointvalue converts a 1.0 price move on 1 contract into
// account currency — it is 1 for stocks/crypto but 50 for ES, so omitting it
// would size futures positions 50x too large.
// Caveat: when syminfo.currency differs from the account currency, riskCapital and
// perUnitRisk are denominated differently — convert with request.currency_rate().
positionQty(float stopDist, float px) =>
    float equityBasis = strategy.equity      // includes open P&L; use
                                             // strategy.initial_capital +
                                             // strategy.netprofit to size off
                                             // closed equity instead
    float riskCapital = equityBasis * riskPercentInput / 100
    float perUnitRisk = stopDist * syminfo.pointvalue
    float riskQty     = perUnitRisk > 0 ? riskCapital / perUnitRisk : 0.0
    float notional    = px * syminfo.pointvalue
    // Separate from the risk cap: a very tight stop implies leverage the engine
    // would reject outright (margin_long=100), losing the trade with no error.
    float leverageQty = notional > 0 ? equityBasis * maxNotionalPctInput / 100 / notional : 0.0
    float equityQty   = notional > 0 ? equityBasis * equityPercentInput / 100 / notional : 0.0
    float raw = switch sizingModeInput
        SIZE_RISK => math.min(riskQty, leverageQty)
        SIZE_EQ   => math.min(equityQty, leverageQty)
        SIZE_FIX  => fixedQtyInput
        => math.min(riskQty, leverageQty)
    // Rounding down CAN produce 0 — the caller must gate on qty > 0, otherwise
    // trades silently vanish and the backtest is biased toward whichever regime
    // happened to size above one lot.
    qtyStepInput > 0 ? math.floor(raw / qtyStepInput) * qtyStepInput : raw

// ————— Account risk guards
// Called unconditionally at global scope, which is what the engine expects.
// A guard is "off" when it holds its maximum value, so there is no branch here
// and no bar on which the configuration differs.
strategy.risk.max_intraday_loss(maxIntradayLossInput, strategy.percent_of_equity)
strategy.risk.max_drawdown(maxDrawdownInput, strategy.percent_of_equity)
strategy.risk.max_cons_loss_days(maxConsLossDaysInput)
strategy.risk.max_intraday_filled_orders(maxDailyOrdersInput)

// ————— Calculations
color bullColor  = getBullColor(themeInput)
color bearColor  = getBearColor(themeInput)
color textColor  = getTextColor(themeInput)
color mutedColor = getMutedColor(themeInput)

// Note: For heavy calculation caching on realtime bars, see references/performance-guide.md.

// Every ta.*/request.* result is computed into a named variable BEFORE any
// condition uses it — v6 evaluates and/or lazily, so a call placed after an
// `and` may not run on every bar, which corrupts its internal state.
float fastMa   = ta.ema(close, fastLengthInput)
float slowMa   = ta.ema(close, slowLengthInput)
float atrValue = ta.atr(atrLenInput)
float trendMa  = request.security(
     syminfo.tickerid, trendTfInput, ta.ema(close, trendMaLenInput)[1],
     lookahead=barmerge.lookahead_off)

// Example: Gradient coloring for visual polish
float rsiValue = ta.rsi(close, 14)
color gradientColor = color.from_gradient(rsiValue, 30, 70, bearColor, bullColor)

// ————— Visuals (Optional for strategies, but good for debugging)
// Use display.pane to keep the status line clean for plots that are just visual aids
plot(fastMa, "Fast MA", color=gradientColor, linewidth=2, display=display.pane)
// Example of a hidden series used only for external referencing
plot(rsiValue, "RSI (Hidden)", display=display.none)

float atrPercent  = close > 0 ? atrValue / close * 100 : 0.0
bool  barOk       = not confirmedOnlyInput or barstate.isconfirmed
bool  longAllowed  = tradeDirectionInput != DIR_SHORT
bool  shortAllowed = tradeDirectionInput != DIR_LONG
bool  trendUp     = not useTrendFilterInput or na(trendMa) or close > trendMa
bool  trendDown   = not useTrendFilterInput or na(trendMa) or close < trendMa
bool  volOk       = not useVolFilterInput or atrPercent >= minAtrPercentInput
// time(...) returns na OUTSIDE the session, so this must be an na test, not != 0.
bool  inSession   = not useSessionInput or not na(time(timeframe.period, sessionInput, sessionTzInput))
// `time` is the bar's OPEN time, so the end bound compares against the bar that
// starts before the cut-off.
bool  inWindow    = not useDateWindowInput or (time >= startDateInput and time <= endDateInput)

var int lastExitBar = na
bool cooldownOk = cooldownBarsInput == 0 or na(lastExitBar) or (bar_index - lastExitBar) >= cooldownBarsInput

// >>> Replace this block with your own idea. Everything below it is generic. <<<
bool longSignal  = ta.crossover(fastMa, slowMa)
bool shortSignal = ta.crossunder(fastMa, slowMa)

bool baseOk     = barOk and volOk and inSession and inWindow and cooldownOk
bool roomLeft   = strategy.opentrades < maxEntriesInput
bool longEntry  = longSignal and longAllowed and trendUp and baseOk and roomLeft
bool shortEntry = shortSignal and shortAllowed and trendDown and baseOk and roomLeft

// ————— Calculations: risk state
var float entryRef   = na    // price the plan was built from
var float initStop   = na
var float tp1Price   = na
var float tp2Price   = na
var float activeStop = na
var float runExtreme = na    // running high (long) / low (short) since entry
var bool  beArmed    = false
var bool  tp1Done    = false
var int   entryCount = 0

// Reset only on the bar we JUST went flat. Writing `if strategy.position_size == 0`
// would also fire on the signal bar (the entry order hasn't filled yet), wiping
// the plan armed a few lines below.
if strategy.position_size == 0 and strategy.position_size[1] != 0
    entryRef    := na
    initStop    := na
    tp1Price    := na
    tp2Price    := na
    activeStop  := na
    runExtreme  := na
    beArmed     := false
    tp1Done     := false
    lastExitBar := bar_index

float stopDist   = stopDistance(stopModeInput, atrValue, close)
float plannedQty = positionQty(stopDist, close)

// ————— Strategy calls: entries
if longEntry and plannedQty > 0
    if strategy.position_size < 0 and oppositeModeInput == OPP_CLOSE
        strategy.close(ID_SHORT, comment="Opposite signal")
    if strategy.position_size <= 0 or oppositeModeInput != OPP_HOLD
        entryRef    := close
        initStop    := math.round_to_mintick(entryRef - stopDist)
        float rDist  = entryRef - initStop
        tp1Price    := math.round_to_mintick(entryRef + rDist * tp1RmultInput)
        tp2Price    := math.round_to_mintick(entryRef + rDist * tp2RmultInput)
        activeStop  := initStop
        runExtreme  := high
        beArmed     := false
        tp1Done     := false
        strategy.entry(ID_LONG, strategy.long, qty=plannedQty,
             alert_message='{"action":"buy","ticker":"{{ticker}}","price":{{close}},"time":"{{time}}"}')

if shortEntry and plannedQty > 0
    if strategy.position_size > 0 and oppositeModeInput == OPP_CLOSE
        strategy.close(ID_LONG, comment="Opposite signal")
    if strategy.position_size >= 0 or oppositeModeInput != OPP_HOLD
        entryRef    := close
        initStop    := math.round_to_mintick(entryRef + stopDist)
        float rDist  = initStop - entryRef
        tp1Price    := math.round_to_mintick(entryRef - rDist * tp1RmultInput)
        tp2Price    := math.round_to_mintick(entryRef - rDist * tp2RmultInput)
        activeStop  := initStop
        runExtreme  := low
        beArmed     := false
        tp1Done     := false
        strategy.entry(ID_SHORT, strategy.short, qty=plannedQty,
             alert_message='{"action":"sell","ticker":"{{ticker}}","price":{{close}},"time":"{{time}}"}')

// Re-anchor the plan to the REAL fill price once it exists — this is what makes
// gaps and slippage honest instead of assuming a fill at the signal close.
bool justFilled = strategy.position_size != 0 and strategy.position_size[1] == 0
if justFilled
    entryCount += 1
    if reanchorInput
        float avgFill = strategy.position_avg_price
        bool  isLong  = strategy.position_size > 0
        initStop    := math.round_to_mintick(isLong ? avgFill - stopDist : avgFill + stopDist)
        float rDist  = math.abs(avgFill - initStop)
        tp1Price    := math.round_to_mintick(isLong ? avgFill + rDist * tp1RmultInput : avgFill - rDist * tp1RmultInput)
        tp2Price    := math.round_to_mintick(isLong ? avgFill + rDist * tp2RmultInput : avgFill - rDist * tp2RmultInput)
        activeStop  := initStop

// ————— Strategy calls: breakeven + trailing resolved into ONE stop
// Deliberately NOT handing breakeven to stop= and trailing to trail_price= in the
// same exit: in v6 whichever level triggers first wins, so two independent
// mechanisms interact unpredictably and never ratchet against each other. One
// number, ratcheted with math.max/min, is predictable and plottable. The cost is
// that it only moves on bar close.
if strategy.position_size > 0 and not na(initStop)
    float avgPrice = strategy.position_avg_price
    float rDist    = avgPrice - initStop
    runExtreme := math.max(nz(runExtreme, high), high)
    if useBreakevenInput and not beArmed and rDist > 0 and high >= avgPrice + rDist * beTriggerRmultInput
        beArmed := true
    // The offset defaults above 0 because position_avg_price excludes commission —
    // a literal-breakeven stop still books a round-trip-cost loss.
    float beStop = beArmed and rDist > 0 ? math.round_to_mintick(avgPrice + rDist * beOffsetRmultInput) : na
    float trStop = na
    if useTrailInput and rDist > 0 and runExtreme >= avgPrice + rDist * trailStartRmultInput
        trStop := math.round_to_mintick(runExtreme - atrValue * trailAtrMultInput)
    float best = math.max(nz(beStop, initStop), nz(trStop, initStop))
    activeStop := math.max(nz(activeStop, initStop), best)

if strategy.position_size < 0 and not na(initStop)
    float avgPrice = strategy.position_avg_price
    float rDist    = initStop - avgPrice
    runExtreme := math.min(nz(runExtreme, low), low)
    if useBreakevenInput and not beArmed and rDist > 0 and low <= avgPrice - rDist * beTriggerRmultInput
        beArmed := true
    float beStop = beArmed and rDist > 0 ? math.round_to_mintick(avgPrice - rDist * beOffsetRmultInput) : na
    float trStop = na
    if useTrailInput and rDist > 0 and runExtreme <= avgPrice - rDist * trailStartRmultInput
        trStop := math.round_to_mintick(runExtreme + atrValue * trailAtrMultInput)
    float best = math.min(nz(beStop, initStop), nz(trStop, initStop))
    activeStop := math.min(nz(activeStop, initStop), best)

// ————— Strategy calls: exits
// Calling strategy.exit() with the SAME id every bar UPDATES the live order — that
// is the mechanism that makes the computed trailing stop actually move. A
// bar-varying id would stack duplicate orders instead.
if strategy.position_size > 0 and not na(activeStop)
    // qty_percent of a 1-contract position is 0.5; on whole-lot instruments the
    // partial would round away or close everything, so require room to split.
    bool canSplit = usePartialsInput and (qtyStepInput <= 0 or strategy.position_size >= qtyStepInput * 2)
    if canSplit and not tp1Done
        strategy.exit(ID_L_TP1, from_entry=ID_LONG, qty_percent=tp1PercentInput,
             limit=tp1Price, stop=activeStop,
             alert_message='{"action":"tp1","ticker":"{{ticker}}","time":"{{time}}"}')
    strategy.exit(ID_L_RUN, from_entry=ID_LONG,
         limit=useTp2Input ? tp2Price : na, stop=activeStop,
         alert_message='{"action":"close_buy","ticker":"{{ticker}}","time":"{{time}}"}')

if strategy.position_size < 0 and not na(activeStop)
    bool canSplit = usePartialsInput and (qtyStepInput <= 0 or math.abs(strategy.position_size) >= qtyStepInput * 2)
    if canSplit and not tp1Done
        strategy.exit(ID_S_TP1, from_entry=ID_SHORT, qty_percent=tp1PercentInput,
             limit=tp1Price, stop=activeStop,
             alert_message='{"action":"tp1","ticker":"{{ticker}}","time":"{{time}}"}')
    strategy.exit(ID_S_RUN, from_entry=ID_SHORT,
         limit=useTp2Input ? tp2Price : na, stop=activeStop,
         alert_message='{"action":"close_sell","ticker":"{{ticker}}","time":"{{time}}"}')

// A partial filled when the position shrank without going flat. position_avg_price
// does NOT change on a partial close, so the breakeven/trail math above stays
// valid; position_size DOES change, so nothing derived from it may be cached.
if strategy.position_size != 0 and math.abs(strategy.position_size) < math.abs(nz(strategy.position_size[1]))
    tp1Done := true

// Filters gate ENTRIES only. These flushes run unconditionally, or a position
// opened inside the window would be stranded there forever.
if useDateWindowInput and not inWindow and strategy.position_size != 0
    strategy.close_all(comment="Window end")
if useSessionInput and closeOutsideSessInput and not inSession and strategy.position_size != 0
    strategy.close_all(comment="Session end")

// ————— Visuals
// Emphasis follows proximity: the stop is the loudest line, TP1 (the nearer,
// more actionable target) next, TP2 faintest. display= keeps the three level
// series out of the status line, where three `na`s add nothing.
plot(showLevelsInput and strategy.position_size != 0 ? activeStop : na, "Stop",
     color=color.new(bearColor, 10), style=plot.style_linebr, linewidth=2,
     display=display.pane + display.price_scale)
plot(showLevelsInput and strategy.position_size != 0 and not tp1Done ? tp1Price : na, "TP1",
     color=color.new(bullColor, 20), style=plot.style_linebr, linewidth=2,
     display=display.pane + display.price_scale)
plot(showLevelsInput and strategy.position_size != 0 and useTp2Input ? tp2Price : na, "TP2",
     color=color.new(bullColor, 45), style=plot.style_linebr, linewidth=1,
     display=display.pane + display.price_scale)

// plotshape rather than label.new: markers on every signal bar would otherwise
// consume the finite 500-label pool and silently drop the oldest ones.
plotshape(longEntry and plannedQty > 0, "Long Entry", shape.triangleup,
     location.belowbar, color=bullColor, size=size.tiny)
plotshape(shortEntry and plannedQty > 0, "Short Entry", shape.triangledown,
     location.abovebar, color=bearColor, size=size.tiny)

// Rows 1-4 are live position state, rows 6-10 are backtest quality; row 5 is a
// divider so the two groups don't read as one undifferentiated wall.
var table dashboard = table.new(
     tablePosFrom(tablePosInput), 2, 12,
     bgcolor=getPanelColor(themeInput, panelTranspInput), border_width=1,
     border_color=getPanelColor(themeInput, 100),
     frame_color=color.new(mutedColor, 70), frame_width=1)

if showDashboardInput and barstate.islast
    float winRate    = strategy.closedtrades > 0 ? strategy.wintrades / strategy.closedtrades * 100 : na
    float profitFact = strategy.grossloss > 0 ? strategy.grossprofit / strategy.grossloss : na
    float netPct     = strategy.initial_capital > 0 ? strategy.netprofit / strategy.initial_capital * 100 : na
    bool  isLong     = strategy.position_size > 0
    bool  isFlat     = strategy.position_size == 0
    string posTxt    = isFlat ? "Flat" :
         (isLong ? "Long " : "Short ") + str.tostring(math.abs(strategy.position_size), "#.####")
    string stopTag   = beArmed ? " (BE)" : ""
    bool   realismOk = entryCount >= 100 and not na(profitFact) and profitFact > 1
    table.cell(dashboard, 0, 0, "{{SHORTTITLE}}", text_color=textColor,
         text_size=size.normal, text_halign=text.align_center)
    table.cell(dashboard, 1, 0, "", text_color=textColor)
    table.merge_cells(dashboard, 0, 0, 1, 0)
    dashRow(dashboard, 1, "Position", posTxt, mutedColor,
         isFlat ? mutedColor : isLong ? bullColor : bearColor)
    dashRow(dashboard, 2, "Entry",
         isFlat ? "--" : str.tostring(strategy.position_avg_price, format.mintick),
         mutedColor, textColor)
    dashRow(dashboard, 3, "Stop",
         isFlat or na(activeStop) ? "--" : str.tostring(activeStop, format.mintick) + stopTag,
         mutedColor, isFlat ? mutedColor : bearColor)
    dashRow(dashboard, 4, "TP1 / TP2", isFlat or na(tp1Price) ? "--" :
         str.tostring(tp1Price, format.mintick) + (tp1Done ? " x" : "") + " / " +
         (useTp2Input ? str.tostring(tp2Price, format.mintick) : "--"),
         mutedColor, isFlat ? mutedColor : bullColor)
    table.cell(dashboard, 0, 5, "", height=0.4, bgcolor=color.new(mutedColor, 60))
    table.cell(dashboard, 1, 5, "", height=0.4, bgcolor=color.new(mutedColor, 60))
    dashRow(dashboard, 6, "Positions taken", str.tostring(entryCount), mutedColor, textColor)
    dashRow(dashboard, 7, "Closed records", str.tostring(strategy.closedtrades) +
         (usePartialsInput ? " (incl. partials)" : ""), mutedColor, textColor)
    dashRow(dashboard, 8, "Win rate",
         na(winRate) ? "--" : str.tostring(winRate, "#.#") + "%", mutedColor, textColor)
    dashRow(dashboard, 9, "Profit factor",
         na(profitFact) ? "--" : str.tostring(profitFact, "#.##"), mutedColor,
         na(profitFact) ? mutedColor : profitFact > 1 ? bullColor : bearColor)
    dashRow(dashboard, 10, "Net profit",
         na(netPct) ? "--" : str.tostring(netPct, "#.#") + "%", mutedColor,
         na(netPct) ? mutedColor : netPct >= 0 ? bullColor : bearColor)
    table.cell(dashboard, 0, 11, "Realism", text_color=mutedColor, text_size=size.small,
         text_halign=text.align_left)
    table.cell(dashboard, 1, 11, realismOk ? "OK" : entryCount < 100 ? "Too few trades" : "PF <= 1",
         text_size=size.small, text_halign=text.align_right,
         bgcolor=color.new(realismOk ? bullColor : bearColor, 85),
         text_color=realismOk ? bullColor : bearColor)

// ————— Alerts
alertcondition(longEntry, "Long Entry Signal", "Long entry signal fired.")
alertcondition(shortEntry, "Short Entry Signal", "Short entry signal fired.")

// ————— Debug / Test block (keep testModeInput false when handing off)
int TEST_COUNT = 6      // keep in sync with the assertFailed() calls below
var int passCount = 0
var int failCount = 0

// @function Draws a failure marker and RETURNS 1 on failure, 0 on pass.
// Pine forbids a function from modifying a global (CE10088), so the counting
// happens at the call site. Drawing only on failure keeps a long backtest from
// exhausting the 500-label budget on passing bars.
assertFailed(bool condition, string description) =>
    if not condition
        label.new(bar_index, high, "FAIL: " + description, color=bearColor,
             textcolor=color.white, style=label.style_label_down, size=size.small)
    condition ? 0 : 1

if testModeInput
    int fails = 0
    // 1. Sizing must never request a negative quantity.
    fails += assertFailed(plannedQty >= 0, "planned qty is negative")
    // 2. A zero stop distance would divide by zero in the sizing math.
    fails += assertFailed(stopDist > 0, "stop distance collapsed to zero")
    // 3. The stop may only ever ratchet in favour of an open long.
    fails += assertFailed(
         strategy.position_size <= 0 or na(activeStop) or na(activeStop[1]) or activeStop >= activeStop[1],
         "long stop moved against the position")
    // 4. Realised risk on the arming bar must respect the configured risk cap.
    fails += assertFailed(
         sizingModeInput != SIZE_RISK or not justFilled or na(entryRef) or na(initStop) or
         math.abs(plannedQty * (entryRef - initStop) * syminfo.pointvalue) <=
         strategy.equity * riskPercentInput / 100 * 1.05,
         "position risk exceeds the configured risk %")
    // 5. Targets must sit on the profitable side of the entry.
    fails += assertFailed(
         na(tp1Price) or na(entryRef) or strategy.position_size == 0 or
         (strategy.position_size > 0 ? tp1Price > entryRef : tp1Price < entryRef),
         "TP1 is on the wrong side of entry")
    // 6. No position may survive outside the backtest window.
    fails += assertFailed(
         not useDateWindowInput or inWindow or strategy.position_size == 0 or
         na(strategy.position_size[1]) or strategy.position_size[1] != 0,
         "position held outside the date window")
    failCount += fails
    passCount += TEST_COUNT - fails

var table testTable = table.new(
     position.bottom_left, 2, 2, border_width=1,
     bgcolor=getPanelColor(themeInput, panelTranspInput),
     frame_color=color.new(mutedColor, 70), frame_width=1)
if testModeInput and barstate.islast
    dashRow(testTable, 0, "Passed", str.tostring(passCount), mutedColor, bullColor)
    dashRow(testTable, 1, "Failed", str.tostring(failCount), mutedColor,
         failCount > 0 ? bearColor : bullColor)

Overview

Strategy Template uses a placeholder Exponential Moving Average crossover signal combined with customizable filters for higher-timeframe trend alignment, volatility regime thresholds, session hours, and date windows. Orders are managed with risk-based position sizing, ATR or percentage stops, partial profit targets at R-multiples, and trailing exits. Execution parameters and risk controls can be adjusted directly from the settings pane.

Details

Published by

MoDarK-MK on GitHub

Code credit

{{SHORTTITLE}}, as written in the script's header

Type

Strategy, Pine Script v6

Draws

Separate pane below the chart

Category

Trend, Moving averages, Volatility & bands, Sessions & time

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

ci-cdindicatorslinterpine-scriptpythonstrategiestradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Fast EMA Lengthint12fastLengthInput
Slow EMA Lengthint26slowLengthInput
Act Only On Confirmed BarsbooltrueconfirmedOnlyInput
Trade DirectionstringDIR_BOTHtradeDirectionInput
Use HTF Trend FilterbooltrueuseTrendFilterInput
Trend Timeframetimeframe240trendTfInput
Trend MA Lengthint200trendMaLenInput
Skip Low-Volatility RegimesboolfalseuseVolFilterInput
Min ATR % Of Pricefloat0.15minAtrPercentInput
Restrict To SessionboolfalseuseSessionInput
Sessionsession0930-1600sessionInput
Session TimezonestringAmerica/New_YorksessionTzInput
Show all 40 settings
SettingTypeDefaultVariable
Flatten At Session EndbooltruecloseOutsideSessInput
Restrict To Date WindowboolfalseuseDateWindowInput
Starttimetimestamp("01 Jan 2020 00:00")startDateInput
Endtimetimestamp("01 Jan 2030 00:00")endDateInput
Sizing ModestringSIZE_RISKsizingModeInput
Risk % Of Equity Per Tradefloat1.0riskPercentInput
Equity % Per Tradefloat10.0equityPercentInput
Fixed Quantityfloat1.0fixedQtyInput
Max Notional % Of Equityfloat100.0maxNotionalPctInput
Quantity Step (0 = fractional ok)float0.0qtyStepInput
Stop ModelstringSTOP_ATRstopModeInput
ATR Lengthint14atrLenInput
ATR Stop Multiplefloat2.0atrStopMultInput
Percent Stopfloat2.0stopPercentInput
Minimum Stop (ticks)int4minStopTicksInput
TP1 (R multiple)float1.0tp1RmultInput
TP2 (R multiple)float2.0tp2RmultInput
Take Partial At TP1booltrueusePartialsInput
TP1 Size (% of position)int50tp1PercentInput
Use TP2 On The RunnerbooltrueuseTp2Input
Move Stop To BreakevenbooltrueuseBreakevenInput
Breakeven Trigger (R)float1.0beTriggerRmultInput
Breakeven Offset (R)float0.1beOffsetRmultInput
Use Trailing StopbooltrueuseTrailInput
Trail Starts At (R)float1.5trailStartRmultInput
Trail Distance (ATR)float2.0trailAtrMultInput
On Opposite SignalstringOPP_FLIPoppositeModeInput
Cooldown After Exit (bars)int0cooldownBarsInput
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

4 stars

This strategy trades directional momentum breakouts for crude oil on the price chart. It enters long or short positions when a fast exponential moving average crosses a slow exponential moving average, provided that current Average True Range exceeds its movin

Strategy v6 TrendMoving averages
Updated 2 months ago · MIT GitHub repository of CL Crude Oil Momentum [trugurpala]

Session Wicks

by g-moe
3 stars

This strategy executes long and short trades on the price chart by combining multi-timeframe smoothed Heikin-Ashi trends, higher-timeframe Hull Moving Average variations, and an ATR-based trailing stop. Users can adjust timeframes, smoothing lengths, Hull form

Strategy v5 TrendMoving averages Repaints
Updated 5 months ago · None stated GitHub repository of Session Wicks

3Commas Bot

by regalouisei
2 stars

This strategy draws two moving averages on the price chart and uses their bias to open long or short trades. It places the stop beyond a swing high or low using an ATR risk multiple, then sets a limit exit from the reward-to-risk ratio. An optional ATR trail c

Strategy v5 TrendMoving averages
Updated 8 months ago · None stated GitHub repository of 3Commas Bot
1 stars

The IBKR Safe Forex Strategy is a Pine Script v6 strategy that trades forex pairs using crossovers of 10-period and 50-period EMAs, filtered by a 200-period EMA and restricted to London or New York session overlap. It enters long positions on fast EMA above sl

Strategy v6 TrendMoving averages
Updated 1 year ago · None stated GitHub repository of IBKR Safe Forex Strategy
1 stars

This Pine Script v6 strategy uses multi-timeframe analysis to identify intraday trends and generate trading signals. It requests higher-timeframe data for trend context via EMAs and Supertrend direction. On the local timeframe it plots EMA cross signals for lo

Strategy v6 TrendMoving averages
Updated 2 months ago · None stated GitHub repository of Tn Sovereign MTF Intraday v6

8 Whittle Down

by hasnocool
106 stars

This strategy executes short positions during the end-of-day session (14:29–15:00) when the price is below the 200-day simple moving average. If price moves higher against the initial short trade, it scales into the position by submitting a second short order

Strategy v4 TrendMoving averages
Updated 2 years ago · GPL-3.0 GitHub repository of 8 Whittle Down

More strategys

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers