using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// DeMark trendline breakout strategy converted from MetaTrader indicator.
/// </summary>
public class DeMarkLinesStrategy : Strategy
{
private readonly StrategyParam<int> _pivotDepth;
private readonly StrategyParam<int> _minBarsBetweenPivots;
private readonly StrategyParam<decimal> _breakoutBuffer;
private readonly StrategyParam<DataType> _candleType;
private decimal[] _highBuffer = Array.Empty<decimal>();
private decimal[] _lowBuffer = Array.Empty<decimal>();
private DateTimeOffset[] _timeBuffer = Array.Empty<DateTimeOffset>();
private int _windowSize;
private int _bufferCount;
private long _processedBars;
private decimal _pipSize;
private PivotPoint _previousHigh;
private PivotPoint _recentHigh;
private PivotPoint _previousLow;
private PivotPoint _recentLow;
private long _lastLongSignalIndex;
private long _lastShortSignalIndex;
/// <summary>
/// Gets or sets the number of confirmation bars on both sides of a pivot.
/// </summary>
public int PivotDepth
{
get => _pivotDepth.Value;
set => _pivotDepth.Value = value;
}
/// <summary>
/// Gets or sets the minimum number of bars between successive pivots of the same type.
/// </summary>
public int MinBarsBetweenPivots
{
get => _minBarsBetweenPivots.Value;
set => _minBarsBetweenPivots.Value = value;
}
/// <summary>
/// Gets or sets the breakout filter expressed in pips.
/// </summary>
public decimal BreakoutBuffer
{
get => _breakoutBuffer.Value;
set => _breakoutBuffer.Value = value;
}
/// <summary>
/// Gets or sets the candle type used for signal detection.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="DeMarkLinesStrategy"/>.
/// </summary>
public DeMarkLinesStrategy()
{
_pivotDepth = Param(nameof(PivotDepth), 2)
.SetGreaterThanZero()
.SetDisplay("Pivot depth", "Number of bars confirming a swing high/low", "Signals")
;
_minBarsBetweenPivots = Param(nameof(MinBarsBetweenPivots), 5)
.SetGreaterThanZero()
.SetDisplay("Minimum bars between pivots", "Prevents overlapping trendline anchors", "Signals")
;
_breakoutBuffer = Param(nameof(BreakoutBuffer), 2m)
.SetDisplay("Breakout buffer (pips)", "Extra distance beyond the trendline before entering", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle type", "Primary timeframe for the analysis", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highBuffer = Array.Empty<decimal>();
_lowBuffer = Array.Empty<decimal>();
_timeBuffer = Array.Empty<DateTimeOffset>();
_windowSize = 0;
_bufferCount = 0;
_processedBars = 0;
_pipSize = 0m;
_previousHigh = CreateInvalidPivot();
_recentHigh = CreateInvalidPivot();
_previousLow = CreateInvalidPivot();
_recentLow = CreateInvalidPivot();
_lastLongSignalIndex = -1;
_lastShortSignalIndex = -1;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_windowSize = Math.Max(3, PivotDepth * 2 + 1);
_highBuffer = new decimal[_windowSize];
_lowBuffer = new decimal[_windowSize];
_timeBuffer = new DateTimeOffset[_windowSize];
_bufferCount = 0;
_processedBars = 0;
_pipSize = CalculatePipSize();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || _windowSize == 0)
return;
// Fill the buffers during the warm-up phase until enough bars are available.
if (_bufferCount < _windowSize)
{
_highBuffer[_bufferCount] = candle.HighPrice;
_lowBuffer[_bufferCount] = candle.LowPrice;
_timeBuffer[_bufferCount] = candle.OpenTime;
_bufferCount++;
_processedBars++;
return;
}
// Shift buffers to keep the rolling window aligned with the latest data.
for (var i = 0; i < _windowSize - 1; i++)
{
_highBuffer[i] = _highBuffer[i + 1];
_lowBuffer[i] = _lowBuffer[i + 1];
_timeBuffer[i] = _timeBuffer[i + 1];
}
_highBuffer[_windowSize - 1] = candle.HighPrice;
_lowBuffer[_windowSize - 1] = candle.LowPrice;
_timeBuffer[_windowSize - 1] = candle.OpenTime;
_processedBars++;
var centerIndex = _windowSize - 1 - PivotDepth;
var pivotBarIndex = _processedBars - PivotDepth - 1;
var pivotTime = _timeBuffer[centerIndex];
var pivotHigh = _highBuffer[centerIndex];
var pivotLow = _lowBuffer[centerIndex];
// Update downtrend anchors when a new swing high appears.
if (IsPivotHigh(centerIndex))
RegisterHighPivot(pivotBarIndex, pivotTime, pivotHigh);
// Update uptrend anchors when a new swing low appears.
if (IsPivotLow(centerIndex))
RegisterLowPivot(pivotBarIndex, pivotTime, pivotLow);
EvaluateBreakouts(candle);
}
private bool IsPivotHigh(int index)
{
var high = _highBuffer[index];
for (var offset = 1; offset <= PivotDepth; offset++)
{
if (high <= _highBuffer[index - offset])
return false;
if (high < _highBuffer[index + offset])
return false;
}
return true;
}
private bool IsPivotLow(int index)
{
var low = _lowBuffer[index];
for (var offset = 1; offset <= PivotDepth; offset++)
{
if (low >= _lowBuffer[index - offset])
return false;
if (low > _lowBuffer[index + offset])
return false;
}
return true;
}
private void RegisterHighPivot(long index, DateTimeOffset time, decimal price)
{
if (_recentHigh.IsValid && index - _recentHigh.Index < MinBarsBetweenPivots)
return;
_previousHigh = _recentHigh;
_recentHigh = CreatePivot(index, time, price);
_lastLongSignalIndex = -1;
}
private void RegisterLowPivot(long index, DateTimeOffset time, decimal price)
{
if (_recentLow.IsValid && index - _recentLow.Index < MinBarsBetweenPivots)
return;
_previousLow = _recentLow;
_recentLow = CreatePivot(index, time, price);
_lastShortSignalIndex = -1;
}
private void EvaluateBreakouts(ICandleMessage candle)
{
if (!IsFormedAndOnlineAndAllowTrading())
return;
var currentIndex = _processedBars - 1;
var priceBuffer = BreakoutBuffer * (_pipSize > 0m ? _pipSize : 1m);
// Look for a bullish breakout through the downtrend line.
if (_recentHigh.IsValid && _previousHigh.IsValid && currentIndex != _lastLongSignalIndex)
{
var resistance = CalculateTrendValue(_previousHigh, _recentHigh, currentIndex);
if (candle.ClosePrice > resistance + priceBuffer && Position <= 0)
{
var volume = Volume + (Position < 0 ? -Position : 0m);
if (volume > 0m)
{
BuyMarket(volume);
_lastLongSignalIndex = currentIndex;
}
}
}
// Look for a bearish breakout through the uptrend line.
if (_recentLow.IsValid && _previousLow.IsValid && currentIndex != _lastShortSignalIndex)
{
var support = CalculateTrendValue(_previousLow, _recentLow, currentIndex);
if (candle.ClosePrice < support - priceBuffer && Position >= 0)
{
var volume = Volume + (Position > 0 ? Position : 0m);
if (volume > 0m)
{
SellMarket(volume);
_lastShortSignalIndex = currentIndex;
}
}
}
}
private decimal CalculatePipSize()
{
var priceStep = Security?.PriceStep;
if (priceStep is not decimal step || step <= 0m)
return 1m;
var decimals = GetDecimalPlaces(step);
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
private static int GetDecimalPlaces(decimal value)
{
var bits = decimal.GetBits(value);
return (bits[3] >> 16) & 0xFF;
}
private static PivotPoint CreatePivot(long index, DateTimeOffset time, decimal price)
=> new()
{
Index = index,
Time = time,
Price = price
};
private static PivotPoint CreateInvalidPivot()
=> new()
{
Index = -1,
Time = default,
Price = 0m
};
private static decimal CalculateTrendValue(PivotPoint older, PivotPoint newer, long currentIndex)
{
var indexDiff = newer.Index - older.Index;
if (indexDiff == 0)
return newer.Price;
var slope = (newer.Price - older.Price) / (decimal)indexDiff;
var offset = currentIndex - newer.Index;
return newer.Price + slope * offset;
}
private struct PivotPoint
{
public long Index;
public DateTimeOffset Time;
public decimal Price;
public bool IsValid => Index >= 0;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class de_mark_lines_strategy(Strategy):
"""
DeMark trendline breakout strategy.
Detects swing highs/lows (pivot points) and draws trendlines between them.
Enters long on bullish breakout through downtrend line,
enters short on bearish breakout through uptrend line.
"""
def __init__(self):
super(de_mark_lines_strategy, self).__init__()
self._pivot_depth = self.Param("PivotDepth", 2) \
.SetDisplay("Pivot depth", "Number of bars confirming a swing high/low", "Signals")
self._min_bars_between = self.Param("MinBarsBetweenPivots", 5) \
.SetDisplay("Min bars between pivots", "Prevents overlapping trendline anchors", "Signals")
self._breakout_buffer = self.Param("BreakoutBuffer", 2.0) \
.SetDisplay("Breakout buffer (pips)", "Extra distance beyond trendline before entering", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle type", "Primary timeframe for the analysis", "Data")
self._high_buffer = []
self._low_buffer = []
self._time_buffer = []
self._window_size = 0
self._buffer_count = 0
self._processed_bars = 0
self._pip_size = 0.0
self._prev_high = {"index": -1, "price": 0.0}
self._recent_high = {"index": -1, "price": 0.0}
self._prev_low = {"index": -1, "price": 0.0}
self._recent_low = {"index": -1, "price": 0.0}
self._last_long_signal = -1
self._last_short_signal = -1
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(de_mark_lines_strategy, self).OnReseted()
self._high_buffer = []
self._low_buffer = []
self._time_buffer = []
self._window_size = 0
self._buffer_count = 0
self._processed_bars = 0
self._pip_size = 0.0
self._prev_high = {"index": -1, "price": 0.0}
self._recent_high = {"index": -1, "price": 0.0}
self._prev_low = {"index": -1, "price": 0.0}
self._recent_low = {"index": -1, "price": 0.0}
self._last_long_signal = -1
self._last_short_signal = -1
def OnStarted2(self, time):
super(de_mark_lines_strategy, self).OnStarted2(time)
self._window_size = max(3, self._pivot_depth.Value * 2 + 1)
self._high_buffer = [0.0] * self._window_size
self._low_buffer = [0.0] * self._window_size
self._time_buffer = [None] * self._window_size
self._buffer_count = 0
self._processed_bars = 0
self._pip_size = self._calculate_pip_size()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished or self._window_size == 0:
return
if self._buffer_count < self._window_size:
self._high_buffer[self._buffer_count] = float(candle.HighPrice)
self._low_buffer[self._buffer_count] = float(candle.LowPrice)
self._time_buffer[self._buffer_count] = candle.OpenTime
self._buffer_count += 1
self._processed_bars += 1
return
for i in range(self._window_size - 1):
self._high_buffer[i] = self._high_buffer[i + 1]
self._low_buffer[i] = self._low_buffer[i + 1]
self._time_buffer[i] = self._time_buffer[i + 1]
self._high_buffer[self._window_size - 1] = float(candle.HighPrice)
self._low_buffer[self._window_size - 1] = float(candle.LowPrice)
self._time_buffer[self._window_size - 1] = candle.OpenTime
self._processed_bars += 1
center = self._window_size - 1 - self._pivot_depth.Value
pivot_bar_index = self._processed_bars - self._pivot_depth.Value - 1
pivot_high = self._high_buffer[center]
pivot_low = self._low_buffer[center]
if self._is_pivot_high(center):
self._register_high_pivot(pivot_bar_index, pivot_high)
if self._is_pivot_low(center):
self._register_low_pivot(pivot_bar_index, pivot_low)
self._evaluate_breakouts(candle)
def _is_pivot_high(self, index):
high = self._high_buffer[index]
depth = self._pivot_depth.Value
for offset in range(1, depth + 1):
if high <= self._high_buffer[index - offset]:
return False
if high < self._high_buffer[index + offset]:
return False
return True
def _is_pivot_low(self, index):
low = self._low_buffer[index]
depth = self._pivot_depth.Value
for offset in range(1, depth + 1):
if low >= self._low_buffer[index - offset]:
return False
if low > self._low_buffer[index + offset]:
return False
return True
def _register_high_pivot(self, index, price):
if self._recent_high["index"] >= 0 and index - self._recent_high["index"] < self._min_bars_between.Value:
return
self._prev_high = dict(self._recent_high)
self._recent_high = {"index": index, "price": price}
self._last_long_signal = -1
def _register_low_pivot(self, index, price):
if self._recent_low["index"] >= 0 and index - self._recent_low["index"] < self._min_bars_between.Value:
return
self._prev_low = dict(self._recent_low)
self._recent_low = {"index": index, "price": price}
self._last_short_signal = -1
def _evaluate_breakouts(self, candle):
if not self.IsFormedAndOnlineAndAllowTrading():
return
current_index = self._processed_bars - 1
price_buffer = self._breakout_buffer.Value * (self._pip_size if self._pip_size > 0 else 1.0)
close = float(candle.ClosePrice)
if (self._recent_high["index"] >= 0 and self._prev_high["index"] >= 0
and current_index != self._last_long_signal):
resistance = self._calc_trend_value(self._prev_high, self._recent_high, current_index)
if close > resistance + price_buffer and self.Position <= 0:
volume = self.Volume + abs(self.Position)
if volume > 0:
self.BuyMarket(volume)
self._last_long_signal = current_index
if (self._recent_low["index"] >= 0 and self._prev_low["index"] >= 0
and current_index != self._last_short_signal):
support = self._calc_trend_value(self._prev_low, self._recent_low, current_index)
if close < support - price_buffer and self.Position >= 0:
volume = self.Volume + abs(self.Position)
if volume > 0:
self.SellMarket(volume)
self._last_short_signal = current_index
def _calc_trend_value(self, older, newer, current_index):
index_diff = newer["index"] - older["index"]
if index_diff == 0:
return newer["price"]
slope = (newer["price"] - older["price"]) / float(index_diff)
offset = current_index - newer["index"]
return newer["price"] + slope * offset
def _calculate_pip_size(self):
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
return 1.0
decimals = 0
if self.Security is not None and self.Security.Decimals is not None:
decimals = int(self.Security.Decimals)
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def CreateClone(self):
return de_mark_lines_strategy()