GitHub で見る

サーフィン 3.0 戦略

概要

この C# 戦略は、MetaTrader 4 エキスパート Surfing 3.0 を忠実に移植したものです。これは、ローソク足の高値と安値から構築された指数移動平均 (EMA) エンベロープを監視するブレイクアウト ロジックを再作成します。前のバーがバンド内で閉じ、最後に閉じたバーがバンドを突き抜けるたびに、システムは方向性のある取引に反応します。変換は、手書きのバッファではなく、StockSharp の高レベルの API、ローソク足のサブスクリプション、および組み込みのインジケーターに依存します。

このアルゴリズムは、構成可能な集計からの完成したキャンドルに対してのみ機能します。元のコードで使用されている iMA および iClose ルックバックをエミュレートするために必要な最小限の状態のみを保持します。すべての決定は閉じたバーごとに 1 回行われ、MQL 実装の「閉じたバー」評価スタイルに一致します。

インジケーター

  • 高値 EMA / 安値 EMA – ローソク足の高値と安値で計算された 2 つの指数移動平均。これらは、ロングエントリーとショートエントリーのブレイクアウトレベルを定義する動的なエンベロープを形成します。
  • 相対強度指数 (RSI) – トレンド フィルターとして機能します。ロング ポジションは RSI が LongRsiThreshold を上回る必要がありますが、ショート ポジションは ShortRsiThreshold を下回っている場合にのみ許可されます。

取引ロジック

  1. CandleType タイプのローソク足をサブスクライブし、終了したすべてのバーの EMA および RSI インジケーターを更新します。
  2. 終値の以前の終値バー値と EMA の高値/安値を保存します。これらは、元の専門家からの PriceClose_2PriceHigh_2、および PriceLow_2 を表します。
  3. 最新の終値バー (PriceClose_1) が高値 EMA を 横切り、前の終値が高値以下であった場合、RSI フィルターは次のことを確認します。
    • オープンしているショートポジションをクローズします。
    • ボリューム OrderVolume のロング成行注文を開きます。
    • ストップロスとテイクプロフィットのオフセットを商品ポイントで計算します。
  4. 最新の終値バーが安値 EMA を 横切り、前の終値がその安値以上であり、RSI が短期しきい値を下回っている場合:
    • オープンしているロングポジションをクローズします。
    • ボリューム OrderVolume で空売り注文を開きます。
    • 同じポイントベースの距離を使用して保護レベルを適用します。
  5. アクティブにできるネット ポジションは 1 つだけです。反転信号は、反対方向に入る前に常に既存のエクスポージャーを平坦化します。
  6. 取引ウィンドウ [TradeStartHour, TradeEndHour) の外では、新しい取引は開始されません。クロックが TradeEndHour に達すると、ストラテジーは残りのポジションを閉じて内部履歴をリセットし、MQL バージョンの closeAllPos() 呼び出しを模倣します。

リスク管理

  • ストップロス/テイクプロフィット – 商品ポイントで表され、証券価格ステップを使用して変換されます。どちらもオプションです。距離を 0 に設定すると、それぞれのレベルが無効になります。
  • セッションフラット – 許可された取引ウィンドウの終了時に、すべてのオープンポジションが市場でクローズされ、ストップ/テイクプロフィットトラッキングがクリアされます。これにより、元の専門家が startHour / endHour で強制したのとまったく同じように、ポジションが一晩で変動するのを防ぎます。

パラメーター

名前 説明 デフォルト
OrderVolume すべての成行注文に使用される取引量。 1
TakeProfitPoints 商品ポイントで表される利食い距離。 80
StopLossPoints インストゥルメントポイントで表されるストップロス距離。 50
MaPeriod 高値と安値に適用される EMA の長さ。 50
RsiPeriod RSI フィルターの期間。 10
LongRsiThreshold 長いエントリを許可するには、最小の RSI 値が必要です。 40
ShortRsiThreshold ショートポジションを入力できる最大値は RSI です。 65
TradeStartHour 新規取引が許可される時間(為替時間)。 8
TradeEndHour ポジションがクローズされ、新しい取引が開始されなくなるまでの時間 (限定)。 18
CandleType すべての計算にローソク足の集計が使用されます (デフォルト: 15 分足のローソク足)。 15m

注意事項

  • シグナルは完成したキャンドルに対して厳密に評価されます。 MetaTrader と同様に、バー内変動は無視されます。
  • この戦略では、異なる日のデータが混在することを避けるために、取引セッションが終了すると、EMA の履歴がリセットされます。
  • Python の翻訳はプロジェクトのガイドラインに従って意図的に省略されています。
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 StockSharp.Algo;
using StockSharp.Algo.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that reproduces the Surfing 3.0 expert advisor logic from MetaTrader.
/// </summary>
public class Surfing30Strategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _longRsiThreshold;
	private readonly StrategyParam<decimal> _shortRsiThreshold;
	private readonly StrategyParam<int> _tradeStartHour;
	private readonly StrategyParam<int> _tradeEndHour;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi = null!;

	private decimal? _previousClose;
	private decimal? _previousHighEma;
	private decimal? _previousLowEma;

	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Initialize <see cref="Surfing30Strategy"/>.
	/// </summary>
	public Surfing30Strategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume applied to every trade.", "Trading")
			
			.SetOptimize(0.1m, 5m, 0.1m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 80)
			.SetNotNegative()
			.SetDisplay("Take Profit Points", "Distance to the take profit in instrument points.", "Risk Management")
			
			.SetOptimize(10, 200, 10);

		_stopLossPoints = Param(nameof(StopLossPoints), 50)
			.SetNotNegative()
			.SetDisplay("Stop Loss Points", "Distance to the stop loss in instrument points.", "Risk Management")
			
			.SetOptimize(10, 150, 10);

		_maPeriod = Param(nameof(MaPeriod), 50)
			.SetRange(1, 1000)
			.SetDisplay("EMA Period", "Length of the exponential moving averages calculated over highs and lows.", "Indicators")
			
			.SetOptimize(10, 120, 5);

		_rsiPeriod = Param(nameof(RsiPeriod), 10)
			.SetRange(1, 1000)
			.SetDisplay("RSI Period", "Length of the RSI filter.", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_longRsiThreshold = Param(nameof(LongRsiThreshold), 30m)
			.SetDisplay("Long RSI Threshold", "Minimum RSI value required for long entries.", "Filters")
			
			.SetOptimize(20m, 60m, 5m);

		_shortRsiThreshold = Param(nameof(ShortRsiThreshold), 70m)
			.SetDisplay("Short RSI Threshold", "Maximum RSI value allowed for short entries.", "Filters")
			
			.SetOptimize(40m, 80m, 5m);

		_tradeStartHour = Param(nameof(TradeStartHour), 0)
			.SetDisplay("Trade Start Hour", "Hour of the day when new trades may start.", "Sessions")
			;

		_tradeEndHour = Param(nameof(TradeEndHour), 23)
			.SetDisplay("Trade End Hour", "Hour of the day when all positions are closed.", "Sessions")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Aggregation used for calculations.", "Data");
	}

	/// <summary>
	/// Volume used for every trade.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set
		{
			_orderVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Distance to the take profit in instrument points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Distance to the stop loss in instrument points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Length of the exponential moving averages calculated over candle highs and lows.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Length of the RSI filter.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Minimum RSI value required for long entries.
	/// </summary>
	public decimal LongRsiThreshold
	{
		get => _longRsiThreshold.Value;
		set => _longRsiThreshold.Value = value;
	}

	/// <summary>
	/// Maximum RSI value allowed for short entries.
	/// </summary>
	public decimal ShortRsiThreshold
	{
		get => _shortRsiThreshold.Value;
		set => _shortRsiThreshold.Value = value;
	}

	/// <summary>
	/// Hour of the day when new trades may start.
	/// </summary>
	public int TradeStartHour
	{
		get => _tradeStartHour.Value;
		set => _tradeStartHour.Value = value;
	}

	/// <summary>
	/// Hour of the day when all positions are closed.
	/// </summary>
	public int TradeEndHour
	{
		get => _tradeEndHour.Value;
		set => _tradeEndHour.Value = value;
	}

	/// <summary>
	/// Candle aggregation used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_previousClose = null;
		_previousHighEma = null;
		_previousLowEma = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

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

		Volume = OrderVolume;

		var sma = new SimpleMovingAverage { Length = MaPeriod };
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(sma, _rsi, ProcessCandle)
			.Start();
	}

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

		var currentClose = candle.ClosePrice;

		if (ManageActivePosition(candle))
		{
			UpdateHistory(currentClose, smaValue, smaValue);
			return;
		}

		if (_previousClose is null || _previousHighEma is null)
		{
			UpdateHistory(currentClose, smaValue, smaValue);
			return;
		}

		var previousClose = _previousClose.Value;
		var previousSma = _previousHighEma.Value;

		var buySignal = previousClose <= previousSma && currentClose > smaValue && rsiValue > LongRsiThreshold;
		var sellSignal = previousClose >= previousSma && currentClose < smaValue && rsiValue < ShortRsiThreshold;

		if (buySignal && Position <= 0)
		{
			if (Position < 0)
			{
				CloseCurrentPosition();
				ResetTargets();
			}

			BuyMarket(OrderVolume);
			SetTargets(currentClose, true);
		}
		else if (sellSignal && Position >= 0)
		{
			if (Position > 0)
			{
				CloseCurrentPosition();
				ResetTargets();
			}

			SellMarket(OrderVolume);
			SetTargets(currentClose, false);
		}

		UpdateHistory(currentClose, smaValue, smaValue);
	}

	private bool ManageActivePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopLossPrice is not null && candle.LowPrice <= _stopLossPrice)
			{
				CloseCurrentPosition();
				ResetTargets();
				return true;
			}

			if (_takeProfitPrice is not null && candle.HighPrice >= _takeProfitPrice)
			{
				CloseCurrentPosition();
				ResetTargets();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (_stopLossPrice is not null && candle.HighPrice >= _stopLossPrice)
			{
				CloseCurrentPosition();
				ResetTargets();
				return true;
			}

			if (_takeProfitPrice is not null && candle.LowPrice <= _takeProfitPrice)
			{
				CloseCurrentPosition();
				ResetTargets();
				return true;
			}
		}

		return false;
	}

	private void CloseCurrentPosition()
	{
		if (Position > 0m)
			SellMarket(Position);
		else if (Position < 0m)
			BuyMarket(Math.Abs(Position));
	}

	private void SetTargets(decimal entryPrice, bool isLong)
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		if (isLong)
		{
			_stopLossPrice = StopLossPoints > 0 ? entryPrice - StopLossPoints * priceStep : null;
			_takeProfitPrice = TakeProfitPoints > 0 ? entryPrice + TakeProfitPoints * priceStep : null;
		}
		else
		{
			_stopLossPrice = StopLossPoints > 0 ? entryPrice + StopLossPoints * priceStep : null;
			_takeProfitPrice = TakeProfitPoints > 0 ? entryPrice - TakeProfitPoints * priceStep : null;
		}
	}

	private void ResetTargets()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private void UpdateHistory(decimal currentClose, decimal currentHighEma, decimal currentLowEma)
	{
		_previousClose = currentClose;
		_previousHighEma = currentHighEma;
		_previousLowEma = currentLowEma;
	}

	private void ResetHistory()
	{
		_previousClose = null;
		_previousHighEma = null;
		_previousLowEma = null;
	}

	private bool IsWithinTradeHours(DateTimeOffset time)
	{
		var startHour = TradeStartHour;
		var endHour = TradeEndHour;

		if (endHour <= startHour)
			return time.Hour >= startHour || time.Hour < endHour;

		return time.Hour >= startHour && time.Hour < endHour;
	}
}