在 GitHub 上查看

MFI 退出超卖区并加仓策略

该策略等待资金流量指数(MFI)进入超卖区。当 MFI 回到超卖水平之上时,在当前收盘价下方按固定百分比设置限价买单。如果在设定的 bar 数内未成交,则取消订单。通过 StartProtection 设置止损和止盈。

详情

  • 入场条件
    • MFI 从低于 MfiOversoldLevel 上穿该水平后,在收盘价下 LongEntryPercentage% 放置限价买单。
  • 多/空:仅多头。
  • 出场条件
    • 通过止盈或止损 (ExitGainPercentage, StopLossPercentage) 平仓。
  • 止损:是,使用 StartProtection。
  • 默认值
    • MfiPeriod = 14
    • MfiOversoldLevel = 20
    • LongEntryPercentage = 0.1
    • StopLossPercentage = 1
    • ExitGainPercentage = 1
    • CancelAfterBars = 5
  • 过滤条件
    • 分类:均值回归
    • 方向:多头
    • 指标:MFI
    • 止损:是
    • 复杂度:低
    • 时间框架:任意
    • 季节性:无
    • 神经网络:无
    • 背离:无
    • 风险等级:低
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>
/// Money Flow Index based strategy that enters long when MFI exits the oversold zone.
/// Places a limit order below the close and cancels it after a specified number of bars.
/// Uses StartProtection for stop-loss and take-profit.
/// </summary>
public class MfiWithOversoldZoneExitAndAveragingStrategy : Strategy
{
	private readonly StrategyParam<int> _mfiPeriod;
	private readonly StrategyParam<decimal> _mfiOversoldLevel;
	private readonly StrategyParam<decimal> _longEntryPercentage;
	private readonly StrategyParam<decimal> _stopLossPercentage;
	private readonly StrategyParam<decimal> _exitGainPercentage;
	private readonly StrategyParam<int> _cancelAfterBars;
	private readonly StrategyParam<int> _signalCooldownBars;
	private readonly StrategyParam<DataType> _candleType;
	
	private Order _entryOrder;
	private decimal _longEntryPrice;
	private int _barsSinceEntryOrder;
	private int _barsFromSignal;
	private bool _inOversoldZone;
	
	/// <summary>
	/// Period for MFI calculation.
	/// </summary>
	public int MfiPeriod { get => _mfiPeriod.Value; set => _mfiPeriod.Value = value; }
	
	/// <summary>
	/// Oversold threshold for MFI.
	/// </summary>
	public decimal MfiOversoldLevel { get => _mfiOversoldLevel.Value; set => _mfiOversoldLevel.Value = value; }
	
	/// <summary>
	/// Percentage below close price for limit entry.
	/// </summary>
	public decimal LongEntryPercentage { get => _longEntryPercentage.Value; set => _longEntryPercentage.Value = value; }
	
	/// <summary>
	/// Stop-loss percentage.
	/// </summary>
	public decimal StopLossPercentage { get => _stopLossPercentage.Value; set => _stopLossPercentage.Value = value; }
	
	/// <summary>
	/// Take-profit percentage.
	/// </summary>
	public decimal ExitGainPercentage { get => _exitGainPercentage.Value; set => _exitGainPercentage.Value = value; }
	
	/// <summary>
	/// Number of bars after which unfilled order is canceled.
	/// </summary>
	public int CancelAfterBars { get => _cancelAfterBars.Value; set => _cancelAfterBars.Value = value; }
	
	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	/// <summary>
	/// Minimum bars between new entry orders.
	/// </summary>
	public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
	
	/// <summary>
	/// Initializes a new instance of the <see cref="MfiWithOversoldZoneExitAndAveragingStrategy"/>.
	/// </summary>
	public MfiWithOversoldZoneExitAndAveragingStrategy()
	{
		_mfiPeriod = Param(nameof(MfiPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("MFI Period", "Period for the MFI indicator", "Indicator");
		_mfiOversoldLevel = Param(nameof(MfiOversoldLevel), 20m)
		.SetRange(0m, 50m)
		.SetDisplay("MFI Oversold", "Oversold level for MFI", "Signal");
		_longEntryPercentage = Param(nameof(LongEntryPercentage), 0.1m)
		.SetRange(0m, 5m)
		.SetDisplay("Entry %", "Percent below close for limit entry", "Trading");
		_stopLossPercentage = Param(nameof(StopLossPercentage), 1m)
		.SetRange(0m, 10m)
		.SetDisplay("Stop Loss %", "Stop-loss percentage", "Risk");
		_exitGainPercentage = Param(nameof(ExitGainPercentage), 1m)
		.SetRange(0m, 10m)
		.SetDisplay("Take Profit %", "Take-profit percentage", "Risk");
		_cancelAfterBars = Param(nameof(CancelAfterBars), 5)
		.SetRange(1, 100)
		.SetDisplay("Cancel After Bars", "Bars before canceling limit order", "Trading");
		_signalCooldownBars = Param(nameof(SignalCooldownBars), 20)
		.SetRange(1, 500)
		.SetDisplay("Signal Cooldown Bars", "Minimum bars between entry orders", "Trading");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles to use", "General");
	}
	
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}
	
	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		
		_entryOrder = null;
		_longEntryPrice = 0m;
		_barsSinceEntryOrder = 0;
		_barsFromSignal = 0;
		_inOversoldZone = false;
	}
	
	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		
		var mfi = new MoneyFlowIndex { Length = MfiPeriod };
		
		var subscription = SubscribeCandles(CandleType);
		
		subscription
		.Bind(mfi, ProcessCandle)
		.Start();
		
		StartProtection(
		new Unit(ExitGainPercentage, UnitTypes.Percent),
		new Unit(StopLossPercentage, UnitTypes.Percent)
		);
		
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, mfi);
			DrawOwnTrades(area);
		}
	}
	
	private void ProcessCandle(ICandleMessage candle, decimal mfiValue)
	{
		if (candle.State != CandleStates.Finished)
		return;
		
		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		_barsFromSignal++;
		
		if (mfiValue < MfiOversoldLevel)
		{
			_inOversoldZone = true;
		}
		else if (_inOversoldZone && mfiValue > MfiOversoldLevel && Position == 0 && _entryOrder == null && _barsFromSignal >= SignalCooldownBars)
		{
			_inOversoldZone = false;
			_longEntryPrice = candle.ClosePrice * (1 - LongEntryPercentage / 100m);
			_entryOrder = BuyLimit(_longEntryPrice, Volume);
			_barsSinceEntryOrder = 0;
			_barsFromSignal = 0;
		}
		
		if (_entryOrder != null)
		{
			if (_entryOrder.State == OrderStates.Active)
			{
				_barsSinceEntryOrder++;
				
				if (_barsSinceEntryOrder >= CancelAfterBars && Position == 0)
				{
					CancelOrder(_entryOrder);
					_entryOrder = null;
				}
			}
			else
			{
				_entryOrder = null;
			}
		}
	}
}