在 GitHub 上查看

摩斯密码策略

概述

摩斯密码策略重现了原始 MetaTrader 5 专家顾问的思想:把每一根收盘完成的 K 线看成「划」或「点」。收盘价大于或等于开盘价的 K 线被编码为 1,收盘价小于或等于开盘价的 K 线被编码为 0。策略持续读取最近的 K 线序列,并与所选的二进制掩码逐位比较。一旦序列完全匹配,即刻按照指定方向开仓,并同步设置以点(pip)为单位的止盈与止损。

实现完全依赖 StockSharp 的高级 API:K 线订阅提供数据流,Bind 负责把完成的 K 线传递给策略,而 StartProtection 自动管理出场订单。无需自定义集合或手动访问指标值,逻辑保持精炼且稳定。

模式逻辑

  • 仅在 K 线完全收盘 (CandleStates.Finished) 后才参与判断。
  • 每根 K 线都会转换成一个二进制位:
    • 1 – 多头或中性 K 线(Close >= Open)。
    • 0 – 空头或中性 K 线(Close <= Open)。十字星同时满足两个值,这与 MT5 原版保持一致。
  • 掩码由 MorsePatternMasks 枚举提供,涵盖了原策略中出现的所有长度 1–5 的二进制组合(例如 000101111111)。
  • 策略维护一段滑动窗口,始终保存最近若干根 K 线。当窗口与掩码完全一致时,即触发入场信号。

该流程等同于 MT5 版本中使用 CopyRates 逐字符比较字符串的实现方式。

交易流程

  1. 订阅指定的 K 线类型,等待累积到足够覆盖掩码长度的历史数据。
  2. 对每一根收盘完成的 K 线执行以下步骤:
    • 更新内部的多头/空头掩码以记录当前 K 线的走势方向。
    • 在未达到掩码所需的 K 线数量前跳过进一步检查。
    • 若最新窗口与掩码完全相同,则根据参数选择交易方向。
    • 调用 BuyMarketSellMarket 发送市价单。当持有相反方向的仓位时,会自动增加委托数量以先平仓再反向开仓,复现 MT5 交易类的行为。
  3. StartProtection 立即以价格单位设置止盈与止损,并使用市价出场,以降低无法成交的风险。

参数

名称 默认值 说明
CandleType 5 分钟 (TimeSpan.FromMinutes(5).TimeFrame()) 用于生成摩斯序列的 K 线类型。
Pattern _0 ("0") 与最新 K 线比较的二进制掩码,取自 MorsePatternMasks 枚举。
Direction Sides.Buy 掩码匹配时执行的方向:做多或做空。
TakeProfitPips 50 止盈距离,单位为点。策略会在遇到 3/5 位报价的外汇品种时自动把 PriceStep 乘以 10。
StopLossPips 50 止损距离,单位为点,计算方式与止盈一致。
Volume(策略属性) 用户自定义 下单手数或合约数,对应 MT5 中的 InpLot

所有参数均可在 StockSharp 参数窗口中调整,也支持加入优化流程。

风险控制

  • StartProtection 根据点数计算出价格偏移,并使用市价单离场,模拟 MT5 在下单时同时设置止盈/止损的机制。
  • 策略不会在已有同向仓位时加仓;若信号出现时持有反向仓位,会自动增大委托数量完成反手。
  • 日志会记录每一次开仓,方便回溯测试结果。

使用建议

  • 掩码长度最多 5 根 K 线,突出摩斯密码的「短促信号」特性。若需要更多模式,可在组合层面部署多组策略。
  • 点数换算基于 PriceStep。若标的使用特殊报价单位,请手动微调 TakeProfitPipsStopLossPips
  • 策略未内置时间或波动率过滤,可根据需要叠加会话管理或其他指标过滤器。
  • 回测或实盘前,请确认 Volume 设置符合期望手数,以便保护模块正常工作。

掩码示例

  • _0"0":单根空头 K 线。
  • _5"11":连续两根多头 K 线。
  • _20"0110":空头—多头交替形成的锯齿形。
  • _33"00011":三根空头后跟随两根多头。
  • _61"11111":连续五根多头 K 线。

策略面板中可选择全部 62 种掩码,从而精确复现所需的摩斯信号。

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>
/// Strategy that trades when a selected Morse code candle pattern appears.
/// </summary>
public class MorseCodeStrategy : Strategy
{
	/// <summary>
	/// Available Morse code style patterns where '1' is bullish and '0' is bearish.
	/// </summary>
	public enum MorsePatternMasks
	{
		_0 = 0,
		_1 = 1,
		_2 = 2,
		_3 = 3,
		_4 = 4,
		_5 = 5,
		_6 = 6,
		_7 = 7,
		_8 = 8,
		_9 = 9,
		_10 = 10,
		_11 = 11,
		_12 = 12,
		_13 = 13,
		_14 = 14,
		_15 = 15,
		_16 = 16,
		_17 = 17,
		_18 = 18,
		_19 = 19,
		_20 = 20,
		_21 = 21,
		_22 = 22,
		_23 = 23,
		_24 = 24,
		_25 = 25,
		_26 = 26,
		_27 = 27,
		_28 = 28,
		_29 = 29,
		_30 = 30,
		_31 = 31,
		_32 = 32,
		_33 = 33,
		_34 = 34,
		_35 = 35,
		_36 = 36,
		_37 = 37,
		_38 = 38,
		_39 = 39,
		_40 = 40,
		_41 = 41,
		_42 = 42,
		_43 = 43,
		_44 = 44,
		_45 = 45,
		_46 = 46,
		_47 = 47,
		_48 = 48,
		_49 = 49,
		_50 = 50,
		_51 = 51,
		_52 = 52,
		_53 = 53,
		_54 = 54,
		_55 = 55,
		_56 = 56,
		_57 = 57,
		_58 = 58,
		_59 = 59,
		_60 = 60,
		_61 = 61
	}

	private static readonly string[] PatternValues = new[]
	{
		"0",
		"1",
		"00",
		"01",
		"10",
		"11",
		"000",
		"001",
		"010",
		"011",
		"100",
		"101",
		"110",
		"111",
		"0000",
		"0001",
		"0010",
		"0011",
		"0100",
		"0101",
		"0110",
		"0111",
		"1000",
		"1001",
		"1010",
		"1011",
		"1100",
		"1101",
		"1110",
		"1111",
		"00000",
		"00000",
		"00010",
		"00011",
		"00100",
		"00101",
		"00111",
		"00111",
		"01000",
		"01001",
		"01010",
		"01011",
		"01100",
		"01101",
		"01110",
		"01111",
		"10000",
		"10001",
		"10010",
		"10011",
		"10100",
		"10101",
		"10110",
		"10111",
		"11000",
		"11001",
		"11010",
		"11011",
		"11100",
		"11101",
		"11110",
		"11111"
	};

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<MorsePatternMasks> _patternMask;
	private readonly StrategyParam<Sides> _direction;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;

	private string _patternText = string.Empty;
	private int _patternLength;
	private int _maskLimit;
	private int _bullMask;
	private int _bearMask;
	private int _processedBars;
	private decimal _pipSize;
	private decimal _takeProfitDistance;
	private decimal _stopLossDistance;

	/// <summary>
	/// Initializes a new instance of the <see cref="MorseCodeStrategy"/> class.
	/// </summary>
	public MorseCodeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for candle analysis", "General");

		_patternMask = Param(nameof(Pattern), MorsePatternMasks._14)
			.SetDisplay("Pattern", "Morse code pattern where 1= bullish and 0 = bearish", "Pattern");

		_direction = Param(nameof(Direction), Sides.Buy)
			.SetDisplay("Direction", "Side to trade when the pattern appears", "Trading");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance from entry to take profit in pips", "Risk Management");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Distance from entry to stop loss in pips", "Risk Management");
	}

	/// <summary>
	/// Candle type used for the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Selected Morse code pattern.
	/// </summary>
	public MorsePatternMasks Pattern
	{
		get => _patternMask.Value;
		set => _patternMask.Value = value;
	}

	/// <summary>
	/// Trade direction used when the pattern is detected.
	/// </summary>
	public Sides Direction
	{
		get => _direction.Value;
		set => _direction.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_patternText = string.Empty;
		_patternLength = 0;
		_maskLimit = 0;
		_bullMask = 0;
		_bearMask = 0;
		_processedBars = 0;
		_pipSize = 0m;
		_takeProfitDistance = 0m;
		_stopLossDistance = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_patternText = GetPatternText(Pattern);
		_patternLength = _patternText.Length;
		if (_patternLength == 0)
			throw new InvalidOperationException("Pattern cannot be empty.");

		_maskLimit = (1 << _patternLength) - 1;
		_bullMask = 0;
		_bearMask = 0;
		_processedBars = 0;

		_pipSize = CalculatePipSize();
		_takeProfitDistance = TakeProfitPips * _pipSize;
		_stopLossDistance = StopLossPips * _pipSize;

		// Configure automatic take profit and stop loss handling
		StartProtection(
			takeProfit: new Unit(_takeProfitDistance, UnitTypes.Absolute),
			stopLoss: new Unit(_stopLossDistance, UnitTypes.Absolute),
			useMarketOrders: true);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only act on completed candles
		if (candle.State != CandleStates.Finished)
			return;

		UpdatePatternMasks(candle);

		// Wait until enough candles were processed to match the pattern
		if (_processedBars < _patternLength)
			return;

		if (!IsPatternMatched())
			return;

		var closePrice = candle.ClosePrice;

		if (Direction == Sides.Buy)
		{
			if (Position > 0m)
				return; // Already in a long position

			EnterLong(closePrice);
		}
		else
		{
			if (Position < 0m)
				return; // Already in a short position

			EnterShort(closePrice);
		}
	}

	private static string GetPatternText(MorsePatternMasks mask)
	{
		var index = (int)mask;
		if (index < 0 || index >= PatternValues.Length)
			throw new ArgumentOutOfRangeException(nameof(mask));

		return PatternValues[index];
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			return 1m;

		var value = step;
		var digits = 0;
		while (value < 1m && digits < 10)
		{
			value *= 10m;
			digits++;
		}

		if (digits == 3 || digits == 5)
			step *= 10m;

		return step;
	}

	private void UpdatePatternMasks(ICandleMessage candle)
	{
		if (_patternLength == 0)
			return;

		var strictBull = candle.ClosePrice > candle.OpenPrice;
		var strictBear = candle.ClosePrice < candle.OpenPrice;

		_bullMask = ((_bullMask << 1) | (strictBull ? 1 : 0)) & _maskLimit;
		_bearMask = ((_bearMask << 1) | (strictBear ? 1 : 0)) & _maskLimit;

		if (_processedBars < _patternLength)
			_processedBars++;
	}

	private bool IsPatternMatched()
	{
		for (var i = 0; i < _patternLength; i++)
		{
			var expected = _patternText[i];
			var isStrictBull = ((_bullMask >> i) & 1) == 1;
			var isStrictBear = ((_bearMask >> i) & 1) == 1;

			if (expected == '1')
			{
				if (isStrictBear)
					return false; // Pattern expects bullish or neutral candle
			}
			else
			{
				if (isStrictBull)
					return false; // Pattern expects bearish or neutral candle
			}
		}

		return true;
	}

	private void EnterLong(decimal price)
	{
		BuyMarket();
		LogInfo($"Entered long position at price {price}.");
	}

	private void EnterShort(decimal price)
	{
		SellMarket();
		LogInfo($"Entered short position at price {price}.");
	}
}