Estrategia Tipu EA Multi-Temporalidad
Descripción general
Esta estrategia recrea la lógica central del Asesor Experto Tipu en StockSharp. Reemplaza los indicadores propietarios Tipu Trend y Tipu Stops con una combinación de medias móviles exponenciales (EMA), filtrado por Average Directional Index (ADX) y controles de riesgo con Average True Range (ATR). El sistema busca la alineación de tendencia entre un marco temporal superior (por defecto 1 hora) y un marco temporal de señal (por defecto 15 minutos), luego gestiona la posición con un módulo de piramidaje de punto de equilibrio, lógica de trailing stop y take profit fijo opcional.
La implementación se centra en instrumentos líquidos y tendenciales donde las señales de momentum en múltiples temporalidades son confiables. El marco temporal superior define el contexto y filtra las fases de rango, mientras que el marco temporal de señal proporciona los puntos de entrada reales.
Suscripciones de datos
- Velas del marco temporal superior (por defecto 1 hora) para la tendencia EMA y la detección de rango ADX.
- Velas del marco temporal de señal (por defecto 15 minutos) para señales de entrada, colocación de stop ATR y actualizaciones de gestión de trades.
Lógica de trading
- Contexto del marco temporal superior
- Calcular EMAs rápida y lenta y detectar cruces. Un cruce alcista produce una señal de tendencia alcista; un cruce bajista produce una señal de tendencia bajista.
- Medir la fortaleza de la tendencia con ADX. Si el ADX está por debajo del umbral configurado, el mercado se marca como en rango y no se permiten nuevos trades.
- Almacenar el timestamp de la última señal del marco temporal superior. La validez de la señal expira tras un número configurable de minutos.
- Entradas en el marco temporal de señal
- Esperar un cruce EMA en el marco temporal de señal y una señal fresca del marco temporal superior en la misma dirección mientras el marco temporal superior no está en rango.
- Las entradas largas requieren que la EMA rápida cruce por encima de la EMA lenta; las entradas cortas requieren lo contrario.
- Antes de enviar una nueva orden, la estrategia opcionalmente cierra la posición opuesta (comportamiento de reversión en señal) y respeta el flag de cobertura.
- La distancia inicial del stop se establece en
ATR * AtrMultipliery está limitada por el parámetroMaxRiskPips. Las órdenes se omiten si el riesgo requerido supera este umbral.
- Gestión de riesgos
- Take profit: objetivo fijo opcional basado en
TakeProfitPips. - Trailing stop: una vez que el precio se mueve
TrailingStartPipsa favor, el stop sigue al mercado con un offset deTrailingCushionPips. - Modo sin riesgo: cuando está habilitado, la estrategia mueve el stop a punto de equilibrio tras
RiskFreeStepPipsde ganancia y añade volumen adicional en pasos dePyramidIncrementVolumehasta alcanzarPyramidMaxVolume. Cada paso de piramidaje también ajusta el stop protector. - Las posiciones se cierran inmediatamente en la señal opuesta si
CloseOnReverseSignales verdadero.
- Take profit: objetivo fijo opcional basado en
Parámetros
AllowHedging– Permitir añadir posiciones sin cerrar primero el lado opuesto.CloseOnReverseSignal– Aplanar la posición actual cuando llega una señal opuesta.EnableTakeProfit,TakeProfitPips– Habilitar y configurar la distancia de take profit fijo en pips.MaxRiskPips– Distancia máxima de stop permitida en pips. Previene entradas con riesgo inicial excesivo.TradeVolume– Tamaño de orden base para la primera posición.EnableRiskFreePyramiding,RiskFreeStepPips,PyramidIncrementVolume,PyramidMaxVolume– Controlar la lógica de piramidaje sin riesgo.EnableTrailingStop,TrailingStartPips,TrailingCushionPips– Configurar el comportamiento del trailing stop.HigherFastLength,HigherSlowLength,LowerFastLength,LowerSlowLength– Longitudes de EMA para detección de tendencia en ambos marcos temporales.AdxLength,AdxThreshold– Parámetros ADX usados para filtrar mercados en rango en el marco temporal superior.AtrLength,AtrMultiplier– Parámetros ATR para el cálculo del stop inicial.HigherSignalWindowMinutes– Período de validez de la señal del marco temporal superior.HigherCandleType,LowerCandleType– Tipos/marcos temporales de velas para el procesamiento de contexto y señal.
Notas de comportamiento
- El precio de entrada promedio se recalcula cada vez que se añade nuevo volumen, asegurando que los trailing stops y el módulo sin riesgo referencien la base de costo real de la posición.
- Todas las decisiones de trading se toman solo en velas completadas; las velas sin terminar se ignoran para evitar señales prematuras.
- La estrategia emite órdenes de mercado (
BuyMarket/SellMarket) y realiza la gestión de posiciones internamente sin depender de órdenes stop pendientes. - Dado que los indicadores Tipu originales son propietarios, se usan combinaciones EMA/ADX/ATR como una aproximación fiel manteniendo las características originales de gestión de trades (reversión en señal, piramidaje de punto de equilibrio y trailing stop).
Consejos de uso
- Optimizar longitudes de EMA, multiplicador ATR y umbral ADX para el instrumento objetivo; los valores por defecto funcionan como punto de partida genérico para divisas principales.
- Establecer
HigherSignalWindowMinutescerca de la duración del marco temporal superior para requerir alineación casi sincrónica, o aumentarlo para permitir más desfase entre señales del marco temporal superior e inferior. - Cuando el piramidaje está deshabilitado, la estrategia aún mueve el stop a punto de equilibrio una vez alcanzada la distancia
RiskFreeStepPips, proporcionando protección básica de riesgo. - Deshabilitar
CloseOnReverseSignalsi prefieres gestionar las salidas manualmente o permitir que el trailing stop gestione todo el 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()