三根K线反转策略
该策略将 MQL5 专家顾问 Exp_ThreeCandles 完整移植到 StockSharp。核心思想是寻找典型的三根K线反转形态:
- 连续两根同向K线(多头反转时为两根阴线,空头反转时为两根阳线)。
- 第三根K线方向反转,并且收盘价突破前一根中间K线的开盘价/高低点。
- 可选的成交量确认,当最老的那根K线波动过大时会自动跳过此过滤。
当出现多头形态时,策略会先平掉空头,再根据参数决定是否开多;出现空头形态时逻辑相反。止损与止盈距离通过当前品种的最小报价单位(PriceStep)来设置。
形态识别
策略维护一段包含 SignalBar + 3 根已完成K线的滑动窗口。每当有新的K线收盘,就会取位于 SignalBar 偏移(默认:向前1根)的那根K线以及再往前的三根K线进行判断:
- 多头反转(准备做多):
- 最老的两根K线(
SignalBar + 3与SignalBar + 2)均为下跌K线。 - 中间K线的收盘价高于最老K线的最低价。
- 位于
SignalBar + 1的最近一根K线为上涨K线,且收盘价高于中间K线的开盘价。
- 最老的两根K线(
- 空头反转(准备做空):
- 上述条件镜像互换。
成交量过滤完全复刻原始指标。当最老K线的振幅换算成价格步长后超过 MaxBarSize,或 VolumeFilter 设置为 None 时跳过过滤。否则需要满足以下条件之一:最老成交量 < 中间成交量、或最近成交量 > 中间成交量、或最近成交量 > 最老成交量。由于高阶API仅提供聚合成交量,Tick 与 Real 模式都使用蜡烛的 TotalVolume 字段。
交易管理
- 启用
AllowSellExit时,检测到多头形态会立即平仓任何空头仓位,再根据AllowBuyEntry判断是否开多;AllowBuyExit对多头仓位执行对称逻辑。 - 只有在当前仓位为空并且对应的
Allow*Entry参数为真时才会开仓,成交量使用策略的默认下单量。 StopLossPips与TakeProfitPips以价格步长为单位,在每根已完成K线上检查是否命中。- 策略缓存最近一次多/空信号的收盘时间,防止在同一根K线的多个tick上重复触发。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
CandleType |
4小时K线 | 策略订阅与处理的K线类型。 |
SignalBar |
1 | 用于评估信号的历史偏移,必须 ≥ 0。 |
MaxBarSize |
300 | 如果最老K线的振幅(乘以 PriceStep)超过该值,则跳过成交量过滤;设置为0则永远跳过。 |
VolumeFilter |
Tick |
成交量模式(Tick、Real 或 None),前两者都使用 TotalVolume。 |
AllowBuyEntry |
true |
允许在多头形态出现时开多。 |
AllowSellEntry |
true |
允许在空头形态出现时开空。 |
AllowBuyExit |
true |
允许在空头形态出现时平掉多头仓位。 |
AllowSellExit |
true |
允许在多头形态出现时平掉空头仓位。 |
StopLossPips |
1000 | 止损距离(价格步长单位,0 表示禁用)。 |
TakeProfitPips |
2000 | 止盈距离(价格步长单位,0 表示禁用)。 |
移植说明
- 原MQL5中通过
TradeAlgorithms.mqh计算头寸大小的资金管理被替换为 StockSharp 的BuyMarket/SellMarket调用,因此仓位规模遵循平台默认设置。 - 信号时序与专家顾问保持一致:在
SignalBar偏移的K线上作出决策,并记录最近一次信号时间以避免重复执行。 - 指标中的声音、邮件和推送提醒在移植时被刻意省略。
- 虽然保留了
Tick/Real两种选项,但由于高阶API无法区分,二者都映射到蜡烛的聚合成交量。 - 所有注释与文档均改写为英文,以符合仓库要求。
该策略在遵循原始逻辑的同时,完全兼容 StockSharp 的高阶订阅与执行模式。
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>
/// Translates the classic Three Candles reversal expert advisor from MQL5.
/// The strategy searches for two candles in one direction followed by a strong opposite candle and trades the expected reversal.
/// </summary>
public class ThreeCandlesReversalStrategy : Strategy
{
public enum ThreeCandlesVolumeTypes
{
Tick,
Real,
None,
}
private readonly List<CandleSample> _candles = new();
private static readonly object _sync = new();
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<int> _maxBarSize;
private readonly StrategyParam<ThreeCandlesVolumeTypes> _volumeFilter;
private readonly StrategyParam<bool> _allowBuyEntry;
private readonly StrategyParam<bool> _allowSellEntry;
private readonly StrategyParam<bool> _allowBuyExit;
private readonly StrategyParam<bool> _allowSellExit;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private DateTimeOffset? _lastBullishSignalTime;
private DateTimeOffset? _lastBearishSignalTime;
private decimal _entryPrice;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int SignalBar { get => _signalBar.Value; set => _signalBar.Value = value; }
public int MaxBarSize { get => _maxBarSize.Value; set => _maxBarSize.Value = value; }
public ThreeCandlesVolumeTypes VolumeFilter { get => _volumeFilter.Value; set => _volumeFilter.Value = value; }
public bool AllowBuyEntry { get => _allowBuyEntry.Value; set => _allowBuyEntry.Value = value; }
public bool AllowSellEntry { get => _allowSellEntry.Value; set => _allowSellEntry.Value = value; }
public bool AllowBuyExit { get => _allowBuyExit.Value; set => _allowBuyExit.Value = value; }
public bool AllowSellExit { get => _allowSellExit.Value; set => _allowSellExit.Value = value; }
public decimal StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
public decimal TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
public ThreeCandlesReversalStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Time frame for the candle subscription", "General");
_signalBar = Param(nameof(SignalBar), 1)
.SetRange(0, 20)
.SetDisplay("Signal Bar", "Historical offset where the signal is evaluated", "Pattern");
_maxBarSize = Param(nameof(MaxBarSize), 300)
.SetRange(0, 100000)
.SetDisplay("Max Bar Size", "Disable the volume filter when the oldest candle range exceeds this value (in price steps)", "Pattern");
_volumeFilter = Param(nameof(VolumeFilter), ThreeCandlesVolumeTypes.Tick)
.SetDisplay("Volume Filter", "Volume filter used to confirm the reversal", "Pattern");
_allowBuyEntry = Param(nameof(AllowBuyEntry), true)
.SetDisplay("Allow Buy Entry", "Enable long entries on bullish signals", "Trading");
_allowSellEntry = Param(nameof(AllowSellEntry), true)
.SetDisplay("Allow Sell Entry", "Enable short entries on bearish signals", "Trading");
_allowBuyExit = Param(nameof(AllowBuyExit), true)
.SetDisplay("Allow Buy Exit", "Close long positions when a bearish pattern appears", "Trading");
_allowSellExit = Param(nameof(AllowSellExit), true)
.SetDisplay("Allow Sell Exit", "Close short positions when a bullish pattern appears", "Trading");
_stopLossPips = Param(nameof(StopLossPips), 1000m)
.SetRange(0m, 100000m)
.SetDisplay("Stop Loss", "Distance to the protective stop in price steps", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 2000m)
.SetRange(0m, 100000m)
.SetDisplay("Take Profit", "Distance to the profit target in price steps", "Risk");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_candles.Clear();
_lastBullishSignalTime = null;
_lastBearishSignalTime = null;
_entryPrice = 0m;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
lock (_sync)
{
var closeTime = candle.CloseTime != default
? candle.CloseTime
: candle.OpenTime + (CandleType.Arg is TimeSpan tf ? tf : TimeSpan.Zero);
_candles.Add(new CandleSample(
candle.OpenTime,
closeTime,
candle.OpenPrice,
candle.HighPrice,
candle.LowPrice,
candle.ClosePrice,
candle.TotalVolume));
var required = SignalBar + 5;
while (_candles.Count > required)
_candles.RemoveAt(0);
if (_candles.Count < required)
return;
var priceStep = Security?.PriceStep ?? 1m;
if (priceStep <= 0m)
priceStep = 1m;
if (CheckRiskManagement(candle, priceStep))
return;
var buffer = _candles.ToArray();
var bullishSignal = IsBullishSignal(buffer, priceStep);
var bearishSignal = IsBearishSignal(buffer, priceStep);
if (bullishSignal)
{
var signalCandle = GetSeries(buffer, SignalBar);
HandleBullish(signalCandle);
}
if (bearishSignal)
{
var signalCandle = GetSeries(buffer, SignalBar);
HandleBearish(signalCandle);
}
}
}
private bool CheckRiskManagement(ICandleMessage candle, decimal priceStep)
{
if (Position == 0m || _entryPrice == 0m)
return false;
var stopDistance = StopLossPips > 0m ? StopLossPips * priceStep : 0m;
var takeDistance = TakeProfitPips > 0m ? TakeProfitPips * priceStep : 0m;
if (Position > 0m)
{
var stopTriggered = stopDistance > 0m && candle.LowPrice <= _entryPrice - stopDistance;
var takeTriggered = takeDistance > 0m && candle.HighPrice >= _entryPrice + takeDistance;
if (stopTriggered || takeTriggered)
{
SellMarket();
ResetTradeState();
return true;
}
}
else if (Position < 0m)
{
var stopTriggered = stopDistance > 0m && candle.HighPrice >= _entryPrice + stopDistance;
var takeTriggered = takeDistance > 0m && candle.LowPrice <= _entryPrice - takeDistance;
if (stopTriggered || takeTriggered)
{
BuyMarket();
ResetTradeState();
return true;
}
}
return false;
}
private void HandleBullish(CandleSample signalCandle)
{
var signalTime = signalCandle.CloseTime;
if (_lastBullishSignalTime == signalTime)
return;
if (AllowSellExit && Position < 0m)
{
BuyMarket();
ResetTradeState();
}
if (AllowBuyEntry && Position == 0m)
{
BuyMarket();
_entryPrice = signalCandle.ClosePrice;
}
_lastBullishSignalTime = signalTime;
}
private void HandleBearish(CandleSample signalCandle)
{
var signalTime = signalCandle.CloseTime;
if (_lastBearishSignalTime == signalTime)
return;
if (AllowBuyExit && Position > 0m)
{
SellMarket();
ResetTradeState();
}
if (AllowSellEntry && Position == 0m)
{
SellMarket();
_entryPrice = signalCandle.ClosePrice;
}
_lastBearishSignalTime = signalTime;
}
private bool IsBullishSignal(IReadOnlyList<CandleSample> candles, decimal priceStep)
{
var last = GetSeries(candles, SignalBar + 1);
var middle = GetSeries(candles, SignalBar + 2);
var oldest = GetSeries(candles, SignalBar + 3);
if (!(oldest.OpenPrice > oldest.ClosePrice &&
middle.OpenPrice > middle.ClosePrice &&
middle.ClosePrice > oldest.LowPrice &&
last.OpenPrice < last.ClosePrice &&
last.ClosePrice > middle.OpenPrice))
{
return false;
}
if (!ShouldApplyVolumeFilter(oldest, priceStep))
return true;
var volOldest = oldest.Volume;
var volMiddle = middle.Volume;
var volLast = last.Volume;
return volOldest < volMiddle || volLast > volMiddle || volLast > volOldest;
}
private bool IsBearishSignal(IReadOnlyList<CandleSample> candles, decimal priceStep)
{
var last = GetSeries(candles, SignalBar + 1);
var middle = GetSeries(candles, SignalBar + 2);
var oldest = GetSeries(candles, SignalBar + 3);
if (!(oldest.OpenPrice < oldest.ClosePrice &&
middle.OpenPrice < middle.ClosePrice &&
middle.ClosePrice < oldest.HighPrice &&
last.OpenPrice > last.ClosePrice &&
last.ClosePrice < middle.OpenPrice))
{
return false;
}
if (!ShouldApplyVolumeFilter(oldest, priceStep))
return true;
var volOldest = oldest.Volume;
var volMiddle = middle.Volume;
var volLast = last.Volume;
return volOldest < volMiddle || volLast > volMiddle || volLast > volOldest;
}
private bool ShouldApplyVolumeFilter(CandleSample oldest, decimal priceStep)
{
if (VolumeFilter == ThreeCandlesVolumeTypes.None)
return false;
if (MaxBarSize <= 0)
return false;
var range = oldest.HighPrice - oldest.LowPrice;
var threshold = MaxBarSize * priceStep;
if (range > threshold)
return false;
return true;
}
private static CandleSample GetSeries(IReadOnlyList<CandleSample> candles, int index)
{
var idx = candles.Count - 1 - index;
return candles[idx];
}
private void ResetTradeState()
{
_entryPrice = 0m;
}
private readonly record struct CandleSample(
DateTimeOffset OpenTime,
DateTimeOffset CloseTime,
decimal OpenPrice,
decimal HighPrice,
decimal LowPrice,
decimal ClosePrice,
decimal Volume);
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
VOL_TICK = 0
VOL_REAL = 1
VOL_NONE = 2
class three_candles_reversal_strategy(Strategy):
def __init__(self):
super(three_candles_reversal_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._signal_bar = self.Param("SignalBar", 1)
self._max_bar_size = self.Param("MaxBarSize", 300)
self._volume_filter = self.Param("VolumeFilter", VOL_TICK)
self._allow_buy_entry = self.Param("AllowBuyEntry", True)
self._allow_sell_entry = self.Param("AllowSellEntry", True)
self._allow_buy_exit = self.Param("AllowBuyExit", True)
self._allow_sell_exit = self.Param("AllowSellExit", True)
self._stop_loss_pips = self.Param("StopLossPips", 1000.0)
self._take_profit_pips = self.Param("TakeProfitPips", 2000.0)
self._candles = []
self._last_bullish_signal_time = None
self._last_bearish_signal_time = None
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def SignalBar(self):
return self._signal_bar.Value
@SignalBar.setter
def SignalBar(self, value):
self._signal_bar.Value = value
@property
def MaxBarSize(self):
return self._max_bar_size.Value
@MaxBarSize.setter
def MaxBarSize(self, value):
self._max_bar_size.Value = value
@property
def VolumeFilter(self):
return self._volume_filter.Value
@VolumeFilter.setter
def VolumeFilter(self, value):
self._volume_filter.Value = value
@property
def AllowBuyEntry(self):
return self._allow_buy_entry.Value
@AllowBuyEntry.setter
def AllowBuyEntry(self, value):
self._allow_buy_entry.Value = value
@property
def AllowSellEntry(self):
return self._allow_sell_entry.Value
@AllowSellEntry.setter
def AllowSellEntry(self, value):
self._allow_sell_entry.Value = value
@property
def AllowBuyExit(self):
return self._allow_buy_exit.Value
@AllowBuyExit.setter
def AllowBuyExit(self, value):
self._allow_buy_exit.Value = value
@property
def AllowSellExit(self):
return self._allow_sell_exit.Value
@AllowSellExit.setter
def AllowSellExit(self, value):
self._allow_sell_exit.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
def OnStarted2(self, time):
super(three_candles_reversal_strategy, self).OnStarted2(time)
self._candles = []
self._last_bullish_signal_time = None
self._last_bearish_signal_time = None
self._entry_price = 0.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
open_time = candle.OpenTime
close_time = candle.CloseTime if candle.CloseTime is not None else open_time
open_price = float(candle.OpenPrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
volume = float(candle.TotalVolume)
self._candles.append((open_time, close_time, open_price, high, low, close, volume))
sb = int(self.SignalBar)
required = sb + 5
while len(self._candles) > required:
self._candles.pop(0)
if len(self._candles) < required:
return
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if price_step <= 0.0:
price_step = 1.0
if self._check_risk_management(candle, price_step):
return
buffer = list(self._candles)
bullish_signal = self._is_bullish_signal(buffer, price_step, sb)
bearish_signal = self._is_bearish_signal(buffer, price_step, sb)
if bullish_signal:
signal_candle = self._get_series(buffer, sb)
self._handle_bullish(signal_candle)
if bearish_signal:
signal_candle = self._get_series(buffer, sb)
self._handle_bearish(signal_candle)
def _check_risk_management(self, candle, price_step):
if self.Position == 0 or self._entry_price == 0.0:
return False
high = float(candle.HighPrice)
low = float(candle.LowPrice)
stop_distance = float(self.StopLossPips) * price_step if float(self.StopLossPips) > 0.0 else 0.0
take_distance = float(self.TakeProfitPips) * price_step if float(self.TakeProfitPips) > 0.0 else 0.0
if self.Position > 0:
stop_triggered = stop_distance > 0.0 and low <= self._entry_price - stop_distance
take_triggered = take_distance > 0.0 and high >= self._entry_price + take_distance
if stop_triggered or take_triggered:
self.SellMarket()
self._entry_price = 0.0
return True
elif self.Position < 0:
stop_triggered = stop_distance > 0.0 and high >= self._entry_price + stop_distance
take_triggered = take_distance > 0.0 and low <= self._entry_price - take_distance
if stop_triggered or take_triggered:
self.BuyMarket()
self._entry_price = 0.0
return True
return False
def _handle_bullish(self, signal_candle):
signal_time = signal_candle[1]
if self._last_bullish_signal_time == signal_time:
return
if self.AllowSellExit and self.Position < 0:
self.BuyMarket()
self._entry_price = 0.0
if self.AllowBuyEntry and self.Position == 0:
self.BuyMarket()
self._entry_price = signal_candle[5]
self._last_bullish_signal_time = signal_time
def _handle_bearish(self, signal_candle):
signal_time = signal_candle[1]
if self._last_bearish_signal_time == signal_time:
return
if self.AllowBuyExit and self.Position > 0:
self.SellMarket()
self._entry_price = 0.0
if self.AllowSellEntry and self.Position == 0:
self.SellMarket()
self._entry_price = signal_candle[5]
self._last_bearish_signal_time = signal_time
def _is_bullish_signal(self, candles, price_step, sb):
last = self._get_series(candles, sb + 1)
middle = self._get_series(candles, sb + 2)
oldest = self._get_series(candles, sb + 3)
if not (oldest[2] > oldest[5] and
middle[2] > middle[5] and
middle[5] > oldest[4] and
last[2] < last[5] and
last[5] > middle[2]):
return False
if not self._should_apply_volume_filter(oldest, price_step):
return True
vol_oldest = oldest[6]
vol_middle = middle[6]
vol_last = last[6]
return vol_oldest < vol_middle or vol_last > vol_middle or vol_last > vol_oldest
def _is_bearish_signal(self, candles, price_step, sb):
last = self._get_series(candles, sb + 1)
middle = self._get_series(candles, sb + 2)
oldest = self._get_series(candles, sb + 3)
if not (oldest[2] < oldest[5] and
middle[2] < middle[5] and
middle[5] < oldest[3] and
last[2] > last[5] and
last[5] < middle[2]):
return False
if not self._should_apply_volume_filter(oldest, price_step):
return True
vol_oldest = oldest[6]
vol_middle = middle[6]
vol_last = last[6]
return vol_oldest < vol_middle or vol_last > vol_middle or vol_last > vol_oldest
def _should_apply_volume_filter(self, oldest, price_step):
if int(self.VolumeFilter) == VOL_NONE:
return False
if int(self.MaxBarSize) <= 0:
return False
bar_range = oldest[3] - oldest[4]
threshold = int(self.MaxBarSize) * price_step
if bar_range > threshold:
return False
return True
def _get_series(self, candles, index):
idx = len(candles) - 1 - index
return candles[idx]
def OnReseted(self):
super(three_candles_reversal_strategy, self).OnReseted()
self._candles = []
self._last_bullish_signal_time = None
self._last_bearish_signal_time = None
self._entry_price = 0.0
def CreateClone(self):
return three_candles_reversal_strategy()