在 GitHub 上查看
Disaster 策略 (MQL #7704)
概述
MetaTrader 专家顾问 disaster.mq4 会在一条超长周期的简单移动平均线(SMA)附近布设止损挂单。价格偏离平均线达到指定点数时,同时挂出买入止损和卖出止损订单,等待价格回归。每根新的 1 分钟 K 线都会重新计算 SMA,并通过 OrderModify 将挂单移动到最新位置。成交后的仓位使用固定止损和自适应止盈保护:如果上一次同方向交易亏损,则下一笔交易的止盈距离减半。
移植说明
- 数据来源:原版通过
iMA(PERIOD_M1, 590) 读取 1 分钟数据。StockSharp 版本订阅可配置的蜡烛序列(默认 1 分钟)并驱动同样长度的 SMA 指标。
- 触发逻辑:MQL 要求 Bid/Ask 与 SMA 之间至少 20 点的差距。C# 版本把
TriggerDistancePips 转换成绝对价格,乘以证券的 PriceStep 或 MinPriceStep,并对 3/5 位小数的外汇品种应用 10 倍放大,等效于 MT4 中的 Point。
- 订单类型:原始 EA 使用
OrderSend(... OP_BUYSTOP/OP_SELLSTOP ...)。移植后使用高层 API BuyStop 和 SellStop,保持两张挂单各自独立。
- 挂单移动:MT4 中每个新 K 线都会调用
OrderModify。StockSharp 通过 ReRegisterOrder 重新注册活动订单,避免反复撤单再下单。
- 经纪商限制:MT4 会读取
MODE_STOPLEVEL。C# 版本将价格四舍五入到最小价格步长,并在计算结果无效(≤ 0)时跳过更新,剩余的合法性检查交由连接器处理。
- 保护单:MT4 在挂单上直接附带止损/止盈。StockSharp 在成交后立即下达独立的止损和止盈订单,价格偏移与原策略一致。
- 自适应止盈:若上一笔多/空单亏损,下一笔同方向止盈距离减半。通过
_lastBuyWasLoss 和 _lastSellWasLoss 标志记录结果。
- 仓位管理:原始脚本根据可用保证金计算手数。移植版提供显式的
Volume 参数,并根据 VolumeStep、MinVolume、MaxVolume 自动对齐。
参数
| 参数 |
默认值 |
说明 |
Volume |
0.1 |
下单手数,自动对齐到交易所的数量步长。 |
MaPeriod |
590 |
作为基准的 SMA 周期长度。 |
StopLossPips |
30 |
入场价与止损价之间的距离。 |
TakeProfitPips |
70 |
基础止盈距离;若上一笔同方向亏损则减半。 |
TriggerDistancePips |
20 |
价格与 SMA 的最小偏离值,满足后才挂出止损单。 |
CandleType |
1 分钟 |
提供数据给 SMA 的蜡烛类型。 |
所有“点数”参数都会乘以 PriceStep/MinPriceStep。对于 3 或 5 位小数的外汇品种,额外乘以 10,以复刻 MetaTrader 的 Point 定义。
工作流程
- 订阅 Level1 行情以及目标周期蜡烛。
- 每次 Level1 更新时记录最新 Bid/Ask。
- 当蜡烛收盘后,更新 SMA 并调用
ReRegisterOrder 调整挂单位置。
- 若当前无持仓且 Bid/Ask 与 SMA 的差距超过阈值,则根据方向挂出卖出止损或买入止损订单。
- 当挂单成交时,立即按设定距离下达止损和止盈订单,同时更新上一次交易结果标志,以便调整下一次止盈距离。
- 策略停止时取消所有挂单和保护单。
与原版的差异
- 保护价位通过额外的止损/止盈订单实现,而不是订单字段中的 SL/TP。
- 对经纪商最小止损距离的检查交由连接器完成;策略仅负责四舍五入和过滤非法价格。
- 手数不再根据自由保证金自动计算,改为显式参数,更便于跨平台复现。
使用前提
- 证券需要提供
PriceStep 或 MinPriceStep,否则点值换算会退回到 0.0001。
- 为了完整重现条件,行情源最好提供 Bid/Ask。若缺失则退化为使用蜡烛收盘价,触发可能不够精确。
- 账户需支持止损单和限价单,以便构建保护仓位。
建议
- 初期请在模拟账户、最小手数下测试,确认点值换算正确。
- 调整
TriggerDistancePips 时同步考虑 TakeProfitPips,以平衡成交频率和盈亏比。
- 通过日志观察
_lastBuyWasLoss / _lastSellWasLoss,确保自适应止盈逻辑与原策略一致。
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Disaster strategy - SMA with momentum breakout.
/// Buys when close is above SMA and momentum crosses above zero.
/// Sells when close is below SMA and momentum crosses below zero.
/// </summary>
public class DisasterStrategy : Strategy
{
private readonly StrategyParam<int> _smaPeriod;
private readonly StrategyParam<int> _momentumPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevMom;
private bool _hasPrev;
public int SmaPeriod { get => _smaPeriod.Value; set => _smaPeriod.Value = value; }
public int MomentumPeriod { get => _momentumPeriod.Value; set => _momentumPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public DisasterStrategy()
{
_smaPeriod = Param(nameof(SmaPeriod), 50)
.SetDisplay("SMA Period", "SMA lookback", "Indicators");
_momentumPeriod = Param(nameof(MomentumPeriod), 14)
.SetDisplay("Momentum Period", "Momentum lookback", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted() { base.OnReseted(); _prevMom = 0m; _hasPrev = false; }
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
var sma = new SimpleMovingAverage { Length = SmaPeriod };
var mom = new Momentum { Length = MomentumPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, mom, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal sma, decimal mom)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
if (!_hasPrev)
{
_prevMom = mom;
_hasPrev = true;
return;
}
// Momentum crosses above zero + above SMA = buy
if (_prevMom <= 0 && mom > 0 && close > sma && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
}
// Momentum crosses below zero + below SMA = sell
else if (_prevMom >= 0 && mom < 0 && close < sma && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
}
_prevMom = mom;
}
}
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, Momentum
from StockSharp.Algo.Strategies import Strategy
class disaster_strategy(Strategy):
def __init__(self):
super(disaster_strategy, self).__init__()
self._sma_period = self.Param("SmaPeriod", 50).SetDisplay("SMA Period", "SMA lookback", "Indicators")
self._momentum_period = self.Param("MomentumPeriod", 14).SetDisplay("Momentum Period", "Momentum lookback", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_mom = 0.0
self._has_prev = False
@property
def sma_period(self): return self._sma_period.Value
@property
def momentum_period(self): return self._momentum_period.Value
@property
def candle_type(self): return self._candle_type.Value
def OnReseted(self):
super(disaster_strategy, self).OnReseted()
self._prev_mom = 0.0; self._has_prev = False
def OnStarted2(self, time):
super(disaster_strategy, self).OnStarted2(time)
self._has_prev = False
sma = SimpleMovingAverage()
sma.Length = self.sma_period
mom = Momentum()
mom.Length = self.momentum_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, mom, self.process_candle).Start()
def process_candle(self, candle, sma, mom):
if candle.State != CandleStates.Finished: return
close = float(candle.ClosePrice); sma_val = float(sma); mom_val = float(mom)
if not self._has_prev:
self._prev_mom = mom_val; self._has_prev = True; return
if self._prev_mom <= 0 and mom_val > 0 and close > sma_val and self.Position <= 0:
if self.Position < 0: self.BuyMarket()
self.BuyMarket()
elif self._prev_mom >= 0 and mom_val < 0 and close < sma_val and self.Position >= 0:
if self.Position > 0: self.SellMarket()
self.SellMarket()
self._prev_mom = mom_val
def CreateClone(self): return disaster_strategy()