Gazonkos Expert 策略
概览
本策略移植自 MetaTrader 4 上的 "gazonkos expert" 智能交易系统,原版运行在 EUR/USD 的 1 小时周期。算法在确认出现强劲的单根 K 线动量后,等待价格出现固定幅度的回调再顺势进场,并为仓位设置固定点差的止损与止盈。
原始 MQL4 逻辑
- 持续计算两个历史收盘价之间的差值(
Close[t2] - Close[t1])。默认参数为t1 = 3、t2 = 2,对应于两小时前与三小时前那两根已完成 K 线的收盘价。 - 当
Close[t2] - Close[t1]大于delta时判定为多头动量;若Close[t1] - Close[t2]大于同一阈值则判定为空头动量。 - 触发动量后,EA 会在同一小时内记录价格的极值(多头记录最高价,空头记录最低价)。若随后出现
Otkat点的回调,则按动量方向发送市价单。 - 如果已经存在相同 magic number 的仓位,或该小时内已经开过仓,系统会禁止再次进场。
- 每笔交易同时设置固定距离的止盈(
TakeProfit)和止损(StopLoss),单位均为点。
C# 版本的状态机
移植版本完全保留了原有的状态流转:
- WaitingForSlot:检查当前小时是否已经下单,以及是否超过允许的最大持仓数。
- WaitingForImpulse:根据
Close[t2]与Close[t1]判断多空动量。 - MonitoringRetracement:在动量触发后持续更新极值,并等待价格在同一小时内回调
RetracementPips(原始参数Otkat)。 - AwaitingExecution:在满足回调条件时按动量方向下市价单,并立即按照合约
PriceStep计算止盈止损距离。
策略只处理已完成的蜡烛数据,与原版 EA 相同,忽略尚未收盘的小时数据。
参数说明
| 参数 | 说明 |
|---|---|
TakeProfitPips |
入场价到止盈价之间的距离。 |
RetracementPips |
动量后所需的回调幅度。 |
StopLossPips |
入场价到止损价之间的距离。 |
T1Shift |
动量检测中较旧的参考 K 线索引(默认 3)。 |
T2Shift |
动量检测中较新的参考 K 线索引(默认 2)。 |
DeltaPips |
判定动量所需的最小价差。 |
LotSize |
每笔订单使用的固定手数。 |
MaxActiveTrades |
允许的最大并发仓位数;若大于 1,需要账户支持净头寸叠加。 |
CandleType |
用于分析的蜡烛时间框架(默认 1 小时)。 |
所有以点数表示的距离都会乘以 Security.PriceStep 转换成真实价格差。当合约没有提供价格步长时,默认使用 0.0001,与原始 EUR/USD 设置保持一致。
实现细节
- 使用 StockSharp 的高级 API (
SubscribeCandles().Bind) 订阅蜡烛并驱动逻辑。 - 为模拟 MQL4 中的
Close[i]访问方式,收盘价存储在轻量级的滚动缓冲区内。 - 开仓后会记录当前蜡烛的小时数,在同一小时内禁止再次进场,对应原策略的
LastTradeTime保护机制。 MaxActiveTrades依据当前净仓位进行判断;在净额账户中等同于只允许一笔持仓,与原策略默认行为一致。- 代码中的注释使用英文详细说明状态机逻辑,便于维护与复查。
namespace StockSharp.Samples.Strategies;
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;
/// <summary>
/// Momentum pullback strategy converted from the MetaTrader 4 "gazonkos expert" EA.
/// </summary>
public class GazonkosExpertStrategy : Strategy
{
private enum TradeStates
{
WaitingForSlot,
WaitingForImpulse,
MonitoringRetracement,
AwaitingExecution,
}
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _retracementPips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<int> _t1Shift;
private readonly StrategyParam<int> _t2Shift;
private readonly StrategyParam<decimal> _deltaPips;
private readonly StrategyParam<decimal> _lotSize;
private readonly StrategyParam<int> _maxActiveTrades;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closeHistory = new();
private TradeStates _state = TradeStates.WaitingForSlot;
private Sides? _pendingDirection;
private decimal _extremePrice;
private int? _lastTradeHour;
private int? _lastSignalHour;
private decimal _pointValue;
/// <summary>
/// Initializes a new instance of <see cref="GazonkosExpertStrategy"/>.
/// </summary>
public GazonkosExpertStrategy()
{
_takeProfitPips = Param(nameof(TakeProfitPips), 16m)
.SetDisplay("Take Profit (pips)", "Distance between entry and the take profit level", "Risk")
.SetGreaterThanZero()
;
_retracementPips = Param(nameof(RetracementPips), 16m)
.SetDisplay("Retracement (pips)", "Pullback distance that confirms the entry", "Signals")
.SetGreaterThanZero()
;
_stopLossPips = Param(nameof(StopLossPips), 40m)
.SetDisplay("Stop Loss (pips)", "Distance between entry and the protective stop", "Risk")
.SetGreaterThanZero()
;
_t1Shift = Param(nameof(T1Shift), 3)
.SetDisplay("T1 Shift", "Index of the older reference close used for momentum detection", "Signals")
.SetGreaterThanZero()
;
_t2Shift = Param(nameof(T2Shift), 2)
.SetDisplay("T2 Shift", "Index of the newer reference close used for momentum detection", "Signals")
.SetGreaterThanZero()
;
_deltaPips = Param(nameof(DeltaPips), 40m)
.SetDisplay("Delta (pips)", "Minimum distance between the reference closes to trigger a signal", "Signals")
.SetGreaterThanZero()
;
_lotSize = Param(nameof(LotSize), 0.1m)
.SetDisplay("Lot Size", "Fixed volume used for each trade", "Orders")
.SetGreaterThanZero()
;
_maxActiveTrades = Param(nameof(MaxActiveTrades), 1)
.SetDisplay("Max Active Trades", "Maximum number of simultaneous trades allowed", "Risk")
.SetGreaterThanZero()
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used to evaluate the momentum signal", "General");
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Pullback distance expressed in pips.
/// </summary>
public decimal RetracementPips
{
get => _retracementPips.Value;
set => _retracementPips.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Index of the older candle used in the momentum calculation.
/// </summary>
public int T1Shift
{
get => _t1Shift.Value;
set => _t1Shift.Value = value;
}
/// <summary>
/// Index of the newer candle used in the momentum calculation.
/// </summary>
public int T2Shift
{
get => _t2Shift.Value;
set => _t2Shift.Value = value;
}
/// <summary>
/// Required momentum distance expressed in pips.
/// </summary>
public decimal DeltaPips
{
get => _deltaPips.Value;
set => _deltaPips.Value = value;
}
/// <summary>
/// Fixed lot size of every order.
/// </summary>
public decimal LotSize
{
get => _lotSize.Value;
set => _lotSize.Value = value;
}
/// <summary>
/// Maximum number of simultaneous trades allowed by the strategy.
/// </summary>
public int MaxActiveTrades
{
get => _maxActiveTrades.Value;
set => _maxActiveTrades.Value = value;
}
/// <summary>
/// Candle series type used for signal generation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closeHistory.Clear();
_state = TradeStates.WaitingForSlot;
_pendingDirection = null;
_extremePrice = 0m;
_lastTradeHour = null;
_lastSignalHour = null;
_pointValue = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointValue = Security?.PriceStep ?? 0m;
if (_pointValue <= 0m)
_pointValue = 0.0001m;
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
var takeProfit = TakeProfitPips * _pointValue;
var stopLoss = StopLossPips * _pointValue;
StartProtection(
takeProfit: takeProfit > 0m ? new Unit(takeProfit, UnitTypes.Absolute) : null,
stopLoss: stopLoss > 0m ? new Unit(stopLoss, UnitTypes.Absolute) : null,
useMarketOrders: true);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
StoreClose(candle.ClosePrice);
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!TryGetClose(T1Shift, out var t1Close) || !TryGetClose(T2Shift, out var t2Close))
return;
switch (_state)
{
case TradeStates.WaitingForSlot:
ProcessWaitingForSlot(candle);
break;
case TradeStates.WaitingForImpulse:
ProcessWaitingForImpulse(candle, t1Close, t2Close);
break;
case TradeStates.MonitoringRetracement:
ProcessMonitoringRetracement(candle);
break;
case TradeStates.AwaitingExecution:
ProcessAwaitingExecution(candle);
break;
}
}
private void ProcessWaitingForSlot(ICandleMessage candle)
{
if (CanStartNewCycle(candle.CloseTime))
{
_state = TradeStates.WaitingForImpulse;
LogInfo($"Slot available at {candle.CloseTime:u}.");
}
}
private void ProcessWaitingForImpulse(ICandleMessage candle, decimal t1Close, decimal t2Close)
{
var deltaThreshold = DeltaPips * _pointValue;
if (deltaThreshold <= 0m)
return;
var difference = t2Close - t1Close;
if (difference > deltaThreshold)
{
_pendingDirection = Sides.Buy;
_extremePrice = Math.Max(candle.HighPrice, candle.ClosePrice);
_lastSignalHour = candle.CloseTime.Hour;
_state = TradeStates.MonitoringRetracement;
LogInfo($"Bullish impulse detected at {candle.CloseTime:u} with diff {difference}.");
return;
}
if (-difference > deltaThreshold)
{
_pendingDirection = Sides.Sell;
_extremePrice = candle.LowPrice > 0m ? Math.Min(candle.LowPrice, candle.ClosePrice) : candle.ClosePrice;
_lastSignalHour = candle.CloseTime.Hour;
_state = TradeStates.MonitoringRetracement;
LogInfo($"Bearish impulse detected at {candle.CloseTime:u} with diff {difference}.");
}
}
private void ProcessMonitoringRetracement(ICandleMessage candle)
{
if (_pendingDirection == null)
{
ResetState();
return;
}
if (_lastSignalHour.HasValue && _lastSignalHour.Value != candle.CloseTime.Hour)
{
LogInfo("Signal expired because the hour changed.");
ResetState();
return;
}
var retracementDistance = RetracementPips * _pointValue;
if (retracementDistance <= 0m)
{
ResetState();
return;
}
if (_pendingDirection == Sides.Buy)
{
_extremePrice = Math.Max(_extremePrice, Math.Max(candle.HighPrice, candle.ClosePrice));
var triggerPrice = _extremePrice - retracementDistance;
if (candle.ClosePrice <= triggerPrice)
{
_state = TradeStates.AwaitingExecution;
LogInfo($"Bullish pullback confirmed at {candle.CloseTime:u}. Trigger price {triggerPrice}.");
}
}
else if (_pendingDirection == Sides.Sell)
{
_extremePrice = _extremePrice <= 0m ? candle.LowPrice : Math.Min(_extremePrice, Math.Min(candle.LowPrice, candle.ClosePrice));
var triggerPrice = _extremePrice + retracementDistance;
if (candle.ClosePrice >= triggerPrice)
{
_state = TradeStates.AwaitingExecution;
LogInfo($"Bearish pullback confirmed at {candle.CloseTime:u}. Trigger price {triggerPrice}.");
}
}
}
private void ProcessAwaitingExecution(ICandleMessage candle)
{
if (_pendingDirection == null)
{
ResetState();
return;
}
if (!CanStartNewCycle(candle.CloseTime))
{
LogInfo("Cannot execute because slot conditions are no longer satisfied.");
ResetState();
return;
}
var volume = LotSize;
if (volume <= 0m)
{
ResetState();
return;
}
if (_pendingDirection == Sides.Buy)
{
BuyMarket(volume);
_lastTradeHour = candle.CloseTime.Hour;
LogInfo($"Opened long position at {candle.CloseTime:u} with volume {volume}.");
}
else if (_pendingDirection == Sides.Sell)
{
SellMarket(volume);
_lastTradeHour = candle.CloseTime.Hour;
LogInfo($"Opened short position at {candle.CloseTime:u} with volume {volume}.");
}
ResetState();
}
private bool CanStartNewCycle(DateTimeOffset time)
{
if (_lastTradeHour.HasValue && _lastTradeHour.Value == time.Hour)
return false;
if (MaxActiveTrades <= 0)
return false;
if (LotSize <= 0m)
return false;
var currentTrades = LotSize > 0m ? Math.Abs(Position) / LotSize : 0m;
return currentTrades < MaxActiveTrades;
}
private void ResetState()
{
_state = TradeStates.WaitingForSlot;
_pendingDirection = null;
_extremePrice = 0m;
_lastSignalHour = null;
}
private void StoreClose(decimal value)
{
_closeHistory.Add(value);
var capacity = Math.Max(T1Shift, T2Shift) + 5;
if (_closeHistory.Count > capacity)
_closeHistory.RemoveAt(0);
}
private bool TryGetClose(int shift, out decimal value)
{
value = 0m;
if (shift < 0)
return false;
var index = _closeHistory.Count - 1 - shift;
if (index < 0 || index >= _closeHistory.Count)
return false;
value = _closeHistory[index];
return true;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class gazonkos_expert_strategy(Strategy):
"""
Gazonkos Expert: momentum pullback strategy.
Detects impulse via close difference, waits for retracement,
then enters. Uses StartProtection for SL/TP.
"""
def __init__(self):
super(gazonkos_expert_strategy, self).__init__()
self._take_profit_pips = self.Param("TakeProfitPips", 16.0) \
.SetDisplay("Take Profit (pips)", "Distance to take profit level", "Risk")
self._retracement_pips = self.Param("RetracementPips", 16.0) \
.SetDisplay("Retracement (pips)", "Pullback distance for confirmation", "Signals")
self._stop_loss_pips = self.Param("StopLossPips", 40.0) \
.SetDisplay("Stop Loss (pips)", "Distance to protective stop", "Risk")
self._t1_shift = self.Param("T1Shift", 3) \
.SetDisplay("T1 Shift", "Older reference close index", "Signals")
self._t2_shift = self.Param("T2Shift", 2) \
.SetDisplay("T2 Shift", "Newer reference close index", "Signals")
self._delta_pips = self.Param("DeltaPips", 40.0) \
.SetDisplay("Delta (pips)", "Minimum distance between reference closes", "Signals")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Timeframe for momentum signal", "General")
self._close_history = []
self._state = 0 # 0=WaitSlot, 1=WaitImpulse, 2=MonitorRetracement, 3=Execute
self._pending_direction = None
self._extreme_price = 0.0
self._last_trade_hour = None
self._last_signal_hour = None
self._point_value = 0.0001
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(gazonkos_expert_strategy, self).OnReseted()
self._close_history = []
self._state = 0
self._pending_direction = None
self._extreme_price = 0.0
self._last_trade_hour = None
self._last_signal_hour = None
def OnStarted2(self, time):
super(gazonkos_expert_strategy, self).OnStarted2(time)
ps = 0.0001
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps <= 0:
ps = 0.0001
self._point_value = ps
tp = self._take_profit_pips.Value * ps
sl = self._stop_loss_pips.Value * ps
if tp > 0 and sl > 0:
self.StartProtection(
Unit(tp, UnitTypes.Absolute),
Unit(sl, UnitTypes.Absolute))
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
capacity = max(self._t1_shift.Value, self._t2_shift.Value) + 5
self._close_history.append(close)
while len(self._close_history) > capacity:
self._close_history.pop(0)
t1 = self._t1_shift.Value
t2 = self._t2_shift.Value
if len(self._close_history) - 1 - t1 < 0 or len(self._close_history) - 1 - t2 < 0:
return
t1_close = self._close_history[len(self._close_history) - 1 - t1]
t2_close = self._close_history[len(self._close_history) - 1 - t2]
hour = candle.CloseTime.Hour
if self._state == 0:
if self._can_start_new_cycle(hour):
self._state = 1
if self._state == 1:
delta_threshold = self._delta_pips.Value * self._point_value
if delta_threshold <= 0:
return
diff = t2_close - t1_close
if diff > delta_threshold:
self._pending_direction = 1 # buy
self._extreme_price = max(high, close)
self._last_signal_hour = hour
self._state = 2
elif -diff > delta_threshold:
self._pending_direction = -1 # sell
self._extreme_price = min(low, close) if low > 0 else close
self._last_signal_hour = hour
self._state = 2
if self._state == 2:
if self._pending_direction is None:
self._reset_state()
return
if self._last_signal_hour is not None and self._last_signal_hour != hour:
self._reset_state()
return
retracement = self._retracement_pips.Value * self._point_value
if retracement <= 0:
self._reset_state()
return
if self._pending_direction == 1:
self._extreme_price = max(self._extreme_price, max(high, close))
if close <= self._extreme_price - retracement:
self._state = 3
elif self._pending_direction == -1:
if self._extreme_price <= 0:
self._extreme_price = low
self._extreme_price = min(self._extreme_price, min(low, close))
if close >= self._extreme_price + retracement:
self._state = 3
if self._state == 3:
if self._pending_direction is None:
self._reset_state()
return
if not self._can_start_new_cycle(hour):
self._reset_state()
return
if self._pending_direction == 1:
self.BuyMarket()
self._last_trade_hour = hour
elif self._pending_direction == -1:
self.SellMarket()
self._last_trade_hour = hour
self._reset_state()
def _can_start_new_cycle(self, hour):
if self._last_trade_hour is not None and self._last_trade_hour == hour:
return False
return True
def _reset_state(self):
self._state = 0
self._pending_direction = None
self._extreme_price = 0.0
self._last_signal_hour = None
def CreateClone(self):
return gazonkos_expert_strategy()