GitHub で見る

直近N時間トレンドへの反対方向20ピップス戦略

このStockSharp戦略はMetaTraderエキスパートアドバイザー **«20 Pips Opposite Last N Hour Trend»**の高レベル移植版です。時間足ローソク足を観察し、 直前のN時間の価格の動きを測定し、設定された取引時間が終了したときに 反対方向にポジションをオープンします。取引は 固定20pipsテイクプロフィットターゲットと時間制限で管理され、 連続した損失後にはマーチンゲールスタイルのボリュームスケールが適用されます。

実装はStockSharpのローソク足サブスクリプション、パラメーターシステム、 注文ヘルパー(BuyMarket, SellMarket)を使用しているため、 Designer、API、Runner、Shellのいずれでも変更なしに実行できます。

トレードロジック

  • 戦略は選択されたローソク足タイプ(デフォルト:1時間バー)をサブスクライブします。
  • 各完成したローソク足について、終値を内部履歴に保存します。
  • OpenTime.Hour == TradingHourのローソク足が完成し、十分な 履歴が利用可能な場合:
    • HoursToCheckTrendバー前に発生した終値と 前の終値(1バー前)を比較します。
    • その窓で価格が下落した場合(弱気ドリフト)、戦略は買います; 価格が上昇した場合(強気ドリフト)は売ります。同じ終値の場合は取引をスキップします。
  • 1日に1つの取引のみがオープンされ、設定された取引時間のみに限られます。 他のすべてのローソク足は純粋に管理のためのみに使用されます。

ポジション管理

  • 20pipsターゲット(3/5桁シンボルに対して調整済み)はエントリー直後に 計算されます。完成したローソク足の高値/安値がターゲットに触れたことを示すと、 ポジションはそのレベルで閉じられます。
  • ターゲットが次の時間中に達成されなければ、ポジションは オーバーナイトのエクスポージャーを避けるため、次のローソク足の終わりに閉じられます。
  • 新しい取引日が始まると日次カウンターが自動的にリセットされ、 次の資格のあるシグナルが次のセッションで発動できるようになります。

資金管理

  • Volumeはベース注文サイズを設定します。MaxVolumeはマーチンゲールステップの結果サイズを上限にします。
  • 損失後の退場に続いて、戦略は適切な乗数によって次のポジションを増加させます: 最初の損失 → FirstMultiplier、2番目の損失 → SecondMultiplierなど。5回を超える連続損失は5番目の 乗数を再使用します。いかなる利益またはトントンのクローズもシーケンスをリセットします。
  • ボリューム計算は最後に実行されたポジション価格に依存するため、 完全なブローカーPnLデータなしでも利益/損失検出が決定論的に維持されます。

パラメーター

パラメーター デフォルト値 説明
MaxPositions 9 1日に許可される最大取引数。0に設定すると取引が無効になります。
Volume 0.1 連続の最初の取引のベースボリューム。
MaxVolume 5 乗数後の調整されたボリュームのハードキャップ。
TakeProfitPips 20 pipsでのテイクプロフィット距離。0でTPを無効にします。
TradingHour 7 ポジションをオープンする資格がある時刻(0-23)。
HoursToCheckTrend 24 直前のトレンドを測定するために使用される時間足終値の数。
FirstMultiplier 2 最初の連続損失後に適用される乗数。
SecondMultiplier 4 2番目の連続損失後に適用される乗数。
ThirdMultiplier 8 3番目の連続損失後に適用される乗数。
FourthMultiplier 16 4番目の連続損失後に適用される乗数。
FifthMultiplier 32 5番目以降の連続損失から適用される乗数。
CandleType H1 シグナル生成と管理に使用されるローソク足データタイプ。

追加の注意事項

  • pip サイズはSecurity.PriceStepと小数点以下の桁数から計算されるため、 20pipsターゲットは4桁および5桁のFXシンボルで正しく動作します。
  • StartProtection()は戦略開始時に呼び出され、StockSharpの組み込み 保護(無制限ポジションの自動ストップ、ポートフォリオリセット)を有効にします。
  • ロジックは完成したローソク足のみを使用し、インジケーター値を 直接読み取ることはなく、AGENTS.mdのガイドラインに適合しています。

リスク免責事項: マーチンゲールスタイルのポジションサイジングは 実質的なドローダウンにつながる可能性があります。ライブ取引に展開する前に、 常にヒストリカルデータでパラメーターをテストし、慎重なリスク制限を使用してください。

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>
/// Strategy that trades against the last N hours trend with a fixed take profit.
/// </summary>
public class TwentyPipsOppositeLastNHourTrendStrategy : Strategy
{
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<int> _tradingHour;
	private readonly StrategyParam<int> _hoursToCheckTrend;
	private readonly StrategyParam<int> _firstMultiplier;
	private readonly StrategyParam<int> _secondMultiplier;
	private readonly StrategyParam<int> _thirdMultiplier;
	private readonly StrategyParam<int> _fourthMultiplier;
	private readonly StrategyParam<int> _fifthMultiplier;
	private readonly StrategyParam<DataType> _candleType;

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

	private decimal? _entryPrice;
	private decimal? _takeProfitLevel;
	private decimal _entryVolume;
	private int _positionDirection;
	private int _consecutiveLosses;
	private DateTime? _currentDay;
	private int _tradesToday;

	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}


	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public int TradingHour
	{
		get => _tradingHour.Value;
		set => _tradingHour.Value = value;
	}

	public int HoursToCheckTrend
	{
		get => _hoursToCheckTrend.Value;
		set => _hoursToCheckTrend.Value = value;
	}

	public int FirstMultiplier
	{
		get => _firstMultiplier.Value;
		set => _firstMultiplier.Value = value;
	}

	public int SecondMultiplier
	{
		get => _secondMultiplier.Value;
		set => _secondMultiplier.Value = value;
	}

	public int ThirdMultiplier
	{
		get => _thirdMultiplier.Value;
		set => _thirdMultiplier.Value = value;
	}

	public int FourthMultiplier
	{
		get => _fourthMultiplier.Value;
		set => _fourthMultiplier.Value = value;
	}

	public int FifthMultiplier
	{
		get => _fifthMultiplier.Value;
		set => _fifthMultiplier.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public TwentyPipsOppositeLastNHourTrendStrategy()
	{
		_maxPositions = Param(nameof(MaxPositions), 9)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum trades per day", "Trading");


		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Max Volume", "Maximum allowed volume", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 20m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Trading");

		_tradingHour = Param(nameof(TradingHour), 8)
			.SetRange(0, 23)
			.SetDisplay("Trading Hour", "Hour (0-23) when entries are allowed", "Timing");

		_hoursToCheckTrend = Param(nameof(HoursToCheckTrend), 6)
			.SetRange(2, 240)
			.SetDisplay("Hours To Check", "Lookback hours for trend calculation", "Signals");

		_firstMultiplier = Param(nameof(FirstMultiplier), 2)
			.SetGreaterThanZero()
			.SetDisplay("First Multiplier", "Multiplier after first loss", "Money Management");

		_secondMultiplier = Param(nameof(SecondMultiplier), 4)
			.SetGreaterThanZero()
			.SetDisplay("Second Multiplier", "Multiplier after second loss", "Money Management");

		_thirdMultiplier = Param(nameof(ThirdMultiplier), 8)
			.SetGreaterThanZero()
			.SetDisplay("Third Multiplier", "Multiplier after third loss", "Money Management");

		_fourthMultiplier = Param(nameof(FourthMultiplier), 16)
			.SetGreaterThanZero()
			.SetDisplay("Fourth Multiplier", "Multiplier after fourth loss", "Money Management");

		_fifthMultiplier = Param(nameof(FifthMultiplier), 32)
			.SetGreaterThanZero()
			.SetDisplay("Fifth Multiplier", "Multiplier after fifth loss", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe to process", "Market Data");
	}

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

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

		_closeHistory.Clear();
		_entryPrice = null;
		_takeProfitLevel = null;
		_entryVolume = 0m;
		_positionDirection = 0;
		_consecutiveLosses = 0;
		_currentDay = null;
		_tradesToday = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

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

		// no fixed protection needed
	}

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

		var candleDay = candle.OpenTime.Date;
		if (_currentDay != candleDay)
		{
			_currentDay = candleDay;
			_tradesToday = 0;
		}

		if (_positionDirection != 0)
		{
			if (_takeProfitLevel is decimal target)
			{
				// Take profit when the candle range touches the desired level.
				var hitTarget = _positionDirection > 0
					? candle.HighPrice >= target
					: candle.LowPrice <= target;

				if (hitTarget)
				{
					ClosePosition(target);
				}
			}

			if (_positionDirection != 0 && candle.OpenTime.Hour != TradingHour)
			{
				// Close remaining exposure when the configured session hour has passed.
				ClosePosition(candle.ClosePrice);
			}
		}

		if (_positionDirection != 0)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		if (candle.OpenTime.Hour != TradingHour)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		if (MaxPositions <= 0 || _tradesToday >= MaxPositions)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		var requiredHistory = Math.Max(HoursToCheckTrend, 2);
		if (_closeHistory.Count < requiredHistory)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		var referenceClose = _closeHistory[_closeHistory.Count - HoursToCheckTrend];
		var previousClose = _closeHistory[_closeHistory.Count - 1];

		if (previousClose == referenceClose)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		// Opposite trend logic: buy after bearish drift, sell after bullish drift.
		var goLong = previousClose < referenceClose;
		var orderVolume = CalculateOrderVolume();
		if (orderVolume <= 0)
		{
			UpdateHistory(candle.ClosePrice);
			return;
		}

		if (goLong)
		{
			Volume = orderVolume;
			BuyMarket();
			_positionDirection = 1;
		}
		else
		{
			Volume = orderVolume;
			SellMarket();
			_positionDirection = -1;
		}

		_entryPrice = candle.ClosePrice;
		_entryVolume = orderVolume;

		var distance = GetTakeProfitDistance();

		if (distance > 0m)
		{
			_takeProfitLevel = _positionDirection > 0
				? _entryPrice + distance
				: _entryPrice - distance;
		}
		else
		{
			_takeProfitLevel = null;
		}

		_tradesToday++;

		UpdateHistory(candle.ClosePrice);
	}

	private void ClosePosition(decimal exitPrice)
	{
		var direction = _positionDirection;
		var entryPrice = _entryPrice;
		var volume = Math.Abs(Position);

		if (volume <= 0m && _entryVolume > 0m)
		{
			volume = _entryVolume;
		}

		if (volume <= 0m)
		{
			_positionDirection = 0;
			_takeProfitLevel = null;
			_entryPrice = null;
			_entryVolume = 0m;
			return;
		}

		if (direction > 0)
		{
			SellMarket();
		}
		else if (direction < 0)
		{
			BuyMarket();
		}

		if (entryPrice is decimal price)
		{
			var isLoss = direction > 0
				? exitPrice < price
				: exitPrice > price;

			_consecutiveLosses = isLoss
				? Math.Min(_consecutiveLosses + 1, 5)
				: 0;
		}

		_positionDirection = 0;
		_takeProfitLevel = null;
		_entryPrice = null;
		_entryVolume = 0m;
	}

	private void UpdateHistory(decimal closePrice)
	{
		_closeHistory.Add(closePrice);

		var maxHistory = Math.Max(HoursToCheckTrend, 2);
		if (_closeHistory.Count > maxHistory)
		{
			_closeHistory.RemoveRange(0, _closeHistory.Count - maxHistory);
		}
	}

	private decimal CalculateOrderVolume()
	{
		if (Volume <= 0m)
		{
			return 0m;
		}

		var multiplier = _consecutiveLosses switch
		{
			>= 5 => (decimal)FifthMultiplier,
			4 => (decimal)FourthMultiplier,
			3 => (decimal)ThirdMultiplier,
			2 => (decimal)SecondMultiplier,
			1 => (decimal)FirstMultiplier,
			_ => 1m
		};

		var desiredVolume = Volume * multiplier;

		if (MaxVolume > 0m && desiredVolume > MaxVolume)
		{
			desiredVolume = MaxVolume;
		}

		return desiredVolume;
	}

	private decimal GetTakeProfitDistance()
	{
		var pipSize = GetPipSize();
		return pipSize > 0m
			? TakeProfitPips * pipSize
			: 0m;
	}

	private decimal GetPipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
		{
			priceStep = 0.0001m;
		}

		var decimals = Security?.Decimals ?? 0;
		if (decimals == 3 || decimals == 5)
		{
			return priceStep * 10m;
		}

		return priceStep;
	}
}