Стратегия Tipu EA с мультифреймовым анализом
Общее описание
Стратегия переносит идею советника Tipu в среду StockSharp. Поскольку оригинальные индикаторы Tipu Trend и Tipu Stops недоступны, их функции воспроизводятся с помощью экспоненциальных скользящих средних (EMA), индекса направленного движения (ADX) и среднего истинного диапазона (ATR). Высокий таймфрейм (по умолчанию 1 час) задаёт торговый контекст и фильтрует флэт, а рабочий таймфрейм (по умолчанию 15 минут) формирует точки входа. Управление позицией включает перенос стопа в безубыток с пирамидингом, трейлинг-стоп и опциональный фиксированный тейк-профит.
Стратегия рассчитана на инструменты с хорошей ликвидностью и выраженными трендовыми фазами. Совпадение трендов на двух таймфреймах уменьшает количество ложных сигналов, а блок управления рисками повторяет логику оригинального советника.
Подписка на данные
- Свечи старшего таймфрейма (1 час) для расчёта EMA и фильтра ADX.
- Свечи рабочего таймфрейма (15 минут) для сигналов входа, расчёта ATR и последующего сопровождения позиции.
Логика работы
- Контекст старшего таймфрейма
- Отслеживаются пересечения быстрой и медленной EMA. Пересечение вверх задаёт бычий контекст, пересечение вниз — медвежий.
- ADX используется для оценки силы тренда. Если значение ниже порога, новые сделки запрещены как во флэте.
- Запоминается время последнего сигнала со старшего таймфрейма. Сигнал действителен ограниченное число минут.
- Входы на рабочем таймфрейме
- Необходима синхронизация: пересечение EMA на рабочем таймфрейме и свежий сигнал старшего таймфрейма в том же направлении при отсутствии режима «флэт».
- Перед открытием сделки стратегия может закрыть противоположную позицию (опция
CloseOnReverseSignal) и учитывает флагAllowHedging. - Стоп-лосс выставляется на расстоянии
ATR * AtrMultiplier, но не большеMaxRiskPips. Если риск превышает лимит, вход пропускается.
- Управление риском
- Тейк-профит: фиксированная цель на расстоянии
TakeProfitPips(если включено). - Трейлинг-стоп: при достижении прибыли
TrailingStartPipsстоп подтягивается с отступомTrailingCushionPips. - Безубыточный пирамидинг: при росте прибыли на
RiskFreeStepPipsстоп переносится в безубыток, после чего позиция наращивается шагамиPyramidIncrementVolumeдо потолкаPyramidMaxVolume. Каждое добавление сопровождается подтяжкой стопа. - При включённой опции
CloseOnReverseSignalпозиция немедленно закрывается по противоположному сигналу.
- Тейк-профит: фиксированная цель на расстоянии
Параметры
AllowHedging– разрешать ли держать разнонаправленные позиции.CloseOnReverseSignal– закрывать ли позицию при появлении обратного сигнала.EnableTakeProfit,TakeProfitPips– включение и величина фиксированного тейк-профита (в пунктах).MaxRiskPips– максимальная допустимая дистанция до стопа при входе.TradeVolume– базовый объём первой сделки.EnableRiskFreePyramiding,RiskFreeStepPips,PyramidIncrementVolume,PyramidMaxVolume– настройки блока безубыточного пирамидинга.EnableTrailingStop,TrailingStartPips,TrailingCushionPips– параметры трейлинг-стопа.HigherFastLength,HigherSlowLength,LowerFastLength,LowerSlowLength– периоды EMA на обоих таймфреймах.AdxLength,AdxThreshold– параметры ADX для фильтрации флэта.AtrLength,AtrMultiplier– параметры ATR, применяемого для первоначального стопа.HigherSignalWindowMinutes– время жизни сигнала старшего таймфрейма (в минутах).HigherCandleType,LowerCandleType– используемые таймфреймы свечей.
Особенности реализации
- Средняя цена входа пересчитывается после каждого увеличения позиции, поэтому трейлинг и перенос в безубыток опираются на фактическую себестоимость.
- Обработка сигналов ведётся только по закрытым свечам, что исключает шум от незавершённых баров.
- Стратегия работает исключительно рыночными ордерами
BuyMarket/SellMarket; управление стопами и пирамидинг выполняются внутри алгоритма. - Из-за отсутствия оригинальных индикаторов применяется приближённая схема EMA + ADX + ATR, сохраняя ключевые принципы Tipu EA (разворот по сигналу, перенос стопа в безубыток и ступенчатое наращивание позиции).
Рекомендации по использованию
- Настройте периоды EMA, множитель ATR и порог ADX под конкретный инструмент; стандартные значения подходят для стартового тестирования валютных пар.
- Значение
HigherSignalWindowMinutesвыбирайте вблизи длительности старшего таймфрейма для строгой синхронизации либо увеличивайте, если допустима задержка между сигналами. - Даже без пирамидинга блок безубытка перемещает стоп на цену входа после достижения
RiskFreeStepPips, обеспечивая базовую защиту капитала. - При желании удерживать позицию до срабатывания стопа отключите
CloseOnReverseSignal, оставив управление выходом трейлинг-стопу.
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()