GitHub で見る

Pipsover Chaikin Hedge 戦略

概要

この戦略は、StockSharp 内に MetaTrader エキスパートアドバイザー「Pipsover 2」を再現します。価格が移動平均を突き抜ける 際に Chaikin オシレーターで売られすぎまたは買われすぎの状態を探し、前のローソク足のボディを使って反転を確認します。 StockSharp のポートは元のコードの裁量的なヘッジロジックを維持します:ポジションを保有中に逆のシグナルが現れると、 戦略は新しいバイアスに従うためにネット露出を即座に反転します。

インジケーターとデータ

  • Chaikin オシレーター:Accumulation/Distribution ラインから構築され、2 つの移動平均で平滑化されます。両方の平均は 設定可能で、MetaTrader の実装(単純、指数、平滑化、加重)に一致します。
  • 価格移動平均:設定可能な長さ、シフト、タイプ。前のローソク足の高値または安値が突き抜ける必要がある平均回帰の アンカーとして機能します。
  • 時間軸:戦略は CandleType パラメーターで選択した単一のローソク足ストリームにサブスクライブします。

取引ロジック

  1. 完成したローソク足のみで作業します。前のローソク足のボディ(クローズ対オープン)が方向バイアスを提供します。
  2. 前のローソク足から Chaikin オシレーターの値を読み取ります。大きな負の値は売られすぎを示し、大きな正の値は買われすぎゾーンを示します。
  3. 前のローソク足が現在の移動平均値を突き抜けることを要求します(強気のセットアップでは Low < MA、弱気のセットアップでは High > MA)。
  4. ポジションが開いていない場合にエントリー:
    • ロング:前のローソク足が強気、安値が MA を下回る、Chaikin が -OpenLevel を下回る。
    • ショート:前のローソク足が弱気、高値が MA を上回る、Chaikin が OpenLevel を上回る。
  5. ポジションが存在し逆のセットアップが現れた場合、アルゴリズムはネットポジションを反転します(追加の数量で SellMarket / BuyMarket)。MT5 バージョンのヘッジ動作を反映します。
  6. StockSharp は個別のヘッジされたチケットではなくネットポジションで動作するため、ストップとターゲットはローソク足の高値/安値を使用して戦略内でエミュレートされます。

リスク管理

  • ストップロスとテイクプロフィット:インストゥルメントの価格ステップを通じて変換された pips の距離。どちらもゼロで無効にできます。
  • ブレークイーブン:価格が BreakevenPips 分有利に動いたら、ストップをエントリー価格に移動します。
  • トレーリング:動きが BreakevenPips + TrailingStopPips を超えると、ストップはトレーリング距離で価格を追従します。
  • ポジション状態のリセット:出口が発生するたびに、次のトレードの準備のためにすべての内部価格レベルがクリアされます。

パラメーター

名前 説明
OpenLevel 新規ポジションを開くために必要な Chaikin の大きさ(デフォルト 100)。
CloseLevel 既存のポジションを反転するために必要な Chaikin の大きさ(デフォルト 125)。
StopLossPips pips 単位のストップロス距離(デフォルト 65)。
TakeProfitPips pips 単位のテイクプロフィット距離(デフォルト 100)。
TrailingStopPips pips 単位のトレーリング距離(デフォルト 30)。
BreakevenPips ストップをブレークイーブンに移動する前の pips での利益(デフォルト 15)。
MaPeriod 価格フィルターの移動平均の長さ(デフォルト 20)。
MaShift 移動平均をシフトするバー数(デフォルト 0)。
MaType 移動平均のタイプ(Simple、Exponential、Smoothed、Weighted)。
ChaikinFastPeriod Chaikin オシレーターの速い平滑化の長さ(デフォルト 3)。
ChaikinSlowPeriod Chaikin オシレーターの遅い平滑化の長さ(デフォルト 10)。
ChaikinMaType Chaikin 平滑化に使用される移動平均のタイプ。
CandleType 計算に使用されるローソク足シリーズ。

注意事項

  • StockSharp の基底 Volume プロパティを設定してトレードサイズを制御します。
  • pips はインストゥルメントの PriceStep を使用して計算されます。ステップが 3 または 5 桁の小数点の引用に対応する場合(例:0.00001)、戦略は MetaTrader の pip 間隔に合わせるためにそれを 10 倍します。
  • StockSharp はネットポジションを使用するため、元の MQL エキスパートアドバイザーのヘッジ注文は既存のポジションの即座の反転として表現されます。
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>
/// Chaikin oscillator oversold/overbought strategy with optional reversal hedging and trailing management.
/// </summary>
public class PipsoverChaikinHedgeStrategy : Strategy
{
	private readonly StrategyParam<decimal> _openLevel;
	private readonly StrategyParam<decimal> _closeLevel;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _breakevenPips;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MovingAverageTypeOptions> _maType;
	private readonly StrategyParam<int> _chaikinFastPeriod;
	private readonly StrategyParam<int> _chaikinSlowPeriod;
	private readonly StrategyParam<MovingAverageTypeOptions> _chaikinMaType;
	private readonly StrategyParam<DataType> _candleType;

	private AccumulationDistributionLine _adLine = null!;
	private IIndicator _priceMa = null!;
	private IIndicator _chaikinFast = null!;
	private IIndicator _chaikinSlow = null!;

	private readonly Queue<decimal> _maValues = new();

	private decimal _pipSize;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal _prevOpen;
	private decimal _prevClose;
	private decimal _prevHigh;
	private decimal _prevLow;
	private bool _hasPrevCandle;
	private decimal _prevChaikin;
	private bool _hasPrevChaikin;

	/// <summary>
	/// Chaikin threshold for entries.
	/// </summary>
	public decimal OpenLevel
	{
		get => _openLevel.Value;
		set => _openLevel.Value = value;
	}

	/// <summary>
	/// Chaikin threshold for hedging reversals.
	/// </summary>
	public decimal CloseLevel
	{
		get => _closeLevel.Value;
		set => _closeLevel.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Breakeven activation distance in pips.
	/// </summary>
	public decimal BreakevenPips
	{
		get => _breakevenPips.Value;
		set => _breakevenPips.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Moving average shift in bars.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average type for price filter.
	/// </summary>
	public MovingAverageTypeOptions MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Fast Chaikin moving average length.
	/// </summary>
	public int ChaikinFastPeriod
	{
		get => _chaikinFastPeriod.Value;
		set => _chaikinFastPeriod.Value = value;
	}

	/// <summary>
	/// Slow Chaikin moving average length.
	/// </summary>
	public int ChaikinSlowPeriod
	{
		get => _chaikinSlowPeriod.Value;
		set => _chaikinSlowPeriod.Value = value;
	}

	/// <summary>
	/// Moving average type used in Chaikin oscillator.
	/// </summary>
	public MovingAverageTypeOptions ChaikinMaType
	{
		get => _chaikinMaType.Value;
		set => _chaikinMaType.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="PipsoverChaikinHedgeStrategy"/> class.
	/// </summary>
	public PipsoverChaikinHedgeStrategy()
	{
		_openLevel = Param(nameof(OpenLevel), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Open Level", "Chaikin level for entries", "Chaikin");

		_closeLevel = Param(nameof(CloseLevel), 0.02m)
		.SetGreaterThanZero()
		.SetDisplay("Close Level", "Chaikin level for hedging", "Chaikin");

		_stopLossPips = Param(nameof(StopLossPips), 65m)
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 100m)
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 30m)
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_breakevenPips = Param(nameof(BreakevenPips), 15m)
		.SetDisplay("Breakeven (pips)", "Breakeven activation distance", "Risk");

		_maPeriod = Param(nameof(MaPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("MA Period", "Price moving average length", "Trend");

		_maShift = Param(nameof(MaShift), 0)
		.SetDisplay("MA Shift", "Price moving average shift", "Trend");

		_maType = Param(nameof(MaType), MovingAverageTypeOptions.Simple)
		.SetDisplay("MA Type", "Price moving average type", "Trend");

		_chaikinFastPeriod = Param(nameof(ChaikinFastPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("Chaikin Fast", "Fast Chaikin length", "Chaikin");

		_chaikinSlowPeriod = Param(nameof(ChaikinSlowPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Chaikin Slow", "Slow Chaikin length", "Chaikin");

		_chaikinMaType = Param(nameof(ChaikinMaType), MovingAverageTypeOptions.Exponential)
		.SetDisplay("Chaikin MA Type", "Chaikin moving average type", "Chaikin");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for analysis", "Data");
	}

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

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

		_maValues.Clear();
		_pipSize = 0m;
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_prevOpen = 0m;
		_prevClose = 0m;
		_prevHigh = 0m;
		_prevLow = 0m;
		_prevChaikin = 0m;
		_hasPrevCandle = false;
		_hasPrevChaikin = false;
	}

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

		_pipSize = CalculatePipSize();

		_adLine = new AccumulationDistributionLine();
		_priceMa = CreateMovingAverage(MaType, MaPeriod);
		_chaikinFast = CreateMovingAverage(ChaikinMaType, ChaikinFastPeriod);
		_chaikinSlow = CreateMovingAverage(ChaikinMaType, ChaikinSlowPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_priceMa, _adLine, ProcessCandle)
		.Start();
	}

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

		var prevOpen = _prevOpen;
		var prevClose = _prevClose;
		var prevHigh = _prevHigh;
		var prevLow = _prevLow;
		var prevChaikin = _prevChaikin;
		var hasPrevCandle = _hasPrevCandle;
		var hasPrevChaikin = _hasPrevChaikin;

		var fastValue = _chaikinFast.Process(new DecimalIndicatorValue(_chaikinFast, adValue, candle.OpenTime) { IsFinal = true });
		var slowValue = _chaikinSlow.Process(new DecimalIndicatorValue(_chaikinSlow, adValue, candle.OpenTime) { IsFinal = true });

		if (!fastValue.IsFinal || !slowValue.IsFinal)
		{
			_prevChaikin = fastValue.ToDecimal() - slowValue.ToDecimal();
			_hasPrevChaikin = true;
			StorePreviousCandle(candle);
			return;
		}

		var chaikin = fastValue.ToDecimal() - slowValue.ToDecimal();
		var shiftedMa = UpdateShiftedMa(maValue);

		if (shiftedMa is null)
		{
			_prevChaikin = chaikin;
			_hasPrevChaikin = true;
			StorePreviousCandle(candle);
			return;
		}

		var hasPrevData = hasPrevCandle && hasPrevChaikin;
		var positionClosed = HandleStopsAndTargets(candle);
		var reversed = false;

		if (Position == 0m)
		{
			if (hasPrevData)
			{
				var bullishPrev = prevClose > prevOpen;
				var bearishPrev = prevClose < prevOpen;

				if (bullishPrev && prevLow < shiftedMa && prevChaikin < -OpenLevel)
				{
					BuyMarket(Volume);
					SetupLongTargets(candle.ClosePrice);
				}
				else if (bearishPrev && prevHigh > shiftedMa && prevChaikin > OpenLevel)
				{
					SellMarket(Volume);
					SetupShortTargets(candle.ClosePrice);
				}
			}
		}
		else if (!positionClosed)
		{
			if (hasPrevData)
			{
				var bearishPrev = prevClose < prevOpen;
				var bullishPrev = prevClose > prevOpen;

				if (Position > 0m && bearishPrev && prevHigh > shiftedMa && prevChaikin > CloseLevel)
				{
					var size = Math.Abs(Position) + Volume;
					SellMarket(size);
					SetupShortTargets(candle.ClosePrice);
					reversed = true;
				}
				else if (Position < 0m && bullishPrev && prevLow < shiftedMa && prevChaikin < -CloseLevel)
				{
					var size = Math.Abs(Position) + Volume;
					BuyMarket(size);
					SetupLongTargets(candle.ClosePrice);
					reversed = true;
				}
			}

			if (!reversed)
			UpdateTrailing(candle);
		}

		_prevChaikin = chaikin;
		_hasPrevChaikin = true;
		StorePreviousCandle(candle);
	}
	private decimal? UpdateShiftedMa(decimal maValue)
	{
		var shift = Math.Max(0, MaShift);
		_maValues.Enqueue(maValue);

		while (_maValues.Count > shift + 1)
		_maValues.Dequeue();

		var values = _maValues.ToArray();
		if (values.Length < shift + 1)
		return null;

		return values[0];
	}

	private void StorePreviousCandle(ICandleMessage candle)
	{
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_hasPrevCandle = true;
	}

	private bool HandleStopsAndTargets(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}
		}

		return false;
	}

	private void SetupLongTargets(decimal price)
	{
		_entryPrice = price;

		if (StopLossPips > 0m)
		_stopPrice = price - StopLossPips * _pipSize;
		else
		_stopPrice = null;

		if (TakeProfitPips > 0m)
		_takeProfitPrice = price + TakeProfitPips * _pipSize;
		else
		_takeProfitPrice = null;
	}

	private void SetupShortTargets(decimal price)
	{
		_entryPrice = price;

		if (StopLossPips > 0m)
		_stopPrice = price + StopLossPips * _pipSize;
		else
		_stopPrice = null;

		if (TakeProfitPips > 0m)
		_takeProfitPrice = price - TakeProfitPips * _pipSize;
		else
		_takeProfitPrice = null;
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		if (_entryPrice is not decimal entry)
		return;

		var breakevenDist = BreakevenPips > 0m ? BreakevenPips * _pipSize : 0m;
		var trailingDist = TrailingStopPips > 0m ? TrailingStopPips * _pipSize : 0m;

		if (Position > 0m)
		{
			var move = candle.ClosePrice - entry;

			if (breakevenDist > 0m && move > breakevenDist)
			{
				if (_stopPrice is null || _stopPrice < entry)
				_stopPrice = entry;
			}

			if (trailingDist > 0m)
			{
				var activation = breakevenDist + trailingDist;
				if (move > activation)
				{
					var newStop = candle.ClosePrice - trailingDist;
					if (_stopPrice is null || newStop > _stopPrice)
					_stopPrice = newStop;
				}
			}
		}
		else if (Position < 0m)
		{
			var move = entry - candle.ClosePrice;

			if (breakevenDist > 0m && move > breakevenDist)
			{
				if (_stopPrice is null || _stopPrice > entry)
				_stopPrice = entry;
			}

			if (trailingDist > 0m)
			{
				var activation = breakevenDist + trailingDist;
				if (move > activation)
				{
					var newStop = candle.ClosePrice + trailingDist;
					if (_stopPrice is null || newStop < _stopPrice)
					_stopPrice = newStop;
				}
			}
		}
	}

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

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0.0001m;
		if (step <= 0m)
		step = 0.0001m;

		var tmp = step;
		var decimals = 0;

		while (tmp < 1m && decimals < 10)
		{
			tmp *= 10m;
			decimals++;
		}

		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}

	private static IIndicator CreateMovingAverage(MovingAverageTypeOptions type, int length)
	{
		return type switch
		{
			MovingAverageTypeOptions.Simple => new SimpleMovingAverage { Length = length },
			MovingAverageTypeOptions.Exponential => new ExponentialMovingAverage { Length = length },
			MovingAverageTypeOptions.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageTypeOptions.Weighted => new WeightedMovingAverage { Length = length },
			_ => new SimpleMovingAverage { Length = length }
		};
	}

	/// <summary>
	/// Moving average options matching the MetaTrader configuration.
	/// </summary>
	public enum MovingAverageTypeOptions
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted
	}
}