Skip to content

Program the SMA Crossover Pyramiding trading strategy in TradingView Pine

Program the SMA Crossover Pyramiding trading strategy in TradingView Pine

Section titled “Program the SMA Crossover Pyramiding trading strategy in TradingView Pine”

TL;DR
Build a fast/slow SMA crossover that allows multiple stacked entries (“pyramiding”), sizes trades with ATR risk, and exits via trailing stops.

DifficultyIntermediate
Time to implement15-20 min
CategoryStrategies
//@version=5
strategy("SMA Pyramiding Demo", overlay=true, pyramiding=2, initial_capital=25_000)
fastLen = input.int(20, "Fast SMA")
slowLen = input.int(50, "Slow SMA")
atrLen = input.int(14, "ATR length")
riskATR = input.float(2.0, "Stop ATR multiple", step=0.1)
fast = ta.sma(close, fastLen)
slow = ta.sma(close, slowLen)
atr = ta.atr(atrLen)
longSignal = ta.crossover(fast, slow)
shortSignal = ta.crossunder(fast, slow)
stopOffset = atr * riskATR
if longSignal
strategy.entry("Long", strategy.long)
if shortSignal
strategy.entry("Short", strategy.short)
if strategy.position_size > 0
strategy.exit("Long Exit", "Long", stop=strategy.position_avg_price - stopOffset)
if strategy.position_size < 0
strategy.exit("Short Exit", "Short", stop=strategy.position_avg_price + stopOffset)
plot(fast, "Fast SMA", color=color.new(color.green, 0))
plot(slow, "Slow SMA", color=color.new(color.red, 0))
Tip. `strategy()`’s `pyramiding` setting controls how many additional entries can stack in the same direction. Use inputs to let testers dial this up or down.

Moving averages define trend, but allowing only a single position often underutilises the signal. Pyramiding lets you add to winners while they trend, and ATR-based sizing keeps each leg’s risk consistent regardless of instrument volatility.

  • Configure strategy settings and inputs for SMA crossover pyramiding.
  • Compute adaptive position sizing using ATR and equity risk.
  • Stage primary and add-on entries while keeping risk per leg controlled.
  • Manage trailing exits that close individual legs as the trend reverses.
HelperPurpose
strategy(_, pyramiding=? )Controls how many concurrent entries per direction are allowed.
input.int / input.float / input.boolGather tunable parameters for lengths, risk, and toggles.
ta.sma, ta.atrCompute crossover signals and volatility.
strategy.entry, strategy.exitSubmit entries and link exits to individual legs.
strategy.position_avg_price, strategy.opentradesMonitor filled positions for trailing logic.
  1. Expose strategy settings
    Decide pyramiding depth, risk per leg, and the MA/ATR lengths you want to tune.

    pyramids = input.int(3, "Pyramiding", minval=0, maxval=5)
    riskPct = input.float(0.5, "Risk per leg (%)", minval=0.1, step=0.1)
    strategy("SMA Crossover Pyramiding", overlay=true, pyramiding=pyramids, initial_capital=50_000)
  2. Calculate signals and sizing
    Generate the fast/slow crossover and compute ATR-driven stop distance and quantity.

    fast = ta.sma(close, fastLen)
    slow = ta.sma(close, slowLen)
    atr = ta.atr(atrLen)
    stopDistance = atr * stopATR
    riskCapital = strategy.equity * (riskPct * 0.01)
    qty = stopDistance > 0 ? math.floor(riskCapital / stopDistance) : 0
  3. Submit entries and manage exits
    Use the same entry IDs to add legs, update trailing stops, and clip positions when the trend turns.

    if longSignal
    strategy.entry("Long", strategy.long, qty=qty)
    if shortSignal
    strategy.entry("Short", strategy.short, qty=qty)

    Attach exits that trail with ATR or flip to the opposite MA crossover.

//@version=5
strategy("SMA Crossover Pyramiding", overlay=true, pyramiding=3, initial_capital=50_000, commission_type=strategy.commission.percent, commission_value=0.02)
// === Inputs ===
fastLen = input.int(20, "Fast SMA", minval=1)
slowLen = input.int(50, "Slow SMA", minval=1)
atrLen = input.int(14, "ATR length", minval=1)
stopATR = input.float(2.5, "Stop distance (ATR)", step=0.1)
riskPct = input.float(0.5, "Risk per leg (%)", minval=0.1, step=0.1)
trailOnOpp = input.bool(true, "Exit on opposite crossover")
// === Calculations ===
fast = ta.sma(close, fastLen)
slow = ta.sma(close, slowLen)
atr = ta.atr(atrLen)
longSignal = ta.crossover(fast, slow)
shortSignal = ta.crossunder(fast, slow)
stopDistance = atr * stopATR
riskCapital = strategy.equity * (riskPct * 0.01)
qty = stopDistance > 0 ? math.floor(riskCapital / stopDistance) : 0
// === Entries ===
if longSignal and qty > 0
strategy.entry("Long", strategy.long, qty=qty)
if shortSignal and qty > 0
strategy.entry("Short", strategy.short, qty=qty)
// === Exits ===
if strategy.position_size > 0
stopLong = strategy.position_avg_price - stopDistance
strategy.exit("Long Stop", "Long", stop=stopLong)
if trailOnOpp and shortSignal
strategy.close("Long", comment="Opposite crossover")
if strategy.position_size < 0
stopShort = strategy.position_avg_price + stopDistance
strategy.exit("Short Stop", "Short", stop=stopShort)
if trailOnOpp and longSignal
strategy.close("Short", comment="Opposite crossover")
// === Visuals ===
plot(fast, "Fast SMA", color=color.new(color.green, 0))
plot(slow, "Slow SMA", color=color.new(color.red, 0))
plot(strategy.position_avg_price, "Entry avg", color=color.new(color.blue, 70), style=plot.style_circles)
bgcolor(strategy.position_size > 0 ? color.new(color.green, 90) : strategy.position_size < 0 ? color.new(color.red, 90) : na)
Why this works.
  • Inputs expose every tuning knob—MA lengths, pyramiding depth, ATR stop size, and per-leg risk.
  • ATR sizing keeps position size proportional to volatility so each add-on uses consistent risk.
  • Exit logic trails stops and can optionally close when the opposite crossover appears, protecting gains.
  • Align pyramiding settings with broker margin—adding too many legs can exceed practical exposure.
  • Use strategy.opentrades if you need different stop logic per leg (e.g., tighten after second add-on).
  • When risk sizing returns zero (qty == 0), skip entries to avoid strategy warnings about zero quantity.
  • Consider maximum total risk by multiplying per-leg risk and pyramiding count; cap qty if it exceeds plan limits.

The strategy keeps adding legs on every bar

Section titled “The strategy keeps adding legs on every bar”

Throttle signals with a confirmation filter (e.g., ta.barssince(longSignal) > 5) or only add when price has moved favourably since the last entry.

Ensure stopDistance is greater than the instrument’s syminfo.mintick. If ATR is tiny, apply a minimum like math.max(stopDistance, syminfo.mintick * 3).

How do I cap the total number of open trades?

Section titled “How do I cap the total number of open trades?”

Set pyramiding in strategy() to your desired maximum. Pair it with an input so testers can experiment without editing code.

  • Pyramiding adds legs to winning trades—configure strategy() and risk sizing to support it safely.
  • SMA crossovers provide the trend filter; ATR determines both position size and protective stop distance.
  • Combine fixed stops with optional opposite-signal exits to balance trend following with risk control.
  • Visual cues (plots/backgrounds) help validate that entries and exits align with strategy rules.

Adapted from tradingcode.net, optimised for Algo Trade Analytics users.

Step 5: Output the strategy’s data and visualise signals

Section titled “Step 5: Output the strategy’s data and visualise signals”

Step 6: Open a trading position with entry orders

Section titled “Step 6: Open a trading position with entry orders”

Step 7: Close market positions with exit orders

Section titled “Step 7: Close market positions with exit orders”
Generate the strategy’s stop-loss orders
Section titled “Generate the strategy’s stop-loss orders”

Performance of the SMA Crossover Pyramiding strategy for TradingView

Section titled “Performance of the SMA Crossover Pyramiding strategy for TradingView”

Full code: the SMA Crossover Pyramiding strategy for TradingView

Section titled “Full code: the SMA Crossover Pyramiding strategy for TradingView”

There are three price-based indicators we often use to define a trend: the Donchian Channel, Bollinger bands, and moving averages. Once we define a trend, we need to press our luck to fully profit. Let’s see how we can scale into profitable positions with a moving average 📈 strategy.

For his book Trend Following, Michael Covel (2006) researched some of the world’s best traders. All of those traders were trend followers. That trading approach has a seemingly simple goal: capture the majority of an up or down trend for profit (Covel, 2006). How trend followers achieve that goal is a bit different, however.

While other trading styles estimate when and where trends begin, trend followers simply monitor price. They know what price action defines a trend, and only open a position after they see a new trend emerge (Covel, 2006). As a consequence they do always miss the start of a trend. And they also exit after the trend already ended. But this doesn’t have to be a problem: when the trend is big enough, catching the middle portion still gives a lot of profit.

Most trend-following strategies share the following features (Covel, 2006). They detect big trends by monitoring prices. Profitable trades are held until the trend ends; that makes profits run. Losing trades are, however, closed at a predefined stop-loss level; that cuts losses shorts. And with position sizing each position has a limited risk.

One trend-following strategy that Covel (2006) shares in his book is the SMA Crossover Pyramiding 📈 strategy. This strategy builds on the SMA Crossover strategy with one key difference: after the first entry achieved a certain profit, the strategy doubles the position size. The underlying idea is that, once a trend exists, there’s a higher probability that it will persist (Covel, 2006). And so we take advantage of those situations and expand our open position.

Let’s see what the strategy’s trading rules are and how we code it for TradingView.

The SMA Crossover Pyramiding strategy has the following trading rules (Covel, 2006):

The strategy’s position size is risks 2% of strategy equity with each trade. But when markets gap, prices can move beyond our stop price leaving us with losses bigger than 2%. To prevent the strategy from incurring big losses, we limit the size of each position: we don’t allocate more than 10% of equity to a single trade.

The profitable backtest of the SMA Crossover Pyramiding strategy that Covel (2006) shares used 15 years of daily data from 20 futures markets, including financials (S&P 500, Nasdaq 100, T-Note 5-yr bonds), commodities (crude oil, gold, silver, corn, wheat), currencies (British Pound, Japanese Yen, Euro), and soft commodities (coffee, sugar).

Now let’s turn the above trading rules into a complete TradingView strategy script. A practical way to do so is with a template. That way we have a framework that shows us the different steps to code. Plus it breaks up the programming task into smaller, easier-to-manage pieces.

Here’s the template we’ll use for the SMA Crossover Pyramiding strategy:

If you want to follow along with the code discussion below, make a new strategy script in TradingView’s Pine Editor and paste in the above template. (See the end of this article if you just want the complete strategy code.)

For an idea of what the code we’re going to write does, here’s how the complete SMA Crossover Pyramiding strategy looks on the chart:

Now let’s start and code the SMA Crossover Pyramiding strategy for TradingView Pine.

In the first step we define the strategy’s settings and its user-configurable inputs.

So we begin, as we do with all TradingView strategies, with the strategy’s settings:

Here we configure the strategy script with the strategy() function. We use that function’s title argument to name the 📈 strategy. With overlay the strategy uses the same area as the chart’s instrument.

We also disable pyramiding (pyramiding=0). That seems a bit odd for the SMA Crossover Pyramiding 📈 strategy. But here’s why: we’ll make input options so we can configure the pyramiding settings for long and short trades separately. So we implement our own code instead of using the strategy’s pyramiding option.

With the initial_capital argument we have the strategy begin with 100,000 in the currency of the chart’s instrument. For the trading costs we use 4 currency (commission_type=4) per single trade (📈 strategy.commission.cash_per_contract). These costs are a bit pessimistic, but it’s safer to overestimate transaction costs than underestimate them. For the slippage of market and stop orders we use 2 ticks (slippage=2).

Next we make several input options so we can easily configure the strategy’s parameters. The first settings are for the moving averages:

Here we make two integer inputs with the input.int() function.

The first input is named ‘Fast SMA Length’ and starts with a value of 50 (50). We put its current value in the fastLen variable for use later in the script. This way we can look up the input’s current setting by using the variable.

The other input is called ‘Slow SMA Length’. This one has a default of 100 (100). Its current value is in the slowLen variable.

Then we make inputs for the strategy’s stop-loss settings:

The first input is an integer one (input.int()) named ‘ATR Length’. With this option we set the Average True Range (ATR) length. It has a default of 10 and we access its current value with the atrLen variable.

Next we make two floating-point inputs with the input.float() function. The first, ‘Long Stop Multiple’, specifies how many times we multiply the ATR to calculate the long stop price. We give it a default of 4. That makes the long stop 4 ATR multiples under the current price.

The other input, ‘Short Stop Multiple’, defines how many ATRs the short stop is above the current price. This one also begins with a value of 4.

Next we make inputs that configure the strategy’s position sizing:

The first input, ‘Use Position Sizing?’, is a Boolean true/false input. Those we make with the input.bool() function. We use this input to enable or disable the strategy’s position sizing algorithm. By default this setting is enabled (true). We track this option’s current setting with the usePosSize variable.

Then we make two more floating-point inputs (input.float()). The first, ‘Max Position Risk %’, defines how much of the strategy’s equity we want to risk on a single trade. Its default is 2% and store its value in the maxRisk variable. The second is named ‘Max Position Exposure %’. This setting defines how much of the strategy’s equity we want to invest in a trade’s margin requirements. Its default value is 10% and we track its value with the maxExposure variable.

Next we make the ‘Margin %’ input option. With this setting we specify what the margin rate of the chart’s instrument is. Since we cannot access that data in TradingView Pine at this time, the second best option is to estimate it. We give this setting a default of 10 since the typical margin rate of a futures contract varies from 5 till 15% (Kowalski, 2018). We track this input’s value with the marginPerc variable.

Note that we multiply all percentage-based inputs with 0.01. This expresses the percentage input as a decimal value, which makes it easier to perform the calculations with them later on.

The next group of input options are the ones that affect the strategy’s pyramiding settings:

The first two inputs, ‘Scale Long Profit %’ and ‘Scale Short Profit %’, specify which percentage of profit a trade needs before we open an additional entry. Based on the strategy’s trading rules these floating-point inputs (input.float()) use a default of 1. We track their values with the longThres and shortThres variables.

The two integer inputs (input.int()) define how many entries we want in the long and short direction. These ‘Max Long Entries’ and ‘Max Short Entries’ settings have a default of 2. That matches the strategy’s rules, which allow up to two entries in the same direction. We reference these inputs later on with the maxLongEntries and maxShortEntries variables.

The last inputs configure the strategy’s backtest window:

The purpose of the strategy’s trade window is to have it stop trading near the end of the backtest. That makes all performance metrics based on closed trades. (When there’s a trade left open, values in the ‘Strategy Tester’ window keep fluctuating when the market is open.)

These integer inputs are named ‘End Month Backtest’ and ‘End Year Backtest’. They have default values of 11 and 2018, which combined makes the strategy stop trading in November 2018. We use the endMonth and endYear input variables to track their current settings.

This is, by the way, how all the input options that we make look in the TradingView platform:

In this second step we calculate the strategy’s values. Here’s what we have to figure out: the SMA and ATR, the strategy’s backtest window, the position size, the possible trade profit, and the number of long and short entries.

So first we calculate the moving averages and Average True Range (ATR):

To calculate the 50 and 100-bar Simple Moving Averages (SMAs) we call TradingView’s ta.sma() function. The first sma() function call calculates the average based on close prices (close) and a length of fastLen bars. That fastLen variable holds the value of the ‘Fast SMA Length’ input option we made earlier. We store that 50-bar SMA in the fastMA variable.

The second ta.sma() function call computes the 100-bar SMA of closing prices. We fetch the length with slowLen, the input variable we set to a default of 100 earlier. We put the computed SMA in the slowMA variable for use later.

Next we compute the Average True Range (ATR). For that we call the ta.atr() function with the atrLen argument. That atrLen variable belongs to the ‘ATR Lengh’ input option we set to 14 earlier. We assign the 14-bar ATR to the atrValue variable.

Next we figure out the strategy’s backtest window. That way the script only trades during a certain time window. The code for that is:

The tradeWindow variable gets a true/false value based on a logical expression. That expression compares if the bar’s open time (time) is less than or equal to (<=) a particular point in time. We define that time point with the timestamp() function. We use 5 arguments with that function.

endYear is the input variable that has a default of 2018. endMonth is the input variable with a default of 11. Then we use 1 to set the day of the month, and 0 and 0 specify the hour and minutes. All in all, this makes timestamp() define the November 1, 2018 at 00:00 o’clock point in time here.

That also means that tradeWindow is true whenever the bar’s time is before November 1, 2018. And that variable is false on and after that particular day.

Then we determine the strategy’s position size:

The position sizing algorithm of the SMA Crossover Pyramiding strategy essentially has two parts: the amount of equity to risk on a trade, and the estimate trade risk. Then we balance that with the maximum position size.

For the risk equity we multiply the maxRisk input variable with 📈 strategy.equity. That latter variable returns the sum of the strategy’s initial capital, close trade profit, and open position profit (TradingView, n.d.). Since we set the ‘Max Position Risk %’ input option to 2% earlier, the value of the riskEquity variable we make here is two percent of the strategy’s equity.

For the trade risk estimation we make two variables: riskLong and riskShort. For the first we multiply the longStpMult input variable with the 10-bar ATR (atrValue). That gets us how many ATR distances the stop is placed from the current price. Then we multiply with syminfo.pointvalue, a variable returns how much currency one full point of price movement is worth. (For the E-mini S&P 500 future that variable for instance returns 50. And with crude oil futures we get 1,000.)

Next we figure out the strategy’s maximum position size. There are two components to that. The first is how much strategy equity we want to invest in a position’s margin requirements. The second is how much margin we need for a single contract. When we divide these two we know the maximum position size.

To see how much equity to allocate we multiply the maxExposure input variable with 📈 strategy.equity. Since we set that ‘Max Position Exposure %’ input option to 10 earlier, that gets us one tenth of the strategy’s capital to allocate for margin requirements.

For the amount of cash that a single futures contract needs we multiply the marginPerc input variable with the current price (close) and multiply again with the syminfo.pointvalue variable. That marginPerc input variable corresponds to the ‘Margin %’ input option that we earlier gave a default of 10%.

Then we divide the equity for margin with the margin needs of one contract to get the maximum position size (maxPos). We do that division inside the math.floor() function. That way we round the outcome down to the nearest full integer.

Then we determine the position size for long (posSizeLong) and short (posSizeShort) trades. First we have TradingView’s conditional operator (?:) evaluate the usePosSize input variable. That variable of the ‘Use Position Sizing?’ input option can either be enabled (true) or disabled (false).

When enabled we perform two operations to get the position size. First we divide riskEquity with riskLong (or riskShort for the short position size). We divide inside the math.floor() function. That gets us the rounded down position size. Then we have the math.min() function either return that computed position size or the value of maxPos, whichever is less. This way we never trade a position that’s too big.

Now when usePosSize is false, then the conditional operator (?:) simply returns 1 for both the posSizeLong and posSizeShort variables. This way the script always trades orders with a size of 1 when the ‘Use Position Sizing?’ input is turned off.

Another thing we compute is the trade profit as a percentage:

The tradeProfit variable we make here is set to the difference between the current price (close) and the strategy’s average entry price (📈 strategy.position_avg_price), taken as a percentage change from the strategy’s entry price times 100.

This value tells us how much the price rose or declined since we opened a trade. When there’s no open market position, then tradeProfit simply holds na (which works fine with our script too).

The last thing to figure out are the number of long and short entries:

Here we first declare the longEntries variable. Then we use the := operator to give the variable its actual value. There are two conditional operators (?:) to figure out what value to give that variable.

We rely heavily on the 📈 strategy.position_size variable here. When the strategy is long, that variable returns the long position size. When we’re short, 📈 strategy.position_size returns a negative value with the short position size. And when the script is flat 📈 strategy.position_size returns 0 (TradingView, n.d.).

The first conditional operator looks if the strategy is flat or short. That happens when 📈 strategy.position_size is less than (<) 1. In that case we return 0 as the value for longEntries. After all, when the strategy is not long, there are zero long entries.

The second conditional operator runs whenever the strategy is long. Here we check if the value of 📈 strategy.position_size increased (>) compared to the previous bar (📈 strategy.position_size[1]). When that happens we know that the long position became bigger, so we add 1 to the previous bar’s number of long entries (longEntries[1] + 1).

When the long position size did not increase, we simply use the same number of entries as the previous bar had. And so the second conditional operator returns longEntries[1] when its expression is false.

The way we come up with the value of the shortEntries variable is similar. First we check if the strategy is not short (📈 strategy.position_size > -1). When it is, we set shortEntries to 0. Should the strategy be short, we check if the position size became more negative compared with the previous bar (📈 strategy.position_size < 📈 strategy.position_size[1]).

When that happened there had to be another short entry. And so we increase shortEntries with 1. When the position size remained the same, we simply repeat the previous bar value (shortEntries[1]).

For the third step we code the strategy’s long trading rules.

First we figure out when the initial long entry should happen:

This enterLong variable gets a true/false value based on three expressions joined with the and operator. That means all have to be true before the combined result is true too. When one or more are false, then enterLong is false too.

First we check if the 50-bar SMA crossed over the 100-bar average. For this we call the ta.crossover() function with the fastMA and slowMA variables as arguments. This returns true when that fast moving average crossed over the slow one (and false otherwise).

Then we check is not already long, which is the case when the 📈 strategy.position_size variable is less than (<) 1. This way we explicitly look for the initial long entry, not additional long entry.

The third expression is simply the tradeWindow variable. Earlier we made that variable’s value true when the script calculated on a price bar that happened before November 1, 2018. By adding the variable to our enter long logic, we prevent that the strategy trades past that point in time.

Next we code when the strategy should open an additional long entry:

The SMA Crossover Pyramiding strategy opens an extra long when the position reaches a certain profit. But we also limit the number of long entries to the configured maximum, and perform a few extra checks.

So first we look whether the strategy is already long, which is the case when 📈 strategy.position_size is bigger than 0. This way we explicitly look for an extra long entry, and not the initial long entry.

Then we check whether the longEntries variable is less than the maxLongEntries input variable. This makes the strategy not open more trades than specified by the ‘Max Long Entries’ input option.

Next we look if the trade profit on a per-contract-basis (tradeProfit / longEntries) is greater than the ‘Scale Long Profit %’ input option, whose value we track with the longThres input variable. This only opens an extra long trade when the profit is above a certain threshold (1% by default).

And the last expression that affects the value of extraLong is the tradeWindow variable. This has us not open an additional long entry past the point in time we defined earlier.

The last bit of long strategy code figures out the long stop price. Here’s how we code that:

Here we first declare the longStop floating-point variable. Then we use the := operator to update the variable to its actual value.

We set that value conditionally with TradingView’s conditional operator (?:). The condition we evaluate with that operator contains two Boolean true/false variables: enterLong and extraLong. We combine those two with the or logical operator. That means the condition is true when one or both are true.

Based on how we code those variables earlier, we know that enterLong is true when the initial long entry happens. And that extraLong holds true when the long scale in signal happens. In both those cases we (re)calculate the stop price with this expression: close - (atrValue * longStpMult). This computes the long stop based on the current price (close) minus the 10-bar ATR times the ‘Long Stop Multiple’ input option we set to 4 earlier.

Now when both enterLong and extraLong are false, then no long buy signal happened during the current script calculation. In that case we use longStop[1] to simply repeat the long stop price from the previous bar. This way the stop uses a fixed price level while there’s an open long trade.

Next we translate the strategy’s short logic into TradingView code.

First we come up with the initial short entry signal:

The enterShort variable that we make here gets its true/false value based on three expressions joined with the and operator.

First we see if the 50-bar simple moving average dropped below the 100-bar SMA. We evaluate that with TradingView’s ta.crossunder() function, which we execute here with the fastMA and slowMA variables as arguments.

To make this enter short condition be the initial short entry, we also check whether the strategy is not already short. That’s the case when the 📈 strategy.position_size variable has a value greater than (>) -1.

The last expression is tradeWindow. By looking up this variable we only open short trades before the point in time we defined earlier. That way the script does not trade short positions past that November 1, 2018 date.

Then we code when the strategy should open an additional short trade:

This extraShort variable gets its true/false value from 4 expressions joined with the and operator. That means all have to be true before extraShort becomes true as well.

First we see if the strategy is already short (that is, 📈 strategy.position_size < 0). That way the extraShort variable becomes the additional short entry, and not the initial short entry.

Then we check if the number of short entries (shortEntries) is less than the ‘Max Short Entries’ input option (maxShortEntries). This way we don’t open more short trades than specified with that input option.

Then we look if the open short position is profitable enough for an additional entry. For that we divide the trade’s profit in percentage (tradeProfit) with the short entries (shortEntries). Then we look if that value is less than (<) the ‘Scale Short Profit %’ input option (shortThres), which we earlier set to 1%. (We multiply that latter with -1 to get a negative value. This is because we look for a price decline to make our short trade profitable.)

The fourth expression is the tradeWindow variable. This prevents that the strategy enters an additional short trade past the point in time we defined earlier.

The last bit of short code figures out the short stop price:

Here we first declare the shortStop variable with the = operator. Then we give the variable its actual value with the := operator.

We set that value with the conditional operator (?:). First that operator checks whether the enterShort or extraShort variable is true. When one of them is, we calculate the short stop as follows: close + (atrValue * shortStpMult). This increases the current price (close) with the 10-bar Average True Range (ATR) times the ‘Short Stop Multiple’ input option, which we gave a default of 4 earlier.

When there’s no enter short or additional short signal, we simply repeat the short stop price with shortStop[1]. This keeps the stop fixed at the same price level unless there’s a new short trade.

To track the strategy’s behaviour we display its values on the chart in this step.

First we plot both moving averages:

We plot the averages with TradingView’s plot() function. The first plot() function call shows the 50-bar simple moving average on the chart with the fastMA variable. Since we don’t specify the plot type, plot() makes a regular line plot by default. That line shows in the color.orange colour.

The second plot() statement displays the 100-bar SMA (slowMA). This line plot is coloured with color.teal. With the linewidth argument set to 2 this line becomes a bit thicker than the normal plot size.

Next we plot the strategy’s stop prices on the chart:

To not clutter the chart with unnecessary information we use the conditional operator (?:) to plot the stop prices conditionally. That is, when the strategy is long we only show the long stop. And when the script is short we just display the short stop price.

To make that happen the first plot() statement uses the conditional operator to see if the strategy is long. That’s the case when 📈 strategy.position_size is bigger than 0. In that case the operator returns the longStop variable, which we earlier set to the computed long stop price. Else we use the na value to disable the plot on the current bar.

The kind of plot we make with the plot() function is a cross plot (style=plot.style_cross). We have those little ’+’ signs appear in the color.green colour. And with linewidth set to 2 those crosses are a bit bigger than normal.

The second plot() statement is similar. This time we see if the strategy is short, which happens when 📈 strategy.position_size is less than 0. In that case we plot the shortStop values. Else we disable the plot with na. The plot we make here is a cross plot coloured in color.red.

In the sixth step of coding the SMA Crossover Pyramiding strategy we submit the entry orders. First we code orders that open a long or short position. Then we make orders that scale into a position.

Here’s the TradingView code that opens the initial position:

The first if statement evaluates enterLong. Earlier we set that variable to true when the 50-bar SMA crossed over the 100-bar average, while the strategy was not already long and the current bar fell inside our trade window.

When those three requirements happen at the same time, this if statement executes the 📈 strategy.``entry() function to open a long trade (📈 strategy.long). We name that order ‘EL’ (“EL”) and submit it for posSizeLong contracts. That variable either holds the computed long position size or a default of 1 when the ‘Use Position Sizing?’ input option is disabled.

The second if statement looks up the enterShort variable. That one is true when the 50-bar simple moving average fell below the 100-bar SMA, provided the strategy is not already short and this signal happened before our backtest end date. That scenario has 📈 strategy.``entry() open a short trade (📈 strategy.short). We name that order ‘ES’ and send it for posSizeShort contracts. That variable either holds the computed short position size or a default of 1 when the strategy’s position sizing is turned off.

Next we submit the additional entry orders:

The first if statement checks the value of extraLong. That variable holds true when the strategy is long, the number of long entries is below the maximum, the trade profit above 1%, and the script calculates on a price bar that happens before November 1, 2018.

When those four things happen at the same time we call the 📈 strategy.order() function to open an additional long trade (📈 strategy.long). We name that order ‘EL Extra’ and send it for posSizeLong contracts.

The second if statement looks up the extraShort variable. That one holds true when the script is short, the number of short entries is below the maximum, the trade profit is north of 1%, and the script processes a bar that’s before the backtest’s end date.

When that scenario happens we have the 📈 strategy.order() function open a short trade (📈 strategy.short). We name that order ‘ES Extra’ and give it a size of posSizeShort, the variable we set to the computed position size or a default of 1 otherwise.

For the last step we close the strategy’s positions.

First we submit the stop-loss orders:

To only send a long stop when the strategy is long (and a short stop when the script is short), we use two if statements here.

The first if statement looks whether the script is long, which it is when the 📈 strategy.position_size variable is greater than 0. In that case we have the 📈 strategy.``exit() function submit a stop for the longStop variable. We name that order ‘XL’ and submit it for the strategy’s entire open position. (We don’t have to specify that size as 📈 strategy.``exit() closes the entire position by default.)

The second if statement checks if the strategy is short. That happens when 📈 strategy.position_size is less than 0. In that situation the 📈 strategy.``exit() function sends a stop for the shortStop price. This order we name ‘XS’ and it closes the full short position.

The last thing left to code is to close open orders when the strategy’s backtest window ends. Here’s how we do that:

This if statement tests the not tradeWindow expression. What the not operator does is give us the logical opposite. So when we put it before something that’s true, not returns false. And when placed in front of a false value, not gives true.

Earlier we set tradeWindow to true whenever the script computed on a price bar that happened before November 1, 2018 (which is the default backtest end period based on the input options). That also means tradeWindow becomes false when the backtest ends.

But if we want to exit all open orders when the backtest ends, we need a true value. And so we place not before that variable. That makes the if statement execute the 📈 strategy.close_all() function after the backtest’s end date, which in turn makes the strategy flat. Since we also check tradeWindow before we open a position, after that point in time the strategy does not trade anymore.

Now let’s see how the SMA Crossover Pyramiding strategy performs. Like most trend-following strategies, the strategy performs really well with long trends. During those times the strategy rides long trends well because the moving averages don’t cross.

An example of that is the E-mini S&P 500 future chart below. Here the strategy opened a trade around 1,500 points and then closed the position a whopping 500 points later:

A nice feature of SMA Crossover Pyramiding strategy is how it scales into positions. Even though the trading rules only require a single additional entry, we coded the strategy to be more flexible. That is, the script allows any number of entries in the same direction. And because we update the stop loss with each entry, additional entries also move the stop-loss order closed to the current price (which reduces trade risk).

A situation where additional entries worked out nice is the chart below. Here there’s a long up trend, during which the strategy scaled 9 times into the long position. With each entry the stop was moved higher, protecting the trade profits:

Of course, the strategy also has its weaknesses. One is that when markets move sideways, the strategy loses money (just like any other trend-following strategy). In those cases losses happen because trends fail to start, fail to continue long enough for a profitable exit, or because prices simply go in the wrong direction.

An example is the chart below. Here the E-mini S&P 500 future moved sideways for several months. During that time the SMA Crossover Pyramiding strategy got four losing trades in a row:

The performance graph of the strategy (with one additional entry based on the trading rules) shows below. A few things are worth noting. The strategy’s drawdowns are modest in relation to the net profit. Another thing is that the big up spikes in equity show that the strategy relies on big trades. That unfortunately also means that missing one or two trades can ruin a year of trading.

The strategy also has relative long periods in which not much happens. We can see that in the first 50 trades or so, where the strategy remained closed to break even. If we would have given up by then thinking the strategy is a failure, we’d miss the big up swing afterwards.

The backtest results show in the table below. These results use one additional entry (so up to 2 trades in the same direction). But the results are without position sizing. That way the strategy could trade every signal it got, which maximised the number of trades in the backtest report.

The strategy was profitable on both the E-mini S&P 500 future and EuroStoxx 50 future. The win percentage is decent of an unoptimised long-term trend-following 📈 strategy. Plus the strategy’s drawdown is relatively small - just one third of the strategy’s net profit.

Furthermore, the strategy has 3 times as much profit as the regular SMA Crossover strategy, which doesn’t scale into positions (but is otherwise the same as the SMA Crossover Pyramiding strategy). The drawdown in the E-mini S&P 500 future is, however, just $7k bigger. So from that perspective we traded positions that are bigger and got three times as much profit, while we barely increased the strategy’s risks.

One concern is the low number of trades. With just over 2 trades per year there’s not that much data to go on. To really understand whether the SMA Crossover Pyramiding strategy works (or not) we’ll have to do more testing.

While the initial backtest is profitable, there are still things we can explore for better results and a deeper understanding of the 📈 strategy. Here are some ideas you might find valuable to explore further:

For TradingView strategies that are like the SMA Crossover Pyramiding strategy, see the SMA Crossover strategy and the SMA Crossover Weekly trend-following 📈 strategy.

Two other trend-following strategies that also use moving averages are the Triple Moving Average strategy and the Dual Moving Average 📈 strategy.

The complete code of the SMA Crossover Pyramiding strategy shows below. For details and an explanation of the code, see the discussion above.

Covel, M.W. (2006). Trend Following: How Great Traders Make Millions in Up or Down Markets (2nd edition). Upper Saddle River, NJ: FT Press.

Kowalski, C. (2018, August 30). Learn About Futures Margin. Retrieved on October 11, 2018, from https://www.thebalance.com/all-about-futures-margin-on-futures-contracts-809390

TradingView (n.d.). Pine Script Language Reference Manual. Retrieved on November 16, 2018, from https://www.tradingview.com/study-script-reference/

//@version=5 // Step 1. Define strategy settings // Step 2. Calculate strategy values // Step 3. Determine long trading conditions // Step 4. Code short trading conditions // Step 5. Output strategy data // Step 6. Submit entry orders // Step 7. Submit exit orders
// Step 1. Define strategy settings strategy(title="SMA Crossover Pyramiding", overlay=true, pyramiding=0, initial_capital=100000, commission_type=📈 `strategy.`commission.cash_per_order, commission_value=4, slippage=2)
// Moving average inputs fastLen = input.int(50, title="Fast SMA Length") slowLen = input.int(100, title="Slow SMA Length")
// Stop loss inputs atrLen = input.int(10, title="ATR Length") longStpMult = input.float(4, title="Long Stop Multiple") shortStpMult = input.float(4, title="Short Stop Multiple")
// Position sizing inputs usePosSize = input.bool(true, title="Use Position Sizing?") maxRisk = input.float(2, title="Max Position Risk %") * 0.01 maxExposure = input.float(10, title="Max Position Exposure %") * 0.01 marginPerc = input.int(10, title="Margin %") * 0.01
// Pyramiding settings longThres = input.float(1, title="Scale Long Profit %") shortThres = input.float(1, title="Scale Short Profit %") maxLongEntries = input.int(2, title="Max Long Entries") maxShortEntries = input.int(2, title="Max Short Entries")
// Backtest time range settings endMonth = input.int(11, title="End Month Backtest", minval=1, maxval=12) endYear = input.int(2018, title="End Year Backtest", minval=1950, maxval=2030)
// Step 2. Calculate strategy values // Calculate moving averages and ATR fastMA = ta.sma(close, fastLen) slowMA = ta.sma(close, slowLen) atrValue = ta.atr(atrLen)
// Determine backtest window tradeWindow = time <= timestamp(endYear, endMonth, 1, 0, 0)
// Determine position size riskEquity = maxRisk * 📈 `strategy.`equity riskLong = (longStpMult * atrValue) * syminfo.pointvalue riskShort = (shortStpMult * atrValue) * syminfo.pointvalue maxPos = math.floor((maxExposure * 📈 `strategy.`equity) / (marginPerc * close * syminfo.pointvalue)) posSizeLong = usePosSize ? math.min(math.floor(riskEquity / riskLong), maxPos) : 1 posSizeShort = usePosSize ? math.min(math.floor(riskEquity / riskShort), maxPos) : 1
// Compute trade profits (is NaN when flat) tradeProfit = ((close - 📈 `strategy.`position_avg_price) / 📈 `strategy.`position_avg_price) * 100
// Calculate the number of entries into positions longEntries = 0 longEntries := (📈 `strategy.`position_size < 1) ? 0 : (📈 `strategy.`position_size > 📈 `strategy.`position_size[1]) ? longEntries[1] + 1 : longEntries[1] shortEntries = 0 shortEntries := (📈 `strategy.`position_size > -1) ? 0 : (📈 `strategy.`position_size < 📈 `strategy.`position_size[1]) ? shortEntries[1] + 1 : shortEntries[1]
// Step 3. Determine long trading conditions enterLong = ta.crossover(fastMA, slowMA) and 📈 `strategy.`position_size < 1 and tradeWindow
extraLong = 📈 `strategy.`position_size > 0 and longEntries < maxLongEntries and tradeProfit / longEntries > longThres and tradeWindow
// Calculate long stop and update stop price // when there's a long or additional long signal longStop = 0.0 longStop := enterLong or extraLong ? close - (atrValue * longStpMult) : longStop[1]
// Step 4. Code short trading conditions enterShort = ta.crossunder(fastMA, slowMA) and 📈 `strategy.`position_size > -1 and tradeWindow
extraShort = 📈 `strategy.`position_size < 0 and shortEntries < maxShortEntries and tradeProfit / shortEntries < shortThres * -1 and tradeWindow
// Calculate short stop and update stop price // when there's a short or additional short signal shortStop = 0.0 shortStop := enterShort or extraShort ? close + (atrValue * shortStpMult) : shortStop[1]
// Step 5. Output strategy data // Show moving averages on the chart plot(fastMA, color=color.orange, title="Fast SMA") plot(slowMA, color=color.teal, title="Slow SMA", linewidth=2)
// Plot stop levels plot(📈 `strategy.`position_size > 0 ? longStop : na, color=color.green, style=plot.style_cross, linewidth=2, title="Long Stop Price") plot(📈 `strategy.`position_size < 0 ? shortStop : na, color=color.red, style=plot.style_cross, linewidth=2, title="Short Stop Price")
// Step 6. Submit entry orders // Send the strategy's initial entry orders if enterLong 📈 `strategy.`entry("EL", 📈 `strategy.`long, qty=posSizeLong) if enterShort 📈 `strategy.`entry("ES", 📈 `strategy.`short, qty=posSizeShort)
// Send the additional entry orders if extraLong 📈 `strategy.`order("EL Extra", 📈 `strategy.`long, qty=posSizeLong) if extraShort 📈 `strategy.`order("ES Extra", 📈 `strategy.`short, qty=posSizeShort)
// Step 7. Submit exit orders if 📈 `strategy.`position_size > 0 📈 `strategy.`exit("XL", stop=longStop) if 📈 `strategy.`position_size < 0 📈 `strategy.`exit("XS", stop=shortStop)
if not tradeWindow 📈 `strategy.`close_all()
Performance metricE-mini S&P 500 future (ES)EuroStoxx 50 future (FESX)
First trade1998-09-071998-12-22
Last trade2018-10-232018-11-02
Time frame1 day1 day
Net profit$94,144€90,330
Gross profit$273,550€179,631
Gross loss-$179,406-€89,301
Max drawdown$33,215€31,903
Profit factor1.5252.012
Total trades8096
Win percentage38.75%47.92%
Avg trade$1,176€940
Avg win trade$8,824€3,905
Avg losing trade-$3,661-€1,786
Win/loss ratio2.412.186
Commission paid$468€500
Slippage2 ticks2 ticks
  • Enter long:Go long (and exit any open short positions) with a market order the next day at the open when the 50-period Simple Moving Average (SMA) crosses over the 100-period SMA.Enter one additional long position the next day with a market order when prices increase 1% from the entry price.

  • Go long (and exit any open short positions) with a market order the next day at the open when the 50-period Simple Moving Average (SMA) crosses over the 100-period SMA.

  • Enter one additional long position the next day with a market order when prices increase 1% from the entry price.

  • Exit long:Close the entire long position based on the following stop loss: entry price - 4 times the 10-bar Average True Range (ATR).

  • Close the entire long position based on the following stop loss: entry price - 4 times the 10-bar Average True Range (ATR).

  • Enter short:Go short (and close any open longs) with a market order the next day at the open when the 50-bar SMA crosses under the 100-bar SMA.Enter one additional short position the next day with a market order when prices decrease with 1% from the entry price.

  • Go short (and close any open longs) with a market order the next day at the open when the 50-bar SMA crosses under the 100-bar SMA.

  • Enter one additional short position the next day with a market order when prices decrease with 1% from the entry price.

  • Exit short:Close the entire short position with the following stop loss: entry price + 4 times the 10-bar Average True Range (ATR).

  • Close the entire short position with the following stop loss: entry price + 4 times the 10-bar Average True Range (ATR).

  • Position sizing:The initial risk for each position (difference between entry price and stop-loss price) is 2% of equity.The maximum exposure (that is, the margin to equity ratio) for a single position is 10% of equity.

  • The initial risk for each position (difference between entry price and stop-loss price) is 2% of equity.

  • The maximum exposure (that is, the margin to equity ratio) for a single position is 10% of equity.

  • Go long (and exit any open short positions) with a market order the next day at the open when the 50-period Simple Moving Average (SMA) crosses over the 100-period SMA.

  • Enter one additional long position the next day with a market order when prices increase 1% from the entry price.

  • Close the entire long position based on the following stop loss: entry price - 4 times the 10-bar Average True Range (ATR).

  • Go short (and close any open longs) with a market order the next day at the open when the 50-bar SMA crosses under the 100-bar SMA.

  • Enter one additional short position the next day with a market order when prices decrease with 1% from the entry price.

  • Close the entire short position with the following stop loss: entry price + 4 times the 10-bar Average True Range (ATR).

  • The initial risk for each position (difference between entry price and stop-loss price) is 2% of equity.

  • The maximum exposure (that is, the margin to equity ratio) for a single position is 10% of equity.

  • One problem of the SMA Crossover Pyramiding strategy is that the moving average length makes the strategy slow to respond. While a 50-bar SMA does filter out noise in the market, its length also means we have to wait a decent amount of time before an exit signal happens. At that point we likely already gave up some profit or saw losses get worse. Perhaps we can use a 50/100-bar cross for entries, but use a shorter (and therefore quicker) moving average to close positions.

  • Covel (2006) reports an optimisation which shows that the strategy’s best performance happens with a length of 30-40 bars for the fast moving average and 80-100 bars for the slow SMA. We probably want to do our own tests to see which parameter settings are optimal, also because Covel’s (2006) tests happened more than a decade ago.

  • Like most trend-following strategies, the SMA Crossover Pyramiding strategy performs poorly when markets move sideways. If we can filter out those market conditions, the strategy’s performance increases for the better. Things we can try are volume filters, Average True Range (ATR) filters, the ADX, or higher time frame analysis.

  • The strategy’s stop loss uses a fixed price. While that gives positions plenty of room to move, it also means we give up a decent amount of profit before the stop gets hit. Perhaps a trailing stop can help the strategy to close losing positions sooner, and don’t give up as much profit.

  • When we expand on the strategy with additional trading rules and filters, it might be a good idea to temporarily disable the pyramiding and position sizing. That way we can better see how those changes affect the strategy’s performance, without the influence of the strategy’s pyramiding.

  • We can also make the strategy better by taking less risks. For instance, with a maximum position size we prevent trades that are too big. And with a maximum losing day streak we prevent a long drawdown. See the risk management category for other ideas.

Ready to implement this in your own strategies?

This tutorial is based on content from TradingCode.net, adapted for Algo Trade Analytics users.

This tutorial is based on content from tradingcode.net, adapted for Algo Trade Analytics users.

//@version=5 // Step 1. Define strategy settings strategy(title="SMA Crossover Pyramiding", overlay=true, pyramiding=0, initial_capital=100000, commission_type=📈 `strategy.`commission.cash_per_order, commission_value=4, slippage=2) // Moving average inputs fastLen = input.int(50, title="Fast SMA Length") slowLen = input.int(100, title="Slow SMA Length") // Stop loss inputs atrLen = input.int(10, title="ATR Length") longStpMult = input.float(4, title="Long Stop Multiple") shortStpMult = input.float(4, title="Short Stop Multiple") // Position sizing inputs usePosSize = input.bool(true, title="Use Position Sizing?") maxRisk = input.float(2, title="Max Position Risk %") * 0.01 maxExposure = input.float(10, title="Max Position Exposure %") * 0.01 marginPerc = input.int(10, title="Margin %") * 0.01 // Pyramiding settings longThres = input.float(1, title="Scale Long Profit %") shortThres = input.float(1, title="Scale Short Profit %") maxLongEntries = input.int(2, title="Max Long Entries") maxShortEntries = input.int(2, title="Max Short Entries") // Backtest time range settings endMonth = input.int(11, title="End Month Backtest", minval=1, maxval=12) endYear = input.int(2018, title="End Year Backtest", minval=1950, maxval=2030) // Step 2. Calculate strategy values // Calculate moving averages and ATR fastMA = ta.sma(close, fastLen) slowMA = ta.sma(close, slowLen) atrValue = ta.atr(atrLen) // Determine backtest window tradeWindow = time <= timestamp(endYear, endMonth, 1, 0, 0) // Determine position size riskEquity = maxRisk * 📈 `strategy.`equity riskLong = (longStpMult * atrValue) * syminfo.pointvalue riskShort = (shortStpMult * atrValue) * syminfo.pointvalue maxPos = math.floor((maxExposure * 📈 `strategy.`equity) / (marginPerc * close * syminfo.pointvalue)) posSizeLong = usePosSize ? math.min(math.floor(riskEquity / riskLong), maxPos) : 1 posSizeShort = usePosSize ? math.min(math.floor(riskEquity / riskShort), maxPos) : 1 // Compute trade profits (is NaN when flat) tradeProfit = ((close - 📈 `strategy.`position_avg_price) / 📈 `strategy.`position_avg_price) * 100 // Calculate the number of entries into positions longEntries = 0 longEntries := (📈 `strategy.`position_size < 1) ? 0 : (📈 `strategy.`position_size > 📈 `strategy.`position_size[1]) ? longEntries[1] + 1 : longEntries[1] shortEntries = 0 shortEntries := (📈 `strategy.`position_size > -1) ? 0 : (📈 `strategy.`position_size < 📈 `strategy.`position_size[1]) ? shortEntries[1] + 1 : shortEntries[1] // Step 3. Determine long trading conditions enterLong = ta.crossover(fastMA, slowMA) and 📈 `strategy.`position_size < 1 and tradeWindow extraLong = 📈 `strategy.`position_size > 0 and longEntries < maxLongEntries and tradeProfit / longEntries > longThres and tradeWindow // Calculate long stop and update stop price // when there's a long or additional long signal longStop = 0.0 longStop := enterLong or extraLong ? close - (atrValue * longStpMult) : longStop[1] // Step 4. Code short trading conditions enterShort = ta.crossunder(fastMA, slowMA) and 📈 `strategy.`position_size > -1 and tradeWindow extraShort = 📈 `strategy.`position_size < 0 and shortEntries < maxShortEntries and tradeProfit / shortEntries < shortThres * -1 and tradeWindow // Calculate short stop and update stop price // when there's a short or additional short signal shortStop = 0.0 shortStop := enterShort or extraShort ? close + (atrValue * shortStpMult) : shortStop[1] // Step 5. Output strategy data // Show moving averages on the chart plot(fastMA, color=color.orange, title="Fast SMA") plot(slowMA, color=color.teal, title="Slow SMA", linewidth=2) // Plot stop levels plot(📈 `strategy.`position_size > 0 ? longStop : na, color=color.green, style=plot.style_cross, linewidth=2, title="Long Stop Price") plot(📈 `strategy.`position_size < 0 ? shortStop : na, color=color.red, style=plot.style_cross, linewidth=2, title="Short Stop Price") // Step 6. Submit entry orders // Send the strategy's initial entry orders if enterLong 📈 `strategy.`entry("EL", 📈 `strategy.`long, qty=posSizeLong) if enterShort 📈 `strategy.`entry("ES", 📈 `strategy.`short, qty=posSizeShort) // Send the additional entry orders if extraLong 📈 `strategy.`order("EL Extra", 📈 `strategy.`long, qty=posSizeLong) if extraShort 📈 `strategy.`order("ES Extra", 📈 `strategy.`short, qty=posSizeShort) // Step 7. Submit exit orders if 📈 `strategy.`position_size > 0 📈 `strategy.`exit("XL", stop=longStop) if 📈 `strategy.`position_size < 0 📈 `strategy.`exit("XS", stop=shortStop) if not tradeWindow 📈 `strategy.`close_all()

Adapted from tradingcode.net, optimised for Algo Trade Analytics users.