在 GitHub 上查看

V1N1 Lonny 突破策略

概述

V1N1 Lonny 突破策略是对 MetaTrader 平台 "V1N1 LONNY" 专家顾问的复刻版本,重点捕捉伦敦与纽约交易时段交汇处的突破行情。策略在开盘前构建价格区间,等待K线收盘突破该区间后再入场,同时借助指数移动平均线(EMA)判断趋势方向,并用随机指标过滤过度超买或超卖的市场状态。

策略提供两种风险管理方式:按账户权益百分比计算仓位或使用固定手数。还包含点差过滤、移动止损与基于K线数量的超时退出机制,可在动能减弱时及时离场。

交易逻辑

  1. 交易时段:仅在设定的起止时间内允许开仓,可根据伦敦或纽约的夏令时调整时差。
  2. 开盘区间:在时段开始前记录固定数量的K线高低点,用于确定突破区间。
  3. 趋势确认:EMA斜率必须与交易方向一致,多头需要EMA向上,空头需要EMA向下。
  4. 动能过滤:随机指标需保持在以50为中心的指定区间内,避免在极端超买/超卖时追单。
  5. 突破验证:上一根K线收盘价必须突破区间高点或低点,并且突破幅度介于最小与最大阈值之间。
  6. 风险控制:止损位根据区间对侧加上固定点数确定,止盈为止损距离乘以收益系数。可选的移动止损随着价格推进收紧风险,同时可设置最多持仓K线数限制。

参数说明

  • StartTrade:交易开始时间。
  • EndTrade:交易结束时间。
  • SwitchDst:夏令时处理方式(欧洲/美国/不调整)。
  • RiskModes:仓位计算模式(按百分比或固定手数)。
  • PositionRisk:风险百分比或固定手数。
  • TradeRange:构建开盘区间所需的K线数量。
  • MinRangePoints / MaxRangePoints:区间大小的最小与最大限制(点数)。
  • MinBreakRange / MaxBreakRange:突破距离阈值(点数)。
  • StopLossPoints:止损距离(点数)。
  • TpFactor:止盈 = 止损距离 × 系数。
  • TrailStopPoints:移动止损距离(点数),0 表示关闭。
  • TrendPeriod:EMA周期。
  • OverPeriod:随机指标周期。
  • OverLevels:随机指标允许偏离 50 的最大幅度。
  • BarsToClose:持仓允许的最大K线数,0 表示无限制。
  • MaxSpreadPoints:允许的最大点差。
  • SlippagePoints:预期滑点(兼容原版参数)。
  • CandleType:策略使用的K线类型与周期。

使用提示

  • 所有以“点”为单位的参数都会乘以标的物的 PriceStep,从而转换为价格距离。
  • 策略订阅委托簿以估算实时点差;若缺少最佳买卖报价,则跳过点差过滤。
  • 移动止损与超时退出都在K线收盘时检查,与原版MQL逻辑保持一致。
  • RiskModes 设置为百分比时,需要可用的账户权益数值 (Portfolio.CurrentValue);若不可用则退回到固定手数模式。

文件列表

  • CS/V1n1LonnyBreakoutStrategy.cs – StockSharp 平台的策略实现。
  • README.md – 英文说明。
  • README_zh.md – 中文说明。
  • README_ru.md – 俄文说明。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy that mirrors the original "V1N1 LONNY" MQL expert advisor.
/// The strategy forms an opening range from early candles and
/// enters when a candle closes outside that range while trend and momentum filters agree.
/// </summary>
public class V1n1LonnyBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _trendPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _rangeBars;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _ema;
	private RelativeStrengthIndex _rsi;

	private decimal _prevEma;
	private decimal _prevPrevEma;
	private bool _hasPrevEma;
	private bool _hasPrevPrevEma;

	private readonly List<decimal> _highs = new();
	private readonly List<decimal> _lows = new();
	private bool _rangeReady;
	private decimal _rangeHigh;
	private decimal _rangeLow;
	private bool _breakoutUpSeen;
	private bool _breakoutDownSeen;

	/// <summary>
	/// EMA period for the trend filter.
	/// </summary>
	public int TrendPeriod
	{
		get => _trendPeriod.Value;
		set => _trendPeriod.Value = value;
	}

	/// <summary>
	/// RSI period for momentum filter.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Number of initial bars to build the opening range.
	/// </summary>
	public int RangeBars
	{
		get => _rangeBars.Value;
		set => _rangeBars.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="V1n1LonnyBreakoutStrategy"/> class.
	/// </summary>
	public V1n1LonnyBreakoutStrategy()
	{
		_trendPeriod = Param(nameof(TrendPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Trend EMA", "EMA period for trend filter", "Indicators");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI period for momentum filter", "Indicators");

		_rangeBars = Param(nameof(RangeBars), 5)
			.SetGreaterThanZero()
			.SetDisplay("Range Bars", "Bars used to build the opening range", "Breakout");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type and timeframe", "General");
	}

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

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

		_ema = null;
		_rsi = null;
		_prevEma = 0m;
		_prevPrevEma = 0m;
		_hasPrevEma = false;
		_hasPrevPrevEma = false;

		_highs.Clear();
		_lows.Clear();
		_rangeReady = false;
		_rangeHigh = 0m;
		_rangeLow = 0m;
		_breakoutUpSeen = false;
		_breakoutDownSeen = false;
	}

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

		_ema = new ExponentialMovingAverage { Length = TrendPeriod };
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		Indicators.Add(_ema);
		Indicators.Add(_rsi);

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

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent));

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

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

		var rsiResult = _rsi.Process(new DecimalIndicatorValue(_rsi, candle.ClosePrice, candle.OpenTime) { IsFinal = true });

		if (!_rsi.IsFormed || !_ema.IsFormed)
		{
			ShiftEma(emaValue);
			return;
		}

		var rsiValue = rsiResult.ToDecimal();

		// Build the opening range from the first N bars
		if (!_rangeReady)
		{
			_highs.Add(candle.HighPrice);
			_lows.Add(candle.LowPrice);

			if (_highs.Count >= RangeBars)
			{
				_rangeHigh = decimal.MinValue;
				_rangeLow = decimal.MaxValue;

				for (var i = 0; i < _highs.Count; i++)
				{
					if (_highs[i] > _rangeHigh) _rangeHigh = _highs[i];
					if (_lows[i] < _rangeLow) _rangeLow = _lows[i];
				}

				_rangeReady = true;
			}

			ShiftEma(emaValue);
			return;
		}

		if (Position != 0 || !_hasPrevEma || !_hasPrevPrevEma)
		{
			ShiftEma(emaValue);
			return;
		}

		// Trend rising: EMA going up
		var trendUp = _prevEma > _prevPrevEma;
		var trendDown = _prevEma < _prevPrevEma;

		// Long: close above range high + trend up + RSI not overbought
		if (!_breakoutUpSeen && trendUp && rsiValue < 70 && candle.ClosePrice > _rangeHigh)
		{
			BuyMarket();
			_breakoutUpSeen = true;
		}
		// Short: close below range low + trend down + RSI not oversold
		else if (!_breakoutDownSeen && trendDown && rsiValue > 30 && candle.ClosePrice < _rangeLow)
		{
			SellMarket();
			_breakoutDownSeen = true;
		}

		ShiftEma(emaValue);
	}

	private void ShiftEma(decimal emaValue)
	{
		if (_hasPrevEma)
		{
			_prevPrevEma = _prevEma;
			_hasPrevPrevEma = true;
		}

		_prevEma = emaValue;
		_hasPrevEma = true;
	}
}