Linear Continuation Strategy
This example converts the TradingView "Linear Continuation" study to StockSharp. The strategy calculates three moving averages and logs their linear continuation projection. It is intended as an indicator showcase and does not include trading logic.
Details
- MA Type – moving average type (SMA or EMA)
- MA1 Period – period for the first moving average
- MA2 Period – period for the second moving average
- MA3 Period – period for the third moving average
- Aggressive Mode – toggles projection distance calculation
Notes
- No orders are submitted.
- Designed for demonstration purposes.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Triple moving average continuation strategy.
/// Opens positions when MA ordering indicates directional continuation.
/// </summary>
public class LinearContinuationStrategy : Strategy
{
private readonly StrategyParam<MovingAverageTypes> _maType;
private readonly StrategyParam<int> _ma1Period;
private readonly StrategyParam<int> _ma2Period;
private readonly StrategyParam<int> _ma3Period;
private readonly StrategyParam<decimal> _minSpreadPercent;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<bool> _aggressiveMode;
private readonly StrategyParam<DataType> _candleType;
private IIndicator _ma1;
private IIndicator _ma2;
private IIndicator _ma3;
private int _lastTrend;
private int _barsFromSignal;
/// <summary>
/// Moving average type (SMA or EMA).
/// </summary>
public MovingAverageTypes MaType
{
get => _maType.Value;
set => _maType.Value = value;
}
/// <summary>
/// First moving average period.
/// </summary>
public int Ma1Period
{
get => _ma1Period.Value;
set => _ma1Period.Value = value;
}
/// <summary>
/// Second moving average period.
/// </summary>
public int Ma2Period
{
get => _ma2Period.Value;
set => _ma2Period.Value = value;
}
/// <summary>
/// Third moving average period.
/// </summary>
public int Ma3Period
{
get => _ma3Period.Value;
set => _ma3Period.Value = value;
}
/// <summary>
/// Minimum spread between fast and slow MA as percent of close price.
/// </summary>
public decimal MinSpreadPercent
{
get => _minSpreadPercent.Value;
set => _minSpreadPercent.Value = value;
}
/// <summary>
/// Minimum bars between new entry signals.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Use aggressive mode for continuation calculation.
/// </summary>
public bool AggressiveMode
{
get => _aggressiveMode.Value;
set => _aggressiveMode.Value = value;
}
/// <summary>
/// Candle type for subscription.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public LinearContinuationStrategy()
{
_maType = Param(nameof(MaType), MovingAverageTypes.Simple)
.SetDisplay("MA Type", "Moving average type", "General");
_ma1Period = Param(nameof(Ma1Period), 120)
.SetGreaterThanZero()
.SetDisplay("MA1 Period", "Period for MA1", "General")
.SetOptimize(60, 240, 10);
_ma2Period = Param(nameof(Ma2Period), 55)
.SetGreaterThanZero()
.SetDisplay("MA2 Period", "Period for MA2", "General")
.SetOptimize(20, 140, 5);
_ma3Period = Param(nameof(Ma3Period), 21)
.SetGreaterThanZero()
.SetDisplay("MA3 Period", "Period for MA3", "General")
.SetOptimize(8, 80, 4);
_minSpreadPercent = Param(nameof(MinSpreadPercent), 0.03m)
.SetGreaterThanZero()
.SetDisplay("Min Spread %", "Minimal fast/slow MA spread in percent", "General")
.SetOptimize(0.01m, 0.10m, 0.01m);
_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Minimum bars between signals", "General")
.SetOptimize(5, 30, 1);
_aggressiveMode = Param(nameof(AggressiveMode), true)
.SetDisplay("Aggressive Mode", "Use aggressive continuation", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ma1 = null;
_ma2 = null;
_ma3 = null;
_lastTrend = 0;
_barsFromSignal = int.MaxValue;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ma1 = CreateMa(MaType, Ma1Period);
_ma2 = CreateMa(MaType, Ma2Period);
_ma3 = CreateMa(MaType, Ma3Period);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ma1, _ma2, _ma3, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ma1);
DrawIndicator(area, _ma2);
DrawIndicator(area, _ma3);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal ma1Value, decimal ma2Value, decimal ma3Value)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_ma1 is null || _ma2 is null || _ma3 is null)
return;
if (!_ma1.IsFormed || !_ma2.IsFormed || !_ma3.IsFormed)
return;
_barsFromSignal++;
var isBullishContinuation = ma3Value > ma2Value && ma2Value > ma1Value;
var isBearishContinuation = ma3Value < ma2Value && ma2Value < ma1Value;
if (!isBullishContinuation && !isBearishContinuation)
return;
var closePrice = candle.ClosePrice;
if (closePrice <= 0)
return;
var spreadPercent = Math.Abs(ma3Value - ma1Value) / closePrice * 100m;
var minSpread = AggressiveMode ? MinSpreadPercent : MinSpreadPercent * 1.5m;
if (spreadPercent < minSpread)
return;
var cooldownBars = AggressiveMode ? SignalCooldownBars : SignalCooldownBars + GetBr(Ma2Period);
if (_barsFromSignal < cooldownBars)
return;
var trend = isBullishContinuation ? 1 : -1;
if (trend == _lastTrend)
return;
if (trend > 0 && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_lastTrend = trend;
_barsFromSignal = 0;
return;
}
if (trend < 0 && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_lastTrend = trend;
_barsFromSignal = 0;
}
}
private int GetBr(int period)
{
return AggressiveMode ? 1 : (int)Math.Round(period / 4.669m) + 1;
}
private static IIndicator CreateMa(MovingAverageTypes type, int length)
{
return type switch
{
MovingAverageTypes.Simple => new SMA { Length = length },
MovingAverageTypes.Exponential => new EMA { Length = length },
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
/// <summary>
/// Moving average types.
/// </summary>
public enum MovingAverageTypes
{
/// <summary>Simple moving average.</summary>
Simple,
/// <summary>Exponential moving average.</summary>
Exponential,
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class linear_continuation_strategy(Strategy):
def __init__(self):
super(linear_continuation_strategy, self).__init__()
self._ma1_period = self.Param("Ma1Period", 120) \
.SetGreaterThanZero() \
.SetDisplay("MA1 Period", "Period for MA1", "General")
self._ma2_period = self.Param("Ma2Period", 55) \
.SetGreaterThanZero() \
.SetDisplay("MA2 Period", "Period for MA2", "General")
self._ma3_period = self.Param("Ma3Period", 21) \
.SetGreaterThanZero() \
.SetDisplay("MA3 Period", "Period for MA3", "General")
self._min_spread_pct = self.Param("MinSpreadPercent", 0.03) \
.SetGreaterThanZero() \
.SetDisplay("Min Spread Pct", "Minimal fast/slow MA spread in percent", "General")
self._signal_cooldown = self.Param("SignalCooldownBars", 12) \
.SetGreaterThanZero() \
.SetDisplay("Signal Cooldown", "Minimum bars between signals", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._last_trend = 0
self._bars_from_signal = 999999
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(linear_continuation_strategy, self).OnReseted()
self._last_trend = 0
self._bars_from_signal = 999999
def OnStarted2(self, time):
super(linear_continuation_strategy, self).OnStarted2(time)
ma1 = SimpleMovingAverage()
ma1.Length = self._ma1_period.Value
ma2 = SimpleMovingAverage()
ma2.Length = self._ma2_period.Value
ma3 = SimpleMovingAverage()
ma3.Length = self._ma3_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ma1, ma2, ma3, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ma1)
self.DrawIndicator(area, ma2)
self.DrawIndicator(area, ma3)
self.DrawOwnTrades(area)
def OnProcess(self, candle, ma1_val, ma2_val, ma3_val):
if candle.State != CandleStates.Finished:
return
self._bars_from_signal += 1
m1 = float(ma1_val)
m2 = float(ma2_val)
m3 = float(ma3_val)
close = float(candle.ClosePrice)
bullish = m3 > m2 and m2 > m1
bearish = m3 < m2 and m2 < m1
if not bullish and not bearish:
return
if close <= 0.0:
return
spread_pct = abs(m3 - m1) / close * 100.0
min_spread = float(self._min_spread_pct.Value)
if spread_pct < min_spread:
return
cd = self._signal_cooldown.Value
if self._bars_from_signal < cd:
return
trend = 1 if bullish else -1
if trend == self._last_trend:
return
if trend > 0 and self.Position <= 0:
self.BuyMarket()
self._last_trend = trend
self._bars_from_signal = 0
elif trend < 0 and self.Position >= 0:
self.SellMarket()
self._last_trend = trend
self._bars_from_signal = 0
def CreateClone(self):
return linear_continuation_strategy()