Открыть на GitHub

Стратегия Dema RSI

Стратегия торгует пересечения RSI двойной экспоненциальной скользящей средней и её сглаженного значения. Длинная позиция открывается при пересечении RSI выше сглаженной линии, короткая – при обратном пересечении. Позиции защищаются тейк‑профитом, трейлинг‑стопом и опциональным фильтром торговой сессии.

Параметры

  • Тип свечей
  • Длина MA
  • Длина RSI
  • Длина сглаживания RSI
  • Тейк-профит в пунктах
  • Трейлинг-стоп в пунктах
  • Использовать сессию
  • Начало сессии
  • Конец сессии

Исполняемый контракт RSI(DEMA)

Значения по умолчанию: CandleType=1m, MaLength=20, RsiLength=14, RsiSmoothingLength=5, TakeProfitPoints=0, TrailStopPoints=0, UseSession=false, SessionStart=09:00, SessionEnd=17:00. По завершённой свече вычисляем EMA1=EMA(Close,20), EMA2=EMA(EMA1,20), DEMA=2×EMA1−EMA2, R=RSI(DEMA,14), S=SMA(R,5). Покупка при предыдущем R <= S и текущем R > S из нуля или шорта; продажа при предыдущем R >= S и текущем R < S из нуля или лонга. Разворотный объём — Volume + abs(Position). При UseSession=true входы разрешены в интервале [SessionStart,SessionEnd) (поддерживается переход через полночь); выходы по защите не зависят от расписания. TakeProfitPoints и TrailStopPoints — минимальные шаги цены инструмента, а не проценты. Ненулевые значения включают нативный тейк-профит и трейлинг-стоп с абсолютным расстоянием, нуль отключает соответствующую защиту.

Фильтр сессии использует текущее время принятия решения в UTC: открытие сигнальной свечи внутри сессии не разрешает вход после её окончания. Пустые значения 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>
/// Trade strict crossings of RSI computed from a double exponential moving
/// average (DEMA) and its own SMA, with optional session and point protections.
/// </summary>
public class DemaRsiStrategy : Strategy
{
    private readonly StrategyParam<int> _maLength;
    private readonly StrategyParam<int> _rsiLength;
    private readonly StrategyParam<int> _rsiSmoothingLength;
    private readonly StrategyParam<decimal> _takeProfitPoints;
    private readonly StrategyParam<decimal> _trailStopPoints;
    private readonly StrategyParam<bool> _useSession;
    private readonly StrategyParam<TimeSpan> _sessionStart;
    private readonly StrategyParam<TimeSpan> _sessionEnd;
    private readonly StrategyParam<DataType> _candleType;

    private ExponentialMovingAverage _ema1;
    private ExponentialMovingAverage _ema2;
    private RelativeStrengthIndex _rsi;
    private SimpleMovingAverage _smoothRsi;
    private decimal? _previousRsi;
    private decimal? _previousSmooth;

    public int MaLength { get => _maLength.Value; set => _maLength.Value = value; }
    public int RsiLength { get => _rsiLength.Value; set => _rsiLength.Value = value; }
    public int RsiSmoothingLength { get => _rsiSmoothingLength.Value; set => _rsiSmoothingLength.Value = value; }
    public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
    public decimal TrailStopPoints { get => _trailStopPoints.Value; set => _trailStopPoints.Value = value; }
    public bool UseSession { get => _useSession.Value; set => _useSession.Value = value; }
    public TimeSpan SessionStart { get => _sessionStart.Value; set => _sessionStart.Value = value; }
    public TimeSpan SessionEnd { get => _sessionEnd.Value; set => _sessionEnd.Value = value; }
    public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

    public DemaRsiStrategy()
    {
        _maLength = Param(nameof(MaLength), 20)
            .SetGreaterThanZero().SetDisplay("MA Length", "DEMA first/second EMA length", "Indicators");
        _rsiLength = Param(nameof(RsiLength), 14)
            .SetGreaterThanZero().SetDisplay("RSI Length", "RSI of DEMA", "Indicators");
        _rsiSmoothingLength = Param(nameof(RsiSmoothingLength), 5)
            .SetGreaterThanZero().SetDisplay("RSI Smoothing Length", "SMA of RSI(DEMA)", "Indicators");
        _takeProfitPoints = Param(nameof(TakeProfitPoints), 0m)
            .SetRange(0m, 100000m).SetDisplay("Take Profit Points", "Take profit in security price ticks; zero disables", "Risk");
        _trailStopPoints = Param(nameof(TrailStopPoints), 0m)
            .SetRange(0m, 100000m).SetDisplay("Trail Stop Points", "Trailing stop in security price ticks; zero disables", "Risk");
        _useSession = Param(nameof(UseSession), false)
            .SetDisplay("Use Session", "Restrict signal entries to a time window", "Calendar");
        _sessionStart = Param(nameof(SessionStart), TimeSpan.FromHours(9))
            .SetDisplay("Session Start", "Inclusive start of entry session", "Calendar");
        _sessionEnd = Param(nameof(SessionEnd), TimeSpan.FromHours(17))
            .SetDisplay("Session End", "Exclusive end of entry session", "Calendar");
        _candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
            .SetDisplay("Candle Type", "Completed candle interval", "General");
    }

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

    protected override void OnReseted()
    {
        base.OnReseted();
        _ema1 = null;
        _ema2 = null;
        _rsi = null;
        _smoothRsi = null;
        _previousRsi = null;
        _previousSmooth = null;
    }

    protected override void OnStarted2(DateTime time)
    {
        base.OnStarted2(time);
        if (SessionStart < TimeSpan.Zero || SessionStart >= TimeSpan.FromDays(1) ||
            SessionEnd < TimeSpan.Zero || SessionEnd >= TimeSpan.FromDays(1))
            throw new ArgumentException("Session times must be within one day.");

        _ema1 = new ExponentialMovingAverage { Length = MaLength };
        _ema2 = new ExponentialMovingAverage { Length = MaLength };
        _rsi = new RelativeStrengthIndex { Length = RsiLength };
        _smoothRsi = new SimpleMovingAverage { Length = RsiSmoothingLength };
        _previousRsi = null;
        _previousSmooth = null;

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

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

        // Protection uses ABSOLUTE price distance in minimum price ticks, not percent.
        var tick = Security?.PriceStep ?? 0m;
        if (tick > 0m && (TakeProfitPoints > 0m || TrailStopPoints > 0m))
        {
            StartProtection(
                new Unit(TakeProfitPoints * tick, UnitTypes.Absolute),
                new Unit(TrailStopPoints * tick, UnitTypes.Absolute),
                TrailStopPoints > 0m);
        }
    }

    private bool IsInSession(TimeSpan time)
    {
        if (!UseSession || SessionStart == SessionEnd)
            return true;

        return SessionStart < SessionEnd
            ? time >= SessionStart && time < SessionEnd
            : time >= SessionStart || time < SessionEnd;
    }

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

        var secondEma = _ema2.Process(firstEma, candle.OpenTime, true).ToDecimal();
        var dema = 2m * firstEma - secondEma;
        var rsiValue = _rsi.Process(dema, candle.OpenTime, true);
        if (rsiValue.IsEmpty)
            return;
        var rsi = rsiValue.ToDecimal();
        var smoothed = _smoothRsi.Process(rsi, candle.OpenTime, true).ToDecimal();

        var crossUp = _previousRsi is decimal pr && _previousSmooth is decimal ps &&
            pr <= ps && rsi > smoothed;
        var crossDown = _previousRsi is decimal qr && _previousSmooth is decimal qs &&
            qr >= qs && rsi < smoothed;

        _previousRsi = rsi;
        _previousSmooth = smoothed;

        if (!_ema1.IsFormed || !_ema2.IsFormed || !_rsi.IsFormed || !_smoothRsi.IsFormed ||
            !IsFormedAndOnlineAndAllowTrading() || !IsInSession(CurrentTime.TimeOfDay))
            return;

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