GitHub で見る

トレンドスキャルパー戦略 (API/3858)

概要

TrendScalperStrategy は、MetaTrader 4 エキスパート アドバイザー Currencyprofits_01_1.mq4 を C# に変換したものです。オリジナルのロボットは、短期的な EMA/SMA クロスオーバー フィルターと、直近のスイング高値と安値付近のブレイクアウト エントリーを組み合わせた軽量のトレンド追跡スキャルパーです。 StockSharp ポートは、フレームワークの高レベルのローソク足サブスクリプションとインジケーター パイプラインを採用しながら、同じ決定ルールを維持します。

取引ロジック

  1. 指標
    • 終値では高速 EMA (デフォルト 6)。
    • 終値では遅い SMA (デフォルトは 12)。
    • ろうそくの高値と安値から計算された最高値 (デフォルトのウィンドウ 6) と最低値 (デフォルトのウィンドウ 6)。
  2. エントリー条件
    • ロング: 価格は最近のローバンド (Lowest Low) に突入しますが、高速の EMA は低速の SMA を上回ります。この戦略は、資金管理ルールで定義された数量で成行買い注文を送信します。
    • ショート: 価格は最近の高バンド (Highest High) に接していますが、高速の EMA は低速の SMA を下回っています。成行売り注文は、同じ数量計算を使用して発注されます。
    • ポジションがオープンしている間、システムはフラットを維持し、MQL バージョンの単一注文の動作を反映しています。
  3. Exit Conditions
    • ロングイグジット: オープンしているロングポジションがローソク足の高値で記録された Highest High を突破すると、そのポジションは市場でクローズされます。
    • ショートエグジット: オープンショートポジションがローソク足の安値が Lowest Low を通過するのを観察すると、ショートは市場でカバーされます。
    • StopLossPoints がゼロより大きい場合、StartProtection によって管理される保護ストップロスがすべての取引に適用されます。

資金管理

ロットサイジング ロジックは、MQL スクリプトで公開されている 3 つのモードを再現します。

モード 説明 港内での行動
0 固定ロット (LotsIfNoMM)。 構成された FixedVolume を返します。
<0 口座残高とリスクファクターから計算された端数ロット。 ceil(balance * risk / 10000) / 10 を計算します。上限は 100 ロットです。
>0 バランスとリスクファクターからのホールロットスケーリング。 同じ基本式を使用しますが、結果は次の整数に切り上げられ、1 ロットで下限され、100 で上限が設定されます。

残高は Portfolio.CurrentValue から取得されます (BeginValue にフォールバックします)。ポートフォリオの値が利用できない場合、戦略は固定量に戻るため、バックテスト中も注文は発行されます。

リスク管理

  • ストップロス: StopLossPoints パラメータは価格ポイント (pips) で表されます。 OnStarted の間、距離は Security.PriceStep 倍されて StartProtection に渡され、StockSharp は保護命令を維持できます。
  • 単一ポジション: ロジックは新しい取引を開始する前に Position == 0 を強制し、MT4 エキスパートとまったく同様にポジションの重複を防ぎます。

パラメーター

名前 デフォルト 説明
CandleType 15分の時間枠 インジケーターの計算とシグナルに使用されるローソク足シリーズ。
FastLength 6 高速の EMA の期間。
SlowLength 12 遅い SMA の期間。
BreakoutWindow 6 最高高/最低低ブレークアウト フィルターについて検査されたキャンドルの数。
FixedVolume 0.1ロット 資金管理が無効になっている場合、またはフォールバックが必要な場合のボリューム。
MoneyManagementMode 0 ロットサイジングを固定、分数、または丸めから選択します。
MoneyManagementRisk 40 バランスベースのロットサイジングで使用されるリスク係数乗数。
StopLossPoints 50 価格ポイントでのストップロス距離 (StartProtection を呼び出す前に絶対価格に変換)。

実装メモ

  • インジケーター チェーンは高レベルの SubscribeCandles().Bind(...) ワークフローに依存します。手動によるシリーズバッファリングは必要ありません。
  • コード内のコメントは、リポジトリのガイドラインに一致するように英語で追加されました。
  • 単体テストは変更されていません。この変換の焦点は、戦略とそれに付随するドキュメントです。

使用のヒント

  • 元の取引環境(スキャルピングの日中の短い時間枠など)に一致するローソク足の間隔を選択します。
  • 絶対価格へのストップロス変換が正しく機能するように、ポートフォリオに有効な PriceStep があることを確認してください。
  • MoneyManagementRisk は慎重に調整してください。MQL のエキスパートから継承された ceil(balance * risk / 10000) の計算により、値が大きいほどポジションが大きくなります。

厳密なシグナル条件: チャネル高値・安値はシグナル足を除く 直前の確定済み BreakoutWindow 本で計算します。買いは EMA(FastLength) > SMA(SlowLength) かつ currentLow <= previousWindowLow、売りは逆のトレンドかつ currentHigh >= previousWindowHigh が必要です。ロングは currentHigh > previousWindowHigh、ショートは currentLow < previousWindowLow で決済します。両移動平均とチャネルのウォームアップ完了前は取引しません。StopLossPoints > 0 の場合、StartProtection で保護ストップを有効化します。

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _breakoutWindow;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<int> _moneyManagementMode;
	private readonly StrategyParam<decimal> _moneyManagementRisk;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _fastSeed = new();
	private readonly Queue<decimal> _slowCloses = new();
	private readonly Queue<decimal> _highs = new();
	private readonly Queue<decimal> _lows = new();
	private decimal? _fastEma;
	private Order _pendingOrder;

	public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
	public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
	public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
	public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
	public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
	public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public TrendScalperStrategy()
	{
		_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
		_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
		_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
		_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
		_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
		_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
		_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		ClearState();
	}

	private void ClearState()
	{
		_fastSeed.Clear();
		_slowCloses.Clear();
		_highs.Clear();
		_lows.Clear();
		_fastEma = null;
		_pendingOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ClearState();
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");

		if (StopLossPoints > 0m)
		{
			StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
				useMarketOrders: true, isLocalStop: true);
			foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
			{
				var quotes = new Subscription(DataType.Level1, Security);
				quotes.MarketData.BuildField = field;
				SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
			}
		}
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private void ObserveQuote(Level1ChangeMessage message)
	{
		// A Level1 stream is needed for native stop processing between candle closes.
	}

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

		var hasBand = _highs.Count == BreakoutWindow;
		var bandHigh = hasBand ? _highs.Max() : 0m;
		var bandLow = hasBand ? _lows.Min() : 0m;
		_highs.Enqueue(candle.HighPrice);
		_lows.Enqueue(candle.LowPrice);
		while (_highs.Count > BreakoutWindow) _highs.Dequeue();
		while (_lows.Count > BreakoutWindow) _lows.Dequeue();

		var price = candle.ClosePrice;
		if (_fastEma is null)
		{
			_fastSeed.Enqueue(price);
			if (_fastSeed.Count == FastLength)
				_fastEma = _fastSeed.Average();
		}
		else
			_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);

		_slowCloses.Enqueue(price);
		while (_slowCloses.Count > SlowLength)
			_slowCloses.Dequeue();
		if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
			_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
			return;

		if (Position > 0m && candle.HighPrice > bandHigh)
		{
			Submit(Sides.Sell, Position, "Trend scalper band exit");
			return;
		}
		if (Position < 0m && candle.LowPrice < bandLow)
		{
			Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
			return;
		}
		if (Position != 0m)
			return;

		var slow = _slowCloses.Average();
		if (_fastEma > slow && candle.LowPrice <= bandLow)
			Enter(Sides.Buy);
		else if (_fastEma < slow && candle.HighPrice >= bandHigh)
			Enter(Sides.Sell);
	}

	private void Enter(Sides side)
	{
		var volume = CalculateOrderVolume();
		if (volume > 0m)
			Submit(side, volume, "Trend scalper entry");
	}

	private decimal CalculateOrderVolume()
	{
		var volume = FixedVolume;
		if (MoneyManagementMode != 0)
		{
			var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			if (balance > 0m)
			{
				var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
				volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
				volume = Math.Min(volume, 100m);
			}
		}
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = decimal.Floor(volume / step) * step;
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(volume, min);
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(volume, max);
		return volume;
	}

	private void Submit(Sides side, decimal volume, string comment)
	{
		_pendingOrder = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		};
		RegisterOrder(_pendingOrder);
	}
}