Tipu EA 多周期策略
概述
该策略在 StockSharp 中重现 Tipu Expert Advisor 的主要思想。由于原始的 Tipu Trend 与 Tipu Stops 指标为专有实现,这里以指数移动平均线(EMA)、平均趋向指数(ADX)和平均真实波幅(ATR)的组合来完成同样的信号判断与风控。策略在较高周期(默认 1 小时)和信号周期(默认 15 分钟)之间寻找趋势共振,并通过保本加仓、移动止损及可选的固定止盈来管理持仓。
策略适用于流动性充足且具备趋势特征的市场。较高周期负责给出趋势背景并过滤震荡行情,而信号周期负责触发具体的入场条件。
订阅数据
- 较高周期 K 线(默认 1 小时),用于 EMA 趋势判断和 ADX 震荡过滤。
- 信号周期 K 线(默认 15 分钟),用于入场信号、ATR 止损计算以及交易管理。
交易逻辑
- 高周期背景
- 计算快、慢 EMA 并检测金叉/死叉。金叉产生多头方向信号,死叉产生空头方向信号。
- 使用 ADX 衡量趋势强度。当 ADX 低于阈值时认为市场处于震荡,不允许开新仓。
- 记录最近一次高周期信号的时间戳,信号只在设定的时间窗口内有效。
- 信号周期入场
- 必须同时满足信号周期上的 EMA 交叉和高周期最新信号方向一致,且高周期不在震荡状态。
- 多头需要快 EMA 上穿慢 EMA,空头则反向。
- 在下单前根据参数决定是否先平掉相反方向的持仓(反转平仓),并遵守是否允许对冲的设置。
- 初始止损距离为
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– 策略使用的两个时间周期。
实现细节
- 每次加仓后都会重新计算加权平均持仓价,保证移动止损与保本加仓模块基于真实成本进行判断。
- 所有决策均基于已完成的 K 线,未收盘的蜡烛不会触发信号,以避免噪声。
- 策略仅使用市价单(
BuyMarket/SellMarket)完成开平仓,内部自行完成止损、止盈与加仓管理,无需挂出额外的条件单。 - 由于原始 Tipu 指标不可用,采用 EMA + ADX + ATR 的组合来逼近原始行为,同时完整保留反向平仓、保本加仓和移动止损等管理特色。
使用建议
- 在目标品种上优化 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()