在 GitHub 上查看

UP3x1 Premium 策略

UP3x1 Premium 策略是 MetaTrader 指标 up3x1_premium_v2M 的 StockSharp 版本。策略结合快/慢 EMA 交叉、宽幅 K 线过滤以及日线趋势背景,在捕捉动量突破的同时使用固定目标和移动止损控制风险。

工作流程

  1. 趋势识别

    • 在工作级别计算两条 EMA(默认 12 和 26 周期)。
    • 保存前两个 EMA 数值,用于重现原始 MQL 中的交叉判定。
    • 在日线级别计算一条 EMA,用于判断大周期方向。
  2. 入场条件

    • 出现下列任一情形即触发做多:
      • 快 EMA 从下向上穿越慢 EMA,且前两根 K 线开盘价逐步抬高。
      • 上一根 K 线为宽幅阳线,实体大于设定的阈值。
      • 当地时间午夜出现新 K 线时,如果上一日线大幅收低,则允许博弈反弹。
      • 当前价格高于日线 EMA,说明大周期偏多。
    • 做空条件为上述情形的镜像(快 EMA 下穿、宽幅阴线或午夜出现向下反转信号)。
    • 如果多空信号同时出现,则依据当前 EMA 相对位置来决定方向。
  3. 出场管理

    • 以下任一条件满足时平仓:
      • EMA 之间的距离缩小到 ±0.1% 以内,表明趋势动能减弱。
      • 价格触及设定的止盈或止损(以绝对价格单位定义)。
      • 启用移动止损时,止损被逐渐上移/下移并被触发。
  4. 仓位控制

    • 仅在空仓状态下开新仓,与原始 EA 保持一致。
    • OrderVolume 参数控制每次下单的合约数量。

参数说明

参数 说明
OrderVolume 每次交易的下单数量或手数。
FastEmaLength / SlowEmaLength 工作级别快/慢 EMA 的周期。
DailyEmaLength 日线 EMA 的周期。
TakeProfit 绝对止盈距离,0 表示关闭。
StopLoss 绝对止损距离,0 表示关闭。
TrailingStop 移动止损距离,价格达到阈值后开始跟随。
RangeThreshold 认定上一根 K 线为宽幅线所需的最小波动范围。
BodyThreshold 判定动量实体所需的最小 K 线实体。
DailyReversalThreshold 午夜过滤所需的前一日 K 线反转幅度。
CandleType 主交易逻辑使用的时间框架。
DailyCandleType 用于日线趋势过滤的高一级别时间框架。

使用建议

  • 默认参数沿用原始 EA 中的常数(已从点值转换为绝对价格单位)。
  • 请根据交易品种的最小跳动单位,调整止盈、止损、移动止损及宽幅/实体阈值。
  • 在此移植版本中,日线 EMA 用于替代原脚本中“永远做多”的偏向,使交易更贴合高周期趋势。
  • 在真实交易前,请务必先进行历史回测和模拟盘前向测试。
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 UP3x1 Premium expert advisor that relies on EMA momentum with daily context.
/// </summary>
public class Up3x1PremiumStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _dailyEmaLength;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _rangeThreshold;
	private readonly StrategyParam<decimal> _bodyThreshold;
	private readonly StrategyParam<decimal> _dailyReversalThreshold;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _dailyCandleType;

	private decimal? _fastPrev;
	private decimal? _fastPrev2;
	private decimal? _slowPrev;
	private decimal? _slowPrev2;
	private ICandleMessage _prevCandle;
	private ICandleMessage _prevPrevCandle;

	private decimal? _dailyEmaValue;
	private decimal? _prevDailyOpen;
	private decimal? _prevDailyClose;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal? _trailingStopPrice;

	public Up3x1PremiumStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume for each trade", "Trading")
		;

		_fastEmaLength = Param(nameof(FastEmaLength), 12)
		.SetGreaterThanZero()
		.SetDisplay("Fast EMA Length", "Length of the fast EMA", "Indicators")
		;

		_slowEmaLength = Param(nameof(SlowEmaLength), 26)
		.SetGreaterThanZero()
		.SetDisplay("Slow EMA Length", "Length of the slow EMA", "Indicators")
		;

		_dailyEmaLength = Param(nameof(DailyEmaLength), 10)
		.SetGreaterThanZero()
		.SetDisplay("Daily EMA Length", "EMA length for the daily trend filter", "Indicators")
		;

		_takeProfit = Param(nameof(TakeProfit), 0.015m)
		.SetNotNegative()
		.SetDisplay("Take Profit", "Absolute take profit distance", "Risk")
		;

		_stopLoss = Param(nameof(StopLoss), 0.01m)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Absolute stop loss distance", "Risk")
		;

		_trailingStop = Param(nameof(TrailingStop), 0.001m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop", "Distance for trailing stop updates", "Risk")
		;

		_rangeThreshold = Param(nameof(RangeThreshold), 0.006m)
		.SetNotNegative()
		.SetDisplay("Range Threshold", "Minimum candle range to qualify as wide", "Filters")
		;

		_bodyThreshold = Param(nameof(BodyThreshold), 0.005m)
		.SetNotNegative()
		.SetDisplay("Body Threshold", "Minimum candle body for momentum", "Filters")
		;

		_dailyReversalThreshold = Param(nameof(DailyReversalThreshold), 0.006m)
		.SetNotNegative()
		.SetDisplay("Daily Reversal Threshold", "Minimum prior day reversal size", "Filters")
		;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Primary working timeframe", "General");

		_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromDays(1).TimeFrame())
		.SetDisplay("Daily Candle Type", "Higher timeframe for daily context", "General");
	}

	/// <summary>
	/// Trade volume expressed in security lots.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Length of the fast EMA on the working timeframe.
	/// </summary>
	public int FastEmaLength
	{
		get => _fastEmaLength.Value;
		set => _fastEmaLength.Value = value;
	}

	/// <summary>
	/// Length of the slow EMA on the working timeframe.
	/// </summary>
	public int SlowEmaLength
	{
		get => _slowEmaLength.Value;
		set => _slowEmaLength.Value = value;
	}

	/// <summary>
	/// Length of the EMA used on the daily candles.
	/// </summary>
	public int DailyEmaLength
	{
		get => _dailyEmaLength.Value;
		set => _dailyEmaLength.Value = value;
	}

	/// <summary>
	/// Absolute take profit expressed in price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Absolute stop loss expressed in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Distance used for trailing stop updates.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Minimum candle range that activates the momentum filter.
	/// </summary>
	public decimal RangeThreshold
	{
		get => _rangeThreshold.Value;
		set => _rangeThreshold.Value = value;
	}

	/// <summary>
	/// Minimum candle body needed to qualify as a thrust.
	/// </summary>
	public decimal BodyThreshold
	{
		get => _bodyThreshold.Value;
		set => _bodyThreshold.Value = value;
	}

	/// <summary>
	/// Size of the prior daily reversal required during the midnight check.
	/// </summary>
	public decimal DailyReversalThreshold
	{
		get => _dailyReversalThreshold.Value;
		set => _dailyReversalThreshold.Value = value;
	}

	/// <summary>
	/// Working timeframe for the main signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe used for the daily EMA filter.
	/// </summary>
	public DataType DailyCandleType
	{
		get => _dailyCandleType.Value;
		set => _dailyCandleType.Value = value;
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType), (Security, DailyCandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_fastPrev = null;
		_fastPrev2 = null;
		_slowPrev = null;
		_slowPrev2 = null;
		_prevCandle = null;
		_prevPrevCandle = null;

		_dailyEmaValue = null;
		_prevDailyOpen = null;
		_prevDailyClose = null;

		ClearTradeLevels();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = OrderVolume;

		// Create EMA indicators for the working timeframe.
		var fastEma = new EMA { Length = FastEmaLength };
		var slowEma = new EMA { Length = SlowEmaLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(fastEma, slowEma, ProcessCandle)
		.Start();

		// Daily subscription provides the higher timeframe confirmation.
		var dailyEma = new EMA { Length = DailyEmaLength };
		var dailySubscription = SubscribeCandles(DailyCandleType);
		dailySubscription
		.Bind(dailyEma, ProcessDailyCandle)
		.Start();

		StartProtection(null, null);

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, fastEma);
			DrawIndicator(area, slowEma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessDailyCandle(ICandleMessage candle, decimal emaValue)
	{
		if (candle.State != CandleStates.Finished)
		return;

		// Store the latest completed daily information for intraday decisions.
		_dailyEmaValue = emaValue;
		_prevDailyOpen = candle.OpenPrice;
		_prevDailyClose = candle.ClosePrice;
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastEma, decimal slowEma)
	{
		if (candle.State != CandleStates.Finished)
		return;

		// Manage an existing position before looking for fresh entries.
		ManageOpenPosition(candle);

		var haveHistory = _prevCandle != null && _prevPrevCandle != null &&
		_fastPrev.HasValue && _fastPrev2.HasValue && _slowPrev.HasValue && _slowPrev2.HasValue;

		if (Position == 0m && haveHistory && IsFormedAndOnlineAndAllowTrading())
		{
			var bullishCross = _fastPrev2.Value < _slowPrev2.Value && _fastPrev.Value > _slowPrev.Value &&
			_prevPrevCandle.OpenPrice < _prevCandle.OpenPrice;

			var wideBullish = (_prevCandle.HighPrice - _prevCandle.LowPrice) > RangeThreshold &&
			_prevCandle.ClosePrice > _prevCandle.OpenPrice &&
			(_prevCandle.ClosePrice - _prevCandle.OpenPrice) > BodyThreshold;

			var midnight = candle.OpenTime.Hour == 0;
			var dailyBounce = midnight &&
			_prevDailyOpen is decimal dayOpen &&
			_prevDailyClose is decimal dayClose &&
			dayOpen > dayClose &&
			(dayOpen - dayClose) > DailyReversalThreshold;

			var priceAboveDaily = _dailyEmaValue is decimal daily && candle.ClosePrice >= daily;

			var longSignal = bullishCross || wideBullish || dailyBounce || priceAboveDaily;

			var bearishCross = _fastPrev2.Value > _slowPrev2.Value && _fastPrev.Value < _slowPrev.Value &&
			_prevPrevCandle.OpenPrice > _prevCandle.OpenPrice;

			var wideBearish = (_prevCandle.HighPrice - _prevCandle.LowPrice) > RangeThreshold &&
			_prevCandle.OpenPrice > _prevCandle.ClosePrice &&
			(_prevCandle.OpenPrice - _prevCandle.ClosePrice) > BodyThreshold;

			var midnightSell = midnight &&
			_prevDailyOpen is decimal dayOpenSell &&
			_prevDailyClose is decimal dayCloseSell &&
			dayOpenSell < dayCloseSell &&
			(dayCloseSell - dayOpenSell) > DailyReversalThreshold;

			var shortSignal = bearishCross || wideBearish || midnightSell;

			if (longSignal && shortSignal)
			{
				// Break ties with the latest EMA relationship.
				if (_fastPrev.Value >= _slowPrev.Value)
				shortSignal = false;
				else
				longSignal = false;
			}

			if (longSignal && OrderVolume > 0m)
			{
				BuyMarket();
				_entryPrice = candle.ClosePrice;
				_stopPrice = StopLoss > 0m ? _entryPrice - StopLoss : null;
				_takeProfitPrice = TakeProfit > 0m ? _entryPrice + TakeProfit : null;
				_trailingStopPrice = TrailingStop > 0m ? _entryPrice - TrailingStop : null;
			}
			else if (shortSignal && OrderVolume > 0m)
			{
				SellMarket();
				_entryPrice = candle.ClosePrice;
				_stopPrice = StopLoss > 0m ? _entryPrice + StopLoss : null;
				_takeProfitPrice = TakeProfit > 0m ? _entryPrice - TakeProfit : null;
				_trailingStopPrice = TrailingStop > 0m ? _entryPrice + TrailingStop : null;
			}
		}

		// Preserve history to mimic the MQL index-based access pattern.
		_prevPrevCandle = _prevCandle;
		_prevCandle = candle;

		_fastPrev2 = _fastPrev;
		_fastPrev = fastEma;
		_slowPrev2 = _slowPrev;
		_slowPrev = slowEma;
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			UpdateTrailingStopForLong(candle);

			var exit = AreEmaNear(_fastPrev, _slowPrev);

			if (!exit && _takeProfitPrice is decimal tp && candle.HighPrice >= tp)
			exit = true;

			if (!exit && _stopPrice is decimal sl && candle.LowPrice <= sl)
			exit = true;

			if (!exit && _trailingStopPrice is decimal trail && candle.LowPrice <= trail)
			exit = true;

			if (exit)
			{
				SellMarket();
				ClearTradeLevels();
			}
		}
		else if (Position < 0m)
		{
			UpdateTrailingStopForShort(candle);

			var exit = AreEmaNear(_fastPrev, _slowPrev);

			if (!exit && _takeProfitPrice is decimal tp && candle.LowPrice <= tp)
			exit = true;

			if (!exit && _stopPrice is decimal sl && candle.HighPrice >= sl)
			exit = true;

			if (!exit && _trailingStopPrice is decimal trail && candle.HighPrice >= trail)
			exit = true;

			if (exit)
			{
				BuyMarket();
				ClearTradeLevels();
			}
		}
	}

	private void UpdateTrailingStopForLong(ICandleMessage candle)
	{
		if (TrailingStop <= 0m || _entryPrice is not decimal entry)
		return;

		var move = candle.HighPrice - entry;
		if (move < TrailingStop)
		return;

		var newStop = candle.HighPrice - TrailingStop;
		if (_trailingStopPrice is null || newStop > _trailingStopPrice)
		_trailingStopPrice = newStop;
	}

	private void UpdateTrailingStopForShort(ICandleMessage candle)
	{
		if (TrailingStop <= 0m || _entryPrice is not decimal entry)
		return;

		var move = entry - candle.LowPrice;
		if (move < TrailingStop)
		return;

		var newStop = candle.LowPrice + TrailingStop;
		if (_trailingStopPrice is null || newStop < _trailingStopPrice)
		_trailingStopPrice = newStop;
	}

	private static bool AreEmaNear(decimal? fast, decimal? slow)
	{
		if (fast is not decimal fastValue || slow is not decimal slowValue)
		return false;

		if (slowValue == 0m)
		return false;

		var diff = Math.Abs(fastValue - slowValue);
		return diff <= Math.Abs(slowValue) * 0.001m;
	}

	private void ClearTradeLevels()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_trailingStopPrice = null;
	}
}