在 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 转换成绝对价格,乘以证券的 PriceStepMinPriceStep,并对 3/5 位小数的外汇品种应用 10 倍放大,等效于 MT4 中的 Point
  • 订单类型:原始 EA 使用 OrderSend(... OP_BUYSTOP/OP_SELLSTOP ...)。移植后使用高层 API BuyStopSellStop,保持两张挂单各自独立。
  • 挂单移动:MT4 中每个新 K 线都会调用 OrderModify。StockSharp 通过 ReRegisterOrder 重新注册活动订单,避免反复撤单再下单。
  • 经纪商限制:MT4 会读取 MODE_STOPLEVEL。C# 版本将价格四舍五入到最小价格步长,并在计算结果无效(≤ 0)时跳过更新,剩余的合法性检查交由连接器处理。
  • 保护单:MT4 在挂单上直接附带止损/止盈。StockSharp 在成交后立即下达独立的止损和止盈订单,价格偏移与原策略一致。
  • 自适应止盈:若上一笔多/空单亏损,下一笔同方向止盈距离减半。通过 _lastBuyWasLoss_lastSellWasLoss 标志记录结果。
  • 仓位管理:原始脚本根据可用保证金计算手数。移植版提供显式的 Volume 参数,并根据 VolumeStepMinVolumeMaxVolume 自动对齐。

参数

参数 默认值 说明
Volume 0.1 下单手数,自动对齐到交易所的数量步长。
MaPeriod 590 作为基准的 SMA 周期长度。
StopLossPips 30 入场价与止损价之间的距离。
TakeProfitPips 70 基础止盈距离;若上一笔同方向亏损则减半。
TriggerDistancePips 20 价格与 SMA 的最小偏离值,满足后才挂出止损单。
CandleType 1 分钟 提供数据给 SMA 的蜡烛类型。

所有“点数”参数都会乘以 PriceStep/MinPriceStep。对于 3 或 5 位小数的外汇品种,额外乘以 10,以复刻 MetaTrader 的 Point 定义。

工作流程

  1. 订阅 Level1 行情以及目标周期蜡烛。
  2. 每次 Level1 更新时记录最新 Bid/Ask。
  3. 当蜡烛收盘后,更新 SMA 并调用 ReRegisterOrder 调整挂单位置。
  4. 若当前无持仓且 Bid/Ask 与 SMA 的差距超过阈值,则根据方向挂出卖出止损或买入止损订单。
  5. 当挂单成交时,立即按设定距离下达止损和止盈订单,同时更新上一次交易结果标志,以便调整下一次止盈距离。
  6. 策略停止时取消所有挂单和保护单。

与原版的差异

  • 保护价位通过额外的止损/止盈订单实现,而不是订单字段中的 SL/TP。
  • 对经纪商最小止损距离的检查交由连接器完成;策略仅负责四舍五入和过滤非法价格。
  • 手数不再根据自由保证金自动计算,改为显式参数,更便于跨平台复现。

使用前提

  • 证券需要提供 PriceStepMinPriceStep,否则点值换算会退回到 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;
	}
}