Стратегия Trend Scalper (API/3858)
Обзор
TrendScalperStrategy — порт экспертного советника MetaTrader 4 Currencyprofits_01_1.mq4 на платформу StockSharp. Исходный робот представляет собой трендового скальпера: он проверяет пересечение быстрой EMA и медленной SMA, а затем ищет ложные проколы недавних экстремумов, чтобы войти в сделку. Переписанная версия сохраняет ту же логику, используя свечные подписки и индикаторы высокого уровня StockSharp.
Логика торговли
- Индикаторы
- Быстрая EMA (по умолчанию 6) по ценам закрытия.
- Медленная SMA (по умолчанию 12) по ценам закрытия.
- Максимум и минимум за последние N свечей (по умолчанию 6) по High/Low.
- Условия входа
- Покупка: цена касается нижней границы (
Lowest Low), при этом быстрая EMA выше медленной SMA. Отправляется рыночная заявка Buy с объёмом согласно выбранному режиму мани-менеджмента.
- Продажа: цена касается верхней границы (
Highest High), а быстрая EMA ниже медленной SMA. Размещается рыночная заявка Sell.
- Пока позиция открыта, новых сделок не открываем — как и в оригинальном советнике, одновременно присутствует только один ордер.
- Условия выхода
- Закрытие Long: при пробое свечой верхней границы
Highest High текущая длинная позиция закрывается по рынку.
- Закрытие Short: при падении ниже
Lowest Low короткая позиция закрывается по рынку.
- При
StopLossPoints > 0 дополнительно активируется StartProtection, который привязывает защитный стоп-лосс к каждой сделке.
Управление капиталом
Реализованы три режима из оригинального MQL-советника:
| Режим |
Описание |
Поведение в порте |
0 |
Фиксированный объём (LotsIfNoMM). |
Используется значение параметра FixedVolume. |
<0 |
Дробные лоты по балансу и фактору риска. |
Вычисляется ceil(balance * risk / 10000) / 10, ограничение сверху — 100 лотов. |
>0 |
Целые лоты по балансу и фактору риска. |
Та же формула, но результат округляется вверх до целого, минимум — 1 лот, максимум — 100. |
Баланс берётся из Portfolio.CurrentValue (с запасным вариантом BeginValue). Если значение недоступно, стратегия возвращается к фиксированному объёму, чтобы сохранить совместимость в тестах.
Управление рисками
- Стоп-лосс: параметр
StopLossPoints задаётся в пунктах. В OnStarted расстояние умножается на Security.PriceStep и передаётся в StartProtection, после чего платформа автоматически сопровождает защитные заявки.
- Единственная позиция: новая сделка отправляется только при
Position == 0, что полностью повторяет поведение MQL-версии.
Параметры
| Имя |
Значение по умолчанию |
Описание |
CandleType |
Таймфрейм 15 минут |
Свечи, по которым считаются индикаторы и формируются сигналы. |
FastLength |
6 |
Период быстрой EMA. |
SlowLength |
12 |
Период медленной SMA. |
BreakoutWindow |
6 |
Количество свечей для расчёта ближайших максимумов/минимумов. |
FixedVolume |
0.1 лота |
Объём сделки при отключённом мани-менеджменте либо при отсутствии данных по балансу. |
MoneyManagementMode |
0 |
Режим расчёта объёма (фиксированный, дробный, округление до целых). |
MoneyManagementRisk |
40 |
Фактор риска для расчёта объёма по балансу. |
StopLossPoints |
50 |
Дистанция стоп-лосса в пунктах (конвертируется в абсолютную цену перед запуском StartProtection). |
Особенности реализации
- Используется цепочка
SubscribeCandles().Bind(...), поэтому нет необходимости самостоятельно хранить историю цен.
- Все комментарии в коде написаны на английском языке в соответствии с требованиями репозитория.
- Тесты и другие файлы проекта не изменялись — задача ограничена переносом стратегии и подготовкой документации.
Рекомендации по использованию
- Подбирайте таймфрейм в соответствии с изначальной идеей робота (короткие внутридневные интервалы для скальпинга).
- Убедитесь, что у инструмента задан корректный
PriceStep, иначе перевод стоп-лосса из пунктов в цену будет неверным.
- Аккуратно меняйте
MoneyManagementRisk: увеличение значения быстро наращивает объём сделки из-за формулы ceil(balance * risk / 10000).
Точное окно сигналов: минимум и максимум вычисляются по предыдущим BreakoutWindow завершённым свечам, без сигнальной свечи. Для покупки нужны EMA(FastLength) > SMA(SlowLength) и currentLow <= previousWindowLow; для продажи — обратное направление средних и currentHigh >= previousWindowHigh. Лонг закрывается при currentHigh > previousWindowHigh, шорт — при currentLow < previousWindowLow. До прогрева обеих средних и канала сделки не допускаются. При положительном StopLossPoints через StartProtection включается реальная защита.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _breakoutWindow;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<int> _moneyManagementMode;
private readonly StrategyParam<decimal> _moneyManagementRisk;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _fastSeed = new();
private readonly Queue<decimal> _slowCloses = new();
private readonly Queue<decimal> _highs = new();
private readonly Queue<decimal> _lows = new();
private decimal? _fastEma;
private Order _pendingOrder;
public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TrendScalperStrategy()
{
_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_fastSeed.Clear();
_slowCloses.Clear();
_highs.Clear();
_lows.Clear();
_fastEma = null;
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");
if (StopLossPoints > 0m)
{
StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
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).Bind(ObserveQuote).Start();
}
}
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ObserveQuote(Level1ChangeMessage message)
{
// A Level1 stream is needed for native stop processing between candle closes.
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var hasBand = _highs.Count == BreakoutWindow;
var bandHigh = hasBand ? _highs.Max() : 0m;
var bandLow = hasBand ? _lows.Min() : 0m;
_highs.Enqueue(candle.HighPrice);
_lows.Enqueue(candle.LowPrice);
while (_highs.Count > BreakoutWindow) _highs.Dequeue();
while (_lows.Count > BreakoutWindow) _lows.Dequeue();
var price = candle.ClosePrice;
if (_fastEma is null)
{
_fastSeed.Enqueue(price);
if (_fastSeed.Count == FastLength)
_fastEma = _fastSeed.Average();
}
else
_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);
_slowCloses.Enqueue(price);
while (_slowCloses.Count > SlowLength)
_slowCloses.Dequeue();
if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && candle.HighPrice > bandHigh)
{
Submit(Sides.Sell, Position, "Trend scalper band exit");
return;
}
if (Position < 0m && candle.LowPrice < bandLow)
{
Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
return;
}
if (Position != 0m)
return;
var slow = _slowCloses.Average();
if (_fastEma > slow && candle.LowPrice <= bandLow)
Enter(Sides.Buy);
else if (_fastEma < slow && candle.HighPrice >= bandHigh)
Enter(Sides.Sell);
}
private void Enter(Sides side)
{
var volume = CalculateOrderVolume();
if (volume > 0m)
Submit(side, volume, "Trend scalper entry");
}
private decimal CalculateOrderVolume()
{
var volume = FixedVolume;
if (MoneyManagementMode != 0)
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance > 0m)
{
var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
volume = Math.Min(volume, 100m);
}
}
if (Security?.VolumeStep is decimal step && step > 0m)
volume = decimal.Floor(volume / step) * step;
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
return volume;
}
private void Submit(Sides side, decimal volume, string comment)
{
_pendingOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = comment,
};
RegisterOrder(_pendingOrder);
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import (
DataType, CandleStates, Level1Fields, OrderTypes, OrderStates, Sides,
Unit, UnitTypes,
)
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class trend_scalper_strategy(Strategy):
"""Fast close EMA, slow close SMA, and previous-bar high/low breakout bands."""
def __init__(self):
super(trend_scalper_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 6).SetGreaterThanZero()
self._slow_length = self.Param("SlowLength", 12).SetGreaterThanZero()
self._breakout_window = self.Param("BreakoutWindow", 6).SetGreaterThanZero()
self._fixed_volume = self.Param("FixedVolume", 0.1).SetGreaterThanZero()
self._money_management_mode = self.Param("MoneyManagementMode", 0)
self._money_management_risk = self.Param("MoneyManagementRisk", 40.0).SetNotNegative()
self._stop_loss_points = self.Param("StopLossPoints", 50.0).SetNotNegative()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._clear_state()
@property
def candle_type(self):
return self._candle_type.Value
def _clear_state(self):
self._fast_seed = []
self._slow_closes = []
self._highs = []
self._lows = []
self._fast_ema = None
self._pending_order = None
def OnReseted(self):
super(trend_scalper_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(trend_scalper_strategy, self).OnStarted2(time)
self._clear_state()
step = float(self.Security.PriceStep or 0)
if step <= 0:
raise ValueError("Trend Scalper requires a positive PriceStep.")
if self._stop_loss_points.Value > 0:
self.StartProtection(
Unit(), Unit(Decimal(float(self._stop_loss_points.Value) * step), UnitTypes.Absolute),
useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
self.SubscribeCandles(self.candle_type).Bind(self._process_candle).Start()
def _observe_quote(self, message):
# Keeps native protection informed between completed candles.
pass
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
has_band = len(self._highs) == self._breakout_window.Value
band_high = max(self._highs) if has_band else 0.0
band_low = min(self._lows) if has_band else 0.0
self._highs.append(float(candle.HighPrice))
self._lows.append(float(candle.LowPrice))
if len(self._highs) > self._breakout_window.Value:
self._highs.pop(0)
self._lows.pop(0)
close = float(candle.ClosePrice)
fast_length = self._fast_length.Value
if self._fast_ema is None:
self._fast_seed.append(close)
if len(self._fast_seed) == fast_length:
self._fast_ema = sum(self._fast_seed) / fast_length
else:
self._fast_ema += (close - self._fast_ema) * 2.0 / (fast_length + 1)
self._slow_closes.append(close)
if len(self._slow_closes) > self._slow_length.Value:
self._slow_closes.pop(0)
if not has_band or self._fast_ema is None or len(self._slow_closes) < self._slow_length.Value:
return
if (self._pending_order is not None and
self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position > 0 and float(candle.HighPrice) > band_high:
self._submit(Sides.Sell, float(self.Position), "Trend scalper band exit")
return
if self.Position < 0 and float(candle.LowPrice) < band_low:
self._submit(Sides.Buy, abs(float(self.Position)), "Trend scalper band exit")
return
if self.Position != 0:
return
slow = sum(self._slow_closes) / len(self._slow_closes)
if self._fast_ema > slow and float(candle.LowPrice) <= band_low:
self._enter(Sides.Buy)
elif self._fast_ema < slow and float(candle.HighPrice) >= band_high:
self._enter(Sides.Sell)
def _enter(self, side):
volume = self._calculate_volume()
if volume > 0:
self._submit(side, volume, "Trend scalper entry")
def _calculate_volume(self):
volume = float(self._fixed_volume.Value)
mode = self._money_management_mode.Value
if mode != 0:
balance = (self.Portfolio.CurrentValue or self.Portfolio.BeginValue
if self.Portfolio is not None else 0)
balance = float(balance or 0)
if balance > 0:
fraction = math.ceil(balance * float(self._money_management_risk.Value) / 10000.0) / 10.0
volume = min(100.0, max(1.0, math.ceil(fraction)) if mode > 0 else fraction)
step = float(self.Security.VolumeStep or 0)
if step > 0:
volume = math.floor(volume / step) * step
minimum = float(self.Security.MinVolume or 0)
maximum = float(self.Security.MaxVolume or 0)
if minimum > 0:
volume = max(volume, minimum)
if maximum > 0:
volume = min(volume, maximum)
return volume
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = Decimal(volume)
order.Comment = comment
self._pending_order = order
self.RegisterOrder(order)
def CreateClone(self):
return trend_scalper_strategy()