在 GitHub 上查看

Cash Machine 5 min Legacy

概述

Cash Machine 5 min Legacy 是将 MetaTrader 4 智能交易系统 CashMachine_5min 迁移到 StockSharp 的版本。策略在五分钟 K 线上结合 DeMarker 指标与快速随机指标的反转信号。当仓位开启后,保护性的止损和止盈仅在策略内部跟踪,不会向经纪商暴露,同时在三个可配置的利润目标上逐步收紧止损。

策略逻辑

入场条件

  • 做多——当上一根已完成 K 线的 DeMarker 低于 0.30 而当前值上穿 0.30,并且随机指标 %K 线同时从 20 下方上穿 20 时触发。仅在当前无持仓时按设置的交易量市价买入。
  • 做空——与做多对称:DeMarker 从 0.70 上方跌破 0.70,且 %K 线从 80 上方跌破 80。只有在上一根 K 线位于阈值另一侧时信号才有效,策略在空仓时市价卖出开空。

仓位管理

  • 隐藏风险阈值——多头在价格下跌到 Hidden Stop Loss 点数或上涨到 Hidden Take Profit 点数时平仓;空头使用镜像条件。这些水平仅在代码中监控,不会发送真实的止损单。
  • 分段式拖尾——三个目标 (Target TP1Target TP2Target TP3) 会在价格到达时上调或下调隐藏止损。多头触发后,将止损抬至当前最高价减去 (target − 13) 点;空头则将止损压至当前最低价加上 (target + 13) 点。每个阶段只执行一次且不会放松。
  • 拖尾执行——一旦任意阶段被激活,只要价格触及拖尾止损就会立即用市价单平仓。

辅助机制

  • 策略会根据交易品种的最小报价步长自动估算“点”的大小,适配 4/2 位和 5/3 位的外汇报价。
  • 指标运算基于所选的蜡烛类型(默认五分钟),仅处理已完成的 K 线。

参数

  • Hidden Take Profit——隐藏的止盈距离(点,默认 60)。
  • Hidden Stop Loss——隐藏的止损距离(点,默认 30)。
  • Target TP1 / TP2 / TP3——触发拖尾的利润目标(点,默认 203550)。
  • Order Volume——开仓所使用的市价单手数(默认 0.2)。
  • DeMarker Length——DeMarker 指标的计算周期(默认 14)。
  • Stochastic Length——随机指标的基础周期(默认 5)。
  • Stochastic %K——%K 线平滑系数(默认 3)。
  • Stochastic %D——%D 线平滑系数(默认 3)。
  • Candle Type——用于计算的 K 线类型(默认五分钟)。

其他说明

  • 策略一次只持有一个方向的仓位,不会立即反手,必须等待当前仓位平仓后才会响应下一个信号。
  • 风险控制通过代码内的市价平仓完成,盘口中不会出现真实的止损挂单。
  • 本包仅提供 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;
	}
}