Открыть на GitHub

Стратегия Tengri (порт на StockSharp)

Данный проект переносит советник MetaTrader Tengri на высокоуровневый API StockSharp. Оригинальный советник торгует EURUSD и USDCHF, комбинируя RSI, фирменный индикатор «Silence» и EMA-фильтр тренда, а также использует сеточную схему наращивания позиций. Перенос сохраняет логику, но адаптирован к учёту нетто-позиций и инфраструктуре S#.

Основные идеи

  • Определение направления – сравнивается текущая цена bid и цена открытия свечи старшего таймфрейма (по умолчанию 30 минут). Если bid выше открытия – ищем покупки, если ниже – продажи.
  • Фильтр импульса – RSI c периодом 14 на часовом графике. Для покупок значение должно быть ниже 70, для продаж выше 30, как и в MQL-версии.
  • Фильтры «тишины» – проприетарный индикатор заменён на ATR, сглаженный EMA, для двух различных таймфреймов. Оба значения должны находиться ниже заданных порогов, чтобы разрешить вход или доливку.
  • Подтверждение тренда – EMA среднего таймфрейма не позволяет усредняться против тенденции: лонги добавляются только при цене выше EMA, шорты – ниже EMA.
  • Сеточное наращивание – первый ордер выставляется фиксированным лотом либо пропорционально капиталу. Каждый следующий ордер умножает предыдущий объём на заданные коэффициенты (по умолчанию 1.70 до StepX и 2.08 после).
  • Расстояние между усреднениями – использует два базовых шага в пунктах (10 и 20) и показатель PipStepExponent для экспоненциального роста дистанции.

Логика торговли

  1. Проверка входа (по EntryCandleType, по умолчанию M1):
    • Получить направление из свечи DealCandleType.
    • Проверить RSI и первый фильтр «тишины».
    • Убедиться, что нет активных ордеров в том же направлении (при наличии противоположной позиции она сначала закрывается, поскольку StockSharp ведёт нетто-учёт).
    • Открыть рынок с рассчитанным объёмом и сохранить цель по тейк-профиту для первого ордера.
  2. Проверка усреднения (по ScaleCandleType, по умолчанию M1):
    • Убедиться, что EMA поддерживает текущий тренд, и второй фильтр «тишины» находится ниже порога.
    • Проверить, что цена ушла от последнего входа на требуемое расстояние в пунктах.
    • Добавить позицию с мартингейльным объёмом, если не превышено ограничение MaxTrades.
  3. Сопровождение:
    • Опциональный глобальный лимит прибыли (UseLimit) закрывает позицию, когда суммы плавающих PnL по лонгам и шортам превышают Equity / LimitDivisor.
    • Хранится цель по тейк-профиту для первого ордера; при достижении этого уровня вся позиция закрывается.
    • Стоп-лосс не используется, как и в исходном советнике.

Параметры

Параметр Назначение
DealCandleType Таймфрейм для сравнения цены открытия и текущего bid.
EntryCandleType Таймфрейм проверки сигналов на вход.
ScaleCandleType Таймфрейм проверки доливки.
MaCandleType Таймфрейм EMA-тренда.
Silence1CandleType / Silence2CandleType Таймфреймы ATR-фильтров.
RsiPeriod Период RSI (по умолчанию 14).
SilencePeriod1/2, SilenceInterpolation1/2, SilenceLevel1/2 Настройки ATR-сглаживания и порогов.
MaPeriod Период EMA.
PipStep, PipStep2, PipStepExponent Расстояния между усреднениями.
LotExponent1, LotExponent2, StepX Коэффициенты увеличения объёма.
LotSize, FixLot, LotStep Денежный менеджмент первого входа.
SlTpPips Тейк-профит в пунктах для первого ордера (0 — отключено).
MaxTrades Максимальное число ордеров в одном направлении.
UseLimit, LimitDivisor Глобальный лимит прибыли.
CloseFriday, CloseFridayHour Фильтр на поздние пятничные сделки.

Отличия от MQL-версии

  • Замена Silence – вместо оригинального индикатора используется ATR с EMA-сглаживанием. Пороговые значения оставлены такими же, но при необходимости их можно перенастроить.
  • Нетто-позиция – StockSharp агрегирует позицию, поэтому стратегия закрывает встречные ордера перед открытием новой серии, вместо «хеджирования» двух направлений одновременно.
  • Тейк-профит – в MetaTrader он задаётся только первому ордеру. В порте при достижении цели закрывается весь нетто-объём, а доливки остаются без TP, как и в оригинале.
  • Выбор инструмента – стратегия работает с тем Security, который установлен в экземпляре. Для торговли несколькими инструментами запускайте отдельные копии стратегии.

Рекомендации по эксплуатации

  • Проверьте шаг объёма (VolumeStep) и минимальный/максимальный объём инструмента, чтобы нормализация лота соответствовала требованиям брокера.
  • Для корректной работы необходим поток стакана (bid/ask) – без Level1 данных невозможно определять направление и закрывать тейк-профит.
  • Отсутствие стоп-лосса предполагает внешний контроль риска (например, ограничения по капиталу или ручной мониторинг).

Визуализация

Подключите графики с используемыми таймфреймами, добавьте EMA и отображение ATR, чтобы оценивать работу фильтров и сетку ордеров. Это приближает отладку к той, что использовалась в MetaTrader.

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>
/// Tengri strategy using WMA crossover with EMA trend filter.
/// Buys when fast WMA crosses above slow WMA and price above EMA.
/// Sells on reverse conditions.
/// </summary>
public class TengriStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private WeightedMovingAverage _fast;
	private WeightedMovingAverage _slow;
	private ExponentialMovingAverage _ema;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public TengriStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 15).SetGreaterThanZero().SetDisplay("Fast Period", "Fast WMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 60).SetGreaterThanZero().SetDisplay("Slow Period", "Slow WMA period", "Indicator");
		_emaPeriod = Param(nameof(EmaPeriod), 200).SetGreaterThanZero().SetDisplay("EMA Period", "Trend EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null; _ema = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new WeightedMovingAverage { Length = FastPeriod };
		_slow = new WeightedMovingAverage { Length = SlowPeriod };
		_ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, _ema, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal emaValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed || !_ema.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && close > emaValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 80; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && close < emaValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 80; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}