在 GitHub 上查看

Reduce Risks 策略

概述

Reduce Risks 策略源自 MetaTrader 专家顾问“Reduce_risks.mq5”,并移植到 StockSharp 高级 API。策略以 1 分钟蜡烛图为核心,辅以 15 分钟和 1 小时的趋势过滤,旨在在波动较低、结构明确的阶段参与趋势行情。推荐应用于流动性充足、点值明确的外汇品种,例如 EURUSD、USDCHF、USDJPY。

时间框架与适用市场

  • 主周期: 1 分钟 K 线用于触发交易。
  • 确认周期: 15 分钟 K 线用于判断波段位置。
  • 趋势过滤: 1 小时 K 线验证更大级别方向。
  • 推荐市场: 点值与主流外汇类似的品种,必要时需调整点值参数。

指标与数据

  • M1:SMA(5)、SMA(8)、SMA(13)、SMA(60),按典型价格 (高+低+收)/3 计算。
  • M15:SMA(4)、SMA(5)、SMA(8) ,同样使用典型价格。
  • H1:SMA(24) 作为趋势均线。
  • 蜡烛结构统计:包括实体长度、影线、区间等。
  • 记录入场后的最高价/最低价,用于实现原始 MQL 策略的回撤退出逻辑。

入场条件

做多

  1. M1、M15 最近三根蜡烛的波动必须低于 20/30 点,且 M15 近三根的整体区间不超过 30 点。
  2. 当前价格突破前一根 M1 与 M15 的高点,同时前一根 M1 蜡烛的区间大于 1.1 倍但小于 3 倍倒数第二根蜡烛。
  3. 均线层级呈多头排列:SMA5 > SMA8 > SMA13,SMA60 上升,收盘价位于全部均线之上。
  4. M15 的 SMA4 上升且高于 SMA8;当前价格高于 SMA4(M15) 及 SMA24(H1)。
  5. 波段确认:SMA8(M1) 在过去三根任意一根蜡烛范围内交叉,SMA5(M15) 位于上一根 M15 蜡烛范围内。
  6. 结构过滤:上一根 M1、M15 蜡烛实体大于整体区间一半,并形成更高的高点,回调不超过区间的 25%,且存在影线。
  7. 所有条件同时满足且无持仓时,执行市价买入。

做空

条件与做多完全对称:价格向下突破支撑,均线呈空头排列 (SMA5 < SMA8 < SMA13,SMA60 下降),价格低于所有均线,上一根 M1、M15 蜡烛显示出明显的空头结构 (更低的低点、实体大、回调小、有影线)。满足条件后执行市价卖出。

平仓规则

  • StopLossPips 与 TakeProfitPips 对应的保护性止损/止盈通过 StartProtection 自动下达。
  • 额外的退出逻辑复刻原策略:
    • 做多:若当前 M1 蜡烛从开盘价下跌 ≥10 点,或开仓超过 1 分钟后出现强烈下跌蜡烛,则平仓。
    • 做多:盈利 ≥10 点时可提前获利;若出现从入场后最高价回撤 ≥20 点且该高点高于入场价,也会触发退出。
    • 做多:若浮亏 ≥20 点,或权益跌破风险阈值,也立即平仓。做空规则取镜像处理。

风险管理

  • 当投资组合权益 ≤ InitialDeposit * (1 - RiskPercent/100) 时,策略会阻止新的入场,并在达到阈值时强制平掉持仓。
  • 原版中关于终端连接、交易权限的检测在 StockSharp 中无需重复实现。

参数

参数 说明 默认值
StopLossPips 止损点数。 30
TakeProfitPips 止盈点数。 60
InitialDeposit 用于计算风险阈值的基准资金。 10000
RiskPercent 相对于基准资金的最大允许回撤百分比。 5
M1CandleType 主周期 M1 数据类型。 1 分钟
M15CandleType 15 分钟确认周期的数据类型。 15 分钟
H1CandleType 1 小时趋势过滤的数据类型。 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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Trend-following strategy converted from the "Reduce risks" MQL5 expert.
/// Uses SMA hierarchy (short/medium/long) for trend detection with risk control exits.
/// Enters on confirmed SMA crossover, exits on reverse cross or stop/take profit.
/// </summary>
public class ReduceRisksStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<decimal> _initialDeposit;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _smaShort;
	private SimpleMovingAverage _smaMedium;
	private SimpleMovingAverage _smaLong;

	private decimal? _smaShortCurr;
	private decimal? _smaShortPrev;
	private decimal? _smaMediumCurr;
	private decimal? _smaMediumPrev;
	private decimal? _smaLongCurr;
	private decimal? _smaLongPrev;

	private decimal _riskThreshold;
	private int _riskExceededCounter;
	private int _barsSinceEntry;
	private decimal _entryPrice;
	private int _barsShortAboveMedium;
	private int _barsShortBelowMedium;
	private bool _enteredLong;
	private bool _enteredShort;

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

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

	/// <summary>
	/// Reference initial deposit used for equity based risk limitation.
	/// </summary>
	public decimal InitialDeposit
	{
		get => _initialDeposit.Value;
		set => _initialDeposit.Value = value;
	}

	/// <summary>
	/// Percentage of the initial deposit allowed to be lost before new entries are blocked.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Candle timeframe for trading.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public ReduceRisksStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 30)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Protective stop distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 60)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Target distance in pips", "Risk");

		_initialDeposit = Param(nameof(InitialDeposit), 1000000m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Deposit", "Reference equity for drawdown protection", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetRange(0m, 100m)
			.SetDisplay("Risk Percent", "Maximum loss allowed relative to the initial deposit", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Timeframe", "Trading timeframe", "Timeframes");
	}

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

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

		_smaShort = null;
		_smaMedium = null;
		_smaLong = null;

		_smaShortCurr = null;
		_smaShortPrev = null;
		_smaMediumCurr = null;
		_smaMediumPrev = null;
		_smaLongCurr = null;
		_smaLongPrev = null;

		_riskThreshold = 0m;
		_riskExceededCounter = 0;
		_barsSinceEntry = 0;
		_entryPrice = 0m;
		_barsShortAboveMedium = 0;
		_barsShortBelowMedium = 0;
		_enteredLong = false;
		_enteredShort = false;
	}

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

		_riskThreshold = InitialDeposit * (100m - RiskPercent) / 100m;

		// SMA periods: ~2h / ~6h / ~12h on 5-min candles
		_smaShort = new SimpleMovingAverage { Length = 24 };
		_smaMedium = new SimpleMovingAverage { Length = 72 };
		_smaLong = new SimpleMovingAverage { Length = 144 };

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _smaShort);
			DrawIndicator(area, _smaMedium);
			DrawIndicator(area, _smaLong);
			DrawOwnTrades(area);
		}
	}

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

		if (_smaShort is null || _smaMedium is null || _smaLong is null)
			return;

		var typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;

		UpdateSma(_smaShort, typical, candle.OpenTime, ref _smaShortCurr, ref _smaShortPrev);
		UpdateSma(_smaMedium, typical, candle.OpenTime, ref _smaMediumCurr, ref _smaMediumPrev);
		UpdateSma(_smaLong, typical, candle.OpenTime, ref _smaLongCurr, ref _smaLongPrev);

		if (_smaShortCurr is not decimal smaS ||
			_smaMediumCurr is not decimal smaM ||
			_smaLongCurr is not decimal smaL)
			return;

		// Track consecutive bars of SMA position
		if (smaS > smaM)
		{
			_barsShortAboveMedium++;
			_barsShortBelowMedium = 0;
		}
		else
		{
			_barsShortBelowMedium++;
			_barsShortAboveMedium = 0;
		}

		// Risk check
		var equity = Portfolio?.CurrentValue ?? InitialDeposit;
		var riskExceeded = equity <= _riskThreshold && InitialDeposit > 0m;

		if (riskExceeded)
		{
			if (_riskExceededCounter < 15)
			{
				LogWarning("Entry blocked. Risk limit of {0}% reached (equity={1:0.##}).", RiskPercent, equity);
				_riskExceededCounter++;
			}
		}
		else
		{
			_riskExceededCounter = 0;
		}

		// When SMA crosses in opposite direction, allow new entry of that type
		if (_barsShortBelowMedium >= 72)
			_enteredLong = false;
		if (_barsShortAboveMedium >= 72)
			_enteredShort = false;

		if (Position == 0 && !riskExceeded)
		{
			// LONG: short crosses above medium, not already entered on this cross
			if (_barsShortAboveMedium == 1 && candle.ClosePrice > smaS && !_enteredLong)
			{
				BuyMarket();
				_barsSinceEntry = 0;
				_enteredLong = true;
			}
			// SHORT: short crosses below medium, not already entered on this cross
			else if (_barsShortBelowMedium == 1 && candle.ClosePrice < smaS && !_enteredShort)
			{
				SellMarket();
				_barsSinceEntry = 0;
				_enteredShort = true;
			}
		}
		else if (Position != 0)
		{
			_barsSinceEntry++;

			if (Position > 0)
			{
				var entryPrice = _entryPrice;
				// Exit on reverse cross after min hold
				var reverseCross = _barsShortBelowMedium >= 3 && _barsSinceEntry >= 30;
				// Stop loss: 4%
				var stopLoss = entryPrice > 0 && candle.ClosePrice < entryPrice * 0.96m;
				// Take profit: 6%
				var takeProfit = entryPrice > 0 && candle.ClosePrice > entryPrice * 1.06m;

				if (reverseCross || stopLoss || takeProfit || riskExceeded)
				{
					SellMarket(Position.Abs());
				}
			}
			else if (Position < 0)
			{
				var entryPrice = _entryPrice;
				// Exit on reverse cross after min hold
				var reverseCross = _barsShortAboveMedium >= 3 && _barsSinceEntry >= 30;
				// Stop loss: 4%
				var stopLoss = entryPrice > 0 && candle.ClosePrice > entryPrice * 1.04m;
				// Take profit: 6%
				var takeProfit = entryPrice > 0 && candle.ClosePrice < entryPrice * 0.94m;

				if (reverseCross || stopLoss || takeProfit || riskExceeded)
				{
					BuyMarket(Position.Abs());
				}
			}
		}

		if (Position == 0)
		{
			_entryPrice = 0m;
			_barsSinceEntry = 0;
		}
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
	}

	private void UpdateSma(SimpleMovingAverage sma, decimal input, DateTimeOffset time, ref decimal? curr, ref decimal? prev)
	{
		var indicatorValue = sma.Process(new DecimalIndicatorValue(sma, input, time.UtcDateTime) { IsFinal = true });
		if (!sma.IsFormed || indicatorValue is not DecimalIndicatorValue decimalValue)
			return;

		prev = curr;
		curr = decimalValue.Value;
	}
}