回撤反弹策略
概述
回撤反弹策略是 MQL5 专家顾问“TST (barabashkakvn's edition)”的 C# 版本。它在 CandleType 指定的时间框架上监控单一品种,并寻找先扩张后回撤的价格形态。当多头K线从高点回落超过设置的回撤阈值时买入;当空头K线从低点反弹超过阈值时卖出。策略使用 StockSharp 的高级 K 线订阅接口,并将所有保护性订单以点(pip)表示后换算成绝对价格。
点值基于标的的 PriceStep 计算,对拥有三位或五位小数的品种会自动乘以 10,以符合 MetaTrader 对 pip 的定义。交易数量取自策略的 Volume 属性。
入场逻辑
- 仅在所选
CandleType的已完成 K 线上评估信号。 - 当
ReverseSignal = false(默认)时:- 多头条件: K 线收盘价低于开盘价,且最高价与收盘价的差值超过
RollbackRatePips(换算成价格)。这意味着价格向上扩张后出现足够的回撤,从而触发逆势买入。 - 空头条件: K 线收盘价高于开盘价,且收盘价与最低价的差值超过
RollbackRatePips,与多头逻辑对称。
- 多头条件: K 线收盘价低于开盘价,且最高价与收盘价的差值超过
- 当
ReverseSignal = true时,买卖条件对调。 - 仅在当前仓位为空或与信号方向相反时开仓,委托数量为
Volume + |Position|,确保反向仓位被先行平仓。
离场逻辑
- 开仓后会记录基于点值换算出的止损和止盈价位,只要 K 线范围触及某个价位,便以市价单离场。
- 将
StopLossPips或TakeProfitPips设为 0 可分别禁用止损或止盈。 - 当浮动利润超过
TrailingStopPips + TrailingStepPips时激活跟踪止损:- 多头仓位的止损移动到
最高价 - TrailingStopPips,前提是该新值至少比旧止损高TrailingStepPips。 - 空头仓位的止损移动到
最低价 + TrailingStopPips,前提是该新值至少比旧止损低TrailingStepPips。 - 如果价格反向突破当前的跟踪止损,立即以市价平仓。
- 多头仓位的止损移动到
- 当仓位清零时会重置所有内部状态,防止遗留数据。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
CandleType |
用于计算的 K 线类型。 | 15 分钟 K 线 |
StopLossPips |
止损距离(点),为 0 时关闭止损。 | 30 |
TakeProfitPips |
止盈距离(点),为 0 时关闭止盈。 | 90 |
TrailingStopPips |
跟踪止损的基础距离(点),为 0 时关闭跟踪。 | 1 |
TrailingStepPips |
每次移动跟踪止损所需的额外盈利(点),启用跟踪时必须大于 0。 | 15 |
RollbackRatePips |
触发信号所需的回撤幅度(点)。 | 15 |
ReverseSignal |
反转买卖方向。 | false |
使用说明
- 启动前需要设置
Volume属性,它决定每次下单的数量。 - 若启用跟踪止损,必须保证
TrailingStopPips > 0且TrailingStepPips > 0,否则策略会在启动时抛出错误。 - 原始 EA 在 K 线生成过程中基于逐笔数据判断,这一版本使用完成 K 线的最高价、最低价和收盘价近似原逻辑,从而保持与 StockSharp 高级 API 的兼容性。
- 策略一次仅处理一个标的,若需多品种交易请创建多个策略实例。
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>
/// Rollback Rebound strategy that follows the TST (barabashkakvn's edition) expert advisor logic.
/// The strategy buys after a bullish bar pulls back from its high and sells after a bearish bar rebounds from its low.
/// Protective orders are managed in pips and include optional trailing logic with a rollback filter.
/// </summary>
public class RollbackReboundStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<decimal> _rollbackRatePips;
private readonly StrategyParam<bool> _reverseSignal;
private decimal _pipSize;
private decimal _stopLossOffset;
private decimal _takeProfitOffset;
private decimal _trailingStopOffset;
private decimal _trailingStepOffset;
private decimal _rollbackOffset;
private decimal _longEntryPrice;
private decimal _longStopPrice;
private decimal _longTakeProfitPrice;
private decimal _shortEntryPrice;
private decimal _shortStopPrice;
private decimal _shortTakeProfitPrice;
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Stop loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Additional profit in pips required before the trailing stop moves.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Pullback threshold in pips to validate signals.
/// </summary>
public decimal RollbackRatePips
{
get => _rollbackRatePips.Value;
set => _rollbackRatePips.Value = value;
}
/// <summary>
/// Inverts entry direction.
/// </summary>
public bool ReverseSignal
{
get => _reverseSignal.Value;
set => _reverseSignal.Value = value;
}
/// <summary>
/// Initialize parameters with defaults derived from the original MQL expert.
/// </summary>
public RollbackReboundStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
.SetDisplay("Candle Type", "Candle series used for calculations.", "General");
_stopLossPips = Param(nameof(StopLossPips), 30m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Distance of the protective stop in pips.", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 90m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Distance of the take profit in pips.", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 20m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop offset in pips.", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 15m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Additional profit required before trailing adjusts.", "Risk");
_rollbackRatePips = Param(nameof(RollbackRatePips), 40m)
.SetNotNegative()
.SetDisplay("Rollback Threshold (pips)", "Minimum pullback from the bar extreme to trigger entries.", "Signal");
_reverseSignal = Param(nameof(ReverseSignal), false)
.SetDisplay("Reverse Signal", "Invert entry logic (buy becomes sell).", "Signal");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_stopLossOffset = 0m;
_takeProfitOffset = 0m;
_trailingStopOffset = 0m;
_trailingStepOffset = 0m;
_rollbackOffset = 0m;
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Validate that trailing configuration matches the behaviour of the original expert.
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");
// Convert pip-based parameters into absolute price offsets.
_pipSize = Security?.PriceStep ?? 1m;
if (Security != null && (Security.Decimals == 3 || Security.Decimals == 5))
_pipSize *= 10m;
_stopLossOffset = StopLossPips * _pipSize;
_takeProfitOffset = TakeProfitPips * _pipSize;
_trailingStopOffset = TrailingStopPips * _pipSize;
_trailingStepOffset = TrailingStepPips * _pipSize;
_rollbackOffset = RollbackRatePips * _pipSize;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
// Work only with finished candles to emulate the IsNewBar check from the MQL expert.
if (candle.State != CandleStates.Finished)
return;
// Update trailing stops and exit conditions before generating new signals.
ManageOpenPosition(candle);
// Skip signal generation until the strategy is online and allowed to trade.
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Translate the rollback filters from the original EA using candle statistics.
var open = candle.OpenPrice;
var close = candle.ClosePrice;
var high = candle.HighPrice;
var low = candle.LowPrice;
var longCondition = open > close && high - close > _rollbackOffset;
var shortCondition = close > open && close - low > _rollbackOffset;
if (ReverseSignal)
{
(longCondition, shortCondition) = (shortCondition, longCondition);
}
if (longCondition && Position <= 0)
{
// Enter long when the rollback condition is met and the strategy is not already in a long position.
var volume = Volume + Math.Abs(Position);
if (volume <= 0m)
return;
BuyMarket(volume);
InitializeLongState(candle);
}
else if (shortCondition && Position >= 0)
{
// Enter short when the bearish rollback occurs and we are not currently short.
var volume = Volume + Math.Abs(Position);
if (volume <= 0m)
return;
SellMarket(volume);
InitializeShortState(candle);
}
}
private void ManageOpenPosition(ICandleMessage candle)
{
// Mirror the trailing and exit logic from the MetaTrader expert to keep behaviour identical.
if (Position > 0)
{
// Use the candle high as the most optimistic price for long positions.
var extreme = candle.HighPrice;
if (_longEntryPrice == 0m)
// Store the actual entry price once the trade is filled.
_longEntryPrice = candle.ClosePrice;
if (_trailingStopOffset > 0m)
{
// Apply the trailing algorithm for the active position.
// Move the stop only when profit exceeds trailing stop plus step, exactly as in the MQL code.
if (extreme - _longEntryPrice > _trailingStopOffset + _trailingStepOffset)
{
var threshold = extreme - (_trailingStopOffset + _trailingStepOffset);
if (_longStopPrice == 0m || _longStopPrice < threshold)
_longStopPrice = extreme - _trailingStopOffset;
}
}
if (_longTakeProfitPrice > 0m && candle.HighPrice >= _longTakeProfitPrice)
{
// Exit the long position once the take-profit level is touched.
SellMarket(Math.Abs(Position));
ResetLongState();
return;
}
if (_longStopPrice > 0m && candle.LowPrice <= _longStopPrice)
{
// Close the long position if the initial or trailing stop is triggered.
SellMarket(Math.Abs(Position));
ResetLongState();
return;
}
}
else if (Position < 0)
{
// Use the candle low as the best price in favour of the short position.
var extreme = candle.LowPrice;
if (_shortEntryPrice == 0m)
// Capture the short entry price after execution.
_shortEntryPrice = candle.ClosePrice;
if (_trailingStopOffset > 0m)
{
// Apply the trailing algorithm for short positions.
// Move the stop only when profit exceeds trailing stop plus step, exactly as in the MQL code.
if (_shortEntryPrice - extreme > _trailingStopOffset + _trailingStepOffset)
{
var threshold = extreme + (_trailingStopOffset + _trailingStepOffset);
if (_shortStopPrice == 0m || _shortStopPrice > threshold)
_shortStopPrice = extreme + _trailingStopOffset;
}
}
if (_shortTakeProfitPrice > 0m && candle.LowPrice <= _shortTakeProfitPrice)
{
// Exit the short position when the take-profit is hit.
BuyMarket(Math.Abs(Position));
ResetShortState();
return;
}
if (_shortStopPrice > 0m && candle.HighPrice >= _shortStopPrice)
{
// Cover the short position if the stop level is breached.
BuyMarket(Math.Abs(Position));
ResetShortState();
return;
}
}
else
{
// Clear cached levels when no position is open.
ResetLongState();
ResetShortState();
}
}
private void InitializeLongState(ICandleMessage candle)
{
// Clear short-side state because the strategy operates in netting mode.
ResetShortState();
var entry = candle.ClosePrice;
// Save reference prices for managing the long position.
_longEntryPrice = entry;
_longStopPrice = StopLossPips > 0m ? entry - _stopLossOffset : 0m;
_longTakeProfitPrice = TakeProfitPips > 0m ? entry + _takeProfitOffset : 0m;
}
private void InitializeShortState(ICandleMessage candle)
{
// Clear long-side state before opening a short position.
ResetLongState();
var entry = candle.ClosePrice;
// Store price references for the short position.
_shortEntryPrice = entry;
_shortStopPrice = StopLossPips > 0m ? entry + _stopLossOffset : 0m;
_shortTakeProfitPrice = TakeProfitPips > 0m ? entry - _takeProfitOffset : 0m;
}
private void ResetLongState()
{
_longEntryPrice = 0m;
_longStopPrice = 0m;
_longTakeProfitPrice = 0m;
}
private void ResetShortState()
{
_shortEntryPrice = 0m;
_shortStopPrice = 0m;
_shortTakeProfitPrice = 0m;
}
}
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.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class rollback_rebound_strategy(Strategy):
def __init__(self):
super(rollback_rebound_strategy, self).__init__()
self._sl_pips = self.Param("StopLossPips", 30.0).SetNotNegative().SetDisplay("Stop Loss (pips)", "SL distance", "Risk")
self._tp_pips = self.Param("TakeProfitPips", 90.0).SetNotNegative().SetDisplay("Take Profit (pips)", "TP distance", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 20.0).SetNotNegative().SetDisplay("Trailing Stop (pips)", "Trailing offset", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 15.0).SetNotNegative().SetDisplay("Trailing Step (pips)", "Trailing step", "Risk")
self._rollback_pips = self.Param("RollbackRatePips", 40.0).SetNotNegative().SetDisplay("Rollback Threshold (pips)", "Pullback threshold", "Signal")
self._reverse_signal = self.Param("ReverseSignal", False).SetDisplay("Reverse Signal", "Invert entry logic", "Signal")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(8))).SetDisplay("Candle Type", "Candle timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(rollback_rebound_strategy, self).OnReseted()
self._pip_size = 0
self._long_entry = 0
self._long_stop = 0
self._long_tp = 0
self._short_entry = 0
self._short_stop = 0
self._short_tp = 0
def OnStarted2(self, time):
super(rollback_rebound_strategy, self).OnStarted2(time)
self._pip_size = 1.0
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
self._pip_size = float(self.Security.PriceStep)
if self.Security.Decimals == 3 or self.Security.Decimals == 5:
self._pip_size *= 10.0
self._sl_offset = self._sl_pips.Value * self._pip_size
self._tp_offset = self._tp_pips.Value * self._pip_size
self._trail_offset = self._trailing_stop_pips.Value * self._pip_size
self._trail_step_offset = self._trailing_step_pips.Value * self._pip_size
self._rollback_offset = self._rollback_pips.Value * self._pip_size
self._long_entry = 0
self._long_stop = 0
self._long_tp = 0
self._short_entry = 0
self._short_stop = 0
self._short_tp = 0
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
self._manage_position(candle)
o = candle.OpenPrice
c = candle.ClosePrice
h = candle.HighPrice
l = candle.LowPrice
long_cond = o > c and h - c > self._rollback_offset
short_cond = c > o and c - l > self._rollback_offset
if self._reverse_signal.Value:
long_cond, short_cond = short_cond, long_cond
if long_cond and self.Position <= 0:
vol = self.Volume + abs(self.Position)
if vol <= 0:
return
self.BuyMarket(vol)
self._short_entry = 0
self._short_stop = 0
self._short_tp = 0
self._long_entry = float(c)
self._long_stop = self._long_entry - self._sl_offset if self._sl_pips.Value > 0 else 0
self._long_tp = self._long_entry + self._tp_offset if self._tp_pips.Value > 0 else 0
elif short_cond and self.Position >= 0:
vol = self.Volume + abs(self.Position)
if vol <= 0:
return
self.SellMarket(vol)
self._long_entry = 0
self._long_stop = 0
self._long_tp = 0
self._short_entry = float(c)
self._short_stop = self._short_entry + self._sl_offset if self._sl_pips.Value > 0 else 0
self._short_tp = self._short_entry - self._tp_offset if self._tp_pips.Value > 0 else 0
def _manage_position(self, candle):
if self.Position > 0:
extreme = float(candle.HighPrice)
if self._long_entry == 0:
self._long_entry = float(candle.ClosePrice)
if self._trail_offset > 0 and self._long_entry > 0:
if extreme - self._long_entry > self._trail_offset + self._trail_step_offset:
threshold = extreme - (self._trail_offset + self._trail_step_offset)
if self._long_stop == 0 or self._long_stop < threshold:
self._long_stop = extreme - self._trail_offset
if self._long_tp > 0 and candle.HighPrice >= self._long_tp:
self.SellMarket(abs(self.Position))
self._long_entry = 0
self._long_stop = 0
self._long_tp = 0
return
if self._long_stop > 0 and candle.LowPrice <= self._long_stop:
self.SellMarket(abs(self.Position))
self._long_entry = 0
self._long_stop = 0
self._long_tp = 0
return
elif self.Position < 0:
extreme = float(candle.LowPrice)
if self._short_entry == 0:
self._short_entry = float(candle.ClosePrice)
if self._trail_offset > 0 and self._short_entry > 0:
if self._short_entry - extreme > self._trail_offset + self._trail_step_offset:
threshold = extreme + (self._trail_offset + self._trail_step_offset)
if self._short_stop == 0 or self._short_stop > threshold:
self._short_stop = extreme + self._trail_offset
if self._short_tp > 0 and candle.LowPrice <= self._short_tp:
self.BuyMarket(abs(self.Position))
self._short_entry = 0
self._short_stop = 0
self._short_tp = 0
return
if self._short_stop > 0 and candle.HighPrice >= self._short_stop:
self.BuyMarket(abs(self.Position))
self._short_entry = 0
self._short_stop = 0
self._short_tp = 0
return
else:
self._long_entry = 0
self._long_stop = 0
self._long_tp = 0
self._short_entry = 0
self._short_stop = 0
self._short_tp = 0
def CreateClone(self):
return rollback_rebound_strategy()