在 GitHub 上查看

快慢均线交叉策略

概述

快慢均线交叉策略 复刻了 MetaTrader 4 专家顾问 _HPCS_FastSlowMACrosssover_MT4_EA_V01_WE 的核心思想。策略订阅所选的K线类型,并对两条指数移动平均线 (EMA) 进行比较:当较快均线在允许的交易时间窗口内上穿或下穿较慢均线时,策略开仓交易。止盈和止损距离以点值(pip)定义,从而与依赖报价小数位的原始 MQL 逻辑保持一致。

交易逻辑

  1. 订阅配置的K线类型(默认:1分钟K线)。
  2. 计算两条EMA:
    • 快速EMA周期(默认 14)。
    • 慢速EMA周期(默认 21)。
  3. 在每根已完成的K线上执行以下检查:
    • 判断K线收盘时间是否位于允许的交易时间窗口内。
    • 当快速EMA上穿慢速EMA时识别出 看涨交叉
    • 当快速EMA下穿慢速EMA时识别出 看跌交叉
  4. 执行交易:
    • 如果存在反向仓位,先行平仓。
    • 按照 Trade Volume 参数规定的数量发送市价单。
    • 使用K线收盘价作为入场价格,用于计算风险控制水平。
  5. 使用K线的最高价和最低价管理持仓:
    • 多头仓位下穿入场价 Stop Loss (pips) 距离时立即平仓。
    • 多头仓位上穿入场价 Take Profit (pips) 距离时立即平仓。
    • 空头仓位按照对称逻辑处理(止损在入场价上方,止盈在入场价下方)。

参数

参数 说明
Fast MA Period 快速EMA的周期长度,用于判定交叉。
Slow MA Period 慢速EMA的周期长度。
Take Profit (pips) 以点值表示的止盈距离,用于计算多空目标价。
Stop Loss (pips) 以点值表示的止损距离。
Start Time 每日交易窗口的起始时间(包含)。
Stop Time 每日交易窗口的结束时间(包含)。
Candle Type 用于计算指标的K线类型。
Trade Volume 每次信号使用的下单数量。

说明

  • 点值根据标的的最小报价步长和小数位数计算。如果品种带有5位或3位小数,策略会将步长乘以 10,以匹配MetaTrader中的点值定义。
  • 时间过滤器支持跨越午夜的会话。当 Start Time 晚于 Stop Time 时,策略会从开始时间交易到午夜,并在午夜之后继续运行直到结束时间。
  • 每根K线只允许触发一次信号,与原始EA在同一根K线上仅提交一次订单的保护机制保持一致。
  • 止盈和止损由策略逻辑主动执行,而不是在交易所挂出等待成交的委托,这与原始EA在提交订单时直接设置止盈止损的方式相呼应。
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>
/// Fast and slow moving average crossover strategy with intraday time filter and pip-based risk controls.
/// </summary>
public class FastSlowMaCrossoverStrategy : Strategy
{
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<TimeSpan> _startTime;
	private readonly StrategyParam<TimeSpan> _stopTime;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private decimal _pipSize;
	private decimal? _previousFast;
	private decimal? _previousSlow;
	private DateTimeOffset? _lastSignalTime;
	private bool _hasActivePosition;
	private bool _isLongPosition;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _targetPrice;

	/// <summary>
	/// Initializes a new instance of the <see cref="FastSlowMaCrossoverStrategy"/> class.
	/// </summary>
	public FastSlowMaCrossoverStrategy()
	{
		_fastMaPeriod = Param(nameof(FastMaPeriod), 30)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Length of the fast moving average", "Parameters")
			;

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Length of the slow moving average", "Parameters")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 80)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance in pips for profit taking", "Risk Management")
			;

		_stopLossPips = Param(nameof(StopLossPips), 80)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Distance in pips for protective stop", "Risk Management")
			;

		_startTime = Param(nameof(StartTime), new TimeSpan(8, 0, 0))
			.SetDisplay("Start Time", "Start of the allowed trading window", "Schedule");

		_stopTime = Param(nameof(StopTime), new TimeSpan(18, 0, 0))
			.SetDisplay("Stop Time", "End of the allowed trading window", "Schedule");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(120).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume of each market order", "Trading")
			;
	}

	/// <summary>
	/// Period of the fast moving average.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the slow moving average.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Profit target distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Start time of the allowed trading window.
	/// </summary>
	public TimeSpan StartTime
	{
		get => _startTime.Value;
		set => _startTime.Value = value;
	}

	/// <summary>
	/// Stop time of the allowed trading window.
	/// </summary>
	public TimeSpan StopTime
	{
		get => _stopTime.Value;
		set => _stopTime.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Volume used for each market order.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

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

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

		_pipSize = 0m;
		_previousFast = null;
		_previousSlow = null;
		_lastSignalTime = null;
		ResetPositionState();
	}

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

		Volume = TradeVolume;
		_pipSize = CalculatePipSize();

		var fastMa = new EMA { Length = FastMaPeriod };
		var slowMa = new EMA { Length = SlowMaPeriod };

		SubscribeCandles(CandleType)
			.Bind(fastMa, slowMa, (candle, fastValue, slowValue) => ProcessCandle(candle, fastValue, slowValue, fastMa.IsFormed && slowMa.IsFormed))
			.Start();
	}

	private decimal CalculatePipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			return 0.0001m;

		var decimals = Security?.Decimals ?? 0;
		var factor = (decimals == 3 || decimals == 5) ? 10m : 1m;
		return priceStep * factor;
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, bool indicatorsFormed)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var timeOfDay = candle.CloseTime.TimeOfDay;
		if (!IsWithinTradingWindow(timeOfDay))
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		ManageExistingPosition(candle);

		if (!indicatorsFormed)
		{
			_previousFast = fastValue;
			_previousSlow = slowValue;
			return;
		}

		if (_previousFast is null || _previousSlow is null)
		{
			_previousFast = fastValue;
			_previousSlow = slowValue;
			return;
		}

		var crossUp = _previousFast <= _previousSlow && fastValue > slowValue;
		var crossDown = _previousFast >= _previousSlow && fastValue < slowValue;

		if (_pipSize <= 0m)
		{
			_previousFast = fastValue;
			_previousSlow = slowValue;
			return;
		}

		var currentCandleTime = candle.CloseTime;

		if (crossUp && Position <= 0m && _lastSignalTime != currentCandleTime)
		{
			var volume = TradeVolume;

			if (Position < 0m)
				volume += -Position;

			if (volume > 0m)
			{
				BuyMarket(volume);
				RecordEntryState(candle.ClosePrice, true);
				_lastSignalTime = currentCandleTime;
			}
		}
		else if (crossDown && Position >= 0m && _lastSignalTime != currentCandleTime)
		{
			var volume = TradeVolume;

			if (Position > 0m)
				volume += Position;

			if (volume > 0m)
			{
				SellMarket(volume);
				RecordEntryState(candle.ClosePrice, false);
				_lastSignalTime = currentCandleTime;
			}
		}

		_previousFast = fastValue;
		_previousSlow = slowValue;
	}

	private void ManageExistingPosition(ICandleMessage candle)
	{
		if (!_hasActivePosition || _pipSize <= 0m)
			return;

		var high = candle.HighPrice;
		var low = candle.LowPrice;

		if (_isLongPosition)
		{
			if (StopLossPips > 0 && low <= _stopPrice)
			{
				SellMarket(Position);
				ResetPositionState();
				return;
			}

			if (TakeProfitPips > 0 && high >= _targetPrice)
			{
				SellMarket(Position);
				ResetPositionState();
			}
		}
		else
		{
			if (StopLossPips > 0 && high >= _stopPrice)
			{
				BuyMarket(-Position);
				ResetPositionState();
				return;
			}

			if (TakeProfitPips > 0 && low <= _targetPrice)
			{
				BuyMarket(-Position);
				ResetPositionState();
			}
		}
	}

	private void RecordEntryState(decimal closePrice, bool isLong)
	{
		_hasActivePosition = true;
		_isLongPosition = isLong;
		_entryPrice = closePrice;

		var takeOffset = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
		var stopOffset = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;

		if (isLong)
		{
			_targetPrice = takeOffset > 0m ? (_entryPrice + takeOffset) : 0m;
			_stopPrice = stopOffset > 0m ? (_entryPrice - stopOffset) : 0m;
		}
		else
		{
			_targetPrice = takeOffset > 0m ? (_entryPrice - takeOffset) : 0m;
			_stopPrice = stopOffset > 0m ? (_entryPrice + stopOffset) : 0m;
		}
	}

	private bool IsWithinTradingWindow(TimeSpan current)
	{
		var start = StartTime;
		var stop = StopTime;

		if (start == stop)
			return true;

		if (start < stop)
			return current >= start && current <= stop;

		return current >= start || current <= stop;
	}

	private void ResetPositionState()
	{
		_hasActivePosition = false;
		_isLongPosition = false;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_targetPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
		{
			ResetPositionState();
		}
		else
		{
			_hasActivePosition = true;
			_isLongPosition = Position > 0m;
		}
	}
}