在 GitHub 上查看

Nova 策略

概述

  • 由 MetaTrader 5 的 "Nova" 专家顾问转换而来,用于跟踪固定秒数内的价格动量。
  • 通过 CandleType 参数选择任意蜡烛类型,并仅在蜡烛完结时执行逻辑。
  • 使用 Level1 数据跟踪最优买价和卖价,并保存 SecondsAgo 秒之前的报价作为比较基准。
  • 当前一根蜡烛收阳且当前卖价相对基准卖价上升至少 StepPips 时建立多头仓位。
  • 当前一根蜡烛收阴且当前买价相对基准卖价下降至少 StepPips 时建立空头仓位。
  • 当止损或止盈参数大于零时,利用 StockSharp 的保护机制自动放置止损/止盈。
  • 若出现亏损(触发止损),下一笔交易的手数乘以 LossCoefficient;若盈利退出,则手数恢复为 BaseVolume

参数

  • SecondsAgo – 用于比较的历史报价与当前时刻之间的秒数。
  • StepPips – 突破过滤阈值(以点为单位);根据品种的最小报价单位自动换算为价格增量(3/5 位小数的品种乘以 10)。
  • BaseVolume – 初始下单手数;会按照交易所的最小变动、最小/最大手数进行归一化处理。
  • StopLossPips – 止损距离(点);为 0 时表示不放置止损。
  • TakeProfitPips – 止盈距离(点);为 0 时表示不放置止盈。
  • LossCoefficient – 发生亏损后用于放大下一次下单手数的倍数。
  • CandleType – 用于生成信号的蜡烛类型(时间框、tick、range 等)。

其他说明

  • 为完整复刻 MT5 行为,策略需要 Level1 (最优买/卖) 数据;若不可用则回退为蜡烛的收盘价。
  • 手数归一化过程会遵循 Security.VolumeStepSecurity.MinVolumeSecurity.MaxVolume 的限制,避免无效订单。
  • 价格换算基于 Security.PriceStepSecurity.Decimals,可适配 4/5 位外汇品种及其他市场品种。
  • 本策略不提供 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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Nova strategy converted from MQL that compares the current price with the price N seconds ago.
/// Opens a long position when the previous candle is bullish and the ask price has moved up by a threshold.
/// Opens a short position when the previous candle is bearish and the bid price has dropped below the stored ask price.
/// After a stop-loss the position size is multiplied by a coefficient, after a take-profit it resets to the base volume.
/// </summary>
public class NovaStrategy : Strategy
{
	private readonly StrategyParam<int> _secondsAgo;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<decimal> _lossCoefficient;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _referenceAsk;
	private decimal? _referenceBid;
	private DateTimeOffset? _lastCheckTime;
	private decimal _stepOffset;
	private decimal _stopLossOffset;
	private decimal _takeProfitOffset;
	private decimal _currentVolume;
	private decimal? _lastTradeVolume;
	private decimal _previousPnL;
	private decimal? _currentAsk;
	private decimal? _currentBid;
	private bool _hasPreviousCandle;
	private decimal _prevCandleOpen;
	private decimal _prevCandleClose;

	/// <summary>
	/// Seconds to look back for the price comparison.
	/// </summary>
	public int SecondsAgo
	{
		get => _secondsAgo.Value;
		set => _secondsAgo.Value = value;
	}

	/// <summary>
	/// Step in pips that is required for the breakout condition.
	/// </summary>
	public int StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Base trading volume.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

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

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Coefficient used to increase the volume after a stop-loss.
	/// </summary>
	public decimal LossCoefficient
	{
		get => _lossCoefficient.Value;
		set => _lossCoefficient.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public NovaStrategy()
	{
		_secondsAgo = Param(nameof(SecondsAgo), 10)
		.SetGreaterThanZero()
		.SetDisplay("Seconds window", "Seconds to look back for price comparison", "General")
		
		.SetOptimize(5, 30, 5);

		_stepPips = Param(nameof(StepPips), 1)
		.SetNotNegative()
		.SetDisplay("Step (pips)", "Price offset in pips for breakout check", "Signals")
		
		.SetOptimize(0, 5, 1);

		_baseVolume = Param(nameof(BaseVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Base volume", "Initial order volume", "Risk")
		
		.SetOptimize(0.05m, 0.5m, 0.05m);

		_stopLossPips = Param(nameof(StopLossPips), 500)
		.SetNotNegative()
		.SetDisplay("Stop-loss (pips)", "Stop-loss distance in pips", "Risk")
		.SetOptimize(0, 5, 1);

		_takeProfitPips = Param(nameof(TakeProfitPips), 500)
		.SetNotNegative()
		.SetDisplay("Take-profit (pips)", "Take-profit distance in pips", "Risk")
		.SetOptimize(0, 5, 1);

		_lossCoefficient = Param(nameof(LossCoefficient), 1.6m)
		.SetGreaterThanZero()
		.SetDisplay("Loss coefficient", "Multiplier for the next trade after a stop-loss", "Risk")
		
		.SetOptimize(1m, 2.5m, 0.1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle type", "Candles used for signal calculations", "General");
	}

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

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

		_referenceAsk = null;
		_referenceBid = null;
		_lastCheckTime = null;
		_stepOffset = 0m;
		_stopLossOffset = 0m;
		_takeProfitOffset = 0m;
		_currentVolume = 0m;
		_lastTradeVolume = null;
		_previousPnL = 0m;
		_currentAsk = null;
		_currentBid = null;
		_hasPreviousCandle = false;
		_prevCandleOpen = 0m;
		_prevCandleClose = 0m;
	}

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

		_previousPnL = PnL;

		var pipSize = GetPipSize();
		_stepOffset = StepPips * pipSize;
		_stopLossOffset = StopLossPips * pipSize;
		_takeProfitOffset = TakeProfitPips * pipSize;

		_currentVolume = NormalizeVolume(BaseVolume);
		Volume = _currentVolume;

		var candleSubscription = SubscribeCandles(CandleType);
		candleSubscription
		.Bind(ProcessCandle)
		.Start();

		if (StopLossPips > 0 || TakeProfitPips > 0)
		{
			StartProtection(
			TakeProfitPips > 0 ? new Unit(_takeProfitOffset, UnitTypes.Absolute) : default,
			StopLossPips > 0 ? new Unit(_stopLossOffset, UnitTypes.Absolute) : default);
		}

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, candleSubscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessLevel1(Level1ChangeMessage level1)
	{
		if (level1.Changes.TryGetValue(Level1Fields.BestAskPrice, out var ask))
		_currentAsk = (decimal)ask;

		if (level1.Changes.TryGetValue(Level1Fields.BestBidPrice, out var bid))
		_currentBid = (decimal)bid;
	}

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

		UpdateVolumeFromPnL();

		if (!_hasPreviousCandle)
		{
			_hasPreviousCandle = true;
			_prevCandleOpen = candle.OpenPrice;
			_prevCandleClose = candle.ClosePrice;
			return;
		}

		if (Position != 0)
		return;

		var now = candle.CloseTime;
		var interval = TimeSpan.FromSeconds(SecondsAgo);

		if (_lastCheckTime != null && now - _lastCheckTime < interval)
		return;

		var currentAsk = candle.ClosePrice;
		var currentBid = candle.ClosePrice;

		if (currentAsk == 0m || currentBid == 0m)
		{
			_referenceAsk = null;
			_referenceBid = null;
			_lastCheckTime = now;
			return;
		}

		if (_referenceAsk is null || _referenceBid is null)
		{
			_referenceAsk = currentAsk;
			_referenceBid = currentBid;
			_lastCheckTime = now;
			return;
		}

		var bullishPrevious = _prevCandleClose > _prevCandleOpen;
		var bearishPrevious = _prevCandleClose < _prevCandleOpen;
		var referenceAsk = _referenceAsk.Value;

		if (bullishPrevious && currentAsk - _stepOffset > referenceAsk)
		{
			TryEnterLong(currentAsk);
		}
		else if (bearishPrevious && currentBid + _stepOffset < referenceAsk)
		{
			TryEnterShort(currentBid);
		}

		_referenceAsk = currentAsk;
		_referenceBid = currentBid;
		_lastCheckTime = now;
		_prevCandleOpen = candle.OpenPrice;
		_prevCandleClose = candle.ClosePrice;
	}

	private void TryEnterLong(decimal price)
	{
		if (_currentVolume <= 0m)
		return;

		BuyMarket();
		_lastTradeVolume = _currentVolume;
		LogInfo($"Open long at {price:F5} with volume {_currentVolume:F2}");
	}

	private void TryEnterShort(decimal price)
	{
		if (_currentVolume <= 0m)
		return;

		SellMarket();
		_lastTradeVolume = _currentVolume;
		LogInfo($"Open short at {price:F5} with volume {_currentVolume:F2}");
	}

	private void UpdateVolumeFromPnL()
	{
		var realizedPnL = PnL;
		if (realizedPnL == _previousPnL)
		return;

		var delta = realizedPnL - _previousPnL;
		_previousPnL = realizedPnL;

		if (delta > 0m)
		{
			_currentVolume = NormalizeVolume(BaseVolume);
			Volume = _currentVolume;
			LogInfo("Reset volume after profitable trade");
		}
		else if (delta < 0m)
		{
			var referenceVolume = _lastTradeVolume ?? _currentVolume;
			_currentVolume = NormalizeVolume(referenceVolume * LossCoefficient);
			Volume = _currentVolume;
			LogInfo($"Increase volume after loss to {_currentVolume:F2}");
		}
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			volume = Math.Floor(volume / step) * step;
		}

		var min = Security?.MinVolume ?? 0m;
		if (min > 0m && volume < min)
		{
			volume = min;
		}

		var max = Security?.MaxVolume ?? 0m;
		if (max > 0m && volume > max)
		{
			volume = max;
		}

		return volume;
	}

	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;
		var factor = decimals is 3 or 5 ? 10m : 1m;
		return step * factor;
	}
}