マトリックス機械学習戦略
概要
行列機械学習は、もともと「MQL5Book」教育プロジェクト内で MetaTrader 5 向けに公開されたニューラル ネットワーク ベースのアプローチです。エキスパート スクリプトはティック価格のウィンドウを収集し、連続する価格差をバイナリ シーケンスに変換し、ホップフィールドリカレント ニューラル ネットワークをトレーニングします。トレーニングされたネットワークはサンプル内セグメントで評価され、サンプル外セグメントで検証され、最終的に次の動きの方向を推測するために使用されます。予測バイナリ ベクトルの最初の要素が強気 (+1) または弱気 (-1) の方向を示す場合、ポジションがオープンされます。
C# と Python の実装は、元のロジックを StockSharp の高レベル API に移植し、ティック処理を完成したキャンドルに置き換えて、安定したクロスプラットフォーム動作を保証します。ローソク足の終値ごとにバイナリ価格パターンが更新され、ホップフィールド ネットワークが再トレーニングされ、履歴の精度が評価され、今後のステップのオンライン予測が生成されます。
アルゴリズムの詳細
- 最新の
HistoryDepthローソク足の終値を収集します。最新のForwardDepthポイントはサンプル外セットを形成し、残りの値はトレーニング セグメントを作成します。 - 連続した近い差をバイナリ シーケンスに変換します。正またはゼロのデルタは
+1になり、負のデルタは-1になります。 - 予測子の長さが
PredictorLengthに等しく、応答の長さがForecastLengthに等しい場合のすべての予測子/出力ペアの外積を合計することによって、ホップフィールド重み行列を学習させます。 - トレーニング セットとフォワード セットでトレーニングされた行列を評価します。精度メトリックは元のスクリプトと一致します。予測された応答ベクトルと実際の応答ベクトルの間のドット積が平均され、パーセンテージに再スケールされます。
- 最新のオンライン バイナリ パターンを構築し、ホップフィールド推論ループ (収束しきい値を使用した 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[0];
if (direction > 0 && Position <= 0m)
{
BuyMarket(Volume + Math.Abs(Position));
}
else if (direction < 0 && Position >= 0m)
{
SellMarket(Volume + Math.Abs(Position));
}
}
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();
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Math, TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class matrix_machine_learning_strategy(Strategy):
"""
Hopfield neural network trained on binary price direction sequences.
"""
def __init__(self):
super(matrix_machine_learning_strategy, self).__init__()
self._max_iterations = self.Param("MaxIterations", 100) \
.SetGreaterThanZero() \
.SetDisplay("Max Iterations", "Maximum number of Hopfield iterations executed per forecast.", "Machine Learning")
self._accuracy = self.Param("Accuracy", 0.00001).SetDisplay("Accuracy", "Desired accuracy when checking convergence of neuron states.", "Machine Learning")
self._history_depth = self.Param("HistoryDepth", 120) \
.SetGreaterThanZero() \
.SetDisplay("History Depth", "Total amount of closes stored for the Hopfield network.", "Machine Learning") \
.SetOptimize(80, 200, 10)
self._forward_depth = self.Param("ForwardDepth", 60) \
.SetGreaterThanZero() \
.SetDisplay("Forward Depth", "Amount of closes kept for out-of-sample validation.", "Machine Learning") \
.SetOptimize(30, 120, 10)
self._predictor_length = self.Param("PredictorLength", 20) \
.SetGreaterThanZero() \
.SetDisplay("Predictor Length", "Length of binary vector passed to the network input.", "Machine Learning") \
.SetOptimize(10, 40, 2)
self._forecast_length = self.Param("ForecastLength", 10) \
.SetGreaterThanZero() \
.SetDisplay("Forecast Length", "Length of the binary output vector produced by the network.", "Machine Learning") \
.SetOptimize(5, 20, 1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60))).SetDisplay("Candle Type", "Type of candles requested from the market data source.", "Data")
self._enable_debug_log = self.Param("EnableDebugLog", False).SetDisplay("Debug Log", "Write detailed neural network diagnostics to the log.", "Machine Learning")
self._closes = []
self._weights = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(matrix_machine_learning_strategy, self).OnReseted()
self._closes = []
self._weights = None
def OnStarted2(self, time):
super(matrix_machine_learning_strategy, self).OnStarted2(time)
self.StartProtection(None, None)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._closes.append(candle.ClosePrice)
hd = self._history_depth.Value
if len(self._closes) > hd:
self._closes.pop(0)
pl = self._predictor_length.Value
fl = self._forecast_length.Value
if len(self._closes) < pl + fl + 1:
return
if len(self._closes) < self._forward_depth.Value + 2:
return
closes = list(self._closes)
self._train_network(closes)
forecast = self._forecast(closes)
if forecast is None or len(forecast) == 0:
return
direction = forecast[0]
if direction > 0 and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif direction < 0 and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
def _train_network(self, closes):
history_count = len(closes)
forward_count = min(self._forward_depth.Value, history_count - 1)
training_count = history_count - forward_count
predictor = self._predictor_length.Value
response = self._forecast_length.Value
if training_count <= predictor + response:
return
training_data = self._build_binary_diff(closes, 0, training_count)
if len(training_data) < predictor + response:
return
weights = self._train_weights(training_data, predictor, response)
if weights is None:
return
self._weights = weights
self._evaluate_weights(training_data, "Backtest evaluation")
forward_data = self._build_binary_diff(closes, training_count - 1, forward_count + 1)
if len(forward_data) >= predictor + response:
self._evaluate_weights(forward_data, "Forward evaluation")
def _forecast(self, closes):
if self._weights is None:
return None
pattern = self._build_current_pattern(closes)
if pattern is None:
return None
forecast = self._run_weights(self._weights, pattern)
if self._enable_debug_log.Value:
self.LogInfo("Online pattern: {0}".format(self._format_vector(pattern)))
self.LogInfo("Forecast: {0}".format(self._format_vector(forecast)))
return forecast
@staticmethod
def _build_binary_diff(closes, start_index, length):
if length <= 1 or start_index < 0:
return []
if start_index + length > len(closes):
length = len(closes) - start_index
result_length = length - 1
if result_length <= 0:
return []
result = []
for i in range(result_length):
first = closes[start_index + i]
second = closes[start_index + i + 1]
result.append(1.0 if second - first >= 0 else -1.0)
return result
def _build_current_pattern(self, closes):
predictor = self._predictor_length.Value
required = predictor + 1
if len(closes) < required:
return None
start_index = len(closes) - required
pattern = []
for i in range(predictor):
first = closes[start_index + i]
second = closes[start_index + i + 1]
pattern.append(1.0 if second - first >= 0 else -1.0)
return pattern
@staticmethod
def _train_weights(data, predictor, response):
sample = predictor + response
if len(data) < sample:
return None
count = len(data) - sample + 1
weights = [[0.0 for _ in range(response)] for _ in range(predictor)]
for index in range(count):
for row in range(predictor):
input_value = data[index + row]
for column in range(response):
output_value = data[index + predictor + column]
weights[row][column] += input_value * output_value
return weights
def _evaluate_weights(self, data, title):
if self._weights is None:
return
predictor = len(self._weights)
response = len(self._weights[0]) if predictor > 0 else 0
sample = predictor + response
if response == 0 or len(data) < sample:
return
count = len(data) - sample + 1
if count <= 0:
return
positive = 0
negative = 0
total = 0.0
for index in range(count):
input_values = data[index:index + predictor]
target = data[index + predictor:index + predictor + response]
forecast = self._run_weights(self._weights, input_values)
match = sum(forecast[i] * target[i] for i in range(response))
if match > 0:
positive += 1
elif match < 0:
negative += 1
total += match
if self._enable_debug_log.Value:
self.LogInfo("Sample {0}: forecast={1} target={2} match={3:.3f}".format(index, self._format_vector(forecast), self._format_vector(target), match))
average = total / count
accuracy = (average + response) / (2.0 * response) * 100.0
self.LogInfo("{0}: count={1} positive={2} negative={3} accuracy={4:.2f}%".format(title, count, positive, negative, accuracy))
def _run_weights(self, weights, input_values):
predictor = len(weights)
response = len(weights[0]) if predictor > 0 else 0
forecast = [0.0 for _ in range(response)]
if len(input_values) != predictor:
return forecast
a = list(input_values)
b = [0.0 for _ in range(response)]
for _ in range(self._max_iterations.Value):
previous_a = list(a)
previous_b = list(b)
for column in range(response):
value = 0.0
for row in range(predictor):
value += a[row] * weights[row][column]
b[column] = Math.Tanh(value)
for row in range(predictor):
value = 0.0
for column in range(response):
value += b[column] * weights[row][column]
a[row] = Math.Tanh(value)
diff_a = max([abs(a[i] - previous_a[i]) for i in range(predictor)] or [0.0])
diff_b = max([abs(b[i] - previous_b[i]) for i in range(response)] or [0.0])
if diff_a < self._accuracy.Value and diff_b < self._accuracy.Value:
break
for i in range(response):
forecast[i] = 1.0 if b[i] >= 0 else -1.0
return forecast
@staticmethod
def _format_vector(values):
return "[" + ",".join("{0:.3f}".format(value) for value in values) + "]"
def CreateClone(self):
return matrix_machine_learning_strategy()