Universal MA Cross 策略
概述
Universal MA Cross Strategy 将原始的 MQL5 专家顾问 “UniversalMACrossEA” 移植到 StockSharp 的高级策略框架。该策略比较一条快速与一条慢速移动平均线,并允许为每条均线分别设置周期、平滑方法和价格类型。附加参数可控制信号确认方式、是否立即反向、风险管理以及允许交易的时间窗口。
交易逻辑
指标计算
- 在所选的 K 线序列上计算两条移动平均线。每条均线都可以拥有自己的周期、平滑方法(SMA、EMA、SMMA 或 LWMA)以及价格类型(收盘价、开盘价、最高价、最低价、中价、典型价或加权价)。
- MinCrossDistance 要求在产生交叉的那个 K 线上,两条均线之间的距离至少达到指定的价差。
- 启用 ConfirmedOnEntry 时,交叉信号使用前两个已经完成的 K 线进行验证(对应原始 EA 中的索引 2 与 1)。关闭该选项时,当前完成的 K 线与上一根 K 线比较,以模拟 MQL 中的“实时”模式。
- ReverseCondition 会交换做多与做空的规则,不需要修改任何指标设置。
入场规则
- 当快速均线向上穿越慢速均线,且差值不少于 MinCrossDistance 时开多;向下穿越且差值足够时开空。
- 若启用了 StopAndReverse,在收到相反信号时会先平掉当前仓位,再考虑新的订单。
- 当 OneEntryPerBar 为
true时,策略会记录最近一次入场的 K 线时间,在同一根 K 线内拒绝再次开仓。 - 每笔交易的下单数量由 Volume 参数决定。
仓位管理
- 止损与止盈以绝对价格距离表示,在 PureSar 模式下会被忽略,这与原专家中的 “Pure SAR” 设置一致。
- 当价格相对入场价运行 TrailingStop + TrailingStep 之后启动移动止损;之后每当价格额外前进至少 TrailingStep,止损就会向盈利方向收紧 TrailingStop 的距离。在 PureSar 模式下不会启用移动止损。
- 每根已完成的 K 线都会检查保护水平。如果该 K 线的高低区间触及止损或止盈,仓位会以市价单平仓。
交易时段过滤
- 当 UseHourTrade 启用时,只在 K 线开盘时间位于 StartHour 与 EndHour(包含边界)之间时才允许开仓。即使在时段外,移动止损仍会更新,但不会触发新的入场或“止损反手”。
参数说明
| 参数 | 说明 |
|---|---|
FastMaPeriod, SlowMaPeriod |
快速与慢速移动平均线的周期。 |
FastMaType, SlowMaType |
均线类型:简单、指数、平滑(RMA)或线性加权。 |
FastPriceType, SlowPriceType |
输入到均线的价格类型。 |
StopLoss, TakeProfit |
以价格单位表示的止损与止盈,设为 0 表示关闭。 |
TrailingStop, TrailingStep |
移动止损的偏移量,以及再次移动前所需的额外行程。 |
MinCrossDistance |
交叉时两条均线之间的最小距离。 |
ReverseCondition |
交换多空条件。 |
ConfirmedOnEntry |
仅使用已完成的 K 线确认信号。 |
OneEntryPerBar |
每根 K 线最多只允许一次入场。 |
StopAndReverse |
在相反信号出现时先平仓再反向开仓。 |
PureSar |
关闭止损、止盈与移动止损逻辑。 |
UseHourTrade, StartHour, EndHour |
交易时间过滤(0–23 小时制)。 |
Volume |
每次下单的数量。 |
CandleType |
订阅并用于计算的 K 线类型。 |
转换说明
- 由于 StockSharp 的高级策略基于完成的 K 线运行,保护性订单通过检测 K 线的最高价与最低价来模拟,从而在不使用低级 API 的情况下再现原始 EA 的行为。
- 移动止损的调整与 MQL 实现一致:只有在价格运行了 TrailingStop + TrailingStep 之后才会移动止损。
- 按照要求,此次转换未提供 Python 版本。
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>
/// Universal moving average crossover strategy converted from the original MQL version.
/// The strategy trades based on a fast and a slow moving average with optional signal confirmation,
/// stop-and-reverse behaviour, trailing stop management and time filtering.
/// </summary>
public class UniversalMaCrossStrategy : Strategy
{
/// <summary>
/// Moving average calculation methods supported by the strategy.
/// </summary>
public enum MovingAverageMethods
{
Simple,
Exponential,
Smoothed,
LinearWeighted
}
/// <summary>
/// Price sources that can feed the moving averages.
/// </summary>
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> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _trailingStop;
private readonly StrategyParam<decimal> _trailingStep;
private readonly StrategyParam<decimal> _minCrossDistance;
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<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>
/// Fast moving average method.
/// </summary>
public MovingAverageMethods FastMaType
{
get => _fastMaType.Value;
set => _fastMaType.Value = value;
}
/// <summary>
/// Slow moving average method.
/// </summary>
public MovingAverageMethods SlowMaType
{
get => _slowMaType.Value;
set => _slowMaType.Value = value;
}
/// <summary>
/// Price type used for the fast moving average.
/// </summary>
public AppliedPrices FastPriceType
{
get => _fastPriceType.Value;
set => _fastPriceType.Value = value;
}
/// <summary>
/// Price type used for the slow moving average.
/// </summary>
public AppliedPrices SlowPriceType
{
get => _slowPriceType.Value;
set => _slowPriceType.Value = value;
}
/// <summary>
/// Stop-loss distance in price units.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Take-profit distance in price units.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Trailing stop distance in price units.
/// </summary>
public decimal TrailingStop
{
get => _trailingStop.Value;
set => _trailingStop.Value = value;
}
/// <summary>
/// Additional move required before shifting the trailing stop.
/// </summary>
public decimal TrailingStep
{
get => _trailingStep.Value;
set => _trailingStep.Value = value;
}
/// <summary>
/// Minimum distance between the averages to validate a crossover.
/// </summary>
public decimal MinCrossDistance
{
get => _minCrossDistance.Value;
set => _minCrossDistance.Value = value;
}
/// <summary>
/// Reverse buy and sell conditions.
/// </summary>
public bool ReverseCondition
{
get => _reverseCondition.Value;
set => _reverseCondition.Value = value;
}
/// <summary>
/// Confirm signals on closed candles only.
/// </summary>
public bool ConfirmedOnEntry
{
get => _confirmedOnEntry.Value;
set => _confirmedOnEntry.Value = value;
}
/// <summary>
/// Limit the strategy to a single entry per bar.
/// </summary>
public bool OneEntryPerBar
{
get => _oneEntryPerBar.Value;
set => _oneEntryPerBar.Value = value;
}
/// <summary>
/// Close the current trade and reverse when an opposite signal appears.
/// </summary>
public bool StopAndReverse
{
get => _stopAndReverse.Value;
set => _stopAndReverse.Value = value;
}
/// <summary>
/// Disable protective orders and rely purely on signal reversals.
/// </summary>
public bool PureSar
{
get => _pureSar.Value;
set => _pureSar.Value = value;
}
/// <summary>
/// Enable trading only within the selected hours.
/// </summary>
public bool UseHourTrade
{
get => _useHourTrade.Value;
set => _useHourTrade.Value = value;
}
/// <summary>
/// Hour when trading can start (0-23).
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Hour when trading must end (0-23).
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="UniversalMaCrossStrategy"/>.
/// </summary>
public UniversalMaCrossStrategy()
{
_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Fast MA Period", "Fast moving average length", "Indicators")
.SetOptimize(5, 30, 1);
_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
.SetGreaterThanZero()
.SetDisplay("Slow MA Period", "Slow moving average length", "Indicators")
.SetOptimize(30, 200, 5);
_fastMaType = Param(nameof(FastMaType), MovingAverageMethods.Exponential)
.SetDisplay("Fast MA Type", "Method for fast average", "Indicators");
_slowMaType = Param(nameof(SlowMaType), MovingAverageMethods.Exponential)
.SetDisplay("Slow MA Type", "Method for slow average", "Indicators");
_fastPriceType = Param(nameof(FastPriceType), AppliedPrices.Close)
.SetDisplay("Fast Price Type", "Price source for fast MA", "Indicators");
_slowPriceType = Param(nameof(SlowPriceType), AppliedPrices.Close)
.SetDisplay("Slow Price Type", "Price source for slow MA", "Indicators");
_stopLoss = Param(nameof(StopLoss), 0m)
.SetDisplay("Stop Loss", "Stop-loss distance in price", "Risk");
_takeProfit = Param(nameof(TakeProfit), 0m)
.SetDisplay("Take Profit", "Take-profit distance in price", "Risk");
_trailingStop = Param(nameof(TrailingStop), 0m)
.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk");
_trailingStep = Param(nameof(TrailingStep), 0m)
.SetDisplay("Trailing Step", "Additional move before trailing", "Risk");
_minCrossDistance = Param(nameof(MinCrossDistance), 0m)
.SetDisplay("Min Cross Distance", "Minimum distance between averages", "Filters");
_reverseCondition = Param(nameof(ReverseCondition), false)
.SetDisplay("Reverse Signals", "Swap long and short conditions", "General");
_confirmedOnEntry = Param(nameof(ConfirmedOnEntry), true)
.SetDisplay("Confirmed On Entry", "Use closed candles for signals", "General");
_oneEntryPerBar = Param(nameof(OneEntryPerBar), true)
.SetDisplay("One Entry Per Bar", "Allow only one entry per candle", "General");
_stopAndReverse = Param(nameof(StopAndReverse), true)
.SetDisplay("Stop And Reverse", "Close and reverse on opposite signal", "Risk");
_pureSar = Param(nameof(PureSar), false)
.SetDisplay("Pure SAR", "Disable stop-loss, take-profit and trailing", "Risk");
_useHourTrade = Param(nameof(UseHourTrade), false)
.SetDisplay("Use Hour Filter", "Limit trading by session hours", "Session");
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Trading window start hour", "Session");
_endHour = Param(nameof(EndHour), 23)
.SetDisplay("End Hour", "Trading window end hour", "Session");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle subscription", "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;
ResetProtection();
}
/// <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);
DrawOwnTrades(area);
}
}
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 });
var slowResult = _slowMa.Process(new DecimalIndicatorValue(_slowMa, slowPrice, candle.OpenTime) { IsFinal = true });
if (fastResult.IsEmpty || slowResult.IsEmpty)
return;
var fastValue = fastResult.ToDecimal();
var slowValue = slowResult.ToDecimal();
var prevFast = _fastPrev;
var prevSlow = _slowPrev;
var prevFastPrev = _fastPrevPrev;
var prevSlowPrev = _slowPrevPrev;
_fastPrevPrev = prevFast;
_slowPrevPrev = prevSlow;
_fastPrev = fastValue;
_slowPrev = slowValue;
bool crossUp = false;
bool crossDown = false;
if (ConfirmedOnEntry)
{
if (prevFast.HasValue && prevSlow.HasValue && prevFastPrev.HasValue && prevSlowPrev.HasValue)
{
var fastPrevPrevValue = prevFastPrev.Value;
var slowPrevPrevValue = prevSlowPrev.Value;
var fastPrevValue = prevFast.Value;
var slowPrevValue = prevSlow.Value;
var diff = fastPrevValue - slowPrevValue;
crossUp = fastPrevPrevValue < slowPrevPrevValue && fastPrevValue > slowPrevValue && diff >= MinCrossDistance;
crossDown = fastPrevPrevValue > slowPrevPrevValue && fastPrevValue < slowPrevValue && -diff >= MinCrossDistance;
}
}
else
{
if (prevFast.HasValue && prevSlow.HasValue)
{
var fastPrevValue = prevFast.Value;
var slowPrevValue = prevSlow.Value;
var diff = fastValue - slowValue;
crossUp = fastPrevValue < slowPrevValue && fastValue > slowValue && diff >= MinCrossDistance;
crossDown = fastPrevValue > slowPrevValue && fastValue < slowValue && -diff >= MinCrossDistance;
}
}
bool buySignal;
bool sellSignal;
if (!ReverseCondition)
{
buySignal = crossUp;
sellSignal = crossDown;
}
else
{
buySignal = crossDown;
sellSignal = crossUp;
}
var canTrade = IsWithinTradingHours(candle);
if (!canTrade)
return;
if (StopAndReverse && Position != 0)
{
if ((_lastTrade == TradeDirections.Long && sellSignal) || (_lastTrade == TradeDirections.Short && buySignal))
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
ResetProtection();
}
}
if (Position != 0)
return;
var entryAllowed = !OneEntryPerBar || _lastEntryBar != candle.OpenTime;
if (!entryAllowed)
return;
if (buySignal)
{
BuyMarket(Volume);
SetProtectionLevels(candle.ClosePrice, true);
_lastTrade = TradeDirections.Long;
_lastEntryBar = candle.OpenTime;
}
else if (sellSignal)
{
SellMarket(Volume);
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)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
ResetProtection();
return;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
ResetProtection();
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
ResetProtection();
return;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
ResetProtection();
}
}
}
private void UpdateTrailingStop(ICandleMessage candle)
{
if (PureSar || TrailingStop <= 0m || !_entryPrice.HasValue)
return;
var activationDistance = TrailingStop + TrailingStep;
if (Position > 0)
{
if (candle.ClosePrice - _entryPrice.Value > activationDistance)
{
var activationLevel = candle.ClosePrice - activationDistance;
if (!_stopPrice.HasValue || _stopPrice.Value < activationLevel)
{
var newStop = candle.ClosePrice - TrailingStop;
_stopPrice = _stopPrice.HasValue ? Math.Max(_stopPrice.Value, newStop) : newStop;
}
}
}
else if (Position < 0)
{
if (_entryPrice.Value - candle.ClosePrice > activationDistance)
{
var activationLevel = candle.ClosePrice + activationDistance;
if (!_stopPrice.HasValue || _stopPrice.Value > activationLevel)
{
var newStop = candle.ClosePrice + TrailingStop;
_stopPrice = _stopPrice.HasValue ? Math.Min(_stopPrice.Value, newStop) : newStop;
}
}
}
}
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 length)
{
return method switch
{
MovingAverageMethods.Simple => new SimpleMovingAverage { Length = length },
MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = length },
MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = length },
MovingAverageMethods.LinearWeighted => new WeightedMovingAverage { Length = length },
_ => new SimpleMovingAverage { Length = length }
};
}
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 stop = StopLoss;
var take = TakeProfit;
_stopPrice = stop > 0m ? (isLong ? entryPrice - stop : entryPrice + stop) : null;
_takeProfitPrice = take > 0m ? (isLong ? entryPrice + take : entryPrice - take) : null;
}
private void ResetProtection()
{
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, ExponentialMovingAverage, SmoothedMovingAverage, WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class universal_ma_cross_strategy(Strategy):
"""Universal MA crossover with manual indicator processing, trailing, SL/TP, and time filter."""
def __init__(self):
super(universal_ma_cross_strategy, self).__init__()
self._fast_period = self.Param("FastMaPeriod", 10).SetGreaterThanZero().SetDisplay("Fast MA Period", "Fast MA length", "Indicators")
self._slow_period = self.Param("SlowMaPeriod", 80).SetGreaterThanZero().SetDisplay("Slow MA Period", "Slow MA length", "Indicators")
self._sl = self.Param("StopLoss", 0).SetDisplay("Stop Loss", "SL distance in price", "Risk")
self._tp = self.Param("TakeProfit", 0).SetDisplay("Take Profit", "TP distance in price", "Risk")
self._trailing_stop = self.Param("TrailingStop", 0).SetDisplay("Trailing Stop", "Trailing stop distance", "Risk")
self._trailing_step = self.Param("TrailingStep", 0).SetDisplay("Trailing Step", "Additional move before trailing", "Risk")
self._min_cross_dist = self.Param("MinCrossDistance", 0).SetDisplay("Min Cross Distance", "Min distance between averages", "Filters")
self._stop_and_reverse = self.Param("StopAndReverse", True).SetDisplay("Stop And Reverse", "Reverse on opposite signal", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(universal_ma_cross_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._entry_price = None
self._stop_price = None
self._take_price = None
self._last_trade = 0 # 0=none, 1=long, -1=short
def OnStarted2(self, time):
super(universal_ma_cross_strategy, self).OnStarted2(time)
self._fast_prev = None
self._fast_prev_prev = None
self._slow_prev = None
self._slow_prev_prev = None
self._entry_price = None
self._stop_price = None
self._take_price = None
self._last_trade = 0
self._fast_ma = ExponentialMovingAverage()
self._fast_ma.Length = self._fast_period.Value
self._slow_ma = ExponentialMovingAverage()
self._slow_ma.Length = self._slow_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
# Manage existing position
if self.Position != 0:
self._update_trailing(close)
if self.Position > 0:
if self._stop_price is not None and low <= self._stop_price:
self.SellMarket()
self._reset_protection()
elif self._take_price is not None and high >= self._take_price:
self.SellMarket()
self._reset_protection()
elif self.Position < 0:
if self._stop_price is not None and high >= self._stop_price:
self.BuyMarket()
self._reset_protection()
elif self._take_price is not None and low <= self._take_price:
self.BuyMarket()
self._reset_protection()
# Process indicators manually
fast_result = process_float(self._fast_ma, close, candle.OpenTime, True)
slow_result = process_float(self._slow_ma, close, candle.OpenTime, True)
if fast_result.IsEmpty or slow_result.IsEmpty:
return
fast_val = float(fast_result)
slow_val = 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_val
self._slow_prev = slow_val
cross_up = False
cross_down = False
min_dist = self._min_cross_dist.Value
# Confirmed mode: use prev-prev vs prev
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_dist
cross_down = prev_fast_prev > prev_slow_prev and prev_fast < prev_slow and -diff >= min_dist
buy_signal = cross_up
sell_signal = cross_down
# Stop and reverse
if self._stop_and_reverse.Value and self.Position != 0:
if (self._last_trade == 1 and sell_signal) or (self._last_trade == -1 and buy_signal):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._reset_protection()
if self.Position != 0:
return
if buy_signal:
self.BuyMarket()
self._set_protection(close, True)
self._last_trade = 1
elif sell_signal:
self.SellMarket()
self._set_protection(close, False)
self._last_trade = -1
def _set_protection(self, entry, is_long):
self._entry_price = entry
sl = self._sl.Value
tp = self._tp.Value
self._stop_price = (entry - sl if is_long else entry + sl) if sl > 0 else None
self._take_price = (entry + tp if is_long else entry - tp) if tp > 0 else None
def _reset_protection(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
def _update_trailing(self, close):
trail = self._trailing_stop.Value
step = self._trailing_step.Value
if trail <= 0 or self._entry_price is None:
return
activation = trail + step
if self.Position > 0:
if close - self._entry_price > activation:
new_stop = close - trail
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
elif self.Position < 0:
if self._entry_price - close > activation:
new_stop = close + trail
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
def CreateClone(self):
return universal_ma_cross_strategy()