GitHub で見る

Brandy v1.2 戦略 (C#)

概要

Brandy v1.2 戦略 は、MetaTrader 4 エキスパート アドバイザー「Brandy_v1_2.mq4」を StockSharp の高レベル戦略フレームワークに直接変換したものです。システムは、設定されたローソク足シリーズの終値に基づいて計算された、置き換えられた単純移動平均 (SMA) のペアを評価します。新しいポジションは長期SMAと短期SMAの両方が同じ方向に同期した勢いを示した場合にのみオープンされ、既存の取引はスロープ反転、固定ストップロスレベル、およびオプションのトレーリングストップモジュールを使用して管理されます。

元の MQL スクリプトは、完成したバーごとに 1 回だけ実行されました。このポートは、完成した StockSharp ローソク足を同じ方法で処理し、部分的に形成されたバーに依存することなく、すべての取引決定が閉じられたデータに基づいて行われるようにします。

取引ロジック

  1. インジケーターの準備
    • 2 つの SMA が計算されます。長いベースライン (LongPeriod) と短い確認ライン (ShortPeriod) です。
    • 各平均は 2 回アクセスされます。前のバー (シフト = 1) の値と、それぞれ LongShift/ShortShift バー分離れた別の値です。これは、元の EA に存在する iMA(..., shift) 呼び出しを再現します。
  2. エントリールール
    • 両方の SMA の前足の値が、シフトされた対応する足よりも大きく (両方の傾きが上を向いている)、オープン ポジションがない場合に買います。
    • **両方の SMA の前足の値が、シフトされた対応する足よりも低く (両方の傾きが下を向いている)、オープン ポジションがない場合、売り
    • 常に 1 つのポジションのみをアクティブにでき、MQL ソースの k == 0 チェックをミラーリングします。
  3. 退出ルール
    • スロープ反転: ロング SMA が下降した場合 (longPrev < longShifted)、オープンしているロング ポジションは清算されますが、ロング SMA が上昇した場合 (longPrev > longShifted) にショート ポジションはカバーされます。
    • 固定ストップロス: エントリー時に、ストラテジーはエントリー価格から StopLossPoints × PriceStep オフセットされた初期ストップレベルを保存します。ストップはローソク足の高値/安値範囲に対してチェックされ、元のアドバイザーのティックレベル管理に近似します。
    • トレーリング ストップ: TrailingStopPoints ≥ 100 の場合、ストラテジーはトレーリング ロジック (ts パラメーター) を複製します。変動利益がトレーリング距離を超えると、新しいレベルが既存のストップよりも価格に近い場合、ストップは currentPrice ± trailingDistance に引かれます。この動作は、MQL エキスパートの OrderModify 呼び出しと一致します。

パラメーター

パラメータ デフォルト 説明
LongPeriod 70 プライマリ SMA の長さ (MQL の p1)。 > 0 である必要があります。
LongShift 5 後方シフトは長い SMA 比較 (s1) に適用されます。ゼロでも構いません。
ShortPeriod 20 確認の長さ SMA (p2)。 > 0 である必要があります。
ShortShift 5 短い SMA (s2) の後方シフト。ゼロでも構いません。
StopLossPoints 50 価格ステップの停止距離を修正しました (sl)。ハードストップを無効にするには、0 に設定します。
TrailingStopPoints 150 価格ステップでのトレーリング距離 (ts)。トレーリングは値が 100 以上の場合にのみアクティブになり、元のしきい値が反映されます。
Volume 0.1 エントリ (lots) に使用される注文量。
CandleType 15分の時間枠 戦略によって処理されるキャンドル シリーズ (ユーザー設定可能)。

価格ステップ依存性

どちらの停止パラメータも機器点で動作します。ヘルパー メソッドは、Security.PriceStep を介してそれらを絶対価格デルタに変換します。データ ソースが PriceStep を提供しない場合、戦略は 0.0001 にフォールバックするため、近似的な変換ではありますが、ロジックは引き続き機能します。実際に使用する前に、StockSharp のシンボル メタデータを必ず確認してください。

リスク管理

  • ハードストップ: 内部に保存され、完成したすべてのキャンドルに対して検証されます。価格がストップに違反すると、対応する SellMarket/BuyMarket 呼び出しによってポジション全体がクローズされます。
  • トレーリング ストップ: 元の EA の正確な条件に従い、現在の利益がトレーリング距離を超え、かつ 既存のストップがまだその距離より遠い場合にのみストップを移動します。
  • 単一ポジション: アルゴリズムは決してピラミッド化されません。単一のロングポジションか単一のショートポジションを持つか、またはフラットです。

実装メモ

  • 状態(エントリー価格、ストップレベル、SMA 履歴)は、OnReseted() に自動的にリセットされ、クリーンなバックテストと再起動が保証されます。
  • インジケーター履歴は、GetValue() を呼び出さずに iMA(..., shift) オフセットを再現するために短いローリング バッファーに保存されます。
  • リポジトリ ガイドラインの要件に従って、すべてのインライン コメントは英語のままです。
  • Python に相当するものは提供されていません。 C# の高レベル実装のみが、リクエストに応じて CS/BrandyV12Strategy.cs で提供されます。

使用法

  1. 戦略を StockSharp ソリューションに配置し、目的の商品を選択し、ローソク足データが CandleType で指定されたタイムフレームと一致することを確認します。
  2. UI またはコード経由でパラメーターを構成します。デフォルトでは、元の MT4 値が複製されます。
  3. 戦略を開始します。ローソク足シリーズを購読し、チャート上に両方の SMA を描画し、取引を自動的に管理します。

免責事項: このポートは教育およびテストを目的としています。ライブ市場に展開する前に、常に過去の取引セッションと紙の取引セッションでの動作を検証してください。

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>
/// Trend-following strategy using displaced simple moving averages.
/// </summary>
public class BrandyV12Strategy : Strategy
{
	private readonly StrategyParam<int> _longPeriod;
	private readonly StrategyParam<int> _longShift;
	private readonly StrategyParam<int> _shortPeriod;
	private readonly StrategyParam<int> _shortShift;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _longSma;
	private SimpleMovingAverage _shortSma;
	private readonly List<decimal> _longHistory = new();
	private readonly List<decimal> _shortHistory = new();
	private decimal? _entryPrice;
	private decimal? _stopPrice;

	/// <summary>
	/// Initializes a new instance of <see cref="BrandyV12Strategy"/>.
	/// </summary>
	public BrandyV12Strategy()
	{
		_longPeriod = Param(nameof(LongPeriod), 70)
			.SetGreaterThanZero()
			.SetDisplay("Long SMA Period", "Period for the longer moving average.", "Indicators")
			;

		_longShift = Param(nameof(LongShift), 5)
			.SetNotNegative()
			.SetDisplay("Long SMA Shift", "Backward shift applied to the longer SMA.", "Indicators")
			;

		_shortPeriod = Param(nameof(ShortPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Short SMA Period", "Period for the shorter moving average.", "Indicators")
			;

		_shortShift = Param(nameof(ShortShift), 5)
			.SetNotNegative()
			.SetDisplay("Short SMA Shift", "Backward shift applied to the shorter SMA.", "Indicators")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Initial stop-loss distance expressed in price steps.", "Risk")
			;

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 150m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Trailing stop distance in price steps. Activates when >= 100.", "Risk")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Candle series processed by the strategy.", "General");
	}

	/// <summary>
	/// Period for the longer simple moving average.
	/// </summary>
	public int LongPeriod
	{
		get => _longPeriod.Value;
		set => _longPeriod.Value = value;
	}

	/// <summary>
	/// Backward shift used when evaluating the longer SMA.
	/// </summary>
	public int LongShift
	{
		get => _longShift.Value;
		set => _longShift.Value = value;
	}

	/// <summary>
	/// Period for the shorter simple moving average.
	/// </summary>
	public int ShortPeriod
	{
		get => _shortPeriod.Value;
		set => _shortPeriod.Value = value;
	}

	/// <summary>
	/// Backward shift used when evaluating the shorter SMA.
	/// </summary>
	public int ShortShift
	{
		get => _shortShift.Value;
		set => _shortShift.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in points (price steps).
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in points (price steps).
	/// Trailing activates only when the configured value is at least 100.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

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

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

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

		_longSma = null;
		_shortSma = null;
		_longHistory.Clear();
		_shortHistory.Clear();
		_entryPrice = null;
		_stopPrice = null;
	}

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

		_longSma = new SMA { Length = LongPeriod };
		_shortSma = new SMA { Length = ShortPeriod };

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

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

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

		if (_longSma?.IsFormed != true || _shortSma?.IsFormed != true)
			return;

		var longCapacity = Math.Max(LongShift, 1) + 2;
		var shortCapacity = Math.Max(ShortShift, 1) + 2;
		UpdateHistory(_longHistory, longValue, longCapacity);
		UpdateHistory(_shortHistory, shortValue, shortCapacity);

		if (!TryGetShiftedValue(_longHistory, 1, out var longPrev) ||
			!TryGetShiftedValue(_longHistory, LongShift, out var longShifted) ||
			!TryGetShiftedValue(_shortHistory, 1, out var shortPrev) ||
			!TryGetShiftedValue(_shortHistory, ShortShift, out var shortShifted))
		{
			return;
		}

		if (ManageExistingPosition(candle, longPrev, longShifted))
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position == 0)
		{
			var bullish = longPrev > longShifted && shortPrev > shortShifted;
			var bearish = longPrev < longShifted && shortPrev < shortShifted;

			if (bullish)
			{
				EnterLong(candle);
			}
			else if (bearish)
			{
				EnterShort(candle);
			}
		}
	}

	private bool ManageExistingPosition(ICandleMessage candle, decimal longPrev, decimal longShifted)
	{
		if (Position > 0)
		{
			if (longPrev < longShifted)
			{
				SellMarket(Position);
				ResetPositionState();
				return true;
			}

			if (UpdateLongStops(candle))
			{
				SellMarket(Position);
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (longPrev > longShifted)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (UpdateShortStops(candle))
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}
		}

		return false;
	}

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

		BuyMarket(volume);

		var step = GetPoint();
		var price = candle.ClosePrice;
		_entryPrice = price;

		_stopPrice = StopLossPoints > 0m ? price - StopLossPoints * step : null;
	}

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

		SellMarket(volume);

		var step = GetPoint();
		var price = candle.ClosePrice;
		_entryPrice = price;

		_stopPrice = StopLossPoints > 0m ? price + StopLossPoints * step : null;
	}

	private bool UpdateLongStops(ICandleMessage candle)
	{
		if (_entryPrice is not decimal entry)
			return false;

		var step = GetPoint();
		if (step <= 0m)
			return false;

		if (_stopPrice is null && StopLossPoints > 0m)
		{
			_stopPrice = entry - StopLossPoints * step;
		}

		if (TrailingStopPoints >= 100m)
		{
			var trailingDistance = TrailingStopPoints * step;
			if (trailingDistance > 0m)
			{
				var currentPrice = candle.ClosePrice;
				if (currentPrice - entry > trailingDistance)
				{
					var newStop = currentPrice - trailingDistance;
					if (_stopPrice is not decimal existing || currentPrice - existing > trailingDistance)
					{
						_stopPrice = newStop;
					}
				}
			}
		}

		if (_stopPrice is not decimal stop)
			return false;

		return candle.LowPrice <= stop;
	}

	private bool UpdateShortStops(ICandleMessage candle)
	{
		if (_entryPrice is not decimal entry)
			return false;

		var step = GetPoint();
		if (step <= 0m)
			return false;

		if (_stopPrice is null && StopLossPoints > 0m)
		{
			_stopPrice = entry + StopLossPoints * step;
		}

		if (TrailingStopPoints >= 100m)
		{
			var trailingDistance = TrailingStopPoints * step;
			if (trailingDistance > 0m)
			{
				var currentPrice = candle.ClosePrice;
				if (entry - currentPrice > trailingDistance)
				{
					var newStop = currentPrice + trailingDistance;
					if (_stopPrice is not decimal existing || existing - currentPrice > trailingDistance)
					{
						_stopPrice = newStop;
					}
				}
			}
		}

		if (_stopPrice is not decimal stop)
			return false;

		return candle.HighPrice >= stop;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
	}

	private static void UpdateHistory(List<decimal> history, decimal value, int capacity)
	{
		history.Add(value);
		if (history.Count > capacity)
		{
			history.RemoveAt(0);
		}
	}

	private static bool TryGetShiftedValue(List<decimal> history, int shift, out decimal value)
	{
		value = 0m;

		if (shift < 0)
			return false;

		var index = history.Count - 1 - shift;
		if (index < 0 || index >= history.Count)
			return false;

		value = history[index];
		return true;
	}

	private decimal GetPoint()
	{
		var step = Security?.PriceStep;
		if (step is decimal priceStep && priceStep > 0m)
			return priceStep;

		return 0.0001m;
	}
}