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>
/// TTM Trend strategy with re-entry logic inspired by the MetaTrader expert.
/// Opens positions when the TTM Trend color flips and pyramids after price moves far enough.
/// </summary>
public class TtmTrendReopenStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _compBars;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<decimal> _priceStepPoints;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<bool> _enableLongEntries;
private readonly StrategyParam<bool> _enableShortEntries;
private readonly StrategyParam<bool> _enableLongExits;
private readonly StrategyParam<bool> _enableShortExits;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private TtmTrendIndicator _ttmIndicator;
private readonly List<int> _colorHistory = new();
private int _longEntries;
private int _shortEntries;
private decimal _entryPrice;
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Number of Heikin-Ashi comparison bars maintained by the indicator.
/// </summary>
public int CompBars
{
get => _compBars.Value;
set => _compBars.Value = Math.Max(1, value);
}
/// <summary>
/// Offset of the bar used for signal detection (0 = latest closed candle).
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = Math.Max(0, value);
}
/// <summary>
/// Minimum favorable move in points before adding to an existing position.
/// </summary>
public decimal PriceStepPoints
{
get => _priceStepPoints.Value;
set => _priceStepPoints.Value = Math.Max(0m, value);
}
/// <summary>
/// Maximum number of entries per direction (including the first one).
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = Math.Max(1, value);
}
/// <summary>
/// Allow opening new long positions on bullish colors.
/// </summary>
public bool EnableLongEntries
{
get => _enableLongEntries.Value;
set => _enableLongEntries.Value = value;
}
/// <summary>
/// Allow opening new short positions on bearish colors.
/// </summary>
public bool EnableShortEntries
{
get => _enableShortEntries.Value;
set => _enableShortEntries.Value = value;
}
/// <summary>
/// Allow closing long positions when a bearish color appears.
/// </summary>
public bool EnableLongExits
{
get => _enableLongExits.Value;
set => _enableLongExits.Value = value;
}
/// <summary>
/// Allow closing short positions when a bullish color appears.
/// </summary>
public bool EnableShortExits
{
get => _enableShortExits.Value;
set => _enableShortExits.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in instrument points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = Math.Max(0m, value);
}
/// <summary>
/// Take-profit distance expressed in instrument points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = Math.Max(0m, value);
}
/// <summary>
/// Initializes a new instance of the <see cref="TtmTrendReopenStrategy"/>.
/// </summary>
public TtmTrendReopenStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for the TTM Trend calculation", "General");
_compBars = Param(nameof(CompBars), 6)
.SetGreaterThanZero()
.SetDisplay("Comparison Bars", "Heikin-Ashi bars stored for the color smoothing", "Indicator")
.SetOptimize(3, 12, 1);
_signalBar = Param(nameof(SignalBar), 1)
.SetNotNegative()
.SetDisplay("Signal Bar", "Offset of the bar used for trading decisions", "Indicator")
.SetOptimize(0, 3, 1);
_priceStepPoints = Param(nameof(PriceStepPoints), 1000m)
.SetNotNegative()
.SetDisplay("Re-entry Step", "Minimum favorable move (in points) before pyramiding", "Risk Management")
.SetOptimize(100m, 600m, 100m);
_maxPositions = Param(nameof(MaxPositions), 1)
.SetGreaterThanZero()
.SetDisplay("Max Entries", "Maximum number of stacked entries per direction", "Risk Management")
.SetOptimize(1, 10, 1);
_enableLongEntries = Param(nameof(EnableLongEntries), true)
.SetDisplay("Enable Long Entries", "Allow buying when the TTM Trend turns bullish", "Trading Rules");
_enableShortEntries = Param(nameof(EnableShortEntries), true)
.SetDisplay("Enable Short Entries", "Allow selling when the TTM Trend turns bearish", "Trading Rules");
_enableLongExits = Param(nameof(EnableLongExits), true)
.SetDisplay("Enable Long Exits", "Close longs on bearish colors", "Trading Rules");
_enableShortExits = Param(nameof(EnableShortExits), true)
.SetDisplay("Enable Short Exits", "Close shorts on bullish colors", "Trading Rules");
_stopLossPoints = Param(nameof(StopLossPoints), 1000m)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Protective stop distance in price points", "Risk Management")
.SetOptimize(200m, 2000m, 200m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000m)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Profit target distance in price points", "Risk Management")
.SetOptimize(500m, 4000m, 500m);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_colorHistory.Clear();
_longEntries = 0;
_shortEntries = 0;
_entryPrice = 0m;
_ttmIndicator = null!;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_colorHistory.Clear();
_longEntries = 0;
_shortEntries = 0;
_ttmIndicator = new TtmTrendIndicator
{
CompBars = CompBars
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_ttmIndicator, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ttmIndicator);
DrawOwnTrades(area);
}
var step = Security?.PriceStep ?? 1m;
Unit stopLossUnit = StopLossPoints > 0m ? new Unit(StopLossPoints * step, UnitTypes.Absolute) : null;
Unit takeProfitUnit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints * step, UnitTypes.Absolute) : null;
if (stopLossUnit != null || takeProfitUnit != null)
StartProtection(stopLossUnit, takeProfitUnit);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue ttmValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!ttmValue.IsFinal)
return;
var colorDecimal = ttmValue.GetValue<decimal>();
var color = (int)Math.Round(colorDecimal);
_colorHistory.Add(color);
var offset = Math.Max(0, SignalBar - 1);
var signalIndex = _colorHistory.Count - 1 - offset;
if (signalIndex < 0)
return;
var currentColor = _colorHistory[signalIndex];
int? previousColor = signalIndex > 0 ? _colorHistory[signalIndex - 1] : null;
var isBullishColor = currentColor is 1 or 4;
var isBearishColor = currentColor is 0 or 3;
var wasBullish = previousColor.HasValue && (previousColor.Value == 1 || previousColor.Value == 4);
var wasBearish = previousColor.HasValue && (previousColor.Value == 0 || previousColor.Value == 3);
// Close existing positions before opening new ones.
if (EnableLongExits && isBearishColor && Position > 0)
{
SellMarket(Position);
_longEntries = 0;
}
if (EnableShortExits && isBullishColor && Position < 0)
{
BuyMarket(-Position);
_shortEntries = 0;
}
// Open a fresh long when the color flips to bullish.
if (EnableLongEntries && isBullishColor && previousColor.HasValue && !wasBullish && Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? -Position : 0m));
_longEntries = 1;
_shortEntries = 0;
_entryPrice = candle.ClosePrice;
}
// Open a fresh short when the color flips to bearish.
else if (EnableShortEntries && isBearishColor && previousColor.HasValue && !wasBearish && Position >= 0)
{
SellMarket(Volume + (Position > 0 ? Position : 0m));
_shortEntries = 1;
_longEntries = 0;
_entryPrice = candle.ClosePrice;
}
var step = Security?.PriceStep ?? 1m;
var reentryStep = PriceStepPoints * step;
// Add to an existing long once price moves in favor.
if (EnableLongEntries && Position > 0 && reentryStep > 0m && _longEntries > 0 && _longEntries < MaxPositions)
{
var distance = candle.ClosePrice - _entryPrice;
if (distance >= reentryStep)
{
BuyMarket(Volume);
_longEntries++;
_entryPrice = candle.ClosePrice;
}
}
// Add to an existing short once price moves in favor.
else if (EnableShortEntries && Position < 0 && reentryStep > 0m && _shortEntries > 0 && _shortEntries < MaxPositions)
{
var distance = _entryPrice - candle.ClosePrice;
if (distance >= reentryStep)
{
SellMarket(Volume);
_shortEntries++;
_entryPrice = candle.ClosePrice;
}
}
var keep = Math.Max(offset + 2, 3);
if (_colorHistory.Count > keep)
_colorHistory.RemoveRange(0, _colorHistory.Count - keep);
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (Position == 0m)
{
_longEntries = 0;
_shortEntries = 0;
}
}
/// <summary>
/// Internal indicator reproducing the MetaTrader TTM Trend color output.
/// </summary>
private sealed class TtmTrendIndicator : BaseIndicator
{
private readonly List<TtmEntry> _history = [];
private readonly object _sync = new();
public int CompBars { get; set; } = 6;
private decimal? _prevHaOpen;
private decimal? _prevHaClose;
/// <inheritdoc />
protected override IIndicatorValue OnProcess(IIndicatorValue input)
{
lock (_sync)
{
ICandleMessage candle;
try { candle = input.GetValue<ICandleMessage>(default); }
catch { return new DecimalIndicatorValue(this, default, input.Time); }
if (candle == null || candle.State != CandleStates.Finished)
return new DecimalIndicatorValue(this, default, input.Time);
var haClose = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
decimal haOpen;
if (_prevHaOpen is null || _prevHaClose is null)
{
haOpen = (candle.OpenPrice + candle.ClosePrice) / 2m;
}
else
{
haOpen = (_prevHaOpen.Value + _prevHaClose.Value) / 2m;
}
_prevHaOpen = haOpen;
_prevHaClose = haClose;
var color = CalculateBaseColor(candle, haOpen, haClose);
foreach (var entry in _history)
{
var high = Math.Max(entry.HaOpen, entry.HaClose);
var low = Math.Min(entry.HaOpen, entry.HaClose);
if (haOpen <= high && haOpen >= low && haClose <= high && haClose >= low)
{
color = entry.Color;
break;
}
}
_history.Insert(0, new TtmEntry(haOpen, haClose, color));
while (_history.Count > Math.Max(1, CompBars))
_history.RemoveAt(_history.Count - 1);
IsFormed = true;
return new DecimalIndicatorValue(this, color, input.Time);
}
}
/// <inheritdoc />
public override void Reset()
{
lock (_sync)
{
base.Reset();
_history.Clear();
_prevHaOpen = null;
_prevHaClose = null;
}
}
private static int CalculateBaseColor(ICandleMessage candle, decimal haOpen, decimal haClose)
{
const int neutral = 2;
if (haClose > haOpen)
return candle.OpenPrice <= candle.ClosePrice ? 4 : 3;
if (haClose < haOpen)
return candle.OpenPrice > candle.ClosePrice ? 0 : 1;
return neutral;
}
private readonly record struct TtmEntry(decimal HaOpen, decimal HaClose, int Color);
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import BaseIndicator
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class ttm_trend_reopen_strategy(Strategy):
def __init__(self):
super(ttm_trend_reopen_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Timeframe", "General")
self._comp_bars = self.Param("CompBars", 6).SetGreaterThanZero().SetDisplay("Comparison Bars", "HA bars for color smoothing", "Indicator")
self._signal_bar = self.Param("SignalBar", 1).SetNotNegative().SetDisplay("Signal Bar", "Offset of signal bar", "Indicator")
self._price_step_points = self.Param("PriceStepPoints", 1000.0).SetNotNegative().SetDisplay("Re-entry Step", "Min favorable move for pyramiding", "Risk")
self._max_positions = self.Param("MaxPositions", 1).SetGreaterThanZero().SetDisplay("Max Entries", "Max stacked entries per direction", "Risk")
self._enable_long_entries = self.Param("EnableLongEntries", True).SetDisplay("Enable Long Entries", "Allow buying on bullish", "Trading Rules")
self._enable_short_entries = self.Param("EnableShortEntries", True).SetDisplay("Enable Short Entries", "Allow selling on bearish", "Trading Rules")
self._enable_long_exits = self.Param("EnableLongExits", True).SetDisplay("Enable Long Exits", "Close longs on bearish", "Trading Rules")
self._enable_short_exits = self.Param("EnableShortExits", True).SetDisplay("Enable Short Exits", "Close shorts on bullish", "Trading Rules")
self._sl_points = self.Param("StopLossPoints", 1000.0).SetNotNegative().SetDisplay("Stop Loss (points)", "SL distance", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 2000.0).SetNotNegative().SetDisplay("Take Profit (points)", "TP distance", "Risk")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(ttm_trend_reopen_strategy, self).OnReseted()
self._color_history = []
self._long_entries = 0
self._short_entries = 0
self._entry_price = 0
self._prev_ha_open = None
self._prev_ha_close = None
self._ha_history = []
def OnStarted2(self, time):
super(ttm_trend_reopen_strategy, self).OnStarted2(time)
self._color_history = []
self._long_entries = 0
self._short_entries = 0
self._entry_price = 0
self._prev_ha_open = None
self._prev_ha_close = None
self._ha_history = []
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._on_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
sl = float(self._sl_points.Value)
tp = float(self._tp_points.Value)
sl_unit = Unit(sl * step, UnitTypes.Absolute) if sl > 0 else None
tp_unit = Unit(tp * step, UnitTypes.Absolute) if tp > 0 else None
if sl_unit is not None or tp_unit is not None:
self.StartProtection(sl_unit, tp_unit)
def _calc_base_color(self, candle, ha_open, ha_close):
if ha_close > ha_open:
return 4 if float(candle.OpenPrice) <= float(candle.ClosePrice) else 3
elif ha_close < ha_open:
return 0 if float(candle.OpenPrice) > float(candle.ClosePrice) else 1
else:
return 2
def _on_candle(self, candle):
if candle.State != CandleStates.Finished:
return
o = float(candle.OpenPrice)
h = float(candle.HighPrice)
l = float(candle.LowPrice)
c = float(candle.ClosePrice)
ha_close = (o + h + l + c) / 4.0
if self._prev_ha_open is None or self._prev_ha_close is None:
ha_open = (o + c) / 2.0
else:
ha_open = (self._prev_ha_open + self._prev_ha_close) / 2.0
self._prev_ha_open = ha_open
self._prev_ha_close = ha_close
color = self._calc_base_color(candle, ha_open, ha_close)
comp = int(self._comp_bars.Value)
for entry in self._ha_history:
e_high = max(entry[0], entry[1])
e_low = min(entry[0], entry[1])
if ha_open <= e_high and ha_open >= e_low and ha_close <= e_high and ha_close >= e_low:
color = entry[2]
break
self._ha_history.insert(0, (ha_open, ha_close, color))
while len(self._ha_history) > max(1, comp):
self._ha_history.pop()
self._color_history.append(color)
signal_bar = int(self._signal_bar.Value)
offset = max(0, signal_bar - 1)
signal_index = len(self._color_history) - 1 - offset
if signal_index < 0:
return
current_color = self._color_history[signal_index]
previous_color = self._color_history[signal_index - 1] if signal_index > 0 else None
is_bullish = current_color == 1 or current_color == 4
is_bearish = current_color == 0 or current_color == 3
was_bullish = previous_color is not None and (previous_color == 1 or previous_color == 4)
was_bearish = previous_color is not None and (previous_color == 0 or previous_color == 3)
enable_long_exits = self._enable_long_exits.Value
enable_short_exits = self._enable_short_exits.Value
enable_long_entries = self._enable_long_entries.Value
enable_short_entries = self._enable_short_entries.Value
pos = self.Position
if enable_long_exits and is_bearish and pos > 0:
self.SellMarket()
self._long_entries = 0
if enable_short_exits and is_bullish and pos < 0:
self.BuyMarket()
self._short_entries = 0
pos = self.Position
if enable_long_entries and is_bullish and previous_color is not None and not was_bullish and pos <= 0:
if pos < 0:
self.BuyMarket()
self.BuyMarket()
self._long_entries = 1
self._short_entries = 0
self._entry_price = float(candle.ClosePrice)
elif enable_short_entries and is_bearish and previous_color is not None and not was_bearish and pos >= 0:
if pos > 0:
self.SellMarket()
self.SellMarket()
self._short_entries = 1
self._long_entries = 0
self._entry_price = float(candle.ClosePrice)
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
reentry_step = float(self._price_step_points.Value) * step
max_pos = int(self._max_positions.Value)
pos = self.Position
if enable_long_entries and pos > 0 and reentry_step > 0 and self._long_entries > 0 and self._long_entries < max_pos:
distance = float(candle.ClosePrice) - self._entry_price
if distance >= reentry_step:
self.BuyMarket()
self._long_entries += 1
self._entry_price = float(candle.ClosePrice)
elif enable_short_entries and pos < 0 and reentry_step > 0 and self._short_entries > 0 and self._short_entries < max_pos:
distance = self._entry_price - float(candle.ClosePrice)
if distance >= reentry_step:
self.SellMarket()
self._short_entries += 1
self._entry_price = float(candle.ClosePrice)
keep = max(offset + 2, 3)
if len(self._color_history) > keep:
self._color_history = self._color_history[len(self._color_history) - keep:]
def CreateClone(self):
return ttm_trend_reopen_strategy()