GitHub で見る

ATM 5 分レガシー

概要

Cash Machine 5 min Legacy は、MetaTrader 4 エキスパート アドバイザー CashMachine_5min の StockSharp 移植です。このシステムは、DeMarker オシレーターと 5 分足ローソク足の高速 Stochastic オシレーターによって検出された運動量の反転に反応します。ポジションがオープンになると、この戦略は保護的なストップロスとテイクプロフィットのレベルを非表示にし、ブローカー側のストップが見えないよう内部ロジックにのみ公開します。利益は、ユーザーが定義した 3 つのマイルストーンにわたって段階的に保護されます。

戦略ロジック

エントリー条件

  • 長いセットアップ – DeMarker の値が 0.30 のしきい値を超えて上昇すると同時に、Stochastic %K ラインが 20 を超えるまで待ちます。どちらの条件も、前の終了したローソク足から現在のローソク足に状態を変更する必要があります。フラットの場合、この戦略は設定された注文量を使用して市場で購入します。
  • 短いセットアップ – 長いケースのミラー: DeMarker は 0.70 を下回らなければならず、Stochastic %K は 80 を下回る必要があります。シグナルは、前のローソク足が両方の境界の反対側にあった場合にのみ有効です。この戦略は、オープンなポジションがない場合に市場ごとに空売りを行います。

貿易管理

  • 隠れたリスク制限 – 価格が Hidden Stop Loss 距離だけ下落するか、または価格が Hidden Take Profit 距離だけ上昇した場合、ロングポジションはクローズされます。ショートでは、制限が反転された対称条件が使用されます。レベルは実際の逆指値注文を出すことなく内部で監視されます。
  • 段階的なトレーリングストップ – 3 つの利食いチェックポイント (Target TP1Target TP2Target TP3) は、価格が上昇するにつれてストップを厳しくします。ロングの場合、価格がチェックポイントに達すると、ストップはローソク足の高値から (target − 13) ピッップを引いた値まで引き上げられます。ショートの場合、ストップはローソク足の安値プラス (target + 13) ピップスまで下がります。各段階は 1 回だけ適用され、緩めることはありません。
  • トレーリング約定 – 少なくとも 1 つのステージが準備完了した後、トレーリングストップに触れると成行注文によってポジションがクローズされます。

サポートメカニック

  • この戦略は、証券の価格ステップからピップ サイズを自動的に推定し、4/2 桁と 5/3 桁の両方の外国為替シンボルをサポートします。
  • インジケーターの計算とシグナルは、選択可能なローソク足タイプ (デフォルトでは 5 分足ローソク足) によって駆動されます。完成したキャンドルのみが加工されます。

パラメーター

  • 隠れた利益確定 – ピップ単位の隠れた利益確定距離 (デフォルト: 60)。
  • 隠れストップロス – ピップ単位の隠れストップロス距離 (デフォルト: 30)。
  • ターゲット TP1 / TP2 / TP3 – 段階的なトレーリングストップを準備するピップ単位の利益マイルストーン (デフォルト: 203550)。
  • 注文量 – エントリーに使用される成行注文量 (デフォルト: 0.2)。
  • DeMarker Length – DeMarker オシレーターの平均化期間 (デフォルト: 14)。
  • Stochastic 長さ – Stochastic オシレーターのベース ルックバック (デフォルト: 5)。
  • Stochastic %K – %K ラインの平滑化係数 (デフォルト: 3)。
  • Stochastic %D – %D ラインの平滑化係数 (デフォルト: 3)。
  • ローソク足タイプ – インジケーターの計算に使用される時間枠 (デフォルト: 5 分足ローソク足)。

追加の注意事項

  • この戦略は一度に 1 つのポジションのみをオープンし、すぐには反転しません。新しいシグナルが作用する前に、現在の取引が終了するのを待ちます。
  • 保護レベルは市場の出口を通じてコードで強制されるため、オーダーブックに保留中のストップ注文はありません。
  • パッケージには C# 実装のみが含まれています。 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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Cash Machine strategy converted from the MetaTrader 4 expert advisor.
/// Uses DeMarker and Stochastic oscillator crossovers on five minute candles
/// and gradually tightens a hidden stop when profit targets are reached.
/// </summary>
public class CashMachine5minLegacyStrategy : Strategy
{
	private readonly StrategyParam<decimal> _hiddenTakeProfit;
	private readonly StrategyParam<decimal> _hiddenStopLoss;
	private readonly StrategyParam<decimal> _targetTp1;
	private readonly StrategyParam<decimal> _targetTp2;
	private readonly StrategyParam<decimal> _targetTp3;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _deMarkerLength;
	private readonly StrategyParam<int> _stochasticLength;
	private readonly StrategyParam<int> _stochasticK;
	private readonly StrategyParam<int> _stochasticD;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _previousDeMarker;
	private decimal? _previousStochasticK;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private int _longStage;
	private int _shortStage;
	private decimal _pipSize;
	private decimal _entryPrice;

	/// <summary>
	/// Hidden take profit distance expressed in pips.
	/// </summary>
	public decimal HiddenTakeProfit
	{
		get => _hiddenTakeProfit.Value;
		set => _hiddenTakeProfit.Value = value;
	}

	/// <summary>
	/// Hidden stop loss distance expressed in pips.
	/// </summary>
	public decimal HiddenStopLoss
	{
		get => _hiddenStopLoss.Value;
		set => _hiddenStopLoss.Value = value;
	}

	/// <summary>
	/// First profit threshold in pips.
	/// </summary>
	public decimal TargetTp1
	{
		get => _targetTp1.Value;
		set => _targetTp1.Value = value;
	}

	/// <summary>
	/// Second profit threshold in pips.
	/// </summary>
	public decimal TargetTp2
	{
		get => _targetTp2.Value;
		set => _targetTp2.Value = value;
	}

	/// <summary>
	/// Third profit threshold in pips.
	/// </summary>
	public decimal TargetTp3
	{
		get => _targetTp3.Value;
		set => _targetTp3.Value = value;
	}

	/// <summary>
	/// Order volume used when opening new positions.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// DeMarker averaging period.
	/// </summary>
	public int DeMarkerLength
	{
		get => _deMarkerLength.Value;
		set => _deMarkerLength.Value = value;
	}

	/// <summary>
	/// Stochastic oscillator length.
	/// </summary>
	public int StochasticLength
	{
		get => _stochasticLength.Value;
		set => _stochasticLength.Value = value;
	}

	/// <summary>
	/// %K smoothing factor for the Stochastic oscillator.
	/// </summary>
	public int StochasticK
	{
		get => _stochasticK.Value;
		set => _stochasticK.Value = value;
	}

	/// <summary>
	/// %D smoothing factor for the Stochastic oscillator.
	/// </summary>
	public int StochasticD
	{
		get => _stochasticD.Value;
		set => _stochasticD.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="CashMachine5minLegacyStrategy"/> class.
	/// </summary>
	public CashMachine5minLegacyStrategy()
	{
		_hiddenTakeProfit = Param(nameof(HiddenTakeProfit), 60m)
			.SetGreaterThanZero()
			.SetDisplay("Hidden Take Profit", "Hidden take profit distance in pips", "Risk");

		_hiddenStopLoss = Param(nameof(HiddenStopLoss), 30m)
			.SetGreaterThanZero()
			.SetDisplay("Hidden Stop Loss", "Hidden stop loss distance in pips", "Risk");

		_targetTp1 = Param(nameof(TargetTp1), 20m)
			.SetGreaterThanZero()
			.SetDisplay("Target TP1", "First profit threshold", "Risk");

		_targetTp2 = Param(nameof(TargetTp2), 35m)
			.SetGreaterThanZero()
			.SetDisplay("Target TP2", "Second profit threshold", "Risk");

		_targetTp3 = Param(nameof(TargetTp3), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Target TP3", "Third profit threshold", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 0.2m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Order volume for new trades", "Trading");

		_deMarkerLength = Param(nameof(DeMarkerLength), 14)
			.SetGreaterThanZero()
			.SetDisplay("DeMarker Length", "DeMarker averaging period", "Indicators");

		_stochasticLength = Param(nameof(StochasticLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Length", "Base Stochastic length", "Indicators");

		_stochasticK = Param(nameof(StochasticK), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %K", "%K smoothing length", "Indicators");

		_stochasticD = Param(nameof(StochasticD), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %D", "%D smoothing length", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");

		_pipSize = 0.0001m;
		_entryPrice = 0m;
	}

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

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

		_previousDeMarker = null;
		_previousStochasticK = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_longStage = 0;
		_shortStage = 0;
		_pipSize = 0.0001m;
		_entryPrice = 0m;
	}

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

		_pipSize = CalculatePipSize();

		var deMarker = new DeMarker
		{
			Length = DeMarkerLength,
		};

		var stochastic = new StochasticOscillator();
		stochastic.K.Length = StochasticLength;
		stochastic.D.Length = StochasticD;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(deMarker, stochastic, ProcessCandle)
			.Start();

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, subscription);
			DrawIndicator(priceArea, deMarker);

			var oscillatorArea = CreateChartArea();
			if (oscillatorArea != null)
			{
				DrawIndicator(oscillatorArea, stochastic);
			}

			DrawOwnTrades(priceArea);
		}
	}

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

		if (!stochasticValue.IsFinal || !IsFormedAndOnlineAndAllowTrading())
			return;

		var deMarker = deMarkerValue.ToDecimal();
		var stochastic = (StochasticOscillatorValue)stochasticValue;

		if (stochastic.K is not decimal currentK)
			return;

		if (Position == 0)
		{
			// Reset trailing state whenever the strategy is flat.
			_longStage = 0;
			_shortStage = 0;
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}

		if (Position == 0 && _previousDeMarker is decimal prevDe && _previousStochasticK is decimal prevK)
		{
			var longSignal = prevDe < 0.30m && deMarker >= 0.30m && prevK < 20m && currentK >= 20m;
			var shortSignal = prevDe > 0.70m && deMarker <= 0.70m && prevK > 80m && currentK <= 80m;

			if (longSignal && OrderVolume > 0m)
			{
				// Both oscillators crossed up from oversold zones.
				_entryPrice = candle.ClosePrice;
				BuyMarket(OrderVolume);
			}
			else if (shortSignal && OrderVolume > 0m)
			{
				// Both oscillators crossed down from overbought zones.
				_entryPrice = candle.ClosePrice;
				SellMarket(OrderVolume);
			}
		}
		else if (Position > 0)
		{
			ManageLongPosition(candle);
		}
		else if (Position < 0)
		{
			ManageShortPosition(candle);
		}

		_previousDeMarker = deMarker;
		_previousStochasticK = currentK;
	}

	private void ManageLongPosition(ICandleMessage candle)
	{
		var entryPrice = _entryPrice;
		if (entryPrice <= 0m || _pipSize <= 0m)
			return;

		var stopLossPrice = entryPrice - HiddenStopLoss * _pipSize;
		var takeProfitPrice = entryPrice + HiddenTakeProfit * _pipSize;

		// Close long position if the hidden stop or take profit is hit.
		if (candle.LowPrice <= stopLossPrice || candle.HighPrice >= takeProfitPrice)
		{
			SellMarket(Position);
			return;
		}

		var target1 = entryPrice + TargetTp1 * _pipSize;
		var target2 = entryPrice + TargetTp2 * _pipSize;
		var target3 = entryPrice + TargetTp3 * _pipSize;

		if (_longStage < 3 && candle.HighPrice >= target3)
		{
			var newStop = candle.HighPrice - Math.Max(TargetTp3 - 13m, 0m) * _pipSize;
			_longTrailingStop = _longTrailingStop.HasValue ? Math.Max(_longTrailingStop.Value, newStop) : newStop;
			_longStage = 3;
			return;
		}

		if (_longStage < 2 && candle.HighPrice >= target2)
		{
			var newStop = candle.HighPrice - Math.Max(TargetTp2 - 13m, 0m) * _pipSize;
			_longTrailingStop = _longTrailingStop.HasValue ? Math.Max(_longTrailingStop.Value, newStop) : newStop;
			_longStage = 2;
			return;
		}

		if (_longStage < 1 && candle.HighPrice >= target1)
		{
			var newStop = candle.HighPrice - Math.Max(TargetTp1 - 13m, 0m) * _pipSize;
			_longTrailingStop = _longTrailingStop.HasValue ? Math.Max(_longTrailingStop.Value, newStop) : newStop;
			_longStage = 1;
			return;
		}

		// Exit if the trailing stop is touched after at least one target.
		if (_longTrailingStop is decimal trailing && candle.LowPrice <= trailing)
		{
			SellMarket(Position);
		}
	}

	private void ManageShortPosition(ICandleMessage candle)
	{
		var entryPrice = _entryPrice;
		if (entryPrice <= 0m || _pipSize <= 0m)
			return;

		var stopLossPrice = entryPrice + HiddenStopLoss * _pipSize;
		var takeProfitPrice = entryPrice - HiddenTakeProfit * _pipSize;

		// Close short position if hidden protective levels are reached.
		if (candle.HighPrice >= stopLossPrice || candle.LowPrice <= takeProfitPrice)
		{
			BuyMarket(Math.Abs(Position));
			return;
		}

		var target1 = entryPrice - TargetTp1 * _pipSize;
		var target2 = entryPrice - TargetTp2 * _pipSize;
		var target3 = entryPrice - TargetTp3 * _pipSize;

		if (_shortStage < 3 && candle.LowPrice <= target3)
		{
			var newStop = candle.LowPrice + (TargetTp3 + 13m) * _pipSize;
			_shortTrailingStop = _shortTrailingStop.HasValue ? Math.Min(_shortTrailingStop.Value, newStop) : newStop;
			_shortStage = 3;
			return;
		}

		if (_shortStage < 2 && candle.LowPrice <= target2)
		{
			var newStop = candle.LowPrice + (TargetTp2 + 13m) * _pipSize;
			_shortTrailingStop = _shortTrailingStop.HasValue ? Math.Min(_shortTrailingStop.Value, newStop) : newStop;
			_shortStage = 2;
			return;
		}

		if (_shortStage < 1 && candle.LowPrice <= target1)
		{
			var newStop = candle.LowPrice + (TargetTp1 + 13m) * _pipSize;
			_shortTrailingStop = _shortTrailingStop.HasValue ? Math.Min(_shortTrailingStop.Value, newStop) : newStop;
			_shortStage = 1;
			return;
		}

		if (_shortTrailingStop is decimal trailing && candle.HighPrice >= trailing)
		{
			BuyMarket(Math.Abs(Position));
		}
	}

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

		var inverse = (double)(1m / step);
		var digits = (int)Math.Round(Math.Log10(inverse));
		var adjust = (digits == 3 || digits == 5) ? 10m : 1m;

		return step * adjust;
	}
}