Hedge Average 策略
该策略源自 MetaTrader 的 "Hedge Average" 专家。它比较两个周期内开盘价与收盘价的简单移动平均线。
交易逻辑
- 计算
Period1和Period2的开盘价与收盘价 SMA。 - 当长周期开盘均线高于其收盘均线且短周期开盘均线低于其收盘均线时,开多单。
- 当长周期开盘均线低于其收盘均线且短周期开盘均线高于其收盘均线时,开空单。
- 只有在
StartHour与EndHour之间才允许交易。 - 可选的止损和止盈以绝对价格单位设置,启用时 trailing stop 会随着价格移动。
参数
Period1– 快速均线周期。Period2– 慢速均线周期。StartHour– 开始交易的小时。EndHour– 结束交易的小时。CandleType– 使用的K线周期。TakeProfit– 止盈距离(价格单位)。StopLoss– 止损距离(价格单位)。UseTrailing– 是否启用基于止损距离的追踪止损。
说明
该策略采用单一持仓,不包含原版 MQL 中基于金额的获利目标。
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()