在 GitHub 上查看
Escape 均值回归策略
概述
Escape 策略是 MetaTrader 4 智能交易系统 escape.mq4 的 StockSharp 版本。原始脚本运行在 5 分钟图上,通过均值回归信号交易:当收盘价跌破一个短期均线时买入,当收盘价上穿另一条快速均线时卖出。所有仓位都带有以 MetaTrader 点数表示的固定止盈和止损。C# 实现保留了这种极简结构,并将所有关键距离公开为参数。
交易逻辑
初始化
- 订阅可配置的
CandleType(默认 5 分钟 K 线)。
- 创建两个
SimpleMovingAverage 指标,周期分别为 5 和 4,并使用每根 K 线的开盘价进行更新。
- 根据
Security.PriceStep 计算 MetaTrader Point 的等价值,用于把点数转换成绝对价格距离。
逐根 K 线处理
- 通过
SubscribeCandles(...).WhenCandlesFinished(ProcessCandle) 仅处理已完成的 K 线。
- 首先检查当前持仓是否触及止损或止盈:比较 K 线的最高价/最低价与记录的退出水平。若价格突破相应水平,则发送市价单平仓,并通过内部标志避免重复发单。
- 当账户为空仓、两个均线的上一根数据可用、交易允许且资金充足(
Portfolio.CurrentValue >= MinimumMarginPerLot * TradeVolume)时,计算入场信号:
- 做多 —— 当前收盘价低于上一根 5 周期开盘价 SMA。
- 做空 —— 当前收盘价高于上一根 4 周期开盘价 SMA。
- 触发信号后,根据当前收盘价和配置的点数距离计算止盈与止损价位,并保存以供后续监控。
风险控制
TradeVolume 决定每次市价委托的手数。
MinimumMarginPerLot 近似复刻 MetaTrader 的 AccountFreeMargin 检查。如果可用资金不足,入场会被跳过并写入日志。
参数
| 名称 |
默认值 |
说明 |
LongTakeProfitPoints |
10 |
多头仓位的止盈距离(MetaTrader 点)。设为 0 可关闭止盈。 |
ShortTakeProfitPoints |
10 |
空头仓位的止盈距离(MetaTrader 点)。设为 0 可关闭止盈。 |
LongStopLossPoints |
1000 |
多头仓位的止损距离(MetaTrader 点)。设为 0 可关闭止损。 |
ShortStopLossPoints |
1000 |
空头仓位的止损距离(MetaTrader 点)。设为 0 可关闭止损。 |
TradeVolume |
0.2 |
市价单的下单手数。 |
MinimumMarginPerLot |
500 |
开仓前每手需要的最低资金(近似值)。 |
CandleType |
5 分钟周期 |
用于驱动指标更新和生成信号的 K 线序列。 |
实现细节
- 在
ProcessCandle 中手动使用 K 线开盘价更新均线,确保保存的数值对应前一根柱子,从而模拟 iMA 中的 shift=1 行为。
- 止盈与止损价位存储在
decimal 字段中,没有建立额外的集合,符合高阶 API 的约束。
- 通过比较 K 线的最高价和最低价判断止损/止盈是否触发。由于只有 OHLC 数据,先检查止损再检查止盈,以尽可能贴近 MetaTrader 的执行优先级。
- 如果存在图表区域,策略会绘制 K 线、两条均线以及自有成交,便于视觉验证。
- MetaTrader 会把止损和止盈直接附加在订单上;本移植版本通过监控 K 线高低点并发送市价单平仓来模拟,因此当同一根 K 线同时触及两个水平时,实际触发顺序无法完全保证一致。
- 入场价格取自触发信号的收盘价,而非 MetaTrader 使用的即时买卖价,滑点和点差需要在连接器层面处理。
AccountFreeMargin() 检查替换为 Portfolio.CurrentValue 的比较,如需更精细的保证金模型可以扩展 HasSufficientMargin 方法。
- 原脚本中的颜色、声音、滑点设置等界面元素被省略,C# 版本聚焦于交易逻辑本身。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Escape Mean Reversion: SMA crossover with ATR stops.
/// </summary>
public class EscapeMeanReversionStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _smaLength;
private readonly StrategyParam<int> _atrLength;
private decimal _prevClose;
private decimal _entryPrice;
public EscapeMeanReversionStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_smaLength = Param(nameof(SmaLength), 5)
.SetDisplay("SMA Length", "SMA period.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int SmaLength
{
get => _smaLength.Value;
set => _smaLength.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevClose = 0;
_entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevClose = 0;
_entryPrice = 0;
var sma = new SimpleMovingAverage { Length = SmaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
if (_prevClose == 0 || atrVal <= 0)
{
_prevClose = close;
return;
}
if (Position > 0)
{
if (close >= _entryPrice + atrVal * 2m || close <= _entryPrice - atrVal * 1.5m || close > smaVal)
{
SellMarket();
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (close <= _entryPrice - atrVal * 2m || close >= _entryPrice + atrVal * 1.5m || close < smaVal)
{
BuyMarket();
_entryPrice = 0;
}
}
if (Position == 0)
{
if (close < smaVal && _prevClose >= smaVal)
{
_entryPrice = close;
BuyMarket();
}
else if (close > smaVal && _prevClose <= smaVal)
{
_entryPrice = close;
SellMarket();
}
}
_prevClose = close;
}
}
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
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class escape_mean_reversion_strategy(Strategy):
"""
Escape Mean Reversion: SMA crossover with ATR stops.
Buys when price crosses below SMA, sells when price crosses above SMA.
Exits at ATR-based TP/SL levels.
"""
def __init__(self):
super(escape_mean_reversion_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._sma_length = self.Param("SmaLength", 5) \
.SetDisplay("SMA Length", "SMA period", "Indicators")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period", "Indicators")
self._prev_close = 0.0
self._entry_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(escape_mean_reversion_strategy, self).OnReseted()
self._prev_close = 0.0
self._entry_price = 0.0
def OnStarted2(self, time):
super(escape_mean_reversion_strategy, self).OnStarted2(time)
self._prev_close = 0.0
self._entry_price = 0.0
sma = SimpleMovingAverage()
sma.Length = self._sma_length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, sma_val, atr_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
sma_val = float(sma_val)
atr_val = float(atr_val)
if self._prev_close == 0.0 or atr_val <= 0:
self._prev_close = close
return
if self.Position > 0:
if (close >= self._entry_price + atr_val * 2.0 or
close <= self._entry_price - atr_val * 1.5 or
close > sma_val):
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if (close <= self._entry_price - atr_val * 2.0 or
close >= self._entry_price + atr_val * 1.5 or
close < sma_val):
self.BuyMarket()
self._entry_price = 0.0
if self.Position == 0:
if close < sma_val and self._prev_close >= sma_val:
self._entry_price = close
self.BuyMarket()
elif close > sma_val and self._prev_close <= sma_val:
self._entry_price = close
self.SellMarket()
self._prev_close = close
def CreateClone(self):
return escape_mean_reversion_strategy()