Hedge Average Strategy
This strategy reproduces the "Hedge Average" MetaTrader expert. It compares simple moving averages of open and close prices across two time periods.
Trading Logic
- Calculate SMA of the open and close price for
Period1andPeriod2. - If the long-period open average is above its close average and the short-period open average is below its close average, a long position is opened.
- If the long-period open average is below its close average and the short-period open average is above its close average, a short position is opened.
- Trading is allowed only between
StartHourandEndHour. - Optional stop-loss and take-profit are set in absolute price units. Trailing stop moves the protective stop along with price when enabled.
Parameters
Period1– period for the fast averages.Period2– period for the slow averages.StartHour– hour of day when trading becomes active.EndHour– hour of day when trading stops.CandleType– candle timeframe used for calculations.TakeProfit– take profit distance in price units.StopLoss– stop loss distance in price units.UseTrailing– enable trailing stop based on stop-loss distance.
Notes
The strategy uses a single position approach and does not replicate money-based profit target from the original MQL version.
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Hedge Average strategy comparing open/close SMAs over a fast and a slow period.
/// </summary>
public class HedgeAverageStrategy : Strategy
{
private readonly StrategyParam<int> _period1;
private readonly StrategyParam<int> _period2;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<bool> _useTrailing;
private readonly List<decimal> _opens = [];
private readonly List<decimal> _closes = [];
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal? _bestPrice;
private DateTimeOffset? _entryCandleTime;
public int Period1 { get => _period1.Value; set => _period1.Value = value; }
public int Period2 { get => _period2.Value; set => _period2.Value = value; }
public int StartHour { get => _startHour.Value; set => _startHour.Value = value; }
public int EndHour { get => _endHour.Value; set => _endHour.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public bool UseTrailing { get => _useTrailing.Value; set => _useTrailing.Value = value; }
public HedgeAverageStrategy()
{
_period1 = Param(nameof(Period1), 5).SetGreaterThanZero();
_period2 = Param(nameof(Period2), 20).SetGreaterThanZero();
_startHour = Param(nameof(StartHour), 0).SetRange(0, 23);
_endHour = Param(nameof(EndHour), 23).SetRange(0, 23);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame());
_takeProfit = Param(nameof(TakeProfit), 0m).SetNotNegative();
_stopLoss = Param(nameof(StopLoss), 0m).SetNotNegative();
_useTrailing = Param(nameof(UseTrailing), false);
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_opens.Clear();
_closes.Clear();
ResetProtection();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_opens.Add(candle.OpenPrice);
_closes.Add(candle.ClosePrice);
var keep = Math.Max(Period1, Period2);
if (_opens.Count > keep)
{
_opens.RemoveRange(0, _opens.Count - keep);
_closes.RemoveRange(0, _closes.Count - keep);
}
if (Position != 0 && ApplyProtection(candle))
return;
if (_opens.Count < keep || Position != 0 || !IsTradingHour(candle.OpenTime.Hour))
return;
var fastOpen = AverageTail(_opens, Period1);
var fastClose = AverageTail(_closes, Period1);
var slowOpen = AverageTail(_opens, Period2);
var slowClose = AverageTail(_closes, Period2);
if (slowOpen > slowClose && fastOpen < fastClose)
Enter(Sides.Buy, candle.ClosePrice, candle.OpenTime);
else if (slowOpen < slowClose && fastOpen > fastClose)
Enter(Sides.Sell, candle.ClosePrice, candle.OpenTime);
}
private void Enter(Sides side, decimal price, DateTimeOffset candleTime)
{
if (side == Sides.Buy)
BuyMarket();
else
SellMarket();
_entryPrice = price;
_entryCandleTime = candleTime;
_bestPrice = price;
_stopPrice = StopLoss > 0m ? (side == Sides.Buy ? price - StopLoss : price + StopLoss) : null;
_takePrice = TakeProfit > 0m ? (side == Sides.Buy ? price + TakeProfit : price - TakeProfit) : null;
}
private bool ApplyProtection(ICandleMessage candle)
{
if (_entryCandleTime is DateTimeOffset entryTime && candle.OpenTime <= entryTime)
return false;
if (Position > 0)
{
_bestPrice = _bestPrice is decimal best ? Math.Max(best, candle.HighPrice) : candle.HighPrice;
if (UseTrailing && StopLoss > 0m)
{
var candidate = _bestPrice.Value - StopLoss;
if (_stopPrice is null || candidate > _stopPrice)
_stopPrice = candidate;
}
if ((_stopPrice is decimal stop && candle.LowPrice <= stop) ||
(_takePrice is decimal take && candle.HighPrice >= take))
{
SellMarket(Math.Abs(Position));
ResetProtection();
return true;
}
}
else if (Position < 0)
{
_bestPrice = _bestPrice is decimal best ? Math.Min(best, candle.LowPrice) : candle.LowPrice;
if (UseTrailing && StopLoss > 0m)
{
var candidate = _bestPrice.Value + StopLoss;
if (_stopPrice is null || candidate < _stopPrice)
_stopPrice = candidate;
}
if ((_stopPrice is decimal stop && candle.HighPrice >= stop) ||
(_takePrice is decimal take && candle.LowPrice <= take))
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return true;
}
}
return false;
}
private bool IsTradingHour(int hour)
=> StartHour <= EndHour
? hour >= StartHour && hour <= EndHour
: hour >= StartHour || hour <= EndHour;
private static decimal AverageTail(List<decimal> values, int length)
=> values.Skip(values.Count - length).Average();
private void ResetProtection()
{
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
_bestPrice = null;
_entryCandleTime = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class hedge_average_strategy(Strategy):
def __init__(self):
super(hedge_average_strategy, self).__init__()
self._period1 = self.Param("Period1", 5).SetGreaterThanZero()
self._period2 = self.Param("Period2", 20).SetGreaterThanZero()
self._start_hour = self.Param("StartHour", 0).SetRange(0, 23)
self._end_hour = self.Param("EndHour", 23).SetRange(0, 23)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._take_profit = self.Param("TakeProfit", 0.0).SetNotNegative()
self._stop_loss = self.Param("StopLoss", 0.0).SetNotNegative()
self._use_trailing = self.Param("UseTrailing", False)
self._opens = []
self._closes = []
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
self._best_price = None
self._entry_candle_time = None
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value)]
def OnReseted(self):
super(hedge_average_strategy, self).OnReseted()
self._opens = []
self._closes = []
self._reset_protection()
def OnStarted2(self, time):
super(hedge_average_strategy, self).OnStarted2(time)
self.SubscribeCandles(self._candle_type.Value).Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._opens.append(float(candle.OpenPrice))
self._closes.append(float(candle.ClosePrice))
p1 = int(self._period1.Value)
p2 = int(self._period2.Value)
keep = max(p1, p2)
if len(self._opens) > keep:
del self._opens[:-keep]
del self._closes[:-keep]
if self.Position != 0 and self._apply_protection(candle):
return
if len(self._opens) < keep or self.Position != 0 or not self._is_trading_hour(candle.OpenTime.Hour):
return
fast_open = sum(self._opens[-p1:]) / p1
fast_close = sum(self._closes[-p1:]) / p1
slow_open = sum(self._opens[-p2:]) / p2
slow_close = sum(self._closes[-p2:]) / p2
if slow_open > slow_close and fast_open < fast_close:
self._enter(Sides.Buy, float(candle.ClosePrice), candle.OpenTime)
elif slow_open < slow_close and fast_open > fast_close:
self._enter(Sides.Sell, float(candle.ClosePrice), candle.OpenTime)
def _enter(self, side, price, candle_time):
if side == Sides.Buy:
self.BuyMarket()
else:
self.SellMarket()
sl = float(self._stop_loss.Value)
tp = float(self._take_profit.Value)
self._entry_price = price
self._entry_candle_time = candle_time
self._best_price = price
self._stop_price = (price - sl if side == Sides.Buy else price + sl) if sl > 0 else None
self._take_price = (price + tp if side == Sides.Buy else price - tp) if tp > 0 else None
def _apply_protection(self, candle):
if self._entry_candle_time is not None and candle.OpenTime <= self._entry_candle_time:
return False
sl = float(self._stop_loss.Value)
if self.Position > 0:
high = float(candle.HighPrice)
self._best_price = high if self._best_price is None else max(self._best_price, high)
if bool(self._use_trailing.Value) and sl > 0:
candidate = self._best_price - sl
if self._stop_price is None or candidate > self._stop_price:
self._stop_price = candidate
if ((self._stop_price is not None and float(candle.LowPrice) <= self._stop_price) or
(self._take_price is not None and high >= self._take_price)):
self.SellMarket(Math.Abs(self.Position))
self._reset_protection()
return True
elif self.Position < 0:
low = float(candle.LowPrice)
self._best_price = low if self._best_price is None else min(self._best_price, low)
if bool(self._use_trailing.Value) and sl > 0:
candidate = self._best_price + sl
if self._stop_price is None or candidate < self._stop_price:
self._stop_price = candidate
if ((self._stop_price is not None and float(candle.HighPrice) >= self._stop_price) or
(self._take_price is not None and low <= self._take_price)):
self.BuyMarket(Math.Abs(self.Position))
self._reset_protection()
return True
return False
def _is_trading_hour(self, hour):
start = int(self._start_hour.Value)
end = int(self._end_hour.Value)
return start <= hour <= end if start <= end else hour >= start or hour <= end
def _reset_protection(self):
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
self._best_price = None
self._entry_candle_time = None
def CreateClone(self):
return hedge_average_strategy()