Открыть на GitHub

Стратегия Testinator

Обзор

Стратегия представляет собой C# порт советника MetaTrader Testinator v1.30a. Она работает только с длинными позициями и управляет ими как корзиной. Новая покупка разрешается лишь тогда, когда все включённые фильтры дают сигнал «истина», а цена продвинулась минимум на заданное число пунктов. Логика выхода симметрична входу и основана на другой маске фильтров. Оригинал использует дневной ATR для расчёта защитных уровней, поэтому порт подписывается как на основной таймфрейм, так и на дневные свечи.

Торговая логика

Маска входа (BuySequence)

Нижние девять битов задают список проверок. Каждый активный бит должен выполнить соответствующее условие на предыдущей завершённой свече.

Бит Условие
1 EMA(12) выше SMA(14).
2 EMA(50) ниже минимумов трёх последних свечей.
4 Минимум предыдущей свечи ниже нижней полосы Боллинджера (20, 2).
8 ADX(14) выше -DI, а +DI сильнее -DI.
16 Стохастик (16, 4, 8): %K выше %D и %D > 80.
32 Williams %R(14) выше -20.
64 Линия MACD(12, 26, 9) выше сигнальной линии.
128 Ichimoku: Senkou Span A выше Span B, Tenkan выше Kijun, минимум предыдущей свечи выше Span A.
256 RSI с периодом RsiEntryPeriod выше RsiEntryLevel и растёт относительно предыдущего значения.

Маска выхода (CloseBuySequence)

Бит Условие
1 SMA(14) выше EMA(12).
2 EMA(50) выше максимумов трёх последних свечей.
4 Максимум предыдущей свечи выше верхней полосы выхода (BollingerCloseLength, BollingerCloseDeviation).
8 -DI выше +DI.
16 Стохастик %D ниже 80.
32 Williams %R(14) ниже -80.
64 Линия MACD ниже сигнальной.
128 Ichimoku: Senkou Span B выше Span A.
256 RSI (период RsiClosePeriod) ниже RsiCloseLevel.

Корзина увеличивается только если все активные биты входа истинны, число покупок меньше MaxBuys, а цена ушла от последней заявки минимум на StepPips пунктов. Закрытие происходит при выполнении маски выхода или при срабатывании защитных уровней.

Торговая сессия и риск-менеджмент

  • Торговля разрешена только в интервале TradeStartHourTradeStartHour + TradeDurationHours - 1 (время EET). Если окно закрыто и корзина в плюсе, все покупки закрываются.
  • Параметры TakeProfitPips и StopLossPips задаются в пунктах. Значение -1 отключает уровень, 0 включает ATR-множители (TakeRatio, StopRatio).
  • Параметры StartTrailPips, TrailStepPips и соответствующие коэффициенты управляют трейлинг-стопом, также через ATR.
  • Для вычисления ATR(15) подписывается дневной таймфрейм, что сохраняет поведение оригинального советника.

Параметры

  • TradeVolume – объём каждой рыночной покупки.
  • BuySequence / CloseBuySequence – маски включённых фильтров.
  • MaxBuys – максимальное число одновременно открытых покупок.
  • StepPips – минимальный прогресс цены в пунктах между добавлениями к позиции.
  • TradeStartHour, TradeDurationHours – границы торгового окна.
  • TakeProfitPips, StopLossPips – фиксированный тейк-профит и стоп-лосс (отрицательное значение отключает, ноль использует ATR).
  • StartTrailPips, TrailStepPips – старт и шаг трейлинг-стопа (аналогично, отрицательное отключает, ноль использует ATR).
  • TakeRatio, StopRatio, StartTrailRatio, TrailStepRatio – коэффициенты ATR, применяемые когда фиксированное значение равно нулю.
  • RsiEntryLevel, RsiEntryPeriod – порог и период RSI для маски входа.
  • RsiCloseLevel, RsiClosePeriod – порог и период RSI для маски выхода.
  • BollingerCloseLength, BollingerCloseDeviation – параметры выходных полос Боллинджера.
  • CandleType – основной тип свечей (дневные свечи подписываются автоматически для ATR).

Примечания

  • Логика корзины полностью повторяет оригинал: открываются только рыночные покупки.
  • Предыдущие значения индикаторов сохраняются явно, чтобы воспроизвести обращения вида bar[1] из MetaTrader.
  • Неиспользуемые параметры оригинального советника (TakeAsBasket, StopAsBasket и т. п.) опущены, поскольку они не влияли на расчёты.
namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Testinator strategy: RSI-based entry with EMA trend filter.
/// Buys when RSI rises above threshold and price is above EMA.
/// Sells when RSI drops below threshold and price is below EMA.
/// </summary>
public class TestinatorStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<decimal> _rsiBuyLevel;
	private readonly StrategyParam<decimal> _rsiSellLevel;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int RsiPeriod { get => _rsiPeriod.Value; set => _rsiPeriod.Value = value; }
	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
	public decimal RsiBuyLevel { get => _rsiBuyLevel.Value; set => _rsiBuyLevel.Value = value; }
	public decimal RsiSellLevel { get => _rsiSellLevel.Value; set => _rsiSellLevel.Value = value; }

	public TestinatorStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI period", "Indicators");
		_emaPeriod = Param(nameof(EmaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "EMA trend filter period", "Indicators");
		_rsiBuyLevel = Param(nameof(RsiBuyLevel), 55m)
			.SetDisplay("RSI Buy Level", "RSI threshold for buy signal", "Signals");
		_rsiSellLevel = Param(nameof(RsiSellLevel), 45m)
			.SetDisplay("RSI Sell Level", "RSI threshold for sell signal", "Signals");
	}

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

		var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		var ema = new ExponentialMovingAverage { Length = EmaPeriod };

		decimal? prevClose = null;
		decimal? prevEma = null;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(rsi, ema, (candle, rsiVal, emaVal) =>
			{
				if (candle.State != CandleStates.Finished)
					return;

				if (!IsFormedAndOnlineAndAllowTrading())
					return;

				var close = candle.ClosePrice;

				if (prevClose.HasValue && prevEma.HasValue)
				{
					var crossUp = prevClose.Value <= prevEma.Value && close > emaVal;
					var crossDown = prevClose.Value >= prevEma.Value && close < emaVal;

					if (crossUp && rsiVal > RsiBuyLevel && Position <= 0)
						BuyMarket();
					else if (crossDown && rsiVal < RsiSellLevel && Position >= 0)
						SellMarket();
				}

				prevClose = close;
				prevEma = emaVal;
			})
			.Start();

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