Macd Pattern Trader v02(StockSharp 版本)
本策略基于 MetaTrader 专家顾问 MacdPatternTraderv02.mq4(目录 MQL/8194)移植到 StockSharp 高级 API。它保留了原有的 MACD 形态识别以及分批平仓逻辑,同时提供结构化参数以便优化。
核心思路
- 使用
FastEmaPeriod与SlowEmaPeriod计算 MACD 主线,信号线长度固定为 1 根K线,与 MQL 程序保持一致。 - 仅处理已完成的K线。当 MACD 在零轴附近形成特定的四段序列时,分别触发做空或做多的准备状态:
- 空头形态:MACD 先处于正值阶段,随后回落到
MinThreshold以上的负值,再次转折向下。 - 多头形态:MACD 先处于负值阶段,随后反弹到
MaxThreshold以下的正值,再次转折向上。
- 空头形态:MACD 先处于正值阶段,随后回落到
- 形态确认后按
TradeVolume下达市价单。 - 止损位放在最近
StopLossBars根完成K线的极值之外,并额外加上OffsetPoints个最小报价点。 - 止盈位通过连续扫描长度为
TakeProfitBars的窗口来寻找新的极值,直到不再出现更极端的价格。 - 分批止盈模块与原程序一致:利润达到 5 个点后,如果上一根K线收盘价突破
Ema2Period均线,则减仓三分之一;若上一根K线触及(SMA + EMA3) / 2,再减仓剩余的一半。
参数
| 参数 | 说明 |
|---|---|
StopLossBars |
计算止损极值所使用的已完成K线数量。 |
TakeProfitBars |
顺序搜索止盈目标时的窗口长度。 |
OffsetPoints |
在止损极值基础上附加的点数。 |
FastEmaPeriod |
MACD 主线的快速 EMA 周期。 |
SlowEmaPeriod |
MACD 主线的慢速 EMA 周期。 |
MaxThreshold |
结束空头准备状态的正向阈值。 |
MinThreshold |
结束多头准备状态的负向阈值。 |
Ema1Period |
原策略资金管理模块中的第一条 EMA(保留用于兼容性)。 |
Ema2Period |
用于判定第一次分批止盈的 EMA 周期。 |
SmaPeriod |
与 Ema3Period 共同计算第二次分批止盈均线的 SMA 周期。 |
Ema3Period |
与 SMA 配对的慢速 EMA 周期。 |
TradeVolume |
下单的合约数量(手数)。 |
CandleType |
供指标使用的K线数据类型。 |
交易流程
- 做空:当 MACD 序列
(prev3, prev2, prev1, current)满足原始条件(macdPrev1 < macdPrev3、macdPrev1 > macdPrev2、current < prev1、current < 0且通过幅度检查)时触发。如果当前持有多头,会先平掉多头再开空。 - 做多:逻辑对称(
current > 0等条件),若持有空头则先平仓。 - 止损/止盈:进场后立即计算,不会在持仓过程中重新评估。
- 分批平仓:盈利达到 5 个点后,若上一根K线收盘价站上
EMA2,减仓三分之一;若上一根K线触及(SMA + EMA3) / 2,再减仓剩余的一半。 - 强制退出:价格触及止损或止盈时立即全平仓位,并重置内部状态。
补充说明
- 点值优先使用
Security.PriceStep,如无该信息,则根据品种的小数位推导;若仍无法获得,则使用默认值0.0001。 - 策略会缓存最多 1024 根完成K线,以模拟 MQL 中的
iHighest、iLowest与原版TakeProfit()的分段搜索。 - 代码中的注释全部使用英文,满足仓库的统一要求。
- 按任务要求未创建 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>
/// MACD pattern trader strategy converted from the "MacdPatternTraderv02" MetaTrader expert.
/// The strategy monitors the MACD main line and opens positions when the characteristic reversal pattern appears.
/// It also manages open positions using the original partial close logic based on moving averages.
/// </summary>
public class MacdPatternTraderV02Strategy : Strategy
{
private readonly StrategyParam<decimal> _profitThresholdPoints;
private readonly StrategyParam<int> _maxHistory;
private readonly StrategyParam<int> _stopLossBars;
private readonly StrategyParam<int> _takeProfitBars;
private readonly StrategyParam<int> _offsetPoints;
private readonly StrategyParam<int> _fastEmaPeriod;
private readonly StrategyParam<int> _slowEmaPeriod;
private readonly StrategyParam<decimal> _maxThreshold;
private readonly StrategyParam<decimal> _minThreshold;
private readonly StrategyParam<int> _ema1Period;
private readonly StrategyParam<int> _ema2Period;
private readonly StrategyParam<int> _smaPeriod;
private readonly StrategyParam<int> _ema3Period;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private MovingAverageConvergenceDivergence _macd = null!;
private ExponentialMovingAverage _ema1 = null!;
private ExponentialMovingAverage _ema2 = null!;
private SimpleMovingAverage _sma = null!;
private ExponentialMovingAverage _ema3 = null!;
private readonly List<ICandleMessage> _history = new();
private decimal? _ema1Prev;
private decimal? _ema2Prev;
private decimal? _smaPrev;
private decimal? _ema3Prev;
private decimal? _ema1Last;
private decimal? _ema2Last;
private decimal? _smaLast;
private decimal? _ema3Last;
private decimal? _macdPrev1;
private decimal? _macdPrev2;
private decimal? _macdPrev3;
private bool _maxThresholdReached;
private bool _minThresholdReached;
private bool _sellPatternReady;
private bool _buyPatternReady;
private decimal _patternMinValue;
private decimal _patternMaxValue;
private decimal _pointSize;
private int _entryDirection;
private decimal _entryPrice;
private decimal _openVolume;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private int _longPartialStage;
private int _shortPartialStage;
/// <summary>
/// Number of bars used to calculate protective stop-loss levels.
/// </summary>
public int StopLossBars
{
get => _stopLossBars.Value;
set => _stopLossBars.Value = value;
}
/// <summary>
/// Number of bars evaluated when searching for take-profit targets.
/// </summary>
public int TakeProfitBars
{
get => _takeProfitBars.Value;
set => _takeProfitBars.Value = value;
}
/// <summary>
/// Offset applied to stop-loss levels expressed in price points.
/// </summary>
public int OffsetPoints
{
get => _offsetPoints.Value;
set => _offsetPoints.Value = value;
}
/// <summary>
/// Minimal profit in points required before partial exits are considered.
/// </summary>
public decimal ProfitThresholdPoints
{
get => _profitThresholdPoints.Value;
set => _profitThresholdPoints.Value = value;
}
/// <summary>
/// Fast EMA period for the MACD main line.
/// </summary>
public int FastEmaPeriod
{
get => _fastEmaPeriod.Value;
set => _fastEmaPeriod.Value = value;
}
/// <summary>
/// Slow EMA period for the MACD main line.
/// </summary>
public int SlowEmaPeriod
{
get => _slowEmaPeriod.Value;
set => _slowEmaPeriod.Value = value;
}
/// <summary>
/// Upper MACD threshold that arms the short pattern.
/// </summary>
public decimal MaxThreshold
{
get => _maxThreshold.Value;
set => _maxThreshold.Value = value;
}
/// <summary>
/// Lower MACD threshold that arms the long pattern.
/// </summary>
public decimal MinThreshold
{
get => _minThreshold.Value;
set => _minThreshold.Value = value;
}
/// <summary>
/// Period of the first EMA used in the partial close logic.
/// </summary>
public int Ema1Period
{
get => _ema1Period.Value;
set => _ema1Period.Value = value;
}
/// <summary>
/// Period of the second EMA used in the partial close logic.
/// </summary>
public int Ema2Period
{
get => _ema2Period.Value;
set => _ema2Period.Value = value;
}
/// <summary>
/// Period of the SMA used to detect profit taking levels.
/// </summary>
public int SmaPeriod
{
get => _smaPeriod.Value;
set => _smaPeriod.Value = value;
}
/// <summary>
/// Period of the slow EMA used in the partial close logic.
/// </summary>
public int Ema3Period
{
get => _ema3Period.Value;
set => _ema3Period.Value = value;
}
/// <summary>
/// Trading volume applied to market orders.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Maximum number of finished candles stored in the sliding history window.
/// </summary>
public int MaxHistory
{
get => _maxHistory.Value;
set => _maxHistory.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="MacdPatternTraderV02Strategy"/> class.
/// </summary>
public MacdPatternTraderV02Strategy()
{
_stopLossBars = Param(nameof(StopLossBars), 6)
.SetGreaterThanZero()
.SetDisplay("Stop-Loss Bars", "Number of candles for stop-loss calculation", "Risk");
_takeProfitBars = Param(nameof(TakeProfitBars), 20)
.SetGreaterThanZero()
.SetDisplay("Take-Profit Bars", "Window used when scanning for take-profit", "Risk");
_offsetPoints = Param(nameof(OffsetPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Offset Points", "Additional protective offset in points", "Risk");
_profitThresholdPoints = Param(nameof(ProfitThresholdPoints), 500m)
.SetGreaterThanZero()
.SetDisplay("Profit Threshold Points", "Minimal profit in points before partial exits", "Risk");
_fastEmaPeriod = Param(nameof(FastEmaPeriod), 12)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Fast EMA period for MACD", "Indicators");
_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("Slow EMA", "Slow EMA period for MACD", "Indicators");
_maxThreshold = Param(nameof(MaxThreshold), 50m)
.SetDisplay("Upper Threshold", "Maximum MACD threshold for longs", "Signals");
_minThreshold = Param(nameof(MinThreshold), -50m)
.SetDisplay("Lower Threshold", "Minimum MACD threshold for shorts", "Signals");
_ema1Period = Param(nameof(Ema1Period), 7)
.SetGreaterThanZero()
.SetDisplay("EMA 1", "First EMA period for management", "Management");
_ema2Period = Param(nameof(Ema2Period), 21)
.SetGreaterThanZero()
.SetDisplay("EMA 2", "Second EMA period for management", "Management");
_smaPeriod = Param(nameof(SmaPeriod), 98)
.SetGreaterThanZero()
.SetDisplay("SMA", "SMA period for management", "Management");
_ema3Period = Param(nameof(Ema3Period), 365)
.SetGreaterThanZero()
.SetDisplay("EMA 3", "Slow EMA period for management", "Management");
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Market order volume", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for indicators", "General");
_maxHistory = Param(nameof(MaxHistory), 1024)
.SetGreaterThanZero()
.SetDisplay("History Limit", "Maximum candles stored for pattern recognition", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_history.Clear();
_macdPrev1 = null;
_macdPrev2 = null;
_macdPrev3 = null;
_ema1Prev = null;
_ema2Prev = null;
_smaPrev = null;
_ema3Prev = null;
_ema1Last = null;
_ema2Last = null;
_smaLast = null;
_ema3Last = null;
_maxThresholdReached = false;
_minThresholdReached = false;
_sellPatternReady = false;
_buyPatternReady = false;
_patternMinValue = 0m;
_patternMaxValue = 0m;
_pointSize = 0m;
ResetPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointSize = Security?.PriceStep ?? 0m;
if (_pointSize <= 0m)
{
var decimals = Security?.Decimals;
if (decimals.HasValue)
_pointSize = (decimal)Math.Pow(10, -decimals.Value);
}
if (_pointSize <= 0m)
_pointSize = 0.0001m;
_macd = new MovingAverageConvergenceDivergence();
_macd.ShortMa.Length = FastEmaPeriod;
_macd.LongMa.Length = SlowEmaPeriod;
_ema1 = new EMA { Length = Ema1Period };
_ema2 = new EMA { Length = Ema2Period };
_sma = new SMA { Length = SmaPeriod };
_ema3 = new EMA { Length = Ema3Period };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_macd, _ema1, _ema2, _sma, _ema3, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _macd);
DrawIndicator(area, _ema1);
DrawIndicator(area, _ema2);
DrawIndicator(area, _sma);
DrawIndicator(area, _ema3);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal macdLine, decimal ema1Value, decimal ema2Value, decimal smaValue, decimal ema3Value)
{
if (candle.State != CandleStates.Finished)
return;
_ema1Prev = _ema1Last;
_ema2Prev = _ema2Last;
_smaPrev = _smaLast;
_ema3Prev = _ema3Last;
_ema1Last = ema1Value;
_ema2Last = ema2Value;
_smaLast = smaValue;
_ema3Last = ema3Value;
if (!_macd.IsFormed)
return;
var macdLast = _macdPrev1;
var macdLast2 = _macdPrev2;
var macdLast3 = _macdPrev3;
if (macdLast is null || macdLast2 is null || macdLast3 is null)
{
_macdPrev3 = _macdPrev2;
_macdPrev2 = _macdPrev1;
_macdPrev1 = macdLine;
AddCandle(candle);
return;
}
AddCandle(candle);
ExecutePatternLogic(candle, macdLine, macdLast.Value, macdLast2.Value, macdLast3.Value);
_macdPrev3 = _macdPrev2;
_macdPrev2 = _macdPrev1;
_macdPrev1 = macdLine;
}
private void ExecutePatternLogic(ICandleMessage candle, decimal macdCurrent, decimal macdPrev1, decimal macdPrev2, decimal macdPrev3)
{
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_pointSize <= 0m)
return;
if (macdCurrent > 0m)
{
_maxThresholdReached = true;
_sellPatternReady = false;
}
if (macdCurrent > macdPrev1 && macdPrev1 < macdPrev3 && _maxThresholdReached && macdCurrent > MinThreshold && macdCurrent < 0m && !_sellPatternReady)
{
_sellPatternReady = true;
_patternMinValue = Math.Abs(macdPrev1 * 10000m);
}
var currentMagnitude = Math.Abs(macdCurrent * 10000m);
if (_sellPatternReady && macdCurrent < macdPrev1 && macdPrev1 > macdPrev3 && macdCurrent < 0m && _patternMinValue <= currentMagnitude)
{
_maxThresholdReached = false;
}
if (_sellPatternReady && macdCurrent < macdPrev1 && macdPrev1 > macdPrev3 && macdCurrent < 0m)
{
TryOpenShort(candle);
_sellPatternReady = false;
_maxThresholdReached = false;
}
if (macdCurrent < 0m)
{
_minThresholdReached = true;
_buyPatternReady = false;
}
if (macdCurrent < MaxThreshold && macdCurrent < macdPrev1 && macdPrev1 > macdPrev3 && _minThresholdReached && macdCurrent > 0m && !_buyPatternReady)
{
_buyPatternReady = true;
_patternMaxValue = Math.Abs(macdPrev1 * 10000m);
}
if (_buyPatternReady && macdCurrent > macdPrev1 && macdPrev1 < macdPrev3 && macdCurrent > 0m && _patternMaxValue <= currentMagnitude)
{
_minThresholdReached = false;
}
if (_buyPatternReady && macdCurrent > macdPrev1 && macdPrev1 < macdPrev3 && macdCurrent > 0m)
{
TryOpenLong(candle);
_buyPatternReady = false;
_minThresholdReached = false;
}
ManagePosition(candle);
}
private void TryOpenShort(ICandleMessage candle)
{
if (Position > 0m)
{
var closeVolume = NormalizeVolume(Math.Abs(Position));
if (closeVolume > 0m)
{
SellMarket(closeVolume);
ResetPositionState();
}
}
if (Position < 0m)
return;
var volume = NormalizeVolume(TradeVolume);
if (volume <= 0m)
return;
var entryPrice = candle.ClosePrice;
SellMarket(volume);
RegisterEntry(-1, entryPrice, volume);
}
private void TryOpenLong(ICandleMessage candle)
{
if (Position < 0m)
{
var closeVolume = NormalizeVolume(Math.Abs(Position));
if (closeVolume > 0m)
{
BuyMarket(closeVolume);
ResetPositionState();
}
}
if (Position > 0m)
return;
var volume = NormalizeVolume(TradeVolume);
if (volume <= 0m)
return;
var entryPrice = candle.ClosePrice;
BuyMarket(volume);
RegisterEntry(1, entryPrice, volume);
}
private void RegisterEntry(int direction, decimal entryPrice, decimal volume)
{
_entryDirection = direction;
_entryPrice = entryPrice;
_openVolume = volume;
_stopLossPrice = direction > 0 ? CalculateLongStop() : CalculateShortStop();
_takeProfitPrice = direction > 0 ? CalculateLongTarget() : CalculateShortTarget();
_longPartialStage = 0;
_shortPartialStage = 0;
}
private void ManagePosition(ICandleMessage candle)
{
if (_entryDirection == 0 || _openVolume <= 0m)
return;
if (CheckRiskManagement(candle))
return;
var previousCandle = GetCandle(1);
if (previousCandle is null || _ema2Prev is null || _ema3Prev is null || _smaPrev is null)
return;
var ema2Prev = _ema2Prev.Value;
var ema3Prev = _ema3Prev.Value;
var smaPrev = _smaPrev.Value;
var profitPoints = CalculateOpenProfitPoints(candle.ClosePrice);
if (_entryDirection > 0)
{
if (profitPoints > ProfitThresholdPoints && previousCandle.ClosePrice > ema2Prev && _longPartialStage == 0)
{
var volume = NormalizeVolume(_openVolume / 3m);
if (volume > 0m)
{
SellMarket(volume);
RegisterClose(volume, candle.ClosePrice);
_longPartialStage = 1;
}
}
else if (profitPoints > ProfitThresholdPoints && previousCandle.HighPrice > (smaPrev + ema3Prev) / 2m && _longPartialStage == 1)
{
var volume = NormalizeVolume(_openVolume / 2m);
if (volume > 0m)
{
SellMarket(volume);
RegisterClose(volume, candle.ClosePrice);
_longPartialStage = 2;
}
}
}
else if (_entryDirection < 0)
{
if (profitPoints > ProfitThresholdPoints && previousCandle.ClosePrice < ema2Prev && _shortPartialStage == 0)
{
var volume = NormalizeVolume(_openVolume / 3m);
if (volume > 0m)
{
BuyMarket(volume);
RegisterClose(volume, candle.ClosePrice);
_shortPartialStage = 1;
}
}
else if (profitPoints > ProfitThresholdPoints && previousCandle.LowPrice < (smaPrev + ema3Prev) / 2m && _shortPartialStage == 1)
{
var volume = NormalizeVolume(_openVolume / 2m);
if (volume > 0m)
{
BuyMarket(volume);
RegisterClose(volume, candle.ClosePrice);
_shortPartialStage = 2;
}
}
}
}
private bool CheckRiskManagement(ICandleMessage candle)
{
if (_entryDirection == 0 || _openVolume <= 0m)
return false;
if (_entryDirection > 0)
{
if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
{
SellMarket(_openVolume);
ResetPositionState();
return true;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
SellMarket(_openVolume);
ResetPositionState();
return true;
}
}
else
{
if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
{
BuyMarket(_openVolume);
ResetPositionState();
return true;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
BuyMarket(_openVolume);
ResetPositionState();
return true;
}
}
return false;
}
private decimal CalculateOpenProfitPoints(decimal currentPrice)
{
if (_pointSize <= 0m)
return 0m;
var difference = _entryDirection > 0 ? currentPrice - _entryPrice : _entryPrice - currentPrice;
return Math.Abs(difference / _pointSize);
}
private void RegisterClose(decimal volume, decimal price)
{
_openVolume -= volume;
if (_openVolume <= 0m || Math.Abs(Position) < 1e-6m)
ResetPositionState();
}
private void ResetPositionState()
{
_entryDirection = 0;
_entryPrice = 0m;
_openVolume = 0m;
_stopLossPrice = null;
_takeProfitPrice = null;
_longPartialStage = 0;
_shortPartialStage = 0;
}
private decimal? CalculateShortStop()
{
var candles = GetCandlesRange(StopLossBars, 1);
if (candles.Count == 0)
return null;
var highest = decimal.MinValue;
foreach (var candle in candles)
highest = Math.Max(highest, candle.HighPrice);
return highest + OffsetPoints * _pointSize;
}
private decimal? CalculateLongStop()
{
var candles = GetCandlesRange(StopLossBars, 1);
if (candles.Count == 0)
return null;
var lowest = decimal.MaxValue;
foreach (var candle in candles)
lowest = Math.Min(lowest, candle.LowPrice);
return lowest - OffsetPoints * _pointSize;
}
private decimal? CalculateShortTarget()
{
return ScanSequentialExtremum(TakeProfitBars, true);
}
private decimal? CalculateLongTarget()
{
return ScanSequentialExtremum(TakeProfitBars, false);
}
private decimal? ScanSequentialExtremum(int window, bool isShort)
{
if (window <= 0)
return null;
decimal? best = null;
var shift = 0;
while (true)
{
var candles = GetCandlesRange(window, shift);
if (candles.Count == 0)
break;
decimal candidate;
if (isShort)
{
candidate = decimal.MaxValue;
foreach (var candle in candles)
candidate = Math.Min(candidate, candle.LowPrice);
if (best is null || candidate < best)
{
best = candidate;
shift += window;
continue;
}
}
else
{
candidate = decimal.MinValue;
foreach (var candle in candles)
candidate = Math.Max(candidate, candle.HighPrice);
if (best is null || candidate > best)
{
best = candidate;
shift += window;
continue;
}
}
break;
}
return best;
}
private List<ICandleMessage> GetCandlesRange(int length, int shift)
{
var result = new List<ICandleMessage>();
if (length <= 0)
return result;
var startIndex = _history.Count - 1 - shift;
for (var i = startIndex; i >= 0 && result.Count < length; i--)
result.Add(_history[i]);
return result;
}
private ICandleMessage GetCandle(int shift)
{
var index = _history.Count - 1 - shift;
if (index < 0 || index >= _history.Count)
return null;
return _history[index];
}
private void AddCandle(ICandleMessage candle)
{
_history.Add(candle);
if (_history.Count > MaxHistory)
_history.RemoveAt(0);
}
private decimal NormalizeVolume(decimal volume)
{
var security = Security;
if (security?.VolumeStep is { } step && step > 0m)
volume = Math.Round(volume / step) * step;
if (security?.MinVolume is { } minVolume && minVolume > 0m && volume < minVolume)
return 0m;
return volume.Max(0m);
}
}
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.Strategies import Strategy
from StockSharp.Algo.Indicators import (
MovingAverageConvergenceDivergence, ExponentialMovingAverage,
SimpleMovingAverage
)
class macd_pattern_trader_v02_strategy(Strategy):
def __init__(self):
super(macd_pattern_trader_v02_strategy, self).__init__()
self._stop_loss_bars = self.Param("StopLossBars", 6).SetDisplay("Stop-Loss Bars", "Number of candles for stop-loss calculation", "Risk")
self._take_profit_bars = self.Param("TakeProfitBars", 20).SetDisplay("Take-Profit Bars", "Window used when scanning for take-profit", "Risk")
self._offset_points = self.Param("OffsetPoints", 10).SetDisplay("Offset Points", "Additional protective offset in points", "Risk")
self._profit_threshold_points = self.Param("ProfitThresholdPoints", 500.0).SetDisplay("Profit Threshold Points", "Minimal profit in points before partial exits", "Risk")
self._fast_ema_period = self.Param("FastEmaPeriod", 12).SetDisplay("Fast EMA", "Fast EMA period for MACD", "Indicators")
self._slow_ema_period = self.Param("SlowEmaPeriod", 26).SetDisplay("Slow EMA", "Slow EMA period for MACD", "Indicators")
self._max_threshold = self.Param("MaxThreshold", 50.0).SetDisplay("Upper Threshold", "Maximum MACD threshold for longs", "Signals")
self._min_threshold = self.Param("MinThreshold", -50.0).SetDisplay("Lower Threshold", "Minimum MACD threshold for shorts", "Signals")
self._ema1_period = self.Param("Ema1Period", 7).SetDisplay("EMA 1", "First EMA period for management", "Management")
self._ema2_period = self.Param("Ema2Period", 21).SetDisplay("EMA 2", "Second EMA period for management", "Management")
self._sma_period = self.Param("SmaPeriod", 98).SetDisplay("SMA", "SMA period for management", "Management")
self._ema3_period = self.Param("Ema3Period", 365).SetDisplay("EMA 3", "Slow EMA period for management", "Management")
self._trade_volume = self.Param("TradeVolume", 0.1).SetDisplay("Trade Volume", "Market order volume", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))).SetDisplay("Candle Type", "Candle type used for indicators", "General")
self._max_history_param = self.Param("MaxHistory", 1024).SetDisplay("History Limit", "Maximum candles stored", "General")
self._history = []
self._macd_prev1 = None
self._macd_prev2 = None
self._macd_prev3 = None
self._ema1_prev = None
self._ema2_prev = None
self._sma_prev = None
self._ema3_prev = None
self._ema1_last = None
self._ema2_last = None
self._sma_last = None
self._ema3_last = None
self._max_threshold_reached = False
self._min_threshold_reached = False
self._sell_pattern_ready = False
self._buy_pattern_ready = False
self._pattern_min_value = 0.0
self._pattern_max_value = 0.0
self._point_size = 0.0
self._entry_direction = 0
self._entry_price = 0.0
self._open_volume = 0.0
self._stop_loss_price = None
self._take_profit_price = None
self._long_partial_stage = 0
self._short_partial_stage = 0
@property
def StopLossBars(self): return self._stop_loss_bars.Value
@property
def TakeProfitBars(self): return self._take_profit_bars.Value
@property
def OffsetPoints(self): return self._offset_points.Value
@property
def ProfitThresholdPoints(self): return self._profit_threshold_points.Value
@property
def FastEmaPeriod(self): return self._fast_ema_period.Value
@property
def SlowEmaPeriod(self): return self._slow_ema_period.Value
@property
def MaxThreshold(self): return self._max_threshold.Value
@property
def MinThreshold(self): return self._min_threshold.Value
@property
def Ema1Period(self): return self._ema1_period.Value
@property
def Ema2Period(self): return self._ema2_period.Value
@property
def SmaPeriod(self): return self._sma_period.Value
@property
def Ema3Period(self): return self._ema3_period.Value
@property
def TradeVolume(self): return self._trade_volume.Value
@property
def CandleType(self): return self._candle_type.Value
@property
def MaxHistoryParam(self): return self._max_history_param.Value
def OnStarted2(self, time):
super(macd_pattern_trader_v02_strategy, self).OnStarted2(time)
ps = self.Security.PriceStep if self.Security is not None else None
self._point_size = float(ps) if ps is not None and float(ps) > 0 else 0.0001
macd = MovingAverageConvergenceDivergence()
macd.ShortMa.Length = self.FastEmaPeriod
macd.LongMa.Length = self.SlowEmaPeriod
ema1 = ExponentialMovingAverage()
ema1.Length = self.Ema1Period
ema2 = ExponentialMovingAverage()
ema2.Length = self.Ema2Period
sma = SimpleMovingAverage()
sma.Length = self.SmaPeriod
ema3 = ExponentialMovingAverage()
ema3.Length = self.Ema3Period
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(macd, ema1, ema2, sma, ema3, self.ProcessCandle).Start()
def ProcessCandle(self, candle, macd_line, ema1_value, ema2_value, sma_value, ema3_value):
if candle.State != CandleStates.Finished:
return
mv = float(macd_line)
self._ema1_prev = self._ema1_last
self._ema2_prev = self._ema2_last
self._sma_prev = self._sma_last
self._ema3_prev = self._ema3_last
self._ema1_last = float(ema1_value)
self._ema2_last = float(ema2_value)
self._sma_last = float(sma_value)
self._ema3_last = float(ema3_value)
if self._macd_prev1 is None or self._macd_prev2 is None or self._macd_prev3 is None:
self._macd_prev3 = self._macd_prev2
self._macd_prev2 = self._macd_prev1
self._macd_prev1 = mv
self._add_candle(candle)
return
ml, ml2, ml3 = self._macd_prev1, self._macd_prev2, self._macd_prev3
self._add_candle(candle)
self._execute_pattern_logic(candle, mv, ml, ml2, ml3)
self._macd_prev3 = self._macd_prev2
self._macd_prev2 = self._macd_prev1
self._macd_prev1 = mv
def _execute_pattern_logic(self, candle, mc, mp1, mp2, mp3):
if self._point_size <= 0:
return
min_thr = float(self.MinThreshold)
max_thr = float(self.MaxThreshold)
if mc > 0:
self._max_threshold_reached = True
self._sell_pattern_ready = False
if mc > mp1 and mp1 < mp3 and self._max_threshold_reached and mc > min_thr and mc < 0 and not self._sell_pattern_ready:
self._sell_pattern_ready = True
self._pattern_min_value = abs(mp1 * 10000.0)
cm = abs(mc * 10000.0)
if self._sell_pattern_ready and mc < mp1 and mp1 > mp3 and mc < 0 and self._pattern_min_value <= cm:
self._max_threshold_reached = False
if self._sell_pattern_ready and mc < mp1 and mp1 > mp3 and mc < 0:
self._try_open_short(candle)
self._sell_pattern_ready = False
self._max_threshold_reached = False
if mc < 0:
self._min_threshold_reached = True
self._buy_pattern_ready = False
if mc < max_thr and mc < mp1 and mp1 > mp3 and self._min_threshold_reached and mc > 0 and not self._buy_pattern_ready:
self._buy_pattern_ready = True
self._pattern_max_value = abs(mp1 * 10000.0)
if self._buy_pattern_ready and mc > mp1 and mp1 < mp3 and mc > 0 and self._pattern_max_value <= cm:
self._min_threshold_reached = False
if self._buy_pattern_ready and mc > mp1 and mp1 < mp3 and mc > 0:
self._try_open_long(candle)
self._buy_pattern_ready = False
self._min_threshold_reached = False
self._manage_position(candle)
def _try_open_short(self, candle):
if self.Position > 0:
self.SellMarket(abs(self.Position))
self._reset_position_state()
if self.Position < 0:
return
volume = float(self.TradeVolume)
if volume <= 0:
return
self.SellMarket(volume)
self._register_entry(-1, float(candle.ClosePrice), volume)
def _try_open_long(self, candle):
if self.Position < 0:
self.BuyMarket(abs(self.Position))
self._reset_position_state()
if self.Position > 0:
return
volume = float(self.TradeVolume)
if volume <= 0:
return
self.BuyMarket(volume)
self._register_entry(1, float(candle.ClosePrice), volume)
def _register_entry(self, direction, entry_price, volume):
self._entry_direction = direction
self._entry_price = entry_price
self._open_volume = volume
self._stop_loss_price = self._calculate_long_stop() if direction > 0 else self._calculate_short_stop()
self._take_profit_price = self._calculate_long_target() if direction > 0 else self._calculate_short_target()
self._long_partial_stage = 0
self._short_partial_stage = 0
def _manage_position(self, candle):
if self._entry_direction == 0 or self._open_volume <= 0:
return
if self._check_risk_management(candle):
return
prev_candle = self._get_candle(1)
if prev_candle is None or self._ema2_prev is None or self._ema3_prev is None or self._sma_prev is None:
return
pp = self._calculate_open_profit_points(float(candle.ClosePrice))
pt = float(self.ProfitThresholdPoints)
if self._entry_direction > 0:
if pp > pt and float(prev_candle.ClosePrice) > self._ema2_prev and self._long_partial_stage == 0:
v = self._open_volume / 3.0
if v > 0:
self.SellMarket(v)
self._register_close(v)
self._long_partial_stage = 1
elif pp > pt and float(prev_candle.HighPrice) > (self._sma_prev + self._ema3_prev) / 2.0 and self._long_partial_stage == 1:
v = self._open_volume / 2.0
if v > 0:
self.SellMarket(v)
self._register_close(v)
self._long_partial_stage = 2
elif self._entry_direction < 0:
if pp > pt and float(prev_candle.ClosePrice) < self._ema2_prev and self._short_partial_stage == 0:
v = self._open_volume / 3.0
if v > 0:
self.BuyMarket(v)
self._register_close(v)
self._short_partial_stage = 1
elif pp > pt and float(prev_candle.LowPrice) < (self._sma_prev + self._ema3_prev) / 2.0 and self._short_partial_stage == 1:
v = self._open_volume / 2.0
if v > 0:
self.BuyMarket(v)
self._register_close(v)
self._short_partial_stage = 2
def _check_risk_management(self, candle):
if self._entry_direction == 0 or self._open_volume <= 0:
return False
if self._entry_direction > 0:
if self._stop_loss_price is not None and float(candle.LowPrice) <= self._stop_loss_price:
self.SellMarket(self._open_volume)
self._reset_position_state()
return True
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket(self._open_volume)
self._reset_position_state()
return True
else:
if self._stop_loss_price is not None and float(candle.HighPrice) >= self._stop_loss_price:
self.BuyMarket(self._open_volume)
self._reset_position_state()
return True
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket(self._open_volume)
self._reset_position_state()
return True
return False
def _calculate_open_profit_points(self, current_price):
if self._point_size <= 0:
return 0.0
diff = current_price - self._entry_price if self._entry_direction > 0 else self._entry_price - current_price
return abs(diff / self._point_size)
def _register_close(self, volume):
self._open_volume -= volume
if self._open_volume <= 0 or abs(self.Position) < 1e-6:
self._reset_position_state()
def _reset_position_state(self):
self._entry_direction = 0
self._entry_price = 0.0
self._open_volume = 0.0
self._stop_loss_price = None
self._take_profit_price = None
self._long_partial_stage = 0
self._short_partial_stage = 0
def _calculate_short_stop(self):
candles = self._get_candles_range(int(self.StopLossBars), 1)
if len(candles) == 0:
return None
return max(float(c.HighPrice) for c in candles) + int(self.OffsetPoints) * self._point_size
def _calculate_long_stop(self):
candles = self._get_candles_range(int(self.StopLossBars), 1)
if len(candles) == 0:
return None
return min(float(c.LowPrice) for c in candles) - int(self.OffsetPoints) * self._point_size
def _calculate_short_target(self):
return self._scan_sequential_extremum(int(self.TakeProfitBars), True)
def _calculate_long_target(self):
return self._scan_sequential_extremum(int(self.TakeProfitBars), False)
def _scan_sequential_extremum(self, window, is_short):
if window <= 0:
return None
best = None
shift = 0
while True:
candles = self._get_candles_range(window, shift)
if len(candles) == 0:
break
if is_short:
candidate = min(float(c.LowPrice) for c in candles)
if best is None or candidate < best:
best = candidate
shift += window
continue
else:
candidate = max(float(c.HighPrice) for c in candles)
if best is None or candidate > best:
best = candidate
shift += window
continue
break
return best
def _get_candles_range(self, length, shift):
result = []
if length <= 0:
return result
i = len(self._history) - 1 - shift
while i >= 0 and len(result) < length:
result.append(self._history[i])
i -= 1
return result
def _get_candle(self, shift):
idx = len(self._history) - 1 - shift
if idx < 0 or idx >= len(self._history):
return None
return self._history[idx]
def _add_candle(self, candle):
self._history.append(candle)
mh = int(self.MaxHistoryParam)
if len(self._history) > mh:
self._history.pop(0)
def OnReseted(self):
super(macd_pattern_trader_v02_strategy, self).OnReseted()
self._history = []
self._macd_prev1 = None
self._macd_prev2 = None
self._macd_prev3 = None
self._ema1_prev = None
self._ema2_prev = None
self._sma_prev = None
self._ema3_prev = None
self._ema1_last = None
self._ema2_last = None
self._sma_last = None
self._ema3_last = None
self._max_threshold_reached = False
self._min_threshold_reached = False
self._sell_pattern_ready = False
self._buy_pattern_ready = False
self._pattern_min_value = 0.0
self._pattern_max_value = 0.0
self._point_size = 0.0
self._reset_position_state()
def CreateClone(self):
return macd_pattern_trader_v02_strategy()