V1N1 Lonny 突破策略
概述
V1N1 Lonny 突破策略是对 MetaTrader 平台 "V1N1 LONNY" 专家顾问的复刻版本,重点捕捉伦敦与纽约交易时段交汇处的突破行情。策略在开盘前构建价格区间,等待K线收盘突破该区间后再入场,同时借助指数移动平均线(EMA)判断趋势方向,并用随机指标过滤过度超买或超卖的市场状态。
策略提供两种风险管理方式:按账户权益百分比计算仓位或使用固定手数。还包含点差过滤、移动止损与基于K线数量的超时退出机制,可在动能减弱时及时离场。
交易逻辑
- 交易时段:仅在设定的起止时间内允许开仓,可根据伦敦或纽约的夏令时调整时差。
- 开盘区间:在时段开始前记录固定数量的K线高低点,用于确定突破区间。
- 趋势确认:EMA斜率必须与交易方向一致,多头需要EMA向上,空头需要EMA向下。
- 动能过滤:随机指标需保持在以50为中心的指定区间内,避免在极端超买/超卖时追单。
- 突破验证:上一根K线收盘价必须突破区间高点或低点,并且突破幅度介于最小与最大阈值之间。
- 风险控制:止损位根据区间对侧加上固定点数确定,止盈为止损距离乘以收益系数。可选的移动止损随着价格推进收紧风险,同时可设置最多持仓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;
}
}
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, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class v1n1_lonny_breakout_strategy(Strategy):
def __init__(self):
super(v1n1_lonny_breakout_strategy, self).__init__()
self._trend_period = self.Param("TrendPeriod", 20)
self._rsi_period = self.Param("RsiPeriod", 14)
self._range_bars = self.Param("RangeBars", 5)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._has_prev_ema = False
self._has_prev_prev_ema = False
self._highs = []
self._lows = []
self._range_ready = False
self._range_high = 0.0
self._range_low = 0.0
self._breakout_up_seen = False
self._breakout_down_seen = False
@property
def TrendPeriod(self):
return self._trend_period.Value
@TrendPeriod.setter
def TrendPeriod(self, value):
self._trend_period.Value = value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@RsiPeriod.setter
def RsiPeriod(self, value):
self._rsi_period.Value = value
@property
def RangeBars(self):
return self._range_bars.Value
@RangeBars.setter
def RangeBars(self, value):
self._range_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(v1n1_lonny_breakout_strategy, self).OnStarted2(time)
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._has_prev_ema = False
self._has_prev_prev_ema = False
self._highs = []
self._lows = []
self._range_ready = False
self._range_high = 0.0
self._range_low = 0.0
self._breakout_up_seen = False
self._breakout_down_seen = False
ema = ExponentialMovingAverage()
ema.Length = self.TrendPeriod
self._ema = ema
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self.ProcessCandle).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent))
def ProcessCandle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
ema_val = float(ema_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
rsi_result = process_float(self._rsi, candle.ClosePrice, candle.OpenTime, True)
if not self._rsi.IsFormed or not self._ema.IsFormed:
self._shift_ema(ema_val)
return
rsi_val = float(rsi_result)
if not self._range_ready:
self._highs.append(high)
self._lows.append(low)
if len(self._highs) >= int(self.RangeBars):
self._range_high = max(self._highs)
self._range_low = min(self._lows)
self._range_ready = True
self._shift_ema(ema_val)
return
if self.Position != 0 or not self._has_prev_ema or not self._has_prev_prev_ema:
self._shift_ema(ema_val)
return
trend_up = self._prev_ema > self._prev_prev_ema
trend_down = self._prev_ema < self._prev_prev_ema
if not self._breakout_up_seen and trend_up and rsi_val < 70.0 and close > self._range_high:
self.BuyMarket()
self._breakout_up_seen = True
elif not self._breakout_down_seen and trend_down and rsi_val > 30.0 and close < self._range_low:
self.SellMarket()
self._breakout_down_seen = True
self._shift_ema(ema_val)
def _shift_ema(self, ema_val):
if self._has_prev_ema:
self._prev_prev_ema = self._prev_ema
self._has_prev_prev_ema = True
self._prev_ema = ema_val
self._has_prev_ema = True
def OnReseted(self):
super(v1n1_lonny_breakout_strategy, self).OnReseted()
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._has_prev_ema = False
self._has_prev_prev_ema = False
self._highs = []
self._lows = []
self._range_ready = False
self._range_high = 0.0
self._range_low = 0.0
self._breakout_up_seen = False
self._breakout_down_seen = False
def CreateClone(self):
return v1n1_lonny_breakout_strategy()