Estratégia Tipu EA Multi-Período
Visão geral
Esta estratégia recria a lógica central do Consultor Especializado Tipu no StockSharp. Substitui os indicadores proprietários Tipu Trend e Tipu Stops por uma combinação de médias móveis exponenciais (EMA), filtragem pelo Average Directional Index (ADX) e controles de risco com Average True Range (ATR). O sistema procura alinhamento de tendência entre um período superior (padrão 1 hora) e um período de sinal (padrão 15 minutos), depois gerencia a posição com um módulo de piramidagem de ponto de equilíbrio, lógica de trailing stop e take profit fixo opcional.
A implementação foca em instrumentos líquidos e com tendência, onde os sinais de momentum multi-período são confiáveis. O período superior define o contexto e filtra as fases de range, enquanto o período de sinal fornece as entradas reais.
Assinaturas de dados
- Candles do período superior (padrão 1 hora) para tendência EMA e detecção de range ADX.
- Candles do período de sinal (padrão 15 minutos) para sinais de entrada, posicionamento de stop ATR e atualizações de gestão de trades.
Lógica de trading
- Contexto do período superior
- Calcular EMAs rápida e lenta e detectar cruzamentos. Um cruzamento de alta produz um sinal de tendência de alta; um cruzamento de baixa produz um sinal de tendência de baixa.
- Medir a força da tendência com ADX. Se o ADX estiver abaixo do limite configurado, o mercado é marcado como em range e nenhum novo trade é permitido.
- Armazenar o timestamp do último sinal do período superior. A validade do sinal expira após um número configurável de minutos.
- Entradas no período de sinal
- Aguardar um cruzamento EMA no período de sinal e um sinal fresco do período superior na mesma direção enquanto o período superior não está em range.
- Entradas compradas requerem que a EMA rápida cruze acima da EMA lenta; entradas vendidas requerem o oposto.
- Antes de enviar uma nova ordem, a estratégia opcionalmente fecha a posição oposta (comportamento de reversão no sinal) e respeita o flag de cobertura.
- A distância inicial do stop é definida como
ATR * AtrMultipliere limitada pelo parâmetroMaxRiskPips. As ordens são ignoradas se o risco necessário exceder esse limite.
- Gestão de risco
- Take profit: alvo fixo opcional baseado em
TakeProfitPips. - Trailing stop: uma vez que o preço se move
TrailingStartPipsa favor, o stop segue o mercado com um offset deTrailingCushionPips. - Modo sem risco: quando habilitado, a estratégia move o stop para o ponto de equilíbrio após
RiskFreeStepPipsde lucro e adiciona volume adicional em passos dePyramidIncrementVolumeatéPyramidMaxVolumeser atingido. Cada passo de piramidagem também aperta o stop protetor. - As posições são fechadas imediatamente no sinal oposto se
CloseOnReverseSignalfor verdadeiro.
- Take profit: alvo fixo opcional baseado em
Parâmetros
AllowHedging– Permitir adicionar posições sem primeiro fechar o lado oposto.CloseOnReverseSignal– Nivelar a posição atual quando chegar um sinal oposto.EnableTakeProfit,TakeProfitPips– Habilitar e configurar a distância de take profit fixo em pips.MaxRiskPips– Distância máxima de stop permitida em pips. Previne entradas com risco inicial excessivo.TradeVolume– Tamanho de ordem base para a primeira posição.EnableRiskFreePyramiding,RiskFreeStepPips,PyramidIncrementVolume,PyramidMaxVolume– Controlar a lógica de piramidagem sem risco.EnableTrailingStop,TrailingStartPips,TrailingCushionPips– Configurar o comportamento do trailing stop.HigherFastLength,HigherSlowLength,LowerFastLength,LowerSlowLength– Comprimentos de EMA para detecção de tendência em ambos os períodos.AdxLength,AdxThreshold– Parâmetros ADX usados para filtrar mercados em range no período superior.AtrLength,AtrMultiplier– Parâmetros ATR para cálculo do stop inicial.HigherSignalWindowMinutes– Período de validade para o sinal do período superior.HigherCandleType,LowerCandleType– Tipos/períodos de candles para processamento de contexto e sinal.
Notas de comportamento
- O preço médio de entrada é recalculado sempre que novo volume é adicionado, garantindo que trailing stops e o módulo sem risco referenciem a base de custo real da posição.
- Todas as decisões de negociação são tomadas apenas em candles completados; candles incompletos são ignorados para evitar sinais prematuros.
- A estratégia emite ordens a mercado (
BuyMarket/SellMarket) e realiza o gerenciamento de posições internamente sem depender de ordens stop pendentes. - Como os indicadores Tipu originais são proprietários, combinações EMA/ADX/ATR são usadas como uma aproximação fiel, mantendo as funcionalidades originais de gestão de trades (reversão no sinal, piramidagem de ponto de equilíbrio e trailing stop).
Dicas de uso
- Otimizar comprimentos de EMA, multiplicador ATR e limite ADX para o instrumento alvo; os padrões fornecidos funcionam como ponto de partida genérico para pares de FX principais.
- Definir
HigherSignalWindowMinutespróximo à duração do período superior para exigir alinhamento quase sincronizado, ou aumentá-lo para permitir mais defasagem entre os sinais do período superior e inferior. - Quando a piramidagem está desabilitada, a estratégia ainda move o stop para o ponto de equilíbrio uma vez atingida a distância
RiskFreeStepPips, fornecendo proteção básica de risco. - Desabilitar
CloseOnReverseSignalse preferir gerenciar saídas manualmente ou permitir que o trailing stop gerencie todo o trade.
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>
/// Trend following strategy inspired by the Tipu Expert Advisor.
/// Aligns multi-timeframe momentum signals and adds a risk-free pyramiding module.
/// </summary>
public class TipuEaStrategy : Strategy
{
private readonly StrategyParam<bool> _allowHedging;
private readonly StrategyParam<bool> _closeOnReverseSignal;
private readonly StrategyParam<bool> _enableTakeProfit;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _maxRiskPips;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<bool> _enableRiskFreePyramiding;
private readonly StrategyParam<decimal> _riskFreeStepPips;
private readonly StrategyParam<decimal> _pyramidIncrementVolume;
private readonly StrategyParam<decimal> _pyramidMaxVolume;
private readonly StrategyParam<bool> _enableTrailingStop;
private readonly StrategyParam<decimal> _trailingStartPips;
private readonly StrategyParam<decimal> _trailingCushionPips;
private readonly StrategyParam<int> _higherFastLength;
private readonly StrategyParam<int> _higherSlowLength;
private readonly StrategyParam<int> _lowerFastLength;
private readonly StrategyParam<int> _lowerSlowLength;
private readonly StrategyParam<int> _adxLength;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<int> _higherSignalWindowMinutes;
private readonly StrategyParam<DataType> _higherCandleType;
private readonly StrategyParam<DataType> _lowerCandleType;
private EMA _higherFast = null!;
private EMA _higherSlow = null!;
private EMA _lowerFast = null!;
private EMA _lowerSlow = null!;
private AverageDirectionalIndex _higherAdx = null!;
private AverageTrueRange _lowerAtr = null!;
private bool _higherInitialized;
private bool _lowerInitialized;
private decimal _higherPrevFast;
private decimal _higherPrevSlow;
private decimal _lowerPrevFast;
private decimal _lowerPrevSlow;
private int _higherTrendDirection;
private int _lastHigherSignalDirection;
private DateTimeOffset _lastHigherSignalTime;
private bool _isHigherRange;
private decimal _lastAtrValue;
private decimal _averageEntryPrice;
private decimal _currentStopPrice;
private decimal _currentTargetPrice;
private bool _riskFreeActivated;
private decimal _positionVolume;
private decimal _nextLongPyramidPrice;
private decimal _nextShortPyramidPrice;
public bool AllowHedging
{
get => _allowHedging.Value;
set => _allowHedging.Value = value;
}
public bool CloseOnReverseSignal
{
get => _closeOnReverseSignal.Value;
set => _closeOnReverseSignal.Value = value;
}
public bool EnableTakeProfit
{
get => _enableTakeProfit.Value;
set => _enableTakeProfit.Value = value;
}
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public decimal MaxRiskPips
{
get => _maxRiskPips.Value;
set => _maxRiskPips.Value = value;
}
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
public bool EnableRiskFreePyramiding
{
get => _enableRiskFreePyramiding.Value;
set => _enableRiskFreePyramiding.Value = value;
}
public decimal RiskFreeStepPips
{
get => _riskFreeStepPips.Value;
set => _riskFreeStepPips.Value = value;
}
public decimal PyramidIncrementVolume
{
get => _pyramidIncrementVolume.Value;
set => _pyramidIncrementVolume.Value = value;
}
public decimal PyramidMaxVolume
{
get => _pyramidMaxVolume.Value;
set => _pyramidMaxVolume.Value = value;
}
public bool EnableTrailingStop
{
get => _enableTrailingStop.Value;
set => _enableTrailingStop.Value = value;
}
public decimal TrailingStartPips
{
get => _trailingStartPips.Value;
set => _trailingStartPips.Value = value;
}
public decimal TrailingCushionPips
{
get => _trailingCushionPips.Value;
set => _trailingCushionPips.Value = value;
}
public int HigherFastLength
{
get => _higherFastLength.Value;
set => _higherFastLength.Value = value;
}
public int HigherSlowLength
{
get => _higherSlowLength.Value;
set => _higherSlowLength.Value = value;
}
public int LowerFastLength
{
get => _lowerFastLength.Value;
set => _lowerFastLength.Value = value;
}
public int LowerSlowLength
{
get => _lowerSlowLength.Value;
set => _lowerSlowLength.Value = value;
}
public int AdxLength
{
get => _adxLength.Value;
set => _adxLength.Value = value;
}
public decimal AdxThreshold
{
get => _adxThreshold.Value;
set => _adxThreshold.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
public int HigherSignalWindowMinutes
{
get => _higherSignalWindowMinutes.Value;
set => _higherSignalWindowMinutes.Value = value;
}
public DataType HigherCandleType
{
get => _higherCandleType.Value;
set => _higherCandleType.Value = value;
}
public DataType LowerCandleType
{
get => _lowerCandleType.Value;
set => _lowerCandleType.Value = value;
}
public TipuEaStrategy()
{
_allowHedging = Param(nameof(AllowHedging), false)
.SetDisplay("Allow Hedging", "Allow adding trades without closing opposite direction", "Risk");
_closeOnReverseSignal = Param(nameof(CloseOnReverseSignal), true)
.SetDisplay("Close On Reverse", "Close the active position when the opposite signal appears", "Risk");
_enableTakeProfit = Param(nameof(EnableTakeProfit), true)
.SetDisplay("Enable Take Profit", "Enable fixed take profit target", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50000m)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
_maxRiskPips = Param(nameof(MaxRiskPips), 100000m)
.SetGreaterThanZero()
.SetDisplay("Max Risk (pips)", "Maximum stop distance allowed in pips", "Risk");
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Base order volume", "General");
_enableRiskFreePyramiding = Param(nameof(EnableRiskFreePyramiding), true)
.SetDisplay("Enable Risk Free", "Allow risk-free pyramiding of winners", "Risk");
_riskFreeStepPips = Param(nameof(RiskFreeStepPips), 30000m)
.SetGreaterThanZero()
.SetDisplay("Risk Free Step (pips)", "Profit distance required before locking and adding", "Risk");
_pyramidIncrementVolume = Param(nameof(PyramidIncrementVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Pyramid Increment", "Additional volume added on each pyramid step", "Risk");
_pyramidMaxVolume = Param(nameof(PyramidMaxVolume), 3m)
.SetGreaterThanZero()
.SetDisplay("Pyramid Max Volume", "Maximum accumulated position volume", "Risk");
_enableTrailingStop = Param(nameof(EnableTrailingStop), true)
.SetDisplay("Enable Trailing", "Enable trailing stop once trade is in profit", "Risk");
_trailingStartPips = Param(nameof(TrailingStartPips), 30000m)
.SetGreaterThanZero()
.SetDisplay("Trailing Start (pips)", "Profit in pips required before trailing", "Risk");
_trailingCushionPips = Param(nameof(TrailingCushionPips), 15000m)
.SetGreaterThanZero()
.SetDisplay("Trailing Cushion (pips)", "Distance between price and trailing stop", "Risk");
_higherFastLength = Param(nameof(HigherFastLength), 10)
.SetGreaterThanZero()
.SetDisplay("Higher Fast EMA", "Fast EMA length on higher timeframe", "Signals");
_higherSlowLength = Param(nameof(HigherSlowLength), 21)
.SetGreaterThanZero()
.SetDisplay("Higher Slow EMA", "Slow EMA length on higher timeframe", "Signals");
_lowerFastLength = Param(nameof(LowerFastLength), 8)
.SetGreaterThanZero()
.SetDisplay("Lower Fast EMA", "Fast EMA length on signal timeframe", "Signals");
_lowerSlowLength = Param(nameof(LowerSlowLength), 21)
.SetGreaterThanZero()
.SetDisplay("Lower Slow EMA", "Slow EMA length on signal timeframe", "Signals");
_adxLength = Param(nameof(AdxLength), 14)
.SetGreaterThanZero()
.SetDisplay("ADX Length", "ADX period for range detection", "Signals");
_adxThreshold = Param(nameof(AdxThreshold), 5m)
.SetGreaterThanZero()
.SetDisplay("ADX Threshold", "Below this ADX value the market is treated as ranging", "Signals");
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period for initial stop calculation", "Risk");
_atrMultiplier = Param(nameof(AtrMultiplier), 1.5m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "Multiplier applied to ATR for the initial stop", "Risk");
_higherSignalWindowMinutes = Param(nameof(HigherSignalWindowMinutes), 14400)
.SetGreaterThanZero()
.SetDisplay("Higher Signal Window", "Minutes within which the higher timeframe signal must be recent", "Signals");
_higherCandleType = Param(nameof(HigherCandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Higher Timeframe", "Higher timeframe candles used for context", "General");
_lowerCandleType = Param(nameof(LowerCandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Signal Timeframe", "Primary timeframe used for entries", "General");
// Volume is set externally or defaults to TradeVolume
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, LowerCandleType), (Security, HigherCandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_higherInitialized = false;
_lowerInitialized = false;
_higherPrevFast = 0m;
_higherPrevSlow = 0m;
_lowerPrevFast = 0m;
_lowerPrevSlow = 0m;
_higherTrendDirection = 0;
_lastHigherSignalDirection = 0;
_lastHigherSignalTime = default;
_isHigherRange = false;
_lastAtrValue = 0m;
_averageEntryPrice = 0m;
_currentStopPrice = 0m;
_currentTargetPrice = 0m;
_riskFreeActivated = false;
_positionVolume = 0m;
_nextLongPyramidPrice = 0m;
_nextShortPyramidPrice = 0m;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Volume is set externally or defaults to TradeVolume
_higherFast = new EMA { Length = HigherFastLength };
_higherSlow = new EMA { Length = HigherSlowLength };
_lowerFast = new EMA { Length = LowerFastLength };
_lowerSlow = new EMA { Length = LowerSlowLength };
_higherAdx = new AverageDirectionalIndex { Length = AdxLength };
_lowerAtr = new AverageTrueRange { Length = AtrLength };
var higherSubscription = SubscribeCandles(HigherCandleType);
higherSubscription
.BindEx(_higherFast, _higherSlow, _higherAdx, ProcessHigherCandle)
.Start();
var lowerSubscription = SubscribeCandles(LowerCandleType);
lowerSubscription
.BindEx(_lowerFast, _lowerSlow, _lowerAtr, ProcessLowerCandle)
.Start();
}
private void ProcessHigherCandle(ICandleMessage candle, IIndicatorValue fastValue, IIndicatorValue slowValue, IIndicatorValue adxValue)
{
if (candle.State != CandleStates.Finished)
return;
if (fastValue is not DecimalIndicatorValue { IsFinal: true, Value: var fast })
return;
if (slowValue is not DecimalIndicatorValue { IsFinal: true, Value: var slow })
return;
if (adxValue is not AverageDirectionalIndexValue adx || !adxValue.IsFinal)
return;
if (adx.MovingAverage is not decimal adxStrength)
return;
if (!_higherInitialized)
{
if (!_higherFast.IsFormed || !_higherSlow.IsFormed)
return;
_higherPrevFast = fast;
_higherPrevSlow = slow;
_higherInitialized = true;
_higherTrendDirection = fast > slow ? 1 : fast < slow ? -1 : 0;
_isHigherRange = adxStrength < AdxThreshold;
return;
}
var crossUp = fast > slow && _higherPrevFast <= _higherPrevSlow;
var crossDown = fast < slow && _higherPrevFast >= _higherPrevSlow;
if (crossUp)
{
_higherTrendDirection = 1;
_lastHigherSignalDirection = 1;
_lastHigherSignalTime = GetCandleCloseTime(candle, HigherCandleType);
}
else if (crossDown)
{
_higherTrendDirection = -1;
_lastHigherSignalDirection = -1;
_lastHigherSignalTime = GetCandleCloseTime(candle, HigherCandleType);
}
else if (fast > slow)
{
_higherTrendDirection = 1;
}
else if (fast < slow)
{
_higherTrendDirection = -1;
}
_isHigherRange = adxStrength < AdxThreshold;
_higherPrevFast = fast;
_higherPrevSlow = slow;
}
private void ProcessLowerCandle(ICandleMessage candle, IIndicatorValue fastValue, IIndicatorValue slowValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (fastValue is not DecimalIndicatorValue { IsFinal: true, Value: var fast })
return;
if (slowValue is not DecimalIndicatorValue { IsFinal: true, Value: var slow })
return;
if (atrValue is not DecimalIndicatorValue { IsFinal: true, Value: var atr })
return;
_lastAtrValue = atr;
if (!_lowerInitialized)
{
if (!_lowerFast.IsFormed || !_lowerSlow.IsFormed || !_lowerAtr.IsFormed)
return;
_lowerPrevFast = fast;
_lowerPrevSlow = slow;
_lowerInitialized = true;
return;
}
var crossUp = fast > slow && _lowerPrevFast <= _lowerPrevSlow;
var crossDown = fast < slow && _lowerPrevFast >= _lowerPrevSlow;
_lowerPrevFast = fast;
_lowerPrevSlow = slow;
var closeTime = GetCandleCloseTime(candle, LowerCandleType);
if (crossUp)
HandleLongSignal(candle, closeTime);
if (crossDown)
HandleShortSignal(candle, closeTime);
ManageOpenPosition(candle, crossUp, crossDown);
}
private void HandleLongSignal(ICandleMessage candle, DateTimeOffset closeTime)
{
if (_isHigherRange)
return;
if (!IsHigherSignalValid(closeTime, 1))
return;
// indicators checked via BindEx
if (Position < 0)
{
if (!AllowHedging)
{
if (CloseOnReverseSignal)
{
BuyMarket();
ResetPositionState();
}
else
{
return;
}
}
else if (CloseOnReverseSignal)
{
BuyMarket();
ResetPositionState();
}
}
if (Position > 0)
return;
var entryPrice = candle.ClosePrice;
var atrDistance = _lastAtrValue * AtrMultiplier;
if (atrDistance <= 0m)
return;
var maxRisk = ToPrice(MaxRiskPips);
if (maxRisk > 0m && atrDistance > maxRisk)
atrDistance = maxRisk;
var stopPrice = entryPrice - atrDistance;
if (stopPrice <= 0m)
return;
var volume = TradeVolume;
if (volume <= 0m)
return;
BuyMarket();
var previousVolume = Math.Abs(_positionVolume);
var newVolume = previousVolume + volume;
_averageEntryPrice = previousVolume == 0m ? entryPrice : (previousVolume * _averageEntryPrice + entryPrice * volume) / newVolume;
_positionVolume = newVolume;
_currentStopPrice = stopPrice;
_currentTargetPrice = EnableTakeProfit ? entryPrice + ToPrice(TakeProfitPips) : 0m;
_riskFreeActivated = false;
_nextLongPyramidPrice = _averageEntryPrice + ToPrice(RiskFreeStepPips);
}
private void HandleShortSignal(ICandleMessage candle, DateTimeOffset closeTime)
{
if (_isHigherRange)
return;
if (!IsHigherSignalValid(closeTime, -1))
return;
// indicators checked via BindEx
if (Position > 0)
{
if (!AllowHedging)
{
if (CloseOnReverseSignal)
{
SellMarket();
ResetPositionState();
}
else
{
return;
}
}
else if (CloseOnReverseSignal)
{
SellMarket();
ResetPositionState();
}
}
if (Position < 0)
return;
var entryPrice = candle.ClosePrice;
var atrDistance = _lastAtrValue * AtrMultiplier;
if (atrDistance <= 0m)
return;
var maxRisk = ToPrice(MaxRiskPips);
if (maxRisk > 0m && atrDistance > maxRisk)
atrDistance = maxRisk;
var stopPrice = entryPrice + atrDistance;
var volume = TradeVolume;
if (volume <= 0m)
return;
SellMarket();
var previousVolume = Math.Abs(_positionVolume);
var newVolume = previousVolume + volume;
_averageEntryPrice = previousVolume == 0m ? entryPrice : (previousVolume * _averageEntryPrice + entryPrice * volume) / newVolume;
_positionVolume = -newVolume;
_currentStopPrice = stopPrice;
_currentTargetPrice = EnableTakeProfit ? entryPrice - ToPrice(TakeProfitPips) : 0m;
_riskFreeActivated = false;
_nextShortPyramidPrice = _averageEntryPrice - ToPrice(RiskFreeStepPips);
}
private void ManageOpenPosition(ICandleMessage candle, bool crossUp, bool crossDown)
{
var price = candle.ClosePrice;
if (Position > 0)
{
if (CloseOnReverseSignal && crossDown)
{
ExitLong();
return;
}
if (_currentStopPrice > 0m && price <= _currentStopPrice)
{
ExitLong();
return;
}
if (_currentTargetPrice > 0m && price >= _currentTargetPrice)
{
ExitLong();
return;
}
UpdateTrailingStopLong(price);
UpdateRiskFreeLong(price);
}
else if (Position < 0)
{
if (CloseOnReverseSignal && crossUp)
{
ExitShort();
return;
}
if (_currentStopPrice > 0m && price >= _currentStopPrice)
{
ExitShort();
return;
}
if (_currentTargetPrice > 0m && price <= _currentTargetPrice)
{
ExitShort();
return;
}
UpdateTrailingStopShort(price);
UpdateRiskFreeShort(price);
}
}
private void UpdateTrailingStopLong(decimal price)
{
if (!EnableTrailingStop)
return;
var start = ToPrice(TrailingStartPips);
if (start <= 0m)
return;
if (price - _averageEntryPrice < start)
return;
var cushion = ToPrice(TrailingCushionPips);
if (cushion <= 0m)
return;
var newStop = price - cushion;
if (newStop > _currentStopPrice)
_currentStopPrice = newStop;
}
private void UpdateTrailingStopShort(decimal price)
{
if (!EnableTrailingStop)
return;
var start = ToPrice(TrailingStartPips);
if (start <= 0m)
return;
if (_averageEntryPrice - price < start)
return;
var cushion = ToPrice(TrailingCushionPips);
if (cushion <= 0m)
return;
var newStop = price + cushion;
if (_currentStopPrice == 0m || newStop < _currentStopPrice)
_currentStopPrice = newStop;
}
private void UpdateRiskFreeLong(decimal price)
{
if (!EnableRiskFreePyramiding)
return;
var step = ToPrice(RiskFreeStepPips);
if (step <= 0m)
return;
if (!_riskFreeActivated)
{
if (price - _averageEntryPrice >= step)
{
_currentStopPrice = Math.Max(_currentStopPrice, _averageEntryPrice);
_riskFreeActivated = true;
}
else
{
return;
}
}
if (_nextLongPyramidPrice <= 0m)
_nextLongPyramidPrice = _averageEntryPrice + step;
if (price < _nextLongPyramidPrice)
return;
var currentVolume = Math.Abs(_positionVolume);
var maxVolume = PyramidMaxVolume;
if (maxVolume <= 0m)
return;
if (currentVolume >= maxVolume)
{
_currentStopPrice = Math.Max(_currentStopPrice, price - step);
return;
}
var increment = Math.Min(PyramidIncrementVolume, maxVolume - currentVolume);
if (increment <= 0m)
return;
BuyMarket();
var newVolume = currentVolume + increment;
_averageEntryPrice = (currentVolume * _averageEntryPrice + price * increment) / newVolume;
_positionVolume = newVolume;
_currentStopPrice = Math.Max(_currentStopPrice, price - step);
_nextLongPyramidPrice = price + step;
}
private void UpdateRiskFreeShort(decimal price)
{
if (!EnableRiskFreePyramiding)
return;
var step = ToPrice(RiskFreeStepPips);
if (step <= 0m)
return;
if (!_riskFreeActivated)
{
if (_averageEntryPrice - price >= step)
{
_currentStopPrice = _currentStopPrice == 0m ? _averageEntryPrice : Math.Min(_currentStopPrice, _averageEntryPrice);
_riskFreeActivated = true;
}
else
{
return;
}
}
if (_nextShortPyramidPrice >= _averageEntryPrice || _nextShortPyramidPrice == 0m)
_nextShortPyramidPrice = _averageEntryPrice - step;
if (price > _nextShortPyramidPrice)
return;
var currentVolume = Math.Abs(_positionVolume);
var maxVolume = PyramidMaxVolume;
if (maxVolume <= 0m)
return;
if (currentVolume >= maxVolume)
{
_currentStopPrice = _currentStopPrice == 0m ? price + step : Math.Min(_currentStopPrice, price + step);
return;
}
var increment = Math.Min(PyramidIncrementVolume, maxVolume - currentVolume);
if (increment <= 0m)
return;
SellMarket();
var newVolume = currentVolume + increment;
_averageEntryPrice = (currentVolume * _averageEntryPrice + price * increment) / newVolume;
_positionVolume = -newVolume;
_currentStopPrice = _currentStopPrice == 0m ? price + step : Math.Min(_currentStopPrice, price + step);
_nextShortPyramidPrice = price - step;
}
private void ExitLong()
{
if (Position <= 0)
return;
SellMarket();
ResetPositionState();
}
private void ExitShort()
{
if (Position >= 0)
return;
BuyMarket();
ResetPositionState();
}
private void ResetPositionState()
{
_averageEntryPrice = 0m;
_currentStopPrice = 0m;
_currentTargetPrice = 0m;
_riskFreeActivated = false;
_positionVolume = 0m;
_nextLongPyramidPrice = 0m;
_nextShortPyramidPrice = 0m;
}
private bool IsHigherSignalValid(DateTimeOffset time, int direction)
{
if (_higherTrendDirection != direction)
return false;
if (_lastHigherSignalDirection != direction)
return false;
if (_lastHigherSignalTime == default)
return false;
var window = TimeSpan.FromMinutes(HigherSignalWindowMinutes);
if (window <= TimeSpan.Zero)
return true;
return time - _lastHigherSignalTime <= window;
}
private decimal ToPrice(decimal pips)
{
if (pips <= 0m)
return 0m;
var step = Security?.PriceStep ?? 0.0001m;
return pips * step;
}
private DateTimeOffset GetCandleCloseTime(ICandleMessage candle, DataType candleType)
{
if (candle.CloseTime != default)
return candle.CloseTime;
return candle.OpenTime + GetTimeFrame(candleType);
}
private static TimeSpan GetTimeFrame(DataType dataType)
{
return dataType.Arg switch
{
TimeSpan timeSpan => timeSpan,
_ => TimeSpan.FromMinutes(1)
};
}
}
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.Indicators import (
ExponentialMovingAverage, AverageTrueRange, AverageDirectionalIndex
)
from StockSharp.Algo.Strategies import Strategy
class tipu_ea_strategy(Strategy):
"""Trend following strategy inspired by the Tipu Expert Advisor."""
def __init__(self):
super(tipu_ea_strategy, self).__init__()
self._allow_hedging = self.Param("AllowHedging", False) \
.SetDisplay("Allow Hedging", "Allow adding trades without closing opposite direction", "Risk")
self._close_on_reverse = self.Param("CloseOnReverseSignal", True) \
.SetDisplay("Close On Reverse", "Close the active position when the opposite signal appears", "Risk")
self._enable_tp = self.Param("EnableTakeProfit", True) \
.SetDisplay("Enable Take Profit", "Enable fixed take profit target", "Risk")
self._tp_pips = self.Param("TakeProfitPips", 50000.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
self._max_risk_pips = self.Param("MaxRiskPips", 100000.0) \
.SetGreaterThanZero() \
.SetDisplay("Max Risk (pips)", "Maximum stop distance allowed in pips", "Risk")
self._trade_volume = self.Param("TradeVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Trade Volume", "Base order volume", "General")
self._enable_risk_free = self.Param("EnableRiskFreePyramiding", True) \
.SetDisplay("Enable Risk Free", "Allow risk-free pyramiding of winners", "Risk")
self._risk_free_step = self.Param("RiskFreeStepPips", 30000.0) \
.SetGreaterThanZero() \
.SetDisplay("Risk Free Step (pips)", "Profit distance required before locking and adding", "Risk")
self._pyramid_inc = self.Param("PyramidIncrementVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Pyramid Increment", "Additional volume added on each pyramid step", "Risk")
self._pyramid_max = self.Param("PyramidMaxVolume", 3.0) \
.SetGreaterThanZero() \
.SetDisplay("Pyramid Max Volume", "Maximum accumulated position volume", "Risk")
self._enable_trailing = self.Param("EnableTrailingStop", True) \
.SetDisplay("Enable Trailing", "Enable trailing stop once trade is in profit", "Risk")
self._trailing_start = self.Param("TrailingStartPips", 30000.0) \
.SetGreaterThanZero() \
.SetDisplay("Trailing Start (pips)", "Profit in pips required before trailing", "Risk")
self._trailing_cushion = self.Param("TrailingCushionPips", 15000.0) \
.SetGreaterThanZero() \
.SetDisplay("Trailing Cushion (pips)", "Distance between price and trailing stop", "Risk")
self._higher_fast_len = self.Param("HigherFastLength", 10) \
.SetGreaterThanZero() \
.SetDisplay("Higher Fast EMA", "Fast EMA length on higher timeframe", "Signals")
self._higher_slow_len = self.Param("HigherSlowLength", 21) \
.SetGreaterThanZero() \
.SetDisplay("Higher Slow EMA", "Slow EMA length on higher timeframe", "Signals")
self._lower_fast_len = self.Param("LowerFastLength", 8) \
.SetGreaterThanZero() \
.SetDisplay("Lower Fast EMA", "Fast EMA length on signal timeframe", "Signals")
self._lower_slow_len = self.Param("LowerSlowLength", 21) \
.SetGreaterThanZero() \
.SetDisplay("Lower Slow EMA", "Slow EMA length on signal timeframe", "Signals")
self._adx_length = self.Param("AdxLength", 14) \
.SetGreaterThanZero() \
.SetDisplay("ADX Length", "ADX period for range detection", "Signals")
self._adx_threshold = self.Param("AdxThreshold", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("ADX Threshold", "Below this ADX value the market is treated as ranging", "Signals")
self._atr_length = self.Param("AtrLength", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Length", "ATR period for initial stop calculation", "Risk")
self._atr_mult = self.Param("AtrMultiplier", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("ATR Multiplier", "Multiplier applied to ATR for the initial stop", "Risk")
self._signal_window = self.Param("HigherSignalWindowMinutes", 14400) \
.SetGreaterThanZero() \
.SetDisplay("Higher Signal Window", "Minutes within which the higher timeframe signal must be recent", "Signals")
self._higher_candle_type = self.Param("HigherCandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Higher Timeframe", "Higher timeframe candles used for context", "General")
self._lower_candle_type = self.Param("LowerCandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Signal Timeframe", "Primary timeframe used for entries", "General")
@property
def CandleType(self):
return self._lower_candle_type.Value
def OnReseted(self):
super(tipu_ea_strategy, self).OnReseted()
self._reset_all()
def _reset_all(self):
self._higher_initialized = False
self._lower_initialized = False
self._higher_prev_fast = 0.0
self._higher_prev_slow = 0.0
self._lower_prev_fast = 0.0
self._lower_prev_slow = 0.0
self._higher_trend = 0
self._last_higher_dir = 0
self._last_higher_time = None
self._is_higher_range = False
self._last_atr = 0.0
self._avg_entry = 0.0
self._current_stop = 0.0
self._current_target = 0.0
self._risk_free_activated = False
self._pos_volume = 0.0
self._next_long_pyramid = 0.0
self._next_short_pyramid = 0.0
def OnStarted2(self, time):
super(tipu_ea_strategy, self).OnStarted2(time)
self._reset_all()
h_fast = ExponentialMovingAverage()
h_fast.Length = self._higher_fast_len.Value
h_slow = ExponentialMovingAverage()
h_slow.Length = self._higher_slow_len.Value
adx = AverageDirectionalIndex()
adx.Length = self._adx_length.Value
self._h_fast = h_fast
self._h_slow = h_slow
l_fast = ExponentialMovingAverage()
l_fast.Length = self._lower_fast_len.Value
l_slow = ExponentialMovingAverage()
l_slow.Length = self._lower_slow_len.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
self._l_fast = l_fast
self._l_slow = l_slow
self._l_atr = atr
h_sub = self.SubscribeCandles(self._higher_candle_type.Value)
h_sub.BindEx(h_fast, h_slow, adx, self._on_higher).Start()
l_sub = self.SubscribeCandles(self._lower_candle_type.Value)
l_sub.Bind(l_fast, l_slow, atr, self._on_lower).Start()
def _to_price(self, pips):
if pips <= 0:
return 0.0
sec = self.Security
step = 0.0001
if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0:
step = float(sec.PriceStep)
return float(pips) * step
def _get_candle_close_time(self, candle, candle_type):
if candle.CloseTime is not None and candle.CloseTime != candle.CloseTime.__class__():
return candle.CloseTime
arg = candle_type.Arg
if isinstance(arg, TimeSpan):
return candle.OpenTime + arg
return candle.OpenTime + TimeSpan.FromMinutes(1)
def _on_higher(self, candle, fast_val, slow_val, adx_val):
if candle.State != CandleStates.Finished:
return
if not fast_val.IsFinal or not slow_val.IsFinal or not adx_val.IsFinal:
return
if fast_val.IsEmpty or slow_val.IsEmpty or adx_val.IsEmpty:
return
fast = float(fast_val)
slow = float(slow_val)
# ADX returns AverageDirectionalIndexValue; get MovingAverage for strength
adx_ma = adx_val.MovingAverage
if adx_ma is None:
return
adx_strength = float(adx_ma)
if not self._higher_initialized:
if not self._h_fast.IsFormed or not self._h_slow.IsFormed:
return
self._higher_prev_fast = fast
self._higher_prev_slow = slow
self._higher_initialized = True
if fast > slow:
self._higher_trend = 1
elif fast < slow:
self._higher_trend = -1
else:
self._higher_trend = 0
self._is_higher_range = adx_strength < float(self._adx_threshold.Value)
return
cross_up = fast > slow and self._higher_prev_fast <= self._higher_prev_slow
cross_down = fast < slow and self._higher_prev_fast >= self._higher_prev_slow
close_time = self._get_candle_close_time(candle, self._higher_candle_type.Value)
if cross_up:
self._higher_trend = 1
self._last_higher_dir = 1
self._last_higher_time = close_time
elif cross_down:
self._higher_trend = -1
self._last_higher_dir = -1
self._last_higher_time = close_time
elif fast > slow:
self._higher_trend = 1
elif fast < slow:
self._higher_trend = -1
self._is_higher_range = adx_strength < float(self._adx_threshold.Value)
self._higher_prev_fast = fast
self._higher_prev_slow = slow
def _on_lower(self, candle, fast_val, slow_val, atr_val):
if candle.State != CandleStates.Finished:
return
fast = float(fast_val)
slow = float(slow_val)
atr = float(atr_val)
self._last_atr = atr
if not self._lower_initialized:
if not self._l_fast.IsFormed or not self._l_slow.IsFormed or not self._l_atr.IsFormed:
return
self._lower_prev_fast = fast
self._lower_prev_slow = slow
self._lower_initialized = True
return
cross_up = fast > slow and self._lower_prev_fast <= self._lower_prev_slow
cross_down = fast < slow and self._lower_prev_fast >= self._lower_prev_slow
self._lower_prev_fast = fast
self._lower_prev_slow = slow
close_time = self._get_candle_close_time(candle, self._lower_candle_type.Value)
if cross_up:
self._handle_long(candle, close_time)
if cross_down:
self._handle_short(candle, close_time)
self._manage_position(candle, cross_up, cross_down)
def _is_higher_signal_valid(self, time, direction):
if self._higher_trend != direction:
return False
if self._last_higher_dir != direction:
return False
if self._last_higher_time is None:
return False
window = TimeSpan.FromMinutes(self._signal_window.Value)
if window <= TimeSpan.Zero:
return True
return (time - self._last_higher_time) <= window
def _handle_long(self, candle, close_time):
if self._is_higher_range:
return
if not self._is_higher_signal_valid(close_time, 1):
return
if self.Position < 0:
if not self._allow_hedging.Value:
if self._close_on_reverse.Value:
self.BuyMarket()
self._reset_position()
else:
return
elif self._close_on_reverse.Value:
self.BuyMarket()
self._reset_position()
if self.Position > 0:
return
entry_price = float(candle.ClosePrice)
atr_dist = self._last_atr * float(self._atr_mult.Value)
if atr_dist <= 0:
return
max_risk = self._to_price(float(self._max_risk_pips.Value))
if max_risk > 0 and atr_dist > max_risk:
atr_dist = max_risk
stop_price = entry_price - atr_dist
if stop_price <= 0:
return
volume = float(self._trade_volume.Value)
if volume <= 0:
return
self.BuyMarket()
prev_vol = abs(self._pos_volume)
new_vol = prev_vol + volume
if prev_vol == 0:
self._avg_entry = entry_price
else:
self._avg_entry = (prev_vol * self._avg_entry + entry_price * volume) / new_vol
self._pos_volume = new_vol
self._current_stop = stop_price
if self._enable_tp.Value:
self._current_target = entry_price + self._to_price(float(self._tp_pips.Value))
else:
self._current_target = 0.0
self._risk_free_activated = False
self._next_long_pyramid = self._avg_entry + self._to_price(float(self._risk_free_step.Value))
def _handle_short(self, candle, close_time):
if self._is_higher_range:
return
if not self._is_higher_signal_valid(close_time, -1):
return
if self.Position > 0:
if not self._allow_hedging.Value:
if self._close_on_reverse.Value:
self.SellMarket()
self._reset_position()
else:
return
elif self._close_on_reverse.Value:
self.SellMarket()
self._reset_position()
if self.Position < 0:
return
entry_price = float(candle.ClosePrice)
atr_dist = self._last_atr * float(self._atr_mult.Value)
if atr_dist <= 0:
return
max_risk = self._to_price(float(self._max_risk_pips.Value))
if max_risk > 0 and atr_dist > max_risk:
atr_dist = max_risk
stop_price = entry_price + atr_dist
volume = float(self._trade_volume.Value)
if volume <= 0:
return
self.SellMarket()
prev_vol = abs(self._pos_volume)
new_vol = prev_vol + volume
if prev_vol == 0:
self._avg_entry = entry_price
else:
self._avg_entry = (prev_vol * self._avg_entry + entry_price * volume) / new_vol
self._pos_volume = -new_vol
self._current_stop = stop_price
if self._enable_tp.Value:
self._current_target = entry_price - self._to_price(float(self._tp_pips.Value))
else:
self._current_target = 0.0
self._risk_free_activated = False
self._next_short_pyramid = self._avg_entry - self._to_price(float(self._risk_free_step.Value))
def _manage_position(self, candle, cross_up, cross_down):
price = float(candle.ClosePrice)
if self.Position > 0:
if self._close_on_reverse.Value and cross_down:
self._exit_long()
return
if self._current_stop > 0 and price <= self._current_stop:
self._exit_long()
return
if self._current_target > 0 and price >= self._current_target:
self._exit_long()
return
self._update_trailing_long(price)
self._update_risk_free_long(price)
elif self.Position < 0:
if self._close_on_reverse.Value and cross_up:
self._exit_short()
return
if self._current_stop > 0 and price >= self._current_stop:
self._exit_short()
return
if self._current_target > 0 and price <= self._current_target:
self._exit_short()
return
self._update_trailing_short(price)
self._update_risk_free_short(price)
def _update_trailing_long(self, price):
if not self._enable_trailing.Value:
return
start = self._to_price(float(self._trailing_start.Value))
if start <= 0:
return
if price - self._avg_entry < start:
return
cushion = self._to_price(float(self._trailing_cushion.Value))
if cushion <= 0:
return
new_stop = price - cushion
if new_stop > self._current_stop:
self._current_stop = new_stop
def _update_trailing_short(self, price):
if not self._enable_trailing.Value:
return
start = self._to_price(float(self._trailing_start.Value))
if start <= 0:
return
if self._avg_entry - price < start:
return
cushion = self._to_price(float(self._trailing_cushion.Value))
if cushion <= 0:
return
new_stop = price + cushion
if self._current_stop == 0 or new_stop < self._current_stop:
self._current_stop = new_stop
def _update_risk_free_long(self, price):
if not self._enable_risk_free.Value:
return
step = self._to_price(float(self._risk_free_step.Value))
if step <= 0:
return
if not self._risk_free_activated:
if price - self._avg_entry >= step:
self._current_stop = max(self._current_stop, self._avg_entry)
self._risk_free_activated = True
else:
return
if self._next_long_pyramid <= 0:
self._next_long_pyramid = self._avg_entry + step
if price < self._next_long_pyramid:
return
cur_vol = abs(self._pos_volume)
max_vol = float(self._pyramid_max.Value)
if max_vol <= 0:
return
if cur_vol >= max_vol:
self._current_stop = max(self._current_stop, price - step)
return
inc = min(float(self._pyramid_inc.Value), max_vol - cur_vol)
if inc <= 0:
return
self.BuyMarket()
new_vol = cur_vol + inc
self._avg_entry = (cur_vol * self._avg_entry + price * inc) / new_vol
self._pos_volume = new_vol
self._current_stop = max(self._current_stop, price - step)
self._next_long_pyramid = price + step
def _update_risk_free_short(self, price):
if not self._enable_risk_free.Value:
return
step = self._to_price(float(self._risk_free_step.Value))
if step <= 0:
return
if not self._risk_free_activated:
if self._avg_entry - price >= step:
if self._current_stop == 0:
self._current_stop = self._avg_entry
else:
self._current_stop = min(self._current_stop, self._avg_entry)
self._risk_free_activated = True
else:
return
if self._next_short_pyramid >= self._avg_entry or self._next_short_pyramid == 0:
self._next_short_pyramid = self._avg_entry - step
if price > self._next_short_pyramid:
return
cur_vol = abs(self._pos_volume)
max_vol = float(self._pyramid_max.Value)
if max_vol <= 0:
return
if cur_vol >= max_vol:
if self._current_stop == 0:
self._current_stop = price + step
else:
self._current_stop = min(self._current_stop, price + step)
return
inc = min(float(self._pyramid_inc.Value), max_vol - cur_vol)
if inc <= 0:
return
self.SellMarket()
new_vol = cur_vol + inc
self._avg_entry = (cur_vol * self._avg_entry + price * inc) / new_vol
self._pos_volume = -new_vol
if self._current_stop == 0:
self._current_stop = price + step
else:
self._current_stop = min(self._current_stop, price + step)
self._next_short_pyramid = price - step
def _exit_long(self):
if self.Position <= 0:
return
self.SellMarket()
self._reset_position()
def _exit_short(self):
if self.Position >= 0:
return
self.BuyMarket()
self._reset_position()
def _reset_position(self):
self._avg_entry = 0.0
self._current_stop = 0.0
self._current_target = 0.0
self._risk_free_activated = False
self._pos_volume = 0.0
self._next_long_pyramid = 0.0
self._next_short_pyramid = 0.0
def CreateClone(self):
return tipu_ea_strategy()