Открыть на GitHub

Стратегия Bronze Warrioir

Общее описание

  • Конверсия советника MetaTrader 5 Bronze Warrioir.mq5 в стратегию на высокоуровневом API StockSharp.
  • Работает с одной бумагой по завершённым свечам, совмещая сигналы CCI, Williams %R и авторского осциллятора DayImpuls.
  • Цель — отлавливать импульсы, когда тренд DayImpuls, перекупленность/перепроданность Williams %R и экстремумы CCI совпадают, при этом сохраняются механизмы контроля прибыли/убытка из оригинала.

Набор индикаторов

  • CCI — период задаётся параметром IndicatorPeriod. Для шортов требуется значение выше CciLevel, для лонгов — ниже -CciLevel.
  • Williams %R — рассчитывается на том же периоде. Значение выше WilliamsLevelUp трактуется как перекупленность, ниже WilliamsLevelDown — перепроданность.
  • DayImpuls — полная копия кастомного индикатора из пакета. Тело свечи переводится в пункты (разница close-open делится на стоимость шага цены) и сглаживается двумя экспоненциальными средними одинаковой длины. Рост означает усиление бычьего импульса, падение — медвежьего.

Логика входов

  1. Контроль Equity — сначала вычисляется плавающий PnL текущей позиции. Если он превысил ProfitTarget или упал ниже LossTarget, стратегия немедленно закрывает все позиции.
  2. Фильтр данных — используются только свечи со статусом Finished. Дополнительно хранится предыдущее значение DayImpuls, чтобы воспроизвести проверку custom[1].
  3. Вход в шорт возможен при отсутствии короткой позиции, когда DayImpuls выше DayImpulsLevel и растёт, Williams %R выше WilliamsLevelUp, а CCI превышает CciLevel. В StockSharp объём приказа равен TradeVolume плюс текущий лонг, что обеспечивает разворот одной сделкой.
  4. Вход в лонг зеркальный: DayImpuls ниже порога и снижается, Williams %R ниже WilliamsLevelDown, CCI меньше -CciLevel. Объём равен TradeVolume плюс открытый шорт.
  5. Повторные входы — если плавающий PnL вышел за пределы [-PredTarget/2, PredTarget], оригинал проверял параметр LotCoefficient перед открытием встречной позиции. В портированной версии проверка сохранена, но исполнение реализовано как рыночный разворот (сначала закрытие текущего плеча, затем открытие противоположного) из-за неттинговой модели StockSharp.

Управление рисками

  • StopLossPips и TakeProfitPips преобразуются в ценовые расстояния на основе PriceStep. Для инструментов с 3 или 5 знаками после запятой добавляется множитель 10, что соответствует понятию "пункт" в MT5.
  • Параметры передаются в StartProtection, которая автоматически сопровождает позицию стоп-лоссом и тейк-профитом.
  • В OnOwnTradeReceived ведётся учёт объёмов и средних цен по лонгам/шортам, что позволяет точно пересчитать плавающую прибыль аналогично сумме Commission + Swap + Profit в MT5.

Параметры

Параметр Назначение Значение по умолчанию
TradeVolume Базовый объём (лоты) для каждой сделки. 1
StopLossPips Дистанция стоп-лосса в пунктах. 50
TakeProfitPips Дистанция тейк-профита в пунктах. 50
IndicatorPeriod Общий период индикаторов. 14
CciLevel Порог CCI для открытия позиций. 150
WilliamsLevelUp Порог Williams %R для шорта. -15
WilliamsLevelDown Порог Williams %R для лонга. -85
DayImpulsLevel Уровень DayImpuls, разделяющий бычий/медвежий режим. 50
ProfitTarget Целевой плавающий профит (валюта счёта). 100
LossTarget Предельный плавающий убыток (валюта счёта). -100
PredTarget Коридор прибыли/убытка для повторного входа. 40
LotCoefficient Коэффициент проверки возможности усреднения. 2
CandleType Используемый таймфрейм свечей. 15m

Особенности реализации

  • Индикатор DayImpuls реализован как вложенный класс и полностью повторяет двойное EMA-сглаживание оригинала.
  • Из-за неттинга одновременно держать лонг и шорт невозможно, поэтому для имитации хеджевых входов используется рыночный разворот с суммарным объёмом.
  • Работа со свечами происходит только после проверки IsFormedAndOnlineAndAllowTrading(), что обеспечивает корректную интеграцию с жизненным циклом стратегии.
  • Учёт средних цен и объёмов обновляется при каждом собственном трейде, что делает расчёт плавающего PnL устойчивым к частичным закрытиям и разворотам.
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>
/// Bronze Warrior strategy using EMA crossover.
/// Buys when fast EMA crosses above slow EMA, sells on reverse.
/// </summary>
public class BronzeWarrioirStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	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 StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public BronzeWarrioirStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow 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;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

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

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.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 = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

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

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}