在 GitHub 上查看

DNSE VN301 SMA 与 EMA 交叉策略

该策略通过15周期EMA与60周期SMA的交叉交易VN301指数,并在收盘前平仓,采用百分比止损限制亏损。

测试显示年化收益约为20%,在VN30期货上表现最佳。

当EMA15上穿SMA60且价格位于EMA之上时做多;下穿且价格在EMA之下时做空。仓位在反向信号、达到最大亏损或临近收盘时平仓。

细节

  • 入场条件
    • 多头:EMA15上穿SMA60且价格≥EMA15,并且未到截止时间。
    • 空头:EMA15下穿SMA60且价格≤EMA15。
  • 方向:双向。
  • 出场条件
    • 反向交叉、最大亏损或收盘截止。
  • 止损:是,按百分比设定。
  • 过滤器
    • 交易时段截止。
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>
/// SMA and EMA crossover strategy for VN301 index.
/// </summary>
public class DnseVn301SmaEmaCrossStrategy : Strategy
{
	private readonly StrategyParam<int> _sessionCloseHour;
	private readonly StrategyParam<int> _sessionCloseMinute;
	private readonly StrategyParam<int> _minutesBeforeClose;
	private readonly StrategyParam<decimal> _maxLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private decimal _prevEma15;
	private decimal _prevSma60;

	public int SessionCloseHour { get => _sessionCloseHour.Value; set => _sessionCloseHour.Value = value; }
	public int SessionCloseMinute { get => _sessionCloseMinute.Value; set => _sessionCloseMinute.Value = value; }
	public int MinutesBeforeClose { get => _minutesBeforeClose.Value; set => _minutesBeforeClose.Value = value; }
	public decimal MaxLossPercent { get => _maxLossPercent.Value; set => _maxLossPercent.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public DnseVn301SmaEmaCrossStrategy()
	{
	    _sessionCloseHour = Param(nameof(SessionCloseHour), 14)
	        .SetDisplay("Close Hour", "Session close hour", "General");

	    _sessionCloseMinute = Param(nameof(SessionCloseMinute), 30)
	        .SetDisplay("Close Minute", "Session close minute", "General");

	    _minutesBeforeClose = Param(nameof(MinutesBeforeClose), 5)
	        .SetDisplay("Minutes Before Close", "Exit minutes before close", "General");

	    _maxLossPercent = Param(nameof(MaxLossPercent), 2m)
	        .SetGreaterThanZero()
	        .SetDisplay("Max Loss %", "Stop loss percentage", "Risk");

	    _candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
	        .SetDisplay("Candle Type", "Type of candles", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0;
		_prevEma15 = 0;
		_prevSma60 = 0;
	}

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

	    var ema15 = new ExponentialMovingAverage { Length = 15 };
	    var sma60 = new SimpleMovingAverage { Length = 60 };

	    var subscription = SubscribeCandles(CandleType);
	    subscription.Bind(ema15, sma60, ProcessCandle).Start();
	}

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

	    var crossUp = ema15 > sma60 && _prevEma15 <= _prevSma60;
	    var crossDown = ema15 < sma60 && _prevEma15 >= _prevSma60;
	    _prevEma15 = ema15;
	    _prevSma60 = sma60;

	    if (crossUp && Position <= 0)
	    {
	        BuyMarket();
	        _entryPrice = candle.ClosePrice;
	    }
	    else if (crossDown && Position >= 0)
	    {
	        SellMarket();
	        _entryPrice = candle.ClosePrice;
	    }

	    if (Position > 0)
	    {
	        if (crossDown || candle.ClosePrice <= _entryPrice * (1 - MaxLossPercent / 100m))
	            SellMarket();
	    }
	    else if (Position < 0)
	    {
	        if (crossUp || candle.ClosePrice >= _entryPrice * (1 + MaxLossPercent / 100m))
	            BuyMarket();
	    }
	}

	private void ClosePosition()
	{
	    if (Position > 0)
	        SellMarket();
	    else if (Position < 0)
	        BuyMarket();
	}
}