GitHub で見る

Morse Code 戦略

概要

Morse Code 戦略は、完成したローソク足を「ダッシュ」または「ドット」として扱うオリジナルのMetaTrader 5エキスパートを再現します。強気のローソク足(終値が始値以上)は1としてエンコードされ、弱気のローソク足(終値が始値以下)は0としてエンコードされます。戦略は完成したローソク足の最新シーケンスをスキャンし、ユーザーが選択したバイナリマスクと比較します。最後のローソク足が設定されたシーケンスと完全に一致すると、戦略は選択した方向にポジションを開き、pip単位のテイクプロフィットとストップロスの両方の注文を即座に添付します。

実装はStockSharpの高レベルAPIにのみ依存しています。ローソク足サブスクリプションがデータを提供し、バインディングがイベント配信を処理し、組み込みの保護モジュールがエグジットを管理します。カスタムコレクションやインジケーター値への直接アクセスは不要で、ロジックを簡潔かつ堅牢に保ちます。

パターンロジック

  • ローソク足は完全にクローズされた後にのみ評価されます(CandleStates.Finished)。
  • 各ローソク足はバイナリの桁になります:
    • 1 – ローソク足は強気または中立(Close >= Open)。
    • 0 – ローソク足は弱気または中立(Close <= Open)。ドージローソク足は両方の桁に一致し、オリジナルのエキスパートと同様です。
  • マスクはMorsePatternMasks列挙から選択されます。MT5バージョンに登場した長さ1から5のすべてのバイナリシーケンスが含まれています(例: 000101111111)。
  • 戦略は最新のローソク足のローリングウィンドウを保持します。最新のウィンドウがマスクと一致すると、エントリーシグナルが発火します。

この動作はCopyRatesを呼び出してパターン文字列と各バーを文字ごとに比較したMT5実装を反映しています。

取引ワークフロー

  1. 設定されたローソク足タイプをサブスクライブし、マスクの長さをカバーするのに十分なバーが蓄積されるまで待機します。
  2. 完成したローソク足ごとに:
    • ローソク足を強気、弱気、または中立として分類する内部マスクを更新します。
    • マスクが必要とする数のローソク足が処理されるまで、さらなるチェックを無視します。
    • 最新のローソク足が選択したマスクと完全に一致する場合、希望する方向を確認します。
    • シグナルの方向で成行注文を送信します(BuyMarketまたはSellMarket)。反対のポジションが存在する場合、戦略は最初に注文ボリュームを増やすことでそれを閉じ、オリジナルのエキスパートアドバイザーの動作を再現します。
  3. StartProtectionは価格単位で表されたストップロスとテイクプロフィットを即座に添付します。保護注文は成行エグジットを使用してStockSharpエンジンによって処理され、フィルされない取引を避けます。

パラメーター

名前 デフォルト 説明
CandleType 5分ローソク足(TimeSpan.FromMinutes(5).TimeFrame() Morseシーケンスを構築するために使用するデータタイプ。
Pattern _0"0" 最新のローソク足と照合するバイナリマスク。値はMorsePatternMasksから直接来ます。
Direction Sides.Buy パターンが現れたときにロング(Buy)またはショート(Sell)ポジションを開くかどうか。
TakeProfitPips 50 エントリーからテイクプロフィットまでのpip距離。戦略は価格ステップを10倍することで3桁および5桁のforexクォートに自動的に適応します。
StopLossPips 50 エントリーからストップロスまでのpip距離、上記と同じpip計算を使用します。
Volume(戦略プロパティ) ユーザー定義 ロット/契約単位の注文サイズ、MT5エキスパートのInpLotに相当します。

すべてのパラメーターはStockSharpのパラメーターウィンドウをサポートし、最適化でき、戦略を開始する前に変更できます。

リスク管理

  • StartProtectionはpip設定から導出された価格ベースのオフセットを使用して両方のターゲットを添付します。エグジットは成行注文で実行され、ポジションエントリー時にストップロスとテイクプロフィット値を設定したMT5取引クラスの動作に合わせます。
  • 戦略はピラミッドしないため、同じ方向に既存のポジションがある間は新しい取引が無視されます。反対方向を保持しながらパターンが現れた場合、ポジションを反転するためにボリュームが自動的に増加されます。
  • 標準のStockSharpロギングが戦略ジャーナルへの各エントリーを報告します。

使用上の注意

  • バイナリマスクは意図的に短く(最大5ローソク足)、ロジックをオリジナルのアイデアに忠実に保ちます。より豊富な語彙が必要な場合は、ポートフォリオオーケストレーションを通じて複数のパターンマスクを組み合わせることを検討してください。
  • pip変換はインストゥルメントの価格ステップに依存します。非標準インクリメントのエキゾチックなシンボルには、TakeProfitPipsStopLossPipsを手動で調整できます。
  • 戦略は時間帯やボラティリティでフィルタリングしません。必要に応じてセッションや追加インジケーターを処理する親戦略の中に包み込むことができます。
  • テスト時はVolumeプロパティが期待するロットサイズと一致することを確認してください。StockSharpテスターはライブモードと同じ保護と注文フローを再使用します。

パターンリファレンス

列挙値の例:

  • _0"0"(単一の弱気ローソク足)
  • _5"11"(連続した2本の強気ローソク足)
  • _20"0110"(ジグザグを形成する弱気-強気シーケンス)
  • _33"00011"(3本の弱気ローソク足に続く2本の強気)
  • _61"11111"(5本連続した強気ローソク足)

62のマスクのどれでもパラメーターパネルから選択でき、取引計画が必要とする正確なMorse Code署名を再現できます。

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}.");
	}
}