Lucky Shift Limit 策略
Lucky Shift Limit 策略是 MetaTrader 4 专家 Lucky_acnl6p6j89zn91fa.mq4 的完整移植版。策略实时监听最优买价和卖价(Level 1 报价),根据相邻报价之间的“点”(MetaTrader 点值)跳变进行反向交易:卖价向上跳变触发做空,买价向下跳变触发做多。所有持仓都会被持续跟踪,只要出现盈利或者浮亏超过安全阈值,就会立即平仓,与原始 MQ4 逻辑完全一致。
数据与执行要求
- 行情数据:只需要 Level 1 报价,不依赖蜡烛或深度数据。
- 委托类型:所有进出场均使用市价单,模拟 MetaTrader 中的即时
OrderSend/OrderClose调用。 - 账户模式:兼容对冲和净额账户。净额账户上仓位会累加为单一净头寸,由退出模块统一平仓。
- 下单手数:默认使用
Strategy.Volume,若能获取组合权益,则按照AccountFreeMargin/10000的公式动态计算,与原版GetLots()函数一致。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
Shift points |
3 | 连续两次报价之间需要出现的最小点差,达到后触发新的交易。数值越大越能过滤噪声,越小越敏感。 |
Limit points |
18 | 持仓允许承受的最大不利波动。价格逆行超过该点数时将强制平仓。 |
参数以 MetaTrader 点为单位,内部会依据标的物的最小报价步长自动换算成绝对价格偏移;优化区间与原始专家保持一致。
交易流程
- 初始化
- 使用
Security.PriceStep将点值参数转换为实际价格距离。 - 清空前一笔 Bid/Ask 缓存,并通过高阶
Bind接口启动 Level 1 订阅。
- 使用
- 入场条件
- 当前 Ask 相比上一笔 Ask 上涨至少
Shift points时,策略发送市价卖单(做空以衰减价格尖峰),日志记录触发原因。 - 当前 Bid 相比上一笔 Bid 下跌至少相同点数时,策略发送市价买单。
- 信号可连续触发,完全复现 MQ4 版本中允许多笔持仓的行为。
- 当前 Ask 相比上一笔 Ask 上涨至少
- 离场管理
- 每次报价更新都会调用
TryClosePosition():多头在 Bid 高于均价时立即平仓获利,或者在 Ask 低于开仓价Limit points时止损。 - 空头则在 Ask 低于均价时盈利平仓,或当 Bid 高于开仓价超过限制时止损。
- 平仓全部通过市价单完成,确保同一报价周期内清掉头寸。
- 每次报价更新都会调用
- 仓位控制
- 当可获取账户权益时,按
equity / 10000(四舍五入到 0.1 手)计算下单手数,和 MQ4 的GetLots()完全一致。 - 若权益信息缺失,则回退至策略默认的
Volume数值。
- 当可获取账户权益时,按
实现细节
- 仅使用 StockSharp 的高阶 API:
SubscribeLevel1().Bind(ProcessLevel1),无需编写底层事件处理。 - 只用简单的 nullable 变量保存上一笔报价,不创建自定义集合,满足 AGENTS.md 的限制。
- 损失阈值会根据品种的价格步长自动调整,支持 5 位报价或更细分辨率的交易品种。
- 运行过程中如果用户调整参数,策略会在下一次报价时重新计算阈值。
- 日志会记录每次开仓和平仓的原因,方便回测和实盘排查。
使用建议
- 最适合应用于流动性高、报价跳动频繁的外汇或指数品种。
- 如需额外的风险控制,可结合
StartProtection等账户级保护模块一起使用。 - 在高噪声行情下可提高
Shift points以减少过度交易;若想捕捉更细微的跳变,则降低该参数。 - 该策略本质上是反趋势思路,若想转换为顺势突破,可将触发阈值设得更大或叠加趋势过滤指标。
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>
/// Candle-based reversion strategy that reacts to sudden price jumps (high/low shifts)
/// and enforces a configurable loss cap. Adapted from a Level1 quote-reversion approach
/// to work with candle data for backtesting.
/// </summary>
public class LuckyShiftLimitStrategy : Strategy
{
private readonly StrategyParam<int> _shiftPoints;
private readonly StrategyParam<int> _limitPoints;
private decimal? _previousHigh;
private decimal? _previousLow;
private decimal _shiftThreshold;
private decimal _limitThreshold;
private decimal _entryPrice;
private bool _thresholdsReady;
private int _holdBars;
/// <summary>
/// Minimum price shift (as percentage tenths) required to trigger an entry.
/// </summary>
public int ShiftPoints
{
get => _shiftPoints.Value;
set => _shiftPoints.Value = value;
}
/// <summary>
/// Maximum adverse excursion (as percentage) tolerated before force-closing losing trades.
/// </summary>
public int LimitPoints
{
get => _limitPoints.Value;
set => _limitPoints.Value = value;
}
/// <summary>
/// Initializes the strategy parameters taken from the original MQ4 expert.
/// </summary>
public LuckyShiftLimitStrategy()
{
_shiftPoints = Param(nameof(ShiftPoints), 3)
.SetGreaterThanZero()
.SetDisplay("Shift points", "Minimum price delta between consecutive candles", "Trading")
.SetOptimize(1, 20, 1);
_limitPoints = Param(nameof(LimitPoints), 18)
.SetGreaterThanZero()
.SetDisplay("Limit points", "Maximum allowed drawdown in percentage", "Risk management")
.SetOptimize(5, 80, 5);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousHigh = null;
_previousLow = null;
_shiftThreshold = 0m;
_limitThreshold = 0m;
_entryPrice = 0m;
_thresholdsReady = false;
_holdBars = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var tf = TimeSpan.FromMinutes(5).TimeFrame();
SubscribeCandles(tf)
.Bind(ProcessCandle)
.Start();
}
private void EnsureThresholds(decimal price)
{
if (_thresholdsReady)
return;
if (price <= 0m)
return;
// ShiftPoints=3 -> 0.9% shift threshold, LimitPoints=18 -> 1.8% limit threshold
_shiftThreshold = price * ShiftPoints * 0.003m;
_limitThreshold = price * LimitPoints * 0.01m;
_thresholdsReady = true;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var high = candle.HighPrice;
var low = candle.LowPrice;
var close = candle.ClosePrice;
EnsureThresholds(close);
if (!_thresholdsReady)
return;
// Count hold bars for position management.
if (Position != 0)
_holdBars++;
// Entry logic: detect sudden shifts in high/low between consecutive candles.
// Only enter when flat.
if (Position == 0 && _previousHigh is decimal prevHigh && _previousLow is decimal prevLow)
{
// High jumped up sharply -> sell on expected reversion
if (high - prevHigh >= _shiftThreshold)
{
SellMarket();
_entryPrice = close;
_holdBars = 0;
LogInfo($"Sell triggered: high shift {high - prevHigh:0.##} >= {_shiftThreshold:0.##}. Price={close:0.#####}");
}
// Low dropped sharply -> buy on expected rebound
else if (prevLow - low >= _shiftThreshold)
{
BuyMarket();
_entryPrice = close;
_holdBars = 0;
LogInfo($"Buy triggered: low shift {prevLow - low:0.##} >= {_shiftThreshold:0.##}. Price={close:0.#####}");
}
}
_previousHigh = high;
_previousLow = low;
TryClosePosition(close);
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (Position != 0m && _entryPrice == 0m)
_entryPrice = trade.Trade.Price;
if (Position == 0m)
_entryPrice = 0m;
}
private void TryClosePosition(decimal currentPrice)
{
if (Position == 0)
return;
var avgPrice = _entryPrice;
if (avgPrice <= 0m)
return;
// Minimum hold of 5 bars before checking exit.
if (_holdBars < 5)
return;
// Use half of shift threshold as profit target.
var profitTarget = _shiftThreshold * 0.5m;
if (Position > 0)
{
// Close long on profit or loss cap.
if (currentPrice - avgPrice >= profitTarget)
{
SellMarket();
_holdBars = 0;
LogInfo($"Closed long in profit. Price={currentPrice:0.#####}");
}
else if (_limitThreshold > 0m && avgPrice - currentPrice >= _limitThreshold)
{
SellMarket();
_holdBars = 0;
LogInfo($"Closed long on loss cap. Price={currentPrice:0.#####}");
}
}
else if (Position < 0)
{
// Close short on profit or loss cap.
if (avgPrice - currentPrice >= profitTarget)
{
BuyMarket();
_holdBars = 0;
LogInfo($"Closed short in profit. Price={currentPrice:0.#####}");
}
else if (_limitThreshold > 0m && currentPrice - avgPrice >= _limitThreshold)
{
BuyMarket();
_holdBars = 0;
LogInfo($"Closed short on loss cap. Price={currentPrice:0.#####}");
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class lucky_shift_limit_strategy(Strategy):
"""Candle-based reversion strategy that reacts to sudden price jumps (high/low shifts)
and enforces a configurable loss cap. Adapted from a Level1 quote-reversion approach
to work with candle data for backtesting."""
def __init__(self):
super(lucky_shift_limit_strategy, self).__init__()
self._shift_points = self.Param("ShiftPoints", 3) \
.SetGreaterThanZero() \
.SetDisplay("Shift points", "Minimum price delta between consecutive candles", "Trading")
self._limit_points = self.Param("LimitPoints", 18) \
.SetGreaterThanZero() \
.SetDisplay("Limit points", "Maximum allowed drawdown in percentage", "Risk management")
self._previous_high = None
self._previous_low = None
self._shift_threshold = 0.0
self._limit_threshold = 0.0
self._entry_price = 0.0
self._thresholds_ready = False
self._hold_bars = 0
@property
def ShiftPoints(self):
return self._shift_points.Value
@property
def LimitPoints(self):
return self._limit_points.Value
def OnReseted(self):
super(lucky_shift_limit_strategy, self).OnReseted()
self._previous_high = None
self._previous_low = None
self._shift_threshold = 0.0
self._limit_threshold = 0.0
self._entry_price = 0.0
self._thresholds_ready = False
self._hold_bars = 0
def OnStarted2(self, time):
super(lucky_shift_limit_strategy, self).OnStarted2(time)
tf = DataType.TimeFrame(TimeSpan.FromMinutes(5))
subscription = self.SubscribeCandles(tf)
subscription.Bind(self._process_candle).Start()
def _ensure_thresholds(self, price):
if self._thresholds_ready:
return
if price <= 0:
return
# ShiftPoints=3 -> 0.9% shift threshold, LimitPoints=18 -> 1.8% limit threshold
self._shift_threshold = float(price) * self.ShiftPoints * 0.003
self._limit_threshold = float(price) * self.LimitPoints * 0.01
self._thresholds_ready = True
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
self._ensure_thresholds(close)
if not self._thresholds_ready:
return
# Count hold bars for position management.
if self.Position != 0:
self._hold_bars += 1
# Entry logic: detect sudden shifts in high/low between consecutive candles.
# Only enter when flat.
if self.Position == 0 and self._previous_high is not None and self._previous_low is not None:
prev_high = self._previous_high
prev_low = self._previous_low
# High jumped up sharply -> sell on expected reversion
if high - prev_high >= self._shift_threshold:
self.SellMarket()
self._entry_price = close
self._hold_bars = 0
# Low dropped sharply -> buy on expected rebound
elif prev_low - low >= self._shift_threshold:
self.BuyMarket()
self._entry_price = close
self._hold_bars = 0
self._previous_high = high
self._previous_low = low
self._try_close_position(close)
def OnOwnTradeReceived(self, trade):
super(lucky_shift_limit_strategy, self).OnOwnTradeReceived(trade)
if self.Position != 0 and self._entry_price == 0:
self._entry_price = float(trade.Trade.Price)
if self.Position == 0:
self._entry_price = 0.0
def _try_close_position(self, current_price):
if self.Position == 0:
return
avg_price = self._entry_price
if avg_price <= 0:
return
# Minimum hold of 5 bars before checking exit.
if self._hold_bars < 5:
return
# Use half of shift threshold as profit target.
profit_target = self._shift_threshold * 0.5
if self.Position > 0:
# Close long on profit or loss cap.
if current_price - avg_price >= profit_target:
self.SellMarket()
self._hold_bars = 0
elif self._limit_threshold > 0 and avg_price - current_price >= self._limit_threshold:
self.SellMarket()
self._hold_bars = 0
elif self.Position < 0:
# Close short on profit or loss cap.
if avg_price - current_price >= profit_target:
self.BuyMarket()
self._hold_bars = 0
elif self._limit_threshold > 0 and current_price - avg_price >= self._limit_threshold:
self.BuyMarket()
self._hold_bars = 0
def CreateClone(self):
return lucky_shift_limit_strategy()