Открыть на GitHub

Стратегия внутридневной торговли с управлением рисками

Стратегия открывает длинную позицию, когда быстрая EMA пересекает сверху медленную, а RSI ниже уровня перекупленности. Короткая позиция открывается, когда быстрая EMA пересекает снизу медленную, а RSI выше уровня перепроданности. Управление рисками реализовано через фиксированный процент тейк-профита и стоп-лосса.

Точные торговые правила и защита

По умолчанию: CandleType=1 минута, FastEmaPeriod=20, SlowEmaPeriod=50, RsiLength=14, RsiOverbought=70, RsiOversold=30, TakeProfitPercent=2, StopLossPercent=1. На закрытой свече покупка возможна, если раньше EMA20 <= EMA50, сейчас EMA20 > EMA50 и RSI14 < 70; шорт — если раньше EMA20 >= EMA50, сейчас EMA20 < EMA50 и RSI14 > 30. Из нулевой позиции либо с разворотом существующей противоположной позиции выставляется объём Volume + abs(Position). Без подтверждения RSI пересечение не открывает позицию. Для исполненных позиций действуют нативный тейк-профит 2% и стоп-лосс 1%, без трейлинга, по текущим рыночным событиям, независимо от закрытия свечи. Ранее код содержал только шаблонное пересечение EMA120/450 без RSI и защиты. Числовые defaults не были указаны в исходном README, здесь они заданы явно.

Параметры

  • Тип свечей
  • Длина быстрой EMA
  • Длина медленной EMA
  • Период RSI
  • Уровень перекупленности RSI
  • Уровень перепроданности RSI
  • Процент тейк-профита
  • Процент стоп-лосса
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Intraday EMA cross with an RSI confirmation and native percent take-profit
/// and stop-loss protection on actual executions.
/// </summary>
public class DayTradingStrategy : Strategy
{
    private readonly StrategyParam<int> _fastEmaPeriod;
    private readonly StrategyParam<int> _slowEmaPeriod;
    private readonly StrategyParam<int> _rsiLength;
    private readonly StrategyParam<decimal> _rsiOverbought;
    private readonly StrategyParam<decimal> _rsiOversold;
    private readonly StrategyParam<decimal> _takeProfitPercent;
    private readonly StrategyParam<decimal> _stopLossPercent;
    private readonly StrategyParam<DataType> _candleType;

    private ExponentialMovingAverage _fast;
    private ExponentialMovingAverage _slow;
    private RelativeStrengthIndex _rsi;
    private decimal? _previousFast;
    private decimal? _previousSlow;

    public int FastEmaPeriod { get => _fastEmaPeriod.Value; set => _fastEmaPeriod.Value = value; }
    public int SlowEmaPeriod { get => _slowEmaPeriod.Value; set => _slowEmaPeriod.Value = value; }
    public int RsiLength { get => _rsiLength.Value; set => _rsiLength.Value = value; }
    public decimal RsiOverbought { get => _rsiOverbought.Value; set => _rsiOverbought.Value = value; }
    public decimal RsiOversold { get => _rsiOversold.Value; set => _rsiOversold.Value = value; }
    public decimal TakeProfitPercent { get => _takeProfitPercent.Value; set => _takeProfitPercent.Value = value; }
    public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
    public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

    public DayTradingStrategy()
    {
        _fastEmaPeriod = Param(nameof(FastEmaPeriod), 20)
            .SetGreaterThanZero()
            .SetDisplay("Fast EMA Length", "Fast EMA signal length", "Indicators");
        _slowEmaPeriod = Param(nameof(SlowEmaPeriod), 50)
            .SetGreaterThanZero()
            .SetDisplay("Slow EMA Length", "Slow EMA signal length", "Indicators");
        _rsiLength = Param(nameof(RsiLength), 14)
            .SetGreaterThanZero()
            .SetDisplay("RSI Length", "RSI confirmation length", "Indicators");
        _rsiOverbought = Param(nameof(RsiOverbought), 70m)
            .SetRange(0m, 100m)
            .SetDisplay("RSI Overbought", "Strict upper RSI bound for long entries", "Signals");
        _rsiOversold = Param(nameof(RsiOversold), 30m)
            .SetRange(0m, 100m)
            .SetDisplay("RSI Oversold", "Strict lower RSI bound for short entries", "Signals");
        _takeProfitPercent = Param(nameof(TakeProfitPercent), 2m)
            .SetGreaterThanZero()
            .SetDisplay("Take Profit %", "Native take-profit percent on filled positions", "Risk");
        _stopLossPercent = Param(nameof(StopLossPercent), 1m)
            .SetGreaterThanZero()
            .SetDisplay("Stop Loss %", "Native stop-loss percent on filled positions", "Risk");
        _candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
            .SetDisplay("Candle Type", "Finished candles for EMA and RSI", "General");
    }

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

    protected override void OnReseted()
    {
        base.OnReseted();
        _fast = null;
        _slow = null;
        _rsi = null;
        _previousFast = null;
        _previousSlow = null;
    }

    protected override void OnStarted2(DateTime time)
    {
        base.OnStarted2(time);

        if (FastEmaPeriod >= SlowEmaPeriod)
            throw new ArgumentException("FastEmaPeriod must be less than SlowEmaPeriod.");
        if (RsiOversold >= RsiOverbought)
            throw new ArgumentException("RsiOversold must be less than RsiOverbought.");

        _fast = new ExponentialMovingAverage { Length = FastEmaPeriod };
        _slow = new ExponentialMovingAverage { Length = SlowEmaPeriod };
        _rsi = new RelativeStrengthIndex { Length = RsiLength };
        _previousFast = null;
        _previousSlow = null;

        var subscription = SubscribeCandles(CandleType);
        subscription.Bind(_fast, _slow, _rsi, ProcessCandle).Start();

        var area = CreateChartArea();
        if (area != null)
        {
            DrawCandles(area, subscription);
            DrawIndicator(area, _fast);
            DrawIndicator(area, _slow);
            DrawIndicator(area, _rsi);
            DrawOwnTrades(area);
        }

        StartProtection(
            new Unit(TakeProfitPercent, UnitTypes.Percent),
            new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Start();
		}
    }

    private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow, decimal rsi)
    {
        if (candle.State != CandleStates.Finished)
            return;

        var crossUp = _previousFast is decimal prevFast &&
            _previousSlow is decimal prevSlow &&
            prevFast <= prevSlow && fast > slow;
        var crossDown = _previousFast is decimal prevFastDown &&
            _previousSlow is decimal prevSlowDown &&
            prevFastDown >= prevSlowDown && fast < slow;

        // Always record finished-bar averages, including warmup and periods
        // when signals are not permitted, so the next cross is genuine.
        _previousFast = fast;
        _previousSlow = slow;

        if (!_fast.IsFormed || !_slow.IsFormed || !_rsi.IsFormed ||
            !IsFormedAndOnlineAndAllowTrading())
            return;

        if (crossUp && rsi < RsiOverbought && Position <= 0m)
            BuyMarket(Volume + Math.Abs(Position));
        else if (crossDown && rsi > RsiOversold && Position >= 0m)
            SellMarket(Volume + Math.Abs(Position));
    }
}