Estratégia Universal MA Cross V4
Visão geral
A Estratégia Universal MA Cross V4 é uma versão StockSharp de alto nível do consultor especialista MetaTrader 4 "Universal MACross EA v4". O algoritmo segue a interação entre uma média móvel rápida configurável e uma média móvel lenta. Ele suporta vários tipos de média móvel, fontes de preços selecionáveis, uma janela de negociação de hora em hora e gerenciamento de posição flexível, incluindo comportamento stop-and-reverse, metas de proteção e trailing stops. A estratégia é projetada para execução baseada em barras usando o StockSharp API de alto nível com assinaturas de velas.
Lógica de negociação
Processamento de indicadores
- Duas médias móveis são avaliadas em cada vela finalizada. Cada média móvel pode usar seu próprio comprimento, método de suavização (Simples, Exponencial, Suavizado ou Linear Ponderado) e fonte de preço (fechamento, abertura, máximo, mínimo, mediano, típico ou ponderado).
- O filtro MinCrossDistancePoints exige que as médias rápidas e lentas diverjam em pelo menos o número especificado de etapas de preço na barra de cruzamento. Quando ConfirmedOnEntry está habilitado, a divergência é validada na vela concluída anterior, reproduzindo o modo "confirmado" do EA original.
- A configuração ReverseCondition troca sinais de alta e baixa sem alterar a configuração do indicador.
Regras de entrada
- Uma entrada longa ocorre quando a média rápida ultrapassa a média lenta em pelo menos MinCrossDistancePoints. Uma entrada curta requer a cruz oposta.
- Quando StopAndReverse for verdadeiro, um sinal oposto fecha a posição ativa antes que novas entradas sejam consideradas.
- OneEntryPerBar evita múltiplas entradas dentro da mesma vela rastreando o carimbo de data/hora do pedido mais recente.
- O tamanho do pedido é controlado por TradeVolume. StockSharp aplica automaticamente este volume às ordens de mercado geradas.
Gestão de posição
- As distâncias de stop-loss e take-profit são definidas em pontos por meio de StopLossPoints e TakeProfitPoints. Eles são convertidos em preços absolutos usando a etapa de preço do instrumento. Quando PureSar está ativo, toda a lógica de proteção é desativada, assim como a opção "Pure SAR" na versão MQL.
- O gerenciamento de trailing stop reflete a implementação MQL: quando o preço se move além de TrailingStopPoints em relação ao nível de entrada, o stop é puxado para trás do mercado pela mesma distância. As paradas finais são ignoradas quando PureSar está habilitado.
- Os níveis de proteção são monitorados em cada vela fechada. Se o intervalo da vela violar o stop ou alvo ativo, a estratégia fecha a posição por ordem de mercado para manter o comportamento determinístico nos dados históricos.
Filtro de sessão
- O sinalizador UseHourTrade restringe a negociação à janela inclusiva entre StartHour e EndHour (0–23). Os limites da sessão terminam por volta da meia-noite quando a hora final é menor que a hora inicial. O gerenciamento de posições, incluindo trailing stops, permanece ativo fora da sessão, mas nenhuma nova entrada é permitida.
Parâmetros
| Parâmetro | Descrição |
|---|---|
FastMaPeriod, SlowMaPeriod |
Comprimentos das médias móveis rápidas e lentas. |
FastMaType, SlowMaType |
Métodos de média móvel: Simples, Exponencial, Suavizada ou Linear Ponderada. |
FastPriceType, SlowPriceType |
Fontes de preços alimentadas em cada média móvel. |
StopLossPoints, TakeProfitPoints |
Distâncias de proteção nas etapas de preços. Defina como 0 para desativar. |
TrailingStopPoints |
Distância do trailing stop em etapas de preço. Defina como 0 para desativar o rastreamento. |
MinCrossDistancePoints |
Separação mínima entre as médias necessária para validar um cruzamento. |
ReverseCondition |
Troque as regras de alta e de baixa sem alterar os indicadores. |
ConfirmedOnEntry |
Valide os sinais na barra fechada anterior. Desative para confirmação imediata. |
OneEntryPerBar |
Permita no máximo uma nova posição por vela. |
StopAndReverse |
Feche e inverta a posição atual quando o sinal oposto aparecer. |
PureSar |
Desative a lógica de stop-loss, take-profit e trailing stop. |
UseHourTrade, StartHour, EndHour |
Filtro de sessão que restringe entradas a um intervalo de horas específico. |
TradeVolume |
Volume de pedidos usado por BuyMarket e SellMarket. |
CandleType |
Série de velas inscritas para cálculos de indicadores. |
Notas de conversão
- As distâncias baseadas em preços são expressas em MetaTrader pontos. O auxiliar
GetPriceOffsetconverte esses valores em preços StockSharp usando a etapa de preço do título ou precisão decimal. Isso mantém o comportamento da estratégia alinhado com o EA original, independentemente do instrumento. - Os trailing stops são gerenciados internamente porque StockSharp estratégias de alto nível operam em velas finalizadas. Essa abordagem determinística garante que os backtests usando velas reproduzam a lógica de rastreamento MT4 pretendida.
- Nenhuma porta Python está incluída, correspondendo à solicitação de conversão. Somente a implementação do C# e a documentação multilíngue são fornecidas neste pacote.
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>
/// Port of the "Universal MACross EA v4" MetaTrader expert advisor.
/// The strategy trades the crossover between configurable fast and slow moving averages
/// with optional session filters, stop-and-reverse behaviour and trailing stop management.
/// </summary>
public class UniversalMaCrossV4Strategy : Strategy
{
public enum MovingAverageMethods
{
Simple,
Exponential,
Smoothed,
LinearWeighted
}
public enum AppliedPrices
{
Close,
Open,
High,
Low,
Median,
Typical,
Weighted
}
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<MovingAverageMethods> _fastMaType;
private readonly StrategyParam<MovingAverageMethods> _slowMaType;
private readonly StrategyParam<AppliedPrices> _fastPriceType;
private readonly StrategyParam<AppliedPrices> _slowPriceType;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _minCrossDistancePoints;
private readonly StrategyParam<bool> _reverseCondition;
private readonly StrategyParam<bool> _confirmedOnEntry;
private readonly StrategyParam<bool> _oneEntryPerBar;
private readonly StrategyParam<bool> _stopAndReverse;
private readonly StrategyParam<bool> _pureSar;
private readonly StrategyParam<bool> _useHourTrade;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<decimal> _volume;
private readonly StrategyParam<DataType> _candleType;
private IIndicator _fastMa;
private IIndicator _slowMa;
private decimal? _fastPrev;
private decimal? _fastPrevPrev;
private decimal? _slowPrev;
private decimal? _slowPrevPrev;
private DateTimeOffset? _lastEntryBar;
private TradeDirections _lastTrade = TradeDirections.None;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
/// <summary>
/// Fast moving average period.
/// </summary>
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
/// <summary>
/// Slow moving average period.
/// </summary>
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
/// <summary>
/// Method applied to the fast moving average.
/// </summary>
public MovingAverageMethods FastMaType
{
get => _fastMaType.Value;
set => _fastMaType.Value = value;
}
/// <summary>
/// Method applied to the slow moving average.
/// </summary>
public MovingAverageMethods SlowMaType
{
get => _slowMaType.Value;
set => _slowMaType.Value = value;
}
/// <summary>
/// Price source for the fast moving average.
/// </summary>
public AppliedPrices FastPriceType
{
get => _fastPriceType.Value;
set => _fastPriceType.Value = value;
}
/// <summary>
/// Price source for the slow moving average.
/// </summary>
public AppliedPrices SlowPriceType
{
get => _slowPriceType.Value;
set => _slowPriceType.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Minimum distance between moving averages to validate a crossover.
/// </summary>
public decimal MinCrossDistancePoints
{
get => _minCrossDistancePoints.Value;
set => _minCrossDistancePoints.Value = value;
}
/// <summary>
/// Swap bullish and bearish signals when set to <c>true</c>.
/// </summary>
public bool ReverseCondition
{
get => _reverseCondition.Value;
set => _reverseCondition.Value = value;
}
/// <summary>
/// Require the crossover to be confirmed on the previous closed bar.
/// </summary>
public bool ConfirmedOnEntry
{
get => _confirmedOnEntry.Value;
set => _confirmedOnEntry.Value = value;
}
/// <summary>
/// Allow only one new position per candle.
/// </summary>
public bool OneEntryPerBar
{
get => _oneEntryPerBar.Value;
set => _oneEntryPerBar.Value = value;
}
/// <summary>
/// Close and reverse the active position when the opposite signal appears.
/// </summary>
public bool StopAndReverse
{
get => _stopAndReverse.Value;
set => _stopAndReverse.Value = value;
}
/// <summary>
/// Disable stop-loss, take-profit and trailing stop logic.
/// </summary>
public bool PureSar
{
get => _pureSar.Value;
set => _pureSar.Value = value;
}
/// <summary>
/// Enable the hour-based trading session filter.
/// </summary>
public bool UseHourTrade
{
get => _useHourTrade.Value;
set => _useHourTrade.Value = value;
}
/// <summary>
/// Start hour of the trading window (0-23).
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// End hour of the trading window (0-23).
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Order volume applied to each market order.
/// </summary>
public decimal TradeVolume
{
get => _volume.Value;
set => _volume.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="UniversalMaCrossV4Strategy"/> class.
/// </summary>
public UniversalMaCrossV4Strategy()
{
_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Fast MA Period", "Length of the fast moving average", "Indicators")
.SetOptimize(5, 40, 1);
_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
.SetGreaterThanZero()
.SetDisplay("Slow MA Period", "Length of the slow moving average", "Indicators")
.SetOptimize(30, 200, 5);
_fastMaType = Param(nameof(FastMaType), MovingAverageMethods.Exponential)
.SetDisplay("Fast MA Method", "Smoothing method applied to the fast moving average", "Indicators");
_slowMaType = Param(nameof(SlowMaType), MovingAverageMethods.Exponential)
.SetDisplay("Slow MA Method", "Smoothing method applied to the slow moving average", "Indicators");
_fastPriceType = Param(nameof(FastPriceType), AppliedPrices.Close)
.SetDisplay("Fast MA Price", "Price source injected into the fast moving average", "Indicators");
_slowPriceType = Param(nameof(SlowPriceType), AppliedPrices.Close)
.SetDisplay("Slow MA Price", "Price source injected into the slow moving average", "Indicators");
_stopLossPoints = Param(nameof(StopLossPoints), 100m)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Stop-loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 200m)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Take-profit distance in price steps", "Risk");
_trailingStopPoints = Param(nameof(TrailingStopPoints), 40m)
.SetNotNegative()
.SetDisplay("Trailing Stop (points)", "Trailing stop distance in price steps", "Risk");
_minCrossDistancePoints = Param(nameof(MinCrossDistancePoints), 0m)
.SetNotNegative()
.SetDisplay("Min Cross Distance (points)", "Minimum separation between the moving averages", "Filters");
_reverseCondition = Param(nameof(ReverseCondition), false)
.SetDisplay("Reverse Signals", "Swap bullish and bearish conditions", "General");
_confirmedOnEntry = Param(nameof(ConfirmedOnEntry), true)
.SetDisplay("Confirmed On Entry", "Validate signals on the previous closed bar", "General");
_oneEntryPerBar = Param(nameof(OneEntryPerBar), true)
.SetDisplay("One Entry Per Bar", "Allow at most one entry per candle", "General");
_stopAndReverse = Param(nameof(StopAndReverse), true)
.SetDisplay("Stop And Reverse", "Close and reverse when the opposite signal appears", "Risk");
_pureSar = Param(nameof(PureSar), false)
.SetDisplay("Pure SAR", "Disable protective stops and trailing", "Risk");
_useHourTrade = Param(nameof(UseHourTrade), false)
.SetDisplay("Use Hour Filter", "Restrict trading to a specific session", "Session");
_startHour = Param(nameof(StartHour), 10)
.SetDisplay("Start Hour", "Trading window start hour", "Session");
_endHour = Param(nameof(EndHour), 11)
.SetDisplay("End Hour", "Trading window end hour", "Session");
_volume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Order volume for each market entry", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary candle subscription used by the strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastMa = null;
_slowMa = null;
_fastPrev = null;
_fastPrevPrev = null;
_slowPrev = null;
_slowPrevPrev = null;
_lastEntryBar = null;
_lastTrade = TradeDirections.None;
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
Volume = TradeVolume;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastMa = CreateMovingAverage(FastMaType, FastMaPeriod);
_slowMa = CreateMovingAverage(SlowMaType, SlowMaPeriod);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastMa);
DrawIndicator(area, _slowMa);
DrawOwnTrades(area);
}
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ManageExistingPosition(candle);
if (_fastMa is null || _slowMa is null)
return;
var fastPrice = GetPrice(candle, FastPriceType);
var slowPrice = GetPrice(candle, SlowPriceType);
var fastResult = _fastMa.Process(new DecimalIndicatorValue(_fastMa, fastPrice, candle.OpenTime) { IsFinal = true });
if (fastResult.IsEmpty) return;
var fastValue = fastResult.GetValue<decimal>();
var slowResult = _slowMa.Process(new DecimalIndicatorValue(_slowMa, slowPrice, candle.OpenTime) { IsFinal = true });
if (slowResult.IsEmpty) return;
var slowValue = slowResult.GetValue<decimal>();
var prevFast = _fastPrev;
var prevSlow = _slowPrev;
var prevFastPrev = _fastPrevPrev;
var prevSlowPrev = _slowPrevPrev;
_fastPrevPrev = prevFast;
_slowPrevPrev = prevSlow;
_fastPrev = fastValue;
_slowPrev = slowValue;
var minDistance = GetPriceOffset(MinCrossDistancePoints);
var crossUp = false;
var crossDown = false;
if (ConfirmedOnEntry)
{
// Confirm signals using the previous completed bar (shift 2 -> 1 in MQL terms).
if (prevFast.HasValue && prevSlow.HasValue && prevFastPrev.HasValue && prevSlowPrev.HasValue)
{
var diff = prevFast.Value - prevSlow.Value;
crossUp = prevFastPrev.Value < prevSlowPrev.Value && prevFast.Value > prevSlow.Value && diff >= minDistance;
crossDown = prevFastPrev.Value > prevSlowPrev.Value && prevFast.Value < prevSlow.Value && -diff >= minDistance;
}
}
else
{
// Validate crossovers on the current finished bar.
if (prevFast.HasValue && prevSlow.HasValue)
{
var diff = fastValue - slowValue;
crossUp = prevFast.Value < prevSlow.Value && fastValue > slowValue && diff >= minDistance;
crossDown = prevFast.Value > prevSlow.Value && fastValue < slowValue && -diff >= minDistance;
}
}
bool buySignal;
bool sellSignal;
if (!ReverseCondition)
{
buySignal = crossUp;
sellSignal = crossDown;
}
else
{
buySignal = crossDown;
sellSignal = crossUp;
}
if (!IsWithinTradingHours(candle))
return;
if (StopAndReverse && Position != 0)
{
var reverseToShort = _lastTrade == TradeDirections.Long && sellSignal;
var reverseToLong = _lastTrade == TradeDirections.Short && buySignal;
if (reverseToLong || reverseToShort)
{
ClosePosition();
ResetProtection();
_lastTrade = TradeDirections.None;
}
}
if (Position != 0)
return;
if (OneEntryPerBar && _lastEntryBar == candle.OpenTime)
return;
if (buySignal)
{
BuyMarket(TradeVolume);
SetProtectionLevels(candle.ClosePrice, true);
_lastTrade = TradeDirections.Long;
_lastEntryBar = candle.OpenTime;
}
else if (sellSignal)
{
SellMarket(TradeVolume);
SetProtectionLevels(candle.ClosePrice, false);
_lastTrade = TradeDirections.Short;
_lastEntryBar = candle.OpenTime;
}
}
private void ManageExistingPosition(ICandleMessage candle)
{
if (Position == 0)
{
ResetProtection();
return;
}
UpdateTrailingStop(candle);
if (Position > 0)
{
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
ClosePosition();
ResetProtection();
return;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
ClosePosition();
ResetProtection();
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
ClosePosition();
ResetProtection();
return;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
ClosePosition();
ResetProtection();
}
}
}
private void UpdateTrailingStop(ICandleMessage candle)
{
if (PureSar || TrailingStopPoints <= 0m || !_entryPrice.HasValue)
return;
var trailingDistance = GetPriceOffset(TrailingStopPoints);
if (trailingDistance <= 0m)
return;
if (Position > 0)
{
var move = candle.ClosePrice - _entryPrice.Value;
if (move > trailingDistance)
{
var candidate = candle.ClosePrice - trailingDistance;
if (!_stopPrice.HasValue || candidate > _stopPrice.Value)
{
_stopPrice = candidate;
}
}
}
else if (Position < 0)
{
var move = _entryPrice.Value - candle.ClosePrice;
if (move > trailingDistance)
{
var candidate = candle.ClosePrice + trailingDistance;
if (!_stopPrice.HasValue || candidate < _stopPrice.Value)
{
_stopPrice = candidate;
}
}
}
}
private bool IsWithinTradingHours(ICandleMessage candle)
{
if (!UseHourTrade)
return true;
var hour = candle.OpenTime.Hour;
var start = StartHour;
var end = EndHour;
if (start <= end)
return hour >= start && hour <= end;
return hour >= start || hour <= end;
}
private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
{
return method switch
{
MovingAverageMethods.Simple => new SimpleMovingAverage { Length = period },
MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = period },
MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
MovingAverageMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
_ => new SimpleMovingAverage { Length = period }
};
}
private static decimal GetPrice(ICandleMessage candle, AppliedPrices priceType)
{
return priceType switch
{
AppliedPrices.Open => candle.OpenPrice,
AppliedPrices.High => candle.HighPrice,
AppliedPrices.Low => candle.LowPrice,
AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
_ => candle.ClosePrice,
};
}
private void SetProtectionLevels(decimal entryPrice, bool isLong)
{
_entryPrice = entryPrice;
if (PureSar)
{
_stopPrice = null;
_takeProfitPrice = null;
return;
}
var stopDistance = GetPriceOffset(StopLossPoints);
var takeDistance = GetPriceOffset(TakeProfitPoints);
_stopPrice = stopDistance > 0m ? (isLong ? entryPrice - stopDistance : entryPrice + stopDistance) : null;
_takeProfitPrice = takeDistance > 0m ? (isLong ? entryPrice + takeDistance : entryPrice - takeDistance) : null;
}
private void ResetProtection()
{
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
}
private decimal GetPriceOffset(decimal points)
{
if (points <= 0m)
return 0m;
var step = Security?.PriceStep ?? 0m;
if (step > 0m)
return points * step;
var decimals = Security?.Decimals;
if (decimals.HasValue && decimals.Value > 0)
{
decimal scale = 1m;
for (var i = 0; i < decimals.Value; i++)
scale /= 10m;
return points * scale;
}
return points;
}
private void ClosePosition()
{
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
}
private enum TradeDirections
{
None,
Long,
Short
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import (
ExponentialMovingAverage,
SimpleMovingAverage, SmoothedMovingAverage, WeightedMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
# MA method constants
MA_SIMPLE = 0
MA_EXPONENTIAL = 1
MA_SMOOTHED = 2
MA_LINEAR_WEIGHTED = 3
# Applied price constants
PRICE_CLOSE = 0
PRICE_OPEN = 1
PRICE_HIGH = 2
PRICE_LOW = 3
PRICE_MEDIAN = 4
PRICE_TYPICAL = 5
PRICE_WEIGHTED = 6
# Trade direction constants
DIR_NONE = 0
DIR_LONG = 1
DIR_SHORT = 2
class universal_ma_cross_v4_strategy(Strategy):
"""Universal MA Cross EA v4. Trades crossover between configurable fast and slow
moving averages with optional session filters, stop-and-reverse, and trailing stop."""
def __init__(self):
super(universal_ma_cross_v4_strategy, self).__init__()
self._fast_ma_period = self.Param("FastMaPeriod", 10) \
.SetGreaterThanZero() \
.SetDisplay("Fast MA Period", "Length of the fast moving average", "Indicators")
self._slow_ma_period = self.Param("SlowMaPeriod", 80) \
.SetGreaterThanZero() \
.SetDisplay("Slow MA Period", "Length of the slow moving average", "Indicators")
self._fast_ma_type = self.Param("FastMaType", MA_EXPONENTIAL) \
.SetDisplay("Fast MA Method", "Smoothing method for the fast MA", "Indicators")
self._slow_ma_type = self.Param("SlowMaType", MA_EXPONENTIAL) \
.SetDisplay("Slow MA Method", "Smoothing method for the slow MA", "Indicators")
self._fast_price_type = self.Param("FastPriceType", PRICE_CLOSE) \
.SetDisplay("Fast MA Price", "Price source for the fast MA", "Indicators")
self._slow_price_type = self.Param("SlowPriceType", PRICE_CLOSE) \
.SetDisplay("Slow MA Price", "Price source for the slow MA", "Indicators")
self._stop_loss_points = self.Param("StopLossPoints", 100.0) \
.SetDisplay("Stop Loss (points)", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 200.0) \
.SetDisplay("Take Profit (points)", "Take-profit distance in price steps", "Risk")
self._trailing_stop_points = self.Param("TrailingStopPoints", 40.0) \
.SetDisplay("Trailing Stop (points)", "Trailing stop distance in price steps", "Risk")
self._min_cross_distance_points = self.Param("MinCrossDistancePoints", 0.0) \
.SetDisplay("Min Cross Distance (points)", "Minimum separation between MAs", "Filters")
self._reverse_condition = self.Param("ReverseCondition", False) \
.SetDisplay("Reverse Signals", "Swap bullish and bearish conditions", "General")
self._confirmed_on_entry = self.Param("ConfirmedOnEntry", True) \
.SetDisplay("Confirmed On Entry", "Validate signals on the previous closed bar", "General")
self._one_entry_per_bar = self.Param("OneEntryPerBar", True) \
.SetDisplay("One Entry Per Bar", "Allow at most one entry per candle", "General")
self._stop_and_reverse = self.Param("StopAndReverse", True) \
.SetDisplay("Stop And Reverse", "Close and reverse on opposite signal", "Risk")
self._pure_sar = self.Param("PureSar", False) \
.SetDisplay("Pure SAR", "Disable protective stops and trailing", "Risk")
self._use_hour_trade = self.Param("UseHourTrade", False) \
.SetDisplay("Use Hour Filter", "Restrict trading to a specific session", "Session")
self._start_hour = self.Param("StartHour", 10) \
.SetDisplay("Start Hour", "Trading window start hour", "Session")
self._end_hour = self.Param("EndHour", 11) \
.SetDisplay("End Hour", "Trading window end hour", "Session")
self._volume_param = self.Param("TradeVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Trade Volume", "Order volume for each entry", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Primary candle subscription", "General")
self._fast_ma = None
self._slow_ma = None
self._fast_prev = None
self._fast_prev_prev = None
self._slow_prev = None
self._slow_prev_prev = None
self._last_entry_bar = None
self._last_trade = DIR_NONE
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastMaPeriod(self):
return self._fast_ma_period.Value
@property
def SlowMaPeriod(self):
return self._slow_ma_period.Value
@property
def FastMaType(self):
return self._fast_ma_type.Value
@property
def SlowMaType(self):
return self._slow_ma_type.Value
@property
def FastPriceType(self):
return self._fast_price_type.Value
@property
def SlowPriceType(self):
return self._slow_price_type.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@property
def MinCrossDistancePoints(self):
return self._min_cross_distance_points.Value
@property
def ReverseCondition(self):
return self._reverse_condition.Value
@property
def ConfirmedOnEntry(self):
return self._confirmed_on_entry.Value
@property
def OneEntryPerBar(self):
return self._one_entry_per_bar.Value
@property
def StopAndReverse(self):
return self._stop_and_reverse.Value
@property
def PureSar(self):
return self._pure_sar.Value
@property
def UseHourTrade(self):
return self._use_hour_trade.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def TradeVolume(self):
return self._volume_param.Value
def OnReseted(self):
super(universal_ma_cross_v4_strategy, self).OnReseted()
self._fast_ma = None
self._slow_ma = None
self._fast_prev = None
self._fast_prev_prev = None
self._slow_prev = None
self._slow_prev_prev = None
self._last_entry_bar = None
self._last_trade = DIR_NONE
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
def _create_ma(self, method, period):
if method == MA_SIMPLE:
ma = SimpleMovingAverage()
elif method == MA_SMOOTHED:
ma = SmoothedMovingAverage()
elif method == MA_LINEAR_WEIGHTED:
ma = WeightedMovingAverage()
else:
ma = ExponentialMovingAverage()
ma.Length = period
return ma
def _get_price(self, candle, price_type):
if price_type == PRICE_OPEN:
return float(candle.OpenPrice)
elif price_type == PRICE_HIGH:
return float(candle.HighPrice)
elif price_type == PRICE_LOW:
return float(candle.LowPrice)
elif price_type == PRICE_MEDIAN:
return (float(candle.HighPrice) + float(candle.LowPrice)) / 2.0
elif price_type == PRICE_TYPICAL:
return (float(candle.HighPrice) + float(candle.LowPrice) + float(candle.ClosePrice)) / 3.0
elif price_type == PRICE_WEIGHTED:
return (float(candle.HighPrice) + float(candle.LowPrice) + 2.0 * float(candle.ClosePrice)) / 4.0
return float(candle.ClosePrice)
def _get_price_offset(self, points):
pts = float(points)
if pts <= 0:
return 0.0
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is not None and float(step) > 0:
return pts * float(step)
return pts
def OnStarted2(self, time):
super(universal_ma_cross_v4_strategy, self).OnStarted2(time)
self._fast_ma = self._create_ma(self.FastMaType, self.FastMaPeriod)
self._slow_ma = self._create_ma(self.SlowMaType, self.SlowMaPeriod)
self.Volume = float(self.TradeVolume)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _close_position(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
def _reset_protection(self):
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
def _set_protection_levels(self, entry_price, is_long):
self._entry_price = entry_price
if self.PureSar:
self._stop_price = None
self._take_profit_price = None
return
stop_dist = self._get_price_offset(self.StopLossPoints)
take_dist = self._get_price_offset(self.TakeProfitPoints)
if stop_dist > 0:
self._stop_price = entry_price - stop_dist if is_long else entry_price + stop_dist
else:
self._stop_price = None
if take_dist > 0:
self._take_profit_price = entry_price + take_dist if is_long else entry_price - take_dist
else:
self._take_profit_price = None
def _update_trailing_stop(self, candle):
if self.PureSar or float(self.TrailingStopPoints) <= 0 or self._entry_price is None:
return
trailing_distance = self._get_price_offset(self.TrailingStopPoints)
if trailing_distance <= 0:
return
close = float(candle.ClosePrice)
if self.Position > 0:
move = close - self._entry_price
if move > trailing_distance:
candidate = close - trailing_distance
if self._stop_price is None or candidate > self._stop_price:
self._stop_price = candidate
elif self.Position < 0:
move = self._entry_price - close
if move > trailing_distance:
candidate = close + trailing_distance
if self._stop_price is None or candidate < self._stop_price:
self._stop_price = candidate
def _manage_existing_position(self, candle):
if self.Position == 0:
self._reset_protection()
return
self._update_trailing_stop(candle)
low = float(candle.LowPrice)
high = float(candle.HighPrice)
if self.Position > 0:
if self._stop_price is not None and low <= self._stop_price:
self._close_position()
self._reset_protection()
return
if self._take_profit_price is not None and high >= self._take_profit_price:
self._close_position()
self._reset_protection()
elif self.Position < 0:
if self._stop_price is not None and high >= self._stop_price:
self._close_position()
self._reset_protection()
return
if self._take_profit_price is not None and low <= self._take_profit_price:
self._close_position()
self._reset_protection()
def _is_within_trading_hours(self, candle):
if not self.UseHourTrade:
return True
hour = candle.OpenTime.Hour
start = self.StartHour
end = self.EndHour
if start <= end:
return hour >= start and hour <= end
return hour >= start or hour <= end
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._manage_existing_position(candle)
if self._fast_ma is None or self._slow_ma is None:
return
fast_price = self._get_price(candle, self.FastPriceType)
slow_price = self._get_price(candle, self.SlowPriceType)
time = candle.OpenTime
fast_result = process_float(self._fast_ma, fast_price, time, True)
if fast_result.IsEmpty:
return
fast_value = float(fast_result)
slow_result = process_float(self._slow_ma, slow_price, time, True)
if slow_result.IsEmpty:
return
slow_value = float(slow_result)
prev_fast = self._fast_prev
prev_slow = self._slow_prev
prev_fast_prev = self._fast_prev_prev
prev_slow_prev = self._slow_prev_prev
self._fast_prev_prev = prev_fast
self._slow_prev_prev = prev_slow
self._fast_prev = fast_value
self._slow_prev = slow_value
min_distance = self._get_price_offset(self.MinCrossDistancePoints)
cross_up = False
cross_down = False
if self.ConfirmedOnEntry:
if prev_fast is not None and prev_slow is not None and \
prev_fast_prev is not None and prev_slow_prev is not None:
diff = prev_fast - prev_slow
cross_up = prev_fast_prev < prev_slow_prev and prev_fast > prev_slow and diff >= min_distance
cross_down = prev_fast_prev > prev_slow_prev and prev_fast < prev_slow and -diff >= min_distance
else:
if prev_fast is not None and prev_slow is not None:
diff = fast_value - slow_value
cross_up = prev_fast < prev_slow and fast_value > slow_value and diff >= min_distance
cross_down = prev_fast > prev_slow and fast_value < slow_value and -diff >= min_distance
if not self.ReverseCondition:
buy_signal = cross_up
sell_signal = cross_down
else:
buy_signal = cross_down
sell_signal = cross_up
if not self._is_within_trading_hours(candle):
return
if self.StopAndReverse and self.Position != 0:
reverse_to_short = self._last_trade == DIR_LONG and sell_signal
reverse_to_long = self._last_trade == DIR_SHORT and buy_signal
if reverse_to_long or reverse_to_short:
self._close_position()
self._reset_protection()
self._last_trade = DIR_NONE
if self.Position != 0:
return
if self.OneEntryPerBar and self._last_entry_bar == candle.OpenTime:
return
close = float(candle.ClosePrice)
if buy_signal:
self.BuyMarket()
self._set_protection_levels(close, True)
self._last_trade = DIR_LONG
self._last_entry_bar = candle.OpenTime
elif sell_signal:
self.SellMarket()
self._set_protection_levels(close, False)
self._last_trade = DIR_SHORT
self._last_entry_bar = candle.OpenTime
def CreateClone(self):
return universal_ma_cross_v4_strategy()