Контртрендовая стратегия EMA WMA
Контртрендовая система, сравнивающая экспоненциальную (EMA) и взвешенную (WMA) скользящие средние, рассчитанные по ценам открытия свечей. Когда быстрая EMA опускается ниже WMA, стратегия покупает в расчёте на возврат цены. Когда EMA снова поднимается выше WMA, открывается короткая позиция. Размер сделки определяется процентом риска и расстоянием до защитного стопа, а также дополняется фиксированным стоп-лоссом, тейк-профитом и трейлинг-стопом.
Детали
- Условия входа:
- Лонг: EMA(Open) пересекает WMA(Open) сверху вниз
- Шорт: EMA(Open) пересекает WMA(Open) снизу вверх
- Направление: Лонг и шорт
- Условия выхода:
- Фиксированный стоп-лосс в шагах цены
- Фиксированный тейк-профит в шагах цены
- Трейлинг-стоп, который подтягивается после движения цены на
TrailingStopPoints + TrailingStepPoints - Противоположный сигнал закрывает текущую позицию и открывает новую
- Стопы: Стоп-лосс, тейк-профит и трейлинг-стоп
- Значения по умолчанию:
EmaPeriod= 28WmaPeriod= 8StopLossPoints= 50mTakeProfitPoints= 50mTrailingStopPoints= 50mTrailingStepPoints= 10mRiskPercent= 10mBaseVolume= 1mCandleType= TimeSpan.FromMinutes(1).TimeFrame()
- Фильтры:
- Категория: Скользящие средние, контртренд
- Направление: Лонг и шорт
- Индикаторы: EMA (open), WMA (open)
- Стопы: Да (жёсткий стоп и трейлинг)
- Сложность: Средняя
- Таймфрейм: Внутридневной (по умолчанию 1 минута)
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
Параметры
| Параметр | Описание |
|---|---|
EmaPeriod, WmaPeriod |
Периоды EMA и WMA, рассчитанных по ценам открытия свечей. |
StopLossPoints, TakeProfitPoints |
Расстояние в шагах цены до защитного стопа и тейк-профита. |
TrailingStopPoints |
Расстояние между ценой и трейлинг-стопом после активации. |
TrailingStepPoints |
Дополнительное прибыльное движение, необходимое для подтяжки трейлинг-стопа. Должно быть положительным при включённом трейлинге. |
RiskPercent |
Процент капитала портфеля, которым рискуем в одной сделке. Объём позиции вычисляется как RiskPercent / (StopLossPoints * PriceStep). |
BaseVolume |
Минимальный объём сделки, если расчёт по риску невозможен. |
CandleType |
Тип свечей для расчётов (по умолчанию минутные). |
Примечания
- Обе скользящие средние используют цены открытия, что повторяет логику оригинального эксперта MetaTrader.
- Трейлинг-стоп активируется только после движения в нашу сторону не менее чем на
TrailingStopPoints + TrailingStepPoints, как и в исходной реализации. - Если задан
TrailingStopPoints, ноTrailingStepPointsравен нулю или отрицателен, стратегия сразу остановится, чтобы избежать некорректной работы трейлинга. - Если стоимость портфеля, шаг цены или расстояние стопа недоступны, расчёт объёма откатывается к значению
BaseVolume.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Contrarian crossover between EMA and WMA calculated on candle open prices.
/// Opens a long position when EMA crosses below WMA and a short position on the opposite cross.
/// Supports fixed stop-loss, take-profit, and trailing stop management plus risk-based position sizing.
/// </summary>
public class EmaWmaContrarianStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _wmaPeriod;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingStepPoints;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _ema;
private WeightedMovingAverage _wma;
private bool _hasPrevious;
private decimal _previousEma;
private decimal _previousWma;
private decimal? _entryPrice;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
/// <summary>
/// EMA period.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// WMA period.
/// </summary>
public int WmaPeriod
{
get => _wmaPeriod.Value;
set => _wmaPeriod.Value = value;
}
/// <summary>
/// Stop-loss in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in price steps.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Trailing stop step in price steps.
/// </summary>
public decimal TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
/// <summary>
/// Risk percentage used for position sizing.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Minimum contract volume used when risk sizing cannot be applied.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="EmaWmaContrarianStrategy"/>.
/// </summary>
public EmaWmaContrarianStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 28)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA length calculated on candle open prices", "Indicators")
.SetOptimize(10, 60, 2);
_wmaPeriod = Param(nameof(WmaPeriod), 8)
.SetGreaterThanZero()
.SetDisplay("WMA Period", "WMA length calculated on candle open prices", "Indicators")
.SetOptimize(4, 40, 2);
_stopLossPoints = Param(nameof(StopLossPoints), 50m)
.SetDisplay("Stop Loss (points)", "Stop-loss distance expressed in price steps", "Risk")
.SetOptimize(10m, 150m, 10m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
.SetDisplay("Take Profit (points)", "Take-profit distance expressed in price steps", "Risk")
.SetOptimize(10m, 200m, 10m);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 50m)
.SetDisplay("Trailing Stop (points)", "Trailing stop distance expressed in price steps", "Risk")
.SetOptimize(10m, 150m, 10m);
_trailingStepPoints = Param(nameof(TrailingStepPoints), 10m)
.SetDisplay("Trailing Step (points)", "Minimal favorable move before the trailing stop is advanced", "Risk")
.SetOptimize(5m, 50m, 5m);
_riskPercent = Param(nameof(RiskPercent), 10m)
.SetDisplay("Risk Percent", "Portfolio percentage risked per trade", "Position Sizing")
.SetOptimize(2m, 20m, 2m);
_baseVolume = Param(nameof(BaseVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Fallback volume when risk sizing is unavailable", "Position Sizing");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary candle type used for indicators", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ema = null;
_wma = null;
_hasPrevious = false;
_previousEma = 0m;
_previousWma = 0m;
ClearPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Validate trailing configuration to match original expert advisor behaviour.
if (TrailingStopPoints > 0 && TrailingStepPoints <= 0)
{
Stop();
return;
}
_ema = new ExponentialMovingAverage { Length = EmaPeriod };
_wma = new WeightedMovingAverage { Length = WmaPeriod };
// Subscribe to candles and connect indicators.
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle);
subscription.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
// Process only finished candles to avoid intrabar noise.
if (candle.State != CandleStates.Finished)
return;
if (_ema == null || _wma == null)
return;
// Evaluate protective logic before generating new signals.
ManageActivePosition(candle);
var emaValue = _ema.Process(new DecimalIndicatorValue(_ema, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
var wmaValue = _wma.Process(new DecimalIndicatorValue(_wma, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
// Ensure indicators produced valid values.
if (emaValue.IsEmpty || wmaValue.IsEmpty || !_ema.IsFormed || !_wma.IsFormed)
return;
var ema = emaValue.ToDecimal();
var wma = wmaValue.ToDecimal();
if (!_hasPrevious)
{
_previousEma = ema;
_previousWma = wma;
_hasPrevious = true;
return;
}
// Detect crossovers on open-price moving averages.
var buySignal = ema < wma && _previousEma > _previousWma;
var sellSignal = ema > wma && _previousEma < _previousWma;
if (buySignal)
{
EnterLong(candle);
}
else if (sellSignal)
{
EnterShort(candle);
}
_previousEma = ema;
_previousWma = wma;
}
private void ManageActivePosition(ICandleMessage candle)
{
if (Position > 0)
{
// Manage long position exits.
var currentPrice = candle.ClosePrice;
// Exit long when take-profit is reached.
if (_takeProfitPrice is decimal tp && currentPrice >= tp)
{
SellMarket(Position);
ClearPositionState();
return;
}
// Exit long when stop-loss is hit.
if (_stopLossPrice is decimal sl && currentPrice <= sl)
{
SellMarket(Position);
ClearPositionState();
return;
}
// Advance trailing stop for long trades.
if (_entryPrice is decimal entry)
UpdateTrailingForLong(currentPrice, entry);
}
else if (Position < 0)
{
// Manage short position exits.
var currentPrice = candle.ClosePrice;
// Exit short when take-profit is reached.
if (_takeProfitPrice is decimal tp && currentPrice <= tp)
{
BuyMarket(Math.Abs(Position));
ClearPositionState();
return;
}
// Exit short when stop-loss is hit.
if (_stopLossPrice is decimal sl && currentPrice >= sl)
{
BuyMarket(Math.Abs(Position));
ClearPositionState();
return;
}
// Advance trailing stop for short trades.
if (_entryPrice is decimal entry)
UpdateTrailingForShort(currentPrice, entry);
}
else
{
ClearPositionState();
}
}
private void EnterLong(ICandleMessage candle)
{
var entryPrice = candle.ClosePrice;
var volume = CalculateTradeVolume();
if (volume <= 0)
return;
if (Position < 0)
{
// Close an existing short position before opening a new long.
BuyMarket(Math.Abs(Position));
ClearPositionState();
}
// Open the new long trade.
BuyMarket(volume);
SetupRiskLevels(entryPrice, true);
}
private void EnterShort(ICandleMessage candle)
{
var entryPrice = candle.ClosePrice;
var volume = CalculateTradeVolume();
if (volume <= 0)
return;
if (Position > 0)
{
// Close an existing long position before opening a new short.
SellMarket(Position);
ClearPositionState();
}
// Open the new short trade.
SellMarket(volume);
SetupRiskLevels(entryPrice, false);
}
private void SetupRiskLevels(decimal entryPrice, bool isLong)
{
var priceStep = Security?.PriceStep ?? 1m;
var stopDistance = StopLossPoints > 0 ? StopLossPoints * priceStep : (decimal?)null;
var takeProfitDistance = TakeProfitPoints > 0 ? TakeProfitPoints * priceStep : (decimal?)null;
// Remember entry price for managing exits.
_entryPrice = entryPrice;
_stopLossPrice = stopDistance.HasValue
? isLong ? entryPrice - stopDistance.Value : entryPrice + stopDistance.Value
: null;
_takeProfitPrice = takeProfitDistance.HasValue
? isLong ? entryPrice + takeProfitDistance.Value : entryPrice - takeProfitDistance.Value
: null;
}
private decimal CalculateTradeVolume()
{
// Default to configured base volume.
var volume = BaseVolume;
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
var priceStep = Security?.PriceStep ?? 1m;
var stopDistance = StopLossPoints * priceStep;
// Risk-based sizing uses stop distance to allocate capital.
if (RiskPercent > 0 && portfolioValue > 0 && stopDistance > 0)
{
var riskCapital = portfolioValue * (RiskPercent / 100m);
if (riskCapital > 0)
{
var rawVolume = riskCapital / stopDistance;
var adjusted = AdjustVolume(rawVolume);
if (adjusted > 0)
volume = adjusted;
}
}
return volume;
}
private decimal AdjustVolume(decimal volume)
{
// Align volume with instrument volume step.
var step = Security?.VolumeStep ?? 1m;
if (step <= 0)
step = 1m;
var adjusted = Math.Floor(volume / step) * step;
if (adjusted <= 0)
adjusted = step;
var minVolume = Security?.VolumeStep ?? step;
if (minVolume > 0 && adjusted < minVolume)
adjusted = minVolume;
return adjusted;
}
private void UpdateTrailingForLong(decimal currentPrice, decimal entryPrice)
{
if (TrailingStopPoints <= 0)
return;
var priceStep = Security?.PriceStep ?? 1m;
var trailingDistance = TrailingStopPoints * priceStep;
var trailingStep = TrailingStepPoints * priceStep;
// Trailing stop only applies after sufficient favorable movement.
if (currentPrice - entryPrice <= trailingDistance + trailingStep)
return;
var comparisonLevel = currentPrice - (trailingDistance + trailingStep);
// Raise stop-loss closer to current price.
if (_stopLossPrice is not decimal existing || existing < comparisonLevel)
_stopLossPrice = currentPrice - trailingDistance;
}
private void UpdateTrailingForShort(decimal currentPrice, decimal entryPrice)
{
if (TrailingStopPoints <= 0)
return;
var priceStep = Security?.PriceStep ?? 1m;
var trailingDistance = TrailingStopPoints * priceStep;
var trailingStep = TrailingStepPoints * priceStep;
// Trailing stop only applies after sufficient favorable movement.
if (entryPrice - currentPrice <= trailingDistance + trailingStep)
return;
var comparisonLevel = currentPrice + trailingDistance + trailingStep;
// Lower stop-loss toward market for short trades.
if (_stopLossPrice is not decimal existing || existing > comparisonLevel)
_stopLossPrice = currentPrice + trailingDistance;
}
private void ClearPositionState()
{
_entryPrice = null;
_stopLossPrice = null;
_takeProfitPrice = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import ExponentialMovingAverage, WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math, Decimal
from indicator_extensions import *
class ema_wma_contrarian_strategy(Strategy):
def __init__(self):
super(ema_wma_contrarian_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 28)
self._wma_period = self.Param("WmaPeriod", 8)
self._stop_loss_points = self.Param("StopLossPoints", 50.0)
self._take_profit_points = self.Param("TakeProfitPoints", 50.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 50.0)
self._trailing_step_points = self.Param("TrailingStepPoints", 10.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._ema = None
self._wma = None
self._has_previous = False
self._previous_ema = 0.0
self._previous_wma = 0.0
self._entry_price = None
self._stop_loss_price = None
self._take_profit_price = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(ema_wma_contrarian_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = self._ema_period.Value
self._wma = WeightedMovingAverage()
self._wma.Length = self._wma_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._ema is None or self._wma is None:
return
self._manage_active_position(candle)
open_price = float(candle.OpenPrice)
ema_result = process_float(self._ema, Decimal(float(open_price)), candle.OpenTime, True)
wma_result = process_float(self._wma, Decimal(float(open_price)), candle.OpenTime, True)
if ema_result.IsEmpty or wma_result.IsEmpty or not self._ema.IsFormed or not self._wma.IsFormed:
return
ema = float(ema_result.Value)
wma = float(wma_result.Value)
if not self._has_previous:
self._previous_ema = ema
self._previous_wma = wma
self._has_previous = True
return
buy_signal = ema < wma and self._previous_ema > self._previous_wma
sell_signal = ema > wma and self._previous_ema < self._previous_wma
if buy_signal:
self._enter_long(candle)
elif sell_signal:
self._enter_short(candle)
self._previous_ema = ema
self._previous_wma = wma
def _manage_active_position(self, candle):
price = float(candle.ClosePrice)
if self.Position > 0:
if self._take_profit_price is not None and price >= self._take_profit_price:
self.SellMarket(self.Position)
self._clear_position_state()
return
if self._stop_loss_price is not None and price <= self._stop_loss_price:
self.SellMarket(self.Position)
self._clear_position_state()
return
if self._entry_price is not None:
self._update_trailing_for_long(price, self._entry_price)
elif self.Position < 0:
vol = abs(self.Position)
if self._take_profit_price is not None and price <= self._take_profit_price:
self.BuyMarket(vol)
self._clear_position_state()
return
if self._stop_loss_price is not None and price >= self._stop_loss_price:
self.BuyMarket(vol)
self._clear_position_state()
return
if self._entry_price is not None:
self._update_trailing_for_short(price, self._entry_price)
else:
self._clear_position_state()
def _enter_long(self, candle):
entry_price = float(candle.ClosePrice)
if self.Position < 0:
self.BuyMarket(abs(self.Position))
self._clear_position_state()
self.BuyMarket()
self._setup_risk_levels(entry_price, True)
def _enter_short(self, candle):
entry_price = float(candle.ClosePrice)
if self.Position > 0:
self.SellMarket(self.Position)
self._clear_position_state()
self.SellMarket()
self._setup_risk_levels(entry_price, False)
def _setup_risk_levels(self, entry_price, is_long):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0:
step = 1.0
self._entry_price = entry_price
if self._stop_loss_points.Value > 0:
sd = self._stop_loss_points.Value * step
self._stop_loss_price = entry_price - sd if is_long else entry_price + sd
else:
self._stop_loss_price = None
if self._take_profit_points.Value > 0:
td = self._take_profit_points.Value * step
self._take_profit_price = entry_price + td if is_long else entry_price - td
else:
self._take_profit_price = None
def _update_trailing_for_long(self, current_price, entry_price):
if self._trailing_stop_points.Value <= 0:
return
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0:
step = 1.0
trailing_distance = self._trailing_stop_points.Value * step
trailing_step = self._trailing_step_points.Value * step
if current_price - entry_price <= trailing_distance + trailing_step:
return
comparison_level = current_price - (trailing_distance + trailing_step)
if self._stop_loss_price is None or self._stop_loss_price < comparison_level:
self._stop_loss_price = current_price - trailing_distance
def _update_trailing_for_short(self, current_price, entry_price):
if self._trailing_stop_points.Value <= 0:
return
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0:
step = 1.0
trailing_distance = self._trailing_stop_points.Value * step
trailing_step = self._trailing_step_points.Value * step
if entry_price - current_price <= trailing_distance + trailing_step:
return
comparison_level = current_price + trailing_distance + trailing_step
if self._stop_loss_price is None or self._stop_loss_price > comparison_level:
self._stop_loss_price = current_price + trailing_distance
def _clear_position_state(self):
self._entry_price = None
self._stop_loss_price = None
self._take_profit_price = None
def OnReseted(self):
super(ema_wma_contrarian_strategy, self).OnReseted()
self._ema = None
self._wma = None
self._has_previous = False
self._previous_ema = 0.0
self._previous_wma = 0.0
self._clear_position_state()
def CreateClone(self):
return ema_wma_contrarian_strategy()