摩斯密码策略
概述
摩斯密码策略重现了原始 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 的二进制组合(例如000、1011、11111)。 - 策略维护一段滑动窗口,始终保存最近若干根 K 线。当窗口与掩码完全一致时,即触发入场信号。
该流程等同于 MT5 版本中使用 CopyRates 逐字符比较字符串的实现方式。
交易流程
- 订阅指定的 K 线类型,等待累积到足够覆盖掩码长度的历史数据。
- 对每一根收盘完成的 K 线执行以下步骤:
- 更新内部的多头/空头掩码以记录当前 K 线的走势方向。
- 在未达到掩码所需的 K 线数量前跳过进一步检查。
- 若最新窗口与掩码完全相同,则根据参数选择交易方向。
- 调用
BuyMarket或SellMarket发送市价单。当持有相反方向的仓位时,会自动增加委托数量以先平仓再反向开仓,复现 MT5 交易类的行为。
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。若标的使用特殊报价单位,请手动微调TakeProfitPips与StopLossPips。 - 策略未内置时间或波动率过滤,可根据需要叠加会话管理或其他指标过滤器。
- 回测或实盘前,请确认
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}.");
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
SIDE_BUY = 0
SIDE_SELL = 1
PATTERN_VALUES = [
"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"
]
class morse_code_strategy(Strategy):
def __init__(self):
super(morse_code_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._pattern_mask = self.Param("Pattern", 14)
self._direction = self.Param("Direction", SIDE_BUY)
self._take_profit_pips = self.Param("TakeProfitPips", 50.0)
self._stop_loss_pips = self.Param("StopLossPips", 50.0)
self._pattern_text = ""
self._pattern_length = 0
self._mask_limit = 0
self._bull_mask = 0
self._bear_mask = 0
self._processed_bars = 0
self._pip_size = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def Pattern(self):
return self._pattern_mask.Value
@Pattern.setter
def Pattern(self, value):
self._pattern_mask.Value = value
@property
def Direction(self):
return self._direction.Value
@Direction.setter
def Direction(self, value):
self._direction.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
def _calculate_pip_size(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
return 1.0
value = step
digits = 0
while value < 1.0 and digits < 10:
value *= 10.0
digits += 1
if digits == 3 or digits == 5:
step *= 10.0
return step
def OnStarted2(self, time):
super(morse_code_strategy, self).OnStarted2(time)
idx = int(self.Pattern)
if idx < 0 or idx >= len(PATTERN_VALUES):
idx = 0
self._pattern_text = PATTERN_VALUES[idx]
self._pattern_length = len(self._pattern_text)
self._mask_limit = (1 << self._pattern_length) - 1
self._bull_mask = 0
self._bear_mask = 0
self._processed_bars = 0
self._pip_size = self._calculate_pip_size()
tp_distance = float(self.TakeProfitPips) * self._pip_size
sl_distance = float(self.StopLossPips) * self._pip_size
self.StartProtection(
Unit(tp_distance, UnitTypes.Absolute),
Unit(sl_distance, UnitTypes.Absolute),
False, None, None, True)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_pattern_masks(candle)
if self._processed_bars < self._pattern_length:
return
if not self._is_pattern_matched():
return
direction = int(self.Direction)
if direction == SIDE_BUY:
if self.Position > 0:
return
self.BuyMarket()
else:
if self.Position < 0:
return
self.SellMarket()
def _update_pattern_masks(self, candle):
if self._pattern_length == 0:
return
close = float(candle.ClosePrice)
open_price = float(candle.OpenPrice)
strict_bull = 1 if close > open_price else 0
strict_bear = 1 if close < open_price else 0
self._bull_mask = ((self._bull_mask << 1) | strict_bull) & self._mask_limit
self._bear_mask = ((self._bear_mask << 1) | strict_bear) & self._mask_limit
if self._processed_bars < self._pattern_length:
self._processed_bars += 1
def _is_pattern_matched(self):
for i in range(self._pattern_length):
expected = self._pattern_text[i]
is_strict_bull = ((self._bull_mask >> i) & 1) == 1
is_strict_bear = ((self._bear_mask >> i) & 1) == 1
if expected == '1':
if is_strict_bear:
return False
else:
if is_strict_bull:
return False
return True
def OnReseted(self):
super(morse_code_strategy, self).OnReseted()
self._pattern_text = ""
self._pattern_length = 0
self._mask_limit = 0
self._bull_mask = 0
self._bear_mask = 0
self._processed_bars = 0
self._pip_size = 0.0
def CreateClone(self):
return morse_code_strategy()