GitHub で見る

マトリックス機械学習戦略

概要

行列機械学習は、もともと「MQL5Book」教育プロジェクト内で MetaTrader 5 向けに公開されたニューラル ネットワーク ベースのアプローチです。エキスパート スクリプトはティック価格のウィンドウを収集し、連続する価格差をバイナリ シーケンスに変換し、ホップフィールドリカレント ニューラル ネットワークをトレーニングします。トレーニングされたネットワークはサンプル内セグメントで評価され、サンプル外セグメントで検証され、最終的に次の動きの方向を推測するために使用されます。予測バイナリ ベクトルの最初の要素が強気 (+1) または弱気 (-1) の方向を示す場合、ポジションがオープンされます。

この C# バージョンは、元のロジックを StockSharp の高レベル API に移植し、ティック処理を完成したキャンドルに置き換えて、安定したクロスプラットフォーム動作を保証します。ローソク足の終値ごとにバイナリ価格パターンが更新され、ホップフィールド ネットワークが再トレーニングされ、履歴の精度が評価され、今後のステップのオンライン予測が生成されます。

アルゴリズムの詳細

  1. 最新の HistoryDepth ローソク足の終値を収集します。最新の ForwardDepth ポイントはサンプル外セットを形成し、残りの値はトレーニング セグメントを作成します。
  2. 連続した近い差をバイナリ シーケンスに変換します。正またはゼロのデルタは +1 になり、負のデルタは -1 になります。
  3. 予測子の長さが PredictorLength に等しく、応答の長さが ForecastLength に等しい場合のすべての予測子/出力ペアの外積を合計することによって、ホップフィールド重み行列を学習させます。
  4. トレーニング セットとフォワード セットでトレーニングされた行列を評価します。精度メトリックは元のスクリプトと一致します。予測された応答ベクトルと実際の応答ベクトルの間のドット積が平均され、パーセンテージに再スケールされます。
  5. 最新のオンライン バイナリ パターンを構築し、ホップフィールド推論ループ (収束しきい値を使用した Tanh 活性化) を実行します。最初の予測コンポーネントは取引の決定を左右します。

パラメーター

  • 履歴の深さ – ホップフィールド ネットワークに保存された最近のローソク足の終値の数。 ForwardDepth より大きく、少なくとも PredictorLength + ForecastLength + 1 でなければなりません。
  • 前方深度 – 前方チェック用に予約されている検証ウィンドウのサイズ。少なくとも ForecastLength + 1 が閉じる必要があります。
  • 予測子の長さ – ニューラル ネットワークによって使用されるバイナリ入力ベクトルの長さ。
  • 予測の長さ – ネットワーク出力ベクトルによって予測される将来のステップの数。
  • キャンドル タイプ – コネクタからリクエストされたキャンドル シリーズを説明する StockSharp DataType
  • デバッグ ログ – 有効にすると、詳細な中間ベクトル、サンプル比較、オンライン予測が出力されます。

取引ロジック

  • ホップフィールド予測の最初の要素がポジティブで、戦略がフラットまたはショートの場合、Volume + |Position| の成行買い注文が送信され、ロング ポジションに切り替わります。
  • 最初の要素がネガティブで、戦略がフラットまたはロングの場合、Volume + |Position| に対して成行売り注文が送信され、ショート ポジションに切り替わります。
  • 不必要なチャーンを避けるために、ゼロ予測は無視されます。

この戦略は、チャート領域が利用可能な場合、ローソク足と独自の取引を自動的にプロットします。ホップフィールド ネットワークは、完成したローソク足ごとに再トレーニングして、ニューラル ウェイトを最新の市場構造と同期させます。

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;

using System.Text;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy implementing a Hopfield neural network trained on price direction sequences.
/// </summary>
public class MatrixMachineLearningStrategy : Strategy
{
	private readonly StrategyParam<int> _maxIterations;
	private readonly StrategyParam<double> _accuracy;

	private readonly StrategyParam<int> _historyDepth;
	private readonly StrategyParam<int> _forwardDepth;
	private readonly StrategyParam<int> _predictorLength;
	private readonly StrategyParam<int> _forecastLength;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _enableDebugLog;

	private readonly List<decimal> _closes = new();

	private double[,] _weights;

	/// <summary>
	/// Number of most recent candle closes used for training.
	/// </summary>
	public int HistoryDepth
	{
		get => _historyDepth.Value;
		set => _historyDepth.Value = value;
	}

	/// <summary>
	/// Portion of the history reserved for forward evaluation.
	/// </summary>
	public int ForwardDepth
	{
		get => _forwardDepth.Value;
		set => _forwardDepth.Value = value;
	}

	/// <summary>
	/// Number of binary price movements forming the network input vector.
	/// </summary>
	public int PredictorLength
	{
		get => _predictorLength.Value;
		set => _predictorLength.Value = value;
	}

	/// <summary>
	/// Number of steps predicted by the network output vector.
	/// </summary>
	public int ForecastLength
	{
		get => _forecastLength.Value;
		set => _forecastLength.Value = value;
	}

	/// <summary>
	/// Candle type used to gather prices.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Maximum number of Hopfield iterations executed per forecast.
	/// </summary>
	public int MaxIterations
	{
		get => _maxIterations.Value;
		set => _maxIterations.Value = value;
	}

	/// <summary>
	/// Desired accuracy when checking convergence of neuron states.
	/// </summary>
	public double Accuracy
	{
		get => _accuracy.Value;
		set => _accuracy.Value = value;
	}

	/// <summary>
	/// Enables verbose logging of the neural network state.
	/// </summary>
	public bool EnableDebugLog
	{
		get => _enableDebugLog.Value;
		set => _enableDebugLog.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public MatrixMachineLearningStrategy()
	{
		_maxIterations = Param(nameof(MaxIterations), 100)
			.SetGreaterThanZero()
			.SetDisplay("Max Iterations", "Maximum number of Hopfield iterations executed per forecast.", "Machine Learning");

		_accuracy = Param(nameof(Accuracy), 0.00001)
			.SetDisplay("Accuracy", "Desired accuracy when checking convergence of neuron states.", "Machine Learning");

		_historyDepth = Param(nameof(HistoryDepth), 120)
			.SetGreaterThanZero()
			.SetDisplay("History Depth", "Total amount of closes stored for the Hopfield network.", "Machine Learning")
			
			.SetOptimize(80, 200, 10);

		_forwardDepth = Param(nameof(ForwardDepth), 60)
			.SetGreaterThanZero()
			.SetDisplay("Forward Depth", "Amount of closes kept for out-of-sample validation.", "Machine Learning")
			
			.SetOptimize(30, 120, 10);

		_predictorLength = Param(nameof(PredictorLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Predictor Length", "Length of binary vector passed to the network input.", "Machine Learning")
			
			.SetOptimize(10, 40, 2);

		_forecastLength = Param(nameof(ForecastLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Forecast Length", "Length of the binary output vector produced by the network.", "Machine Learning")
			
			.SetOptimize(5, 20, 1);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles requested from the market data source.", "Data");

		_enableDebugLog = Param(nameof(EnableDebugLog), false)
			.SetDisplay("Debug Log", "Write detailed neural network diagnostics to the log.", "Machine Learning");
	}

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

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

		_closes.Clear();
		_weights = null;
	}

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

		StartProtection(null, null);

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_closes.Add(candle.ClosePrice);
		if (_closes.Count > HistoryDepth)
			_closes.RemoveAt(0);

		if (_closes.Count < PredictorLength + ForecastLength + 1)
			return;

		if (_closes.Count < ForwardDepth + 2)
			return;

		var closes = _closes.ToArray();

		TrainNetwork(closes);

		var forecast = Forecast(closes);
		if (forecast == null || forecast.Length == 0)
			return;

		var direction = forecast.Sum();
		if (direction > 0 && Position <= 0m)
		{
			BuyMarket(Position < 0m ? Math.Abs(Position) + 1 : 1);
		}
		else if (direction < 0 && Position >= 0m)
		{
			SellMarket(Position > 0m ? Math.Abs(Position) + 1 : 1);
		}
	}

	private void TrainNetwork(IReadOnlyList<decimal> closes)
	{
		var historyCount = closes.Count;
		var forwardCount = Math.Min(ForwardDepth, historyCount - 1);
		var trainingCount = historyCount - forwardCount;

		if (trainingCount <= PredictorLength + ForecastLength)
			return;

		var trainingData = BuildBinaryDiff(closes, 0, trainingCount);
		if (trainingData.Length < PredictorLength + ForecastLength)
			return;

		var weights = TrainWeights(trainingData, PredictorLength, ForecastLength);
		if (weights == null)
			return;

		_weights = weights;

		EvaluateWeights(trainingData, "Backtest evaluation");

		var forwardData = BuildBinaryDiff(closes, trainingCount - 1, forwardCount + 1);
		if (forwardData.Length >= PredictorLength + ForecastLength)
			EvaluateWeights(forwardData, "Forward evaluation");
	}

	private double[] Forecast(IReadOnlyList<decimal> closes)
	{
		var weights = _weights;
		if (weights == null)
			return null;

		var pattern = BuildCurrentPattern(closes);
		if (pattern == null)
			return null;

		var forecast = RunWeights(weights, pattern);

		if (EnableDebugLog)
		{
			LogInfo(FormattableString.Invariant($"Online pattern: {FormatVector(pattern)}"));
			LogInfo(FormattableString.Invariant($"Forecast: {FormatVector(forecast)}"));
		}

		return forecast;
	}

	private static double[] BuildBinaryDiff(IReadOnlyList<decimal> closes, int startIndex, int length)
	{
		if (length <= 1 || startIndex < 0)
			return Array.Empty<double>();

		if (startIndex + length > closes.Count)
			length = closes.Count - startIndex;

		var resultLength = length - 1;
		if (resultLength <= 0)
			return Array.Empty<double>();

		var result = new double[resultLength];
		for (var i = 0; i < resultLength; i++)
		{
			var first = closes[startIndex + i];
			var second = closes[startIndex + i + 1];
			var diff = (double)(second - first);
			result[i] = diff >= 0 ? 1d : -1d;
		}

		return result;
	}

	private double[] BuildCurrentPattern(IReadOnlyList<decimal> closes)
	{
		var required = PredictorLength + 1;
		if (closes.Count < required)
			return null;

		var startIndex = closes.Count - required;
		var pattern = new double[PredictorLength];
		for (var i = 0; i < PredictorLength; i++)
		{
			var first = closes[startIndex + i];
			var second = closes[startIndex + i + 1];
			var diff = (double)(second - first);
			pattern[i] = diff >= 0 ? 1d : -1d;
		}

		return pattern;
	}

	private static double[,] TrainWeights(double[] data, int predictor, int response)
	{
		var sample = predictor + response;
		if (data.Length < sample)
			return null;

		var count = data.Length - sample + 1;
		var weights = new double[predictor, response];

		for (var index = 0; index < count; index++)
		{
			for (var row = 0; row < predictor; row++)
			{
				var inputValue = data[index + row];
				for (var column = 0; column < response; column++)
				{
					var outputValue = data[index + predictor + column];
					weights[row, column] += inputValue * outputValue;
				}
			}
		}

		return weights;
	}

	private void EvaluateWeights(double[] data, string title)
	{
		var weights = _weights;
		if (weights == null)
			return;

		var predictor = weights.GetLength(0);
		var response = weights.GetLength(1);
		var sample = predictor + response;

		if (data.Length < sample)
			return;

		var count = data.Length - sample + 1;
		if (count <= 0)
			return;

		var positive = 0;
		var negative = 0;
		double sum = 0;

		for (var index = 0; index < count; index++)
		{
			var input = new double[predictor];
			var target = new double[response];

			for (var i = 0; i < predictor; i++)
				input[i] = data[index + i];

			for (var i = 0; i < response; i++)
				target[i] = data[index + predictor + i];

			var forecast = RunWeights(weights, input);

			double match = 0;
			for (var i = 0; i < response; i++)
				match += forecast[i] * target[i];

			if (match > 0)
				positive++;
			else if (match < 0)
				negative++;

			sum += match;

			if (EnableDebugLog)
			{
				LogInfo(FormattableString.Invariant($"Sample {index}: forecast={FormatVector(forecast)} target={FormatVector(target)} match={match:0.###}"));
			}
		}

		var average = sum / count;
		var accuracy = (average + response) / (2.0 * response) * 100.0;

		LogInfo(FormattableString.Invariant($"{title}: count={count} positive={positive} negative={negative} accuracy={accuracy:0.##}%"));
	}

	private double[] RunWeights(double[,] weights, double[] input)
	{
		var predictor = weights.GetLength(0);
		var response = weights.GetLength(1);
		var forecast = new double[response];

		if (input.Length != predictor)
			return forecast;

		var a = new double[predictor];
		var b = new double[response];

		for (var i = 0; i < predictor; i++)
			a[i] = input[i];

		for (var iteration = 0; iteration < MaxIterations; iteration++)
		{
			var previousA = new double[predictor];
			var previousB = new double[response];

			for (var i = 0; i < predictor; i++)
				previousA[i] = a[i];

			for (var i = 0; i < response; i++)
				previousB[i] = b[i];

			for (var column = 0; column < response; column++)
			{
				double sum = 0;
				for (var row = 0; row < predictor; row++)
					sum += a[row] * weights[row, column];
				b[column] = Math.Tanh(sum);
			}

			for (var row = 0; row < predictor; row++)
			{
				double sum = 0;
				for (var column = 0; column < response; column++)
					sum += b[column] * weights[row, column];
				a[row] = Math.Tanh(sum);
			}

			var diffA = 0d;
			for (var i = 0; i < predictor; i++)
			{
				var delta = Math.Abs(a[i] - previousA[i]);
				if (delta > diffA)
					diffA = delta;
			}

			var diffB = 0d;
			for (var i = 0; i < response; i++)
			{
				var delta = Math.Abs(b[i] - previousB[i]);
				if (delta > diffB)
					diffB = delta;
			}

			if (diffA < Accuracy && diffB < Accuracy)
				break;
		}

		for (var i = 0; i < response; i++)
			forecast[i] = b[i] >= 0 ? 1d : -1d;

		return forecast;
	}

	private static string FormatVector(IReadOnlyList<double> values)
	{
		var builder = new StringBuilder();
		builder.Append('[');
		for (var i = 0; i < values.Count; i++)
		{
			builder.Append(FormattableString.Invariant($"{values[i]:0.###}"));
			if (i + 1 < values.Count)
				builder.Append(',');
		}
		builder.Append(']');
		return builder.ToString();
	}
}