ユニバーサルMAクロスV4戦略
概要
Universal MA Cross V4 戦略 は、MetaTrader 4 エキスパート アドバイザ「Universal MACross EA v4」の上位レベルの StockSharp 移植です。このアルゴリズムは、設定可能な高速移動平均と低速移動平均の間の相互作用に従います。複数の移動平均タイプ、選択可能な価格ソース、時間単位の取引ウィンドウ、ストップアンドリバース動作、保護ターゲット、トレーリングストップを含む柔軟なポジション管理をサポートしています。この戦略は、ローソク足サブスクリプションで StockSharp の高レベル API を使用したバーベースの実行向けに設計されています。
取引ロジック
インジケーター処理
- 完成したローソク足ごとに 2 つの移動平均が評価されます。各移動平均では、独自の長さ、平滑化方法 (単純、指数、平滑化、または線形加重)、および価格ソース (終値、始値、高値、安値、中央値、標準、または加重) を使用できます。
- MinCrossDistancePoints フィルターでは、クロスオーバー バーで少なくとも指定された価格ステップ数だけ発散する高速平均と低速平均が必要です。 confirmedOnEntry が有効になっている場合、発散は以前に完了したローソク足で検証され、元の EA からの「確認済み」モードが再現されます。
- ReverseCondition を設定すると、インジケーターの構成を変更せずに強気シグナルと弱気シグナルを入れ替えます。
エントリールール
- ロングエントリーは、高速平均が低速平均を少なくともMinCrossDistancePoints上を横切るときに発生します。ショートエントリーには逆のクロスが必要です。
- StopAndReverse が true の場合、新しいエントリが考慮される前に、反対のシグナルがアクティブなポジションをクローズします。
- OneEntryPerBar は、最新の注文のタイムスタンプを追跡することで、同じローソク足内での複数のエントリーを防ぎます。
- 注文サイズは Trade Volume によって制御されます。 StockSharp は、生成された成行注文にこの数量を自動的に適用します。
ポジション管理
- ストップロスとテイクプロフィットの距離は、StopLossPoints と TakeProfitPoints を通じてポイントで定義されます。これらは商品価格ステップを使用して絶対価格に変換されます。 PureSar がアクティブな場合、MQL バージョンの「Pure SAR」オプションと同様に、すべての保護ロジックが無効になります。
- トレーリング ストップ管理は、MQL の実装を反映しています。価格がエントリー レベルから TrailingStopPoints を超えて移動すると、ストップは同じ距離だけ市場の後ろに引かれます。 PureSar が有効な場合、トレーリングストップは無視されます。
- 閉じたキャンドルごとに保護レベルが監視されます。ローソク足の範囲がアクティブなストップまたはターゲットに違反した場合、戦略は市場注文によってポジションをクローズし、履歴データに基づく決定的な動作を維持します。
セッションフィルター
- UseHourTrade フラグは、StartHour と EndHour (0 ~ 23) の間の包括的なウィンドウに取引を制限します。終了時間が開始時間より小さい場合、セッション境界は午前 0 時を中心にラップされます。トレーリングストップを含むポジション管理はセッション外でもアクティブのままですが、新しいエントリーは許可されません。
パラメーター
| パラメータ | 説明 |
|---|---|
FastMaPeriod, SlowMaPeriod |
高速移動平均と低速移動平均の長さ。 |
FastMaType, SlowMaType |
移動平均法: 単純、指数、平滑化、または線形加重。 |
FastPriceType, SlowPriceType |
各移動平均に入力される価格ソース。 |
StopLossPoints, TakeProfitPoints |
価格段階における保護距離。無効にするには、0 に設定します。 |
TrailingStopPoints |
価格ステップでのトレーリングストップ距離。末尾を無効にするには、0 に設定します。 |
MinCrossDistancePoints |
クロスを検証するために必要な平均間の最小分離。 |
ReverseCondition |
インジケーターを変更せずに、強気ルールと弱気ルールを交換します。 |
ConfirmedOnEntry |
前の閉じたバーのシグナルを検証します。すぐに確認できるように無効にします。 |
OneEntryPerBar |
ローソクごとに最大 1 つの新しいポジションを許可します。 |
StopAndReverse |
反対の信号が表示されたら、現在位置を閉じて反転します。 |
PureSar |
ストップロス、テイクプロフィット、トレーリングストップロジックを無効にします。 |
UseHourTrade, StartHour, EndHour |
エントリを特定の時間範囲に制限するセッション フィルター。 |
TradeVolume |
BuyMarket と SellMarket が使用した注文量。 |
CandleType |
インジケーター計算用に登録されたローソク足シリーズ。 |
変換メモ
- 価格ベースの距離は MetaTrader ポイントで表されます。ヘルパー
GetPriceOffsetは、証券価格ステップまたは小数精度を使用して、これらの値を StockSharp 価格に変換します。これにより、手段に関係なく、戦略の動作が元の EA と一致するようになります。 - StockSharp の高レベルの戦略は完成したローソク足で動作するため、トレーリング ストップは内部で管理されます。この決定論的なアプローチにより、ローソク足を使用したバックテストで意図した MT4 トレーリング ロジックが確実に再現されます。
- 変換リクエストに一致する Python ポートは含まれていません。このパッケージでは、C# 実装と多言語ドキュメントのみが提供されます。
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()