Tipu EA Multi-Timeframe Strategy
Overview
This strategy recreates the core logic of the Tipu Expert Advisor in StockSharp. It replaces the proprietary Tipu Trend and Tipu Stops indicators with a combination of exponential moving averages (EMA), Average Directional Index (ADX) filtering and Average True Range (ATR) risk controls. The system looks for trend alignment between a higher timeframe (default 1 hour) and a signal timeframe (default 15 minutes), then manages the position with a break-even pyramiding module, trailing stop logic and optional fixed take profit.
The implementation focuses on liquid, trending instruments where multi-timeframe momentum signals are reliable. The higher timeframe defines context and filters out ranging phases, while the signal timeframe supplies actual entries.
Data Subscriptions
- Higher timeframe candles (default 1 hour) for EMA trend and ADX range detection.
- Signal timeframe candles (default 15 minutes) for entry signals, ATR stop placement and trade management updates.
Trading Logic
- Higher timeframe context
- Calculate fast and slow EMAs and detect crossovers. A bullish crossover produces an uptrend signal; a bearish crossover produces a downtrend signal.
- Measure trend strength with ADX. If ADX is below the configured threshold, the market is marked as ranging and no new trades are allowed.
- Store the timestamp of the last higher timeframe signal. Signal validity expires after a configurable number of minutes.
- Signal timeframe entries
- Wait for an EMA crossover on the signal timeframe and a fresh higher timeframe signal in the same direction while the higher timeframe is not ranging.
- Long entries require the fast EMA to cross above the slow EMA; short entries require the opposite.
- Before sending a new order the strategy optionally closes the opposite position (reverse-on-signal behaviour) and respects the hedging flag.
- Initial stop distance is set to
ATR * AtrMultiplierand capped by theMaxRiskPipsparameter. Orders are skipped if the required risk exceeds this threshold.
- Risk management
- Take profit: optional fixed target based on
TakeProfitPips. - Trailing stop: once price moves by
TrailingStartPipsin favour, the stop trails the market with aTrailingCushionPipsoffset. - Risk-free mode: when enabled the strategy moves the stop to break-even after
RiskFreeStepPipsprofit and adds additional volume inPyramidIncrementVolumesteps untilPyramidMaxVolumeis reached. Each pyramid step also tightens the protective stop. - Positions are closed immediately on the opposite signal if
CloseOnReverseSignalis true.
- Take profit: optional fixed target based on
Parameters
AllowHedging– Allow adding positions without first closing the opposite side.CloseOnReverseSignal– Flatten the current position when an opposite signal arrives.EnableTakeProfit,TakeProfitPips– Enable and configure the fixed take profit distance in pips.MaxRiskPips– Maximum stop distance allowed in pips. Prevents entries with excessive initial risk.TradeVolume– Base order size for the first position.EnableRiskFreePyramiding,RiskFreeStepPips,PyramidIncrementVolume,PyramidMaxVolume– Control the risk-free pyramiding logic.EnableTrailingStop,TrailingStartPips,TrailingCushionPips– Configure trailing stop behaviour.HigherFastLength,HigherSlowLength,LowerFastLength,LowerSlowLength– EMA lengths for trend detection on both timeframes.AdxLength,AdxThreshold– ADX parameters used to filter range-bound markets on the higher timeframe.AtrLength,AtrMultiplier– ATR parameters for initial stop calculation.HigherSignalWindowMinutes– Validity period for the higher timeframe signal.HigherCandleType,LowerCandleType– Candle types/timeframes for context and signal processing.
Behaviour Notes
- The average entry price is recalculated whenever new volume is added, ensuring trailing stops and the risk-free module reference the actual position cost basis.
- All trading decisions are taken on completed candles only; unfinished candles are ignored to avoid premature signals.
- The strategy issues market orders (
BuyMarket/SellMarket) and performs position management internally without relying on pending stop orders. - Because the original Tipu indicators are proprietary, EMA/ADX/ATR combinations are used as a faithful approximation while keeping the original trade management features (reverse-on-signal, break-even pyramiding and trailing stop).
Usage Tips
- Optimise EMA lengths, ATR multiplier and ADX threshold for the targeted instrument; the provided defaults work as a generic starting point for FX majors.
- Set
HigherSignalWindowMinutesclose to the higher timeframe duration to require near-synchronous alignment, or increase it to allow more lag between higher and lower timeframe signals. - When pyramiding is disabled, the strategy still moves the stop to break-even once the
RiskFreeStepPipsdistance is reached, providing basic risk protection. - Disable
CloseOnReverseSignalif you prefer to manage exits manually or to allow the trailing stop to manage the entire 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()