GitHub で見る

Burg 外挿予測戦略

概要

Burg Extrapolator 戦略は、MetaTrader 4 エキスパート アドバイザー「Burg Extrapolator」の StockSharp 移植です。オリジナルのシステムは、ブルク自己回帰 (AR) モデルを始値のスライディング ウィンドウ (またはその勢い/ROC 変換) に適合させ、将来の価格の経路を予測します。トレーディングの決定は、最も極端な予測値に基づいて行われます。つまり、一方向への予測変動が十分に大きい場合、戦略は新しいポジションを積み上げるか、反対方向へのエクスポージャーを清算します。変換では同じモデリング ブロックが維持され、ポジション管理と資金管理が StockSharp プリミティブにマッピングされます。

取引ロジック

  1. データの準備
    • 選択した CandleType の PastBars + 1 の始値のローリング履歴を作成します。
    • オプションで、AR モデルにデータを供給する前に、データを対数運動量 (デフォルト) または変化率に変換します。 Raw prices are centered by their moving average to mirror the MT4 code.
  2. バーグ線形予測
    • Burg アルゴリズムを使用して、PastBars * ModelOrder 次数までの反射係数を推定します。
    • AR モデルを再帰的に拡張することで、一連の将来の値 (実際には PastBars ステップ先) を生成します。変換は逆に価格空間に戻されるため、すべての予測は絶対価格単位で機能します。
  3. 信号検出
    • 予測パスをたどって、別の極端な値が現れる前に予測最高値と最低価格を記録します。最初の極値と予測範囲の反対側の間の距離は、MaxLoss および MinProfit のしきい値と比較されます (商品 PriceStep を乗算することで絶対価格に変換されます)。
    • 十分に大きな上昇は OpenSignal = 1 をトリガーし、大きな下降は OpenSignal = -1 を生成します。 If the opposing extreme appears first the logic sets CloseSignal to exit current exposure even if no fresh entry is planned.
  4. 注文管理
    • 新しいシグナルが実行される前に、保護的出口 (ストップロス、テイクプロフィット、およびオプションのトレーリングストップ) が実行されます。トレーリングストップは最後のエントリー以降の最良価格を再利用し、価格が TrailingStop ポイントまでリトレースしたときにポジションを閉じます。これは、保護注文を移動する MT4 の動作と一致します。
    • If a signal asks to close exposure in the opposite direction the strategy sends a market order sized to flatten the current net position.
    • エントリーシグナルは、MaxTrades に達するまで、示された方向に追加の成行注文を積み上げます。注文量は、元の証拠金ベースのサイジング ルーチンの StockSharp に適した置き換えである係数 1 + existingTrades * MaxRisk を使用して、アクティブな取引の数に比例して増加します。

指標とデータ

  • Candle subscription defined by CandleType (default 30-minute time frame).
  • Internal Burg autoregressive model (implemented without external indicators).
  • オプションの対数運動量と変化率の変換。

パラメーター

名前 デフォルト 説明
CandleType 30分キャンドル 戦略によって処理される主な時間枠。
MaxRisk 0.5 複数の取引を積み重ねるときに使用されるリスク乗数。
MaxTrades 5 方向ごとの同時取引の最大数。
MinProfit 160 Minimum predicted profit (in points) required to open new trades.
MaxLoss 130 Maximum tolerated forecasted loss (in points) before closing trades.
TakeProfit 0 オプションの固定テイクプロフィット距離 (ポイント単位) (0 は無効にします)。
StopLoss 180 オプションの固定ストップロス距離 (ポイント単位) (0 は無効にします)。
TrailingStop 10 Trailing stop distance in points, active only when StopLoss > 0.
PastBars 200 Number of historical candles used by the Burg model.
ModelOrder 0.37 PastBars の一部がバーグ注文に変換されました。
UseMomentum 本当の Apply logarithmic momentum transform to input data.
UseRateOfChange 偽 Apply percentage rate of change (ignored when momentum is enabled).

All parameters are StrategyParam<T> instances and can be optimised or adjusted in the StockSharp Designer.

実装メモ

  • Burg アルゴリズムは C# で直接実装され、MT4 バージョンと同じ再帰を維持します。すべての計算は倍精度で実行され、最終予測は信号チェックの前に decimal に変換されます。
  • The original EA could rely on MetaTrader account information to size positions. StockSharp では、資金管理ブロックが、Volume と MaxRisk に基づく決定的なスケーリング ルールに置き換えられます。 Set Volume to the desired base lot and the strategy will scale subsequent entries proportionally.
  • 保護ロジックは、ブローカー側のストップを変更するのではなく、明示的な成行注文でポジションをクローズします。これは、StockSharp の高レベルの API 設計と一致し、シミュレーションでの実行時に古い状態になるのを防ぎます。
  • 予測配列は、PastBars または ModelOrder が変更されるたびに再作成されるため、戦略を再起動することなく、オンザフライのパラメーター編集が AR モデルに即座に影響します。
  • To visualise the behaviour you can attach a chart in Designer: the strategy already draws candles and executed trades on the default area. Extending the sample with custom series (e.g., forecast path) is straightforward if desired.
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>
/// Burg extrapolator strategy converted from MetaTrader 4 implementation.
/// Predicts the future price path with Burg linear prediction coefficients and trades on forecasted extremes.
/// </summary>
public class BurgExtrapolatorForecastStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _maxRisk;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<int> _minProfit;
	private readonly StrategyParam<int> _maxLoss;
	private readonly StrategyParam<int> _takeProfit;
	private readonly StrategyParam<int> _stopLoss;
	private readonly StrategyParam<int> _trailingStop;
	private readonly StrategyParam<int> _pastBars;
	private readonly StrategyParam<decimal> _modelOrder;
	private readonly StrategyParam<bool> _useMomentum;
	private readonly StrategyParam<bool> _useRateOfChange;

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

	private double[] _samples = Array.Empty<double>();
	private double[] _coefficients = Array.Empty<double>();
	private double[] _predictions = Array.Empty<double>();

	private int _np;
	private int _no;
	private int _nf;

	private double _averagePrice;
	private bool _isFirstRun = true;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longHigh;
	private decimal? _shortLow;

	/// <summary>
/// Initializes a new instance of the <see cref="BurgExtrapolatorForecastStrategy"/> class.
/// </summary>
public BurgExtrapolatorForecastStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for forecasting", "General");

		_maxRisk = Param(nameof(MaxRisk), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("Max Risk", "Risk factor controlling position scaling", "Money Management");

		_maxTrades = Param(nameof(MaxTrades), 5)
			.SetGreaterThanZero()
			.SetDisplay("Max Trades", "Maximum stacked trades per direction", "Money Management");

		_minProfit = Param(nameof(MinProfit), 160)
			.SetGreaterThanZero()
			.SetDisplay("Min Profit", "Forecasted profit in points required to open trades", "Signals");

		_maxLoss = Param(nameof(MaxLoss), 130)
			.SetGreaterThanZero()
			.SetDisplay("Max Loss", "Forecasted adverse excursion closing existing trades", "Signals");

		_takeProfit = Param(nameof(TakeProfit), 0)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Optional fixed take profit in points", "Protection")
			;

		_stopLoss = Param(nameof(StopLoss), 180)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Optional fixed stop loss in points", "Protection")
			;

		_trailingStop = Param(nameof(TrailingStop), 10)
			.SetNotNegative()
			.SetDisplay("Trailing Stop", "Trailing distance in points (requires stop loss)", "Protection")
			;

		_pastBars = Param(nameof(PastBars), 200)
			.SetGreaterThanZero()
			.SetDisplay("Past Bars", "History length used for Burg model", "Forecast");

		_modelOrder = Param(nameof(ModelOrder), 0.37m)
			.SetGreaterThanZero()
			.SetDisplay("Model Order", "Fraction of past bars used as Burg order", "Forecast");

		_useMomentum = Param(nameof(UseMomentum), true)
			.SetDisplay("Use Momentum", "Use logarithmic momentum instead of raw prices", "Forecast");

		_useRateOfChange = Param(nameof(UseRateOfChange), false)
			.SetDisplay("Use ROC", "Use percentage rate of change instead of raw prices", "Forecast");
	}

	/// <summary>
	/// Type of candles processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Risk factor used when stacking positions.
	/// </summary>
	public decimal MaxRisk
	{
		get => _maxRisk.Value;
		set => _maxRisk.Value = value;
	}

	/// <summary>
	/// Maximum number of trades allowed in one direction.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Minimum profit in points predicted by the Burg model to initiate new trades.
	/// </summary>
	public int MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Maximum loss in points predicted by the Burg model before closing positions.
	/// </summary>
	public int MaxLoss
	{
		get => _maxLoss.Value;
		set => _maxLoss.Value = value;
	}

	/// <summary>
	/// Optional take profit expressed in points.
	/// </summary>
	public int TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Optional stop loss expressed in points.
	/// </summary>
	public int StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in points.
	/// </summary>
	public int TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Number of historical candles used by the Burg predictor.
	/// </summary>
	public int PastBars
	{
		get => _pastBars.Value;
		set => _pastBars.Value = value;
	}

	/// <summary>
	/// Fraction of <see cref="PastBars"/> used as Burg model order.
	/// </summary>
	public decimal ModelOrder
	{
		get => _modelOrder.Value;
		set => _modelOrder.Value = value;
	}

	/// <summary>
	/// Use logarithmic momentum transformation instead of raw prices.
	/// </summary>
	public bool UseMomentum
	{
		get => _useMomentum.Value;
		set => _useMomentum.Value = value;
	}

	/// <summary>
	/// Use percentage rate of change transformation instead of raw prices.
	/// </summary>
	public bool UseRateOfChange
	{
		get => _useRateOfChange.Value;
		set => _useRateOfChange.Value = value;
	}

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

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

		_openHistory.Clear();
		_samples = Array.Empty<double>();
		_coefficients = Array.Empty<double>();
		_predictions = Array.Empty<double>();
		_np = 0;
		_no = 0;
		_nf = 0;
		_averagePrice = 0.0;
		_isFirstRun = true;
		ResetLongState();
		ResetShortState();
	}

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

		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;

		AddOpenPrice(candle.OpenPrice);

		if (!EnsureModel())
			return;

		TrimHistory();

		if (_openHistory.Count < _np + 1)
			return;

		if (!UpdateSamples())
			return;

		var predictionCount = ComputePredictions();
		if (predictionCount <= 0)
			return;

		var (openSignal, closeSignal) = EvaluateSignals(predictionCount);

		if (ManageProtection(candle))
		{
			// Position has been closed by protective logic, wait for the next candle to re-evaluate.
			return;
		}

		HandleSignalClosures(openSignal, closeSignal);

		if (openSignal == 1)
		{
			TryOpenLong(candle);
		}
		else if (openSignal == -1)
		{
			TryOpenShort(candle);
		}
	}

	private void AddOpenPrice(decimal openPrice)
	{
		_openHistory.Add(openPrice);
	}

	private bool EnsureModel()
	{
		var np = PastBars;
		if (np < 3)
			return false;

		var modelOrder = ModelOrder;
		var no = (int)(modelOrder * np);
		if (no < 1)
			no = 1;
		if (no >= np - 1)
			no = np - 2;

		var nf = np - no - 1;
		if (nf < 1)
			nf = 1;

		var predictionLength = nf + 1;

		if (_np != np || _no != no || _nf != nf || _predictions.Length != predictionLength)
		{
			_np = np;
			_no = no;
			_nf = nf;
			_samples = new double[np];
			_coefficients = new double[no + 1];
			_predictions = new double[predictionLength];
			_averagePrice = 0.0;
			_isFirstRun = true;
		}

		return true;
	}

	private void TrimHistory()
	{
		var maxHistory = _np + 1;
		while (_openHistory.Count > maxHistory)
		{
			_openHistory.RemoveAt(0);
		}
	}

	private bool UpdateSamples()
	{
		if (_np <= 0)
			return false;

		var useMomentum = UseMomentum;
		var useRoc = !useMomentum && UseRateOfChange;

		if (useMomentum || useRoc)
		{
			if (!_isFirstRun)
			{
				for (var i = 0; i < _np - 1; i++)
					_samples[i] = _samples[i + 1];

				var current = GetOpen(0);
				var previous = GetOpen(1);
				if (previous == 0m)
					return false;

				var ratio = (double)(current / previous);
				_samples[_np - 1] = useMomentum ? Math.Log(ratio) : ratio - 1.0;
			}
			else
			{
				for (var i = 0; i < _np; i++)
				{
					var current = GetOpen(i);
					var previous = GetOpen(i + 1);
					if (previous == 0m)
						return false;

					var ratio = (double)(current / previous);
					_samples[_np - 1 - i] = useMomentum ? Math.Log(ratio) : ratio - 1.0;
				}

				_averagePrice = 0.0;
				_isFirstRun = false;
			}
		}
		else
		{
			if (_isFirstRun)
			{
				double sum = 0.0;
				for (var i = 0; i < _np; i++)
					sum += (double)GetOpen(i);

				_averagePrice = sum / _np;

				for (var i = 0; i < _np; i++)
				{
					var open = (double)GetOpen(i);
					_samples[_np - 1 - i] = open - _averagePrice;
				}

				_isFirstRun = false;
			}
			else
			{
				var newest = (double)GetOpen(0);
				var leaving = (double)GetOpen(_np);
				_averagePrice += (newest - leaving) / _np;

				for (var i = 0; i < _np; i++)
				{
					var open = (double)GetOpen(i);
					_samples[_np - 1 - i] = open - _averagePrice;
				}
			}
		}

		return true;
	}

	private int ComputePredictions()
	{
		Array.Clear(_coefficients, 0, _coefficients.Length);
		Array.Clear(_predictions, 0, _predictions.Length);

		double den = 0.0;
		for (var i = 0; i < _np; i++)
		{
			var value = _samples[i];
			den += value * value;
		}

		den *= 2.0;

		var df = new double[_np];
		var db = new double[_np];

		for (var i = 0; i < _np; i++)
		{
			var value = _samples[i];
			df[i] = value;
			db[i] = value;
		}

		double r = 0.0;

		for (var k = 1; k <= _no; k++)
		{
			double num = 0.0;
			for (var i = k; i < _np; i++)
				num += df[i] * db[i - 1];

			var denominator = (1.0 - r * r) * den - df[k - 1] * df[k - 1] - db[_np - 1] * db[_np - 1];
			if (Math.Abs(denominator) < 1e-12)
				return 0;

			r = -2.0 * num / denominator;
			_coefficients[k] = r;

			var half = k / 2;
			for (var i = 1; i <= half; i++)
			{
				var ki = k - i;
				var tmp = _coefficients[i];
				_coefficients[i] += r * _coefficients[ki];
				if (i != ki)
					_coefficients[ki] += r * tmp;
			}

			if (k < _no)
			{
				for (var i = _np - 1; i >= k; i--)
				{
					var tmp = df[i];
					df[i] += r * db[i - 1];
					db[i] = db[i - 1] + r * tmp;
				}
			}

			den = denominator;
		}

		for (var n = _np - 1; n < _np + _nf; n++)
		{
			double sum = 0.0;
			for (var i = 1; i <= _no; i++)
			{
				if (n - i < _np)
					sum -= _coefficients[i] * _samples[n - i];
				else
					sum -= _coefficients[i] * _predictions[n - i - _np + 1];
			}

			var index = n - _np + 1;
			if (index < _predictions.Length)
				_predictions[index] = sum;
		}

		var useMomentum = UseMomentum;
		var useRoc = !useMomentum && UseRateOfChange;

		if (useMomentum || useRoc)
		{
			var startPrice = (double)GetOpen(0);
			_predictions[0] = startPrice;

			for (var i = 1; i < _predictions.Length; i++)
			{
				_predictions[i] = useMomentum
					? _predictions[i - 1] * Math.Exp(_predictions[i])
					: _predictions[i - 1] * (1.0 + _predictions[i]);
			}
		}
		else
		{
			for (var i = 0; i < _predictions.Length; i++)
				_predictions[i] += _averagePrice;
		}

		return _predictions.Length;
	}

	private (int openSignal, int closeSignal) EvaluateSignals(int predictionCount)
	{
		if (predictionCount == 0)
			return (0, 0);

		var step = Security?.PriceStep ?? 1m;
		var maxLossDelta = MaxLoss * step;
		var minProfitDelta = MinProfit * step;

		var ymax = (decimal)_predictions[0];
		var ymin = ymax;
		var imax = 0;
		var imin = 0;
		var openSignal = 0;
		var closeSignal = 0;

		var limit = Math.Min(_np, predictionCount);

		for (var i = 1; i < limit; i++)
		{
			var value = (decimal)_predictions[i];

			if (value > ymax && openSignal == 0)
			{
				ymax = value;
				imax = i;

				if (imin == 0 && ymax - ymin >= maxLossDelta)
					closeSignal = 1;

				if (imin == 0 && ymax - ymin >= minProfitDelta)
					openSignal = 1;
			}

			if (value < ymin && openSignal == 0)
			{
				ymin = value;
				imin = i;

				if (imax == 0 && ymax - ymin >= maxLossDelta)
					closeSignal = -1;

				if (imax == 0 && ymax - ymin >= minProfitDelta)
					openSignal = -1;
			}
		}

		return (openSignal, closeSignal);
	}

	private bool ManageProtection(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 1m;
		var stopDistance = StopLoss * step;
		var takeDistance = TakeProfit * step;
		var trailingDistance = TrailingStop * step;

		if (Position > 0)
		{
			_longEntryPrice ??= candle.ClosePrice;
			_longHigh = _longHigh.HasValue ? Math.Max(_longHigh.Value, candle.HighPrice) : candle.HighPrice;

			if (StopLoss > 0 && _longEntryPrice.HasValue && candle.LowPrice <= _longEntryPrice.Value - stopDistance)
			{
				SellMarket(Position);
				ResetLongState();
				return true;
			}

			if (TakeProfit > 0 && _longEntryPrice.HasValue && candle.HighPrice >= _longEntryPrice.Value + takeDistance)
			{
				SellMarket(Position);
				ResetLongState();
				return true;
			}

			if (TrailingStop > 0 && StopLoss > 0 && _longHigh.HasValue && candle.LowPrice <= _longHigh.Value - trailingDistance)
			{
				SellMarket(Position);
				ResetLongState();
				return true;
			}
		}
		else
		{
			ResetLongState();
		}

		if (Position < 0)
		{
			_shortEntryPrice ??= candle.ClosePrice;
			_shortLow = _shortLow.HasValue ? Math.Min(_shortLow.Value, candle.LowPrice) : candle.LowPrice;

			if (StopLoss > 0 && _shortEntryPrice.HasValue && candle.HighPrice >= _shortEntryPrice.Value + stopDistance)
			{
				BuyMarket(-Position);
				ResetShortState();
				return true;
			}

			if (TakeProfit > 0 && _shortEntryPrice.HasValue && candle.LowPrice <= _shortEntryPrice.Value - takeDistance)
			{
				BuyMarket(-Position);
				ResetShortState();
				return true;
			}

			if (TrailingStop > 0 && StopLoss > 0 && _shortLow.HasValue && candle.HighPrice >= _shortLow.Value + trailingDistance)
			{
				BuyMarket(-Position);
				ResetShortState();
				return true;
			}
		}
		else
		{
			ResetShortState();
		}

		return false;
	}

	private void HandleSignalClosures(int openSignal, int closeSignal)
	{
		if (Position > 0 && (closeSignal == -1 || openSignal == -1))
		{
			SellMarket(Position);
			ResetLongState();
		}
		else if (Position < 0 && (closeSignal == 1 || openSignal == 1))
		{
			BuyMarket(-Position);
			ResetShortState();
		}
	}

	private void TryOpenLong(ICandleMessage candle)
	{
		var baseVolume = Volume;
		if (baseVolume <= 0m)
			return;

		var tradeCount = GetTradeCount(baseVolume);
		if (tradeCount >= MaxTrades)
			return;

		var orderVolume = CalculateOrderVolume(baseVolume, tradeCount);
		if (orderVolume <= 0m)
			return;

		BuyMarket(orderVolume);
		_longEntryPrice = candle.ClosePrice;
		_longHigh = candle.ClosePrice;
		ResetShortState();
	}

	private void TryOpenShort(ICandleMessage candle)
	{
		var baseVolume = Volume;
		if (baseVolume <= 0m)
			return;

		var tradeCount = GetTradeCount(baseVolume);
		if (tradeCount >= MaxTrades)
			return;

		var orderVolume = CalculateOrderVolume(baseVolume, tradeCount);
		if (orderVolume <= 0m)
			return;

		SellMarket(orderVolume);
		_shortEntryPrice = candle.ClosePrice;
		_shortLow = candle.ClosePrice;
		ResetLongState();
	}

	private int GetTradeCount(decimal baseVolume)
	{
		if (baseVolume <= 0m)
			return 0;

		var trades = Math.Abs(Position) / baseVolume;
		return (int)Math.Ceiling((double)(trades - 1e-8m));
	}

	private decimal CalculateOrderVolume(decimal baseVolume, int existingTrades)
	{
		var multiplier = 1m + existingTrades * MaxRisk;
		if (multiplier <= 0m)
			return 0m;

		return baseVolume * multiplier;
	}

	private decimal GetOpen(int shift)
	{
		var index = _openHistory.Count - 1 - shift;
		return index >= 0 && index < _openHistory.Count ? _openHistory[index] : 0m;
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longHigh = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortLow = null;
	}
}