楼梯策略
楼梯策略 完全复刻了最初的 MetaTrader 专家顾问。策略先在当前卖价上下放置对称的止损挂单,然后始终围绕最近一次成交重新搭建网格。未实现利润按照价格跳动(点)累积,而不按照成交量加权,这与源码的做法一致。当达到利润目标时,策略会通过市价单平掉全部仓位,删除所有挂单并重新开始。
交易逻辑
- 当没有持仓时,在当前卖价之上和之下
ChannelSteps / 2个最小价位处分别挂出买入止损和卖出止损单。 - 首单成交后围绕成交价重建网格:
- 如果激活的止损挂单不足两张,就取消旧单。
- 只要当前买价仍在距最后成交价一半通道距离内,就重新在
ChannelSteps的距离外放置买入止损和卖出止损单。 - 当
AddLots为真时,每次成交后都会在下一组挂单中叠加初始手数。
- 维护多头和空头持仓列表,以重现 MT4 版本使用的对冲式仓位篮子。
- 在每根收盘 K 线处,使用最佳买价计算多头利润,使用最佳卖价计算空头利润,并按照品种的最小价格跳动归一化距离,从而精确复刻原始的点值计算。
- 当以下任一阈值被突破时触发总清仓:
ProfitSteps– 仅统计当前品种产生的利润。CommonProfitSteps– 统计整个仓位篮子的利润。
- 清仓时分别发送市价单平掉所有多头和空头头寸,并在仓位归零后撤销剩余的止损挂单。
注意:原始脚本在挂单时附加了止损价位。StockSharp 的高级 API 无法针对单笔挂单附加保护性止损,因此移植版完全依赖上述利润条件来退出交易。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
ChannelSteps |
对称止损挂单之间的距离,单位为最小价格跳动。 | 1000 |
ProfitSteps |
达到该点数增益后平掉当前品种的仓位篮子。 | 1500 |
CommonProfitSteps |
全部仓位篮子的全局利润阈值,超过后强制清仓。 | 1000 |
AddLots |
若为真,每次成交后在下一组挂单中追加初始手数。 | true |
BaseVolume |
第一组止损挂单的下单手数。 | 0.1m |
CandleType |
订阅及管理所使用的时间框架。 | 1 minute |
实现说明
- 通过
SubscribeCandles()与Bind()使用 StockSharp 的高级 API,仅处理收盘后的 K 线。 - 在
OnOwnTradeReceived中跟踪单笔成交,以便利润计算能够复现 MQL 版本的对冲逻辑。 - 利润阈值完全基于价格跳动距离,不与成交量相乘,与 MT4 脚本的点值累加方式保持一致。
- 通过
BuyStop和SellStop创建所有挂单,并使用市价单执行平仓,从而保证跨数据源的可移植性。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Stairs grid strategy: places trades at regular ATR-based intervals,
/// adding to position on trending moves, closing on profit target.
/// </summary>
public class StairsStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _gridMultiplier;
private readonly StrategyParam<int> _maxLayers;
private readonly StrategyParam<decimal> _profitMultiplier;
private readonly StrategyParam<int> _emaLength;
private decimal _entryPrice;
private decimal _lastGridPrice;
private int _gridCount;
private decimal _prevEma;
public StairsStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period for grid step.", "Indicators");
_gridMultiplier = Param(nameof(GridMultiplier), 1.5m)
.SetDisplay("Grid Multiplier", "ATR multiplier for grid step.", "Grid");
_maxLayers = Param(nameof(MaxLayers), 5)
.SetDisplay("Max Layers", "Maximum grid layers.", "Grid");
_profitMultiplier = Param(nameof(ProfitMultiplier), 2.0m)
.SetDisplay("Profit Multiplier", "ATR multiplier for profit target.", "Grid");
_emaLength = Param(nameof(EmaLength), 20)
.SetDisplay("EMA Length", "EMA for trend direction.", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
public decimal GridMultiplier
{
get => _gridMultiplier.Value;
set => _gridMultiplier.Value = value;
}
public int MaxLayers
{
get => _maxLayers.Value;
set => _maxLayers.Value = value;
}
public decimal ProfitMultiplier
{
get => _profitMultiplier.Value;
set => _profitMultiplier.Value = value;
}
public int EmaLength
{
get => _emaLength.Value;
set => _emaLength.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_lastGridPrice = 0;
_gridCount = 0;
_prevEma = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var atr = new AverageTrueRange { Length = AtrLength };
var ema = new ExponentialMovingAverage { Length = EmaLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrVal, decimal emaVal)
{
if (candle.State != CandleStates.Finished)
return;
if (atrVal <= 0 || _prevEma == 0)
{
_prevEma = emaVal;
return;
}
var close = candle.ClosePrice;
var gridStep = atrVal * GridMultiplier;
var profitTarget = atrVal * ProfitMultiplier;
// Check profit target
if (Position > 0 && _entryPrice > 0)
{
if (close - _entryPrice >= profitTarget || close < emaVal)
{
SellMarket();
_gridCount = 0;
_entryPrice = 0;
_lastGridPrice = 0;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (_entryPrice - close >= profitTarget || close > emaVal)
{
BuyMarket();
_gridCount = 0;
_entryPrice = 0;
_lastGridPrice = 0;
}
}
// Grid: add to winning direction
if (Position > 0 && _lastGridPrice > 0 && _gridCount < MaxLayers)
{
if (close - _lastGridPrice >= gridStep)
{
BuyMarket();
_lastGridPrice = close;
_gridCount++;
}
}
else if (Position < 0 && _lastGridPrice > 0 && _gridCount < MaxLayers)
{
if (_lastGridPrice - close >= gridStep)
{
SellMarket();
_lastGridPrice = close;
_gridCount++;
}
}
// Initial entry based on trend
if (Position == 0)
{
var emaRising = emaVal > _prevEma;
var emaFalling = emaVal < _prevEma;
if (emaRising && close > emaVal)
{
_entryPrice = close;
_lastGridPrice = close;
_gridCount = 0;
BuyMarket();
}
else if (emaFalling && close < emaVal)
{
_entryPrice = close;
_lastGridPrice = close;
_gridCount = 0;
SellMarket();
}
}
_prevEma = emaVal;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange, ExponentialMovingAverage
class stairs_strategy(Strategy):
def __init__(self):
super(stairs_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period for grid step", "Indicators")
self._grid_multiplier = self.Param("GridMultiplier", 1.5) \
.SetDisplay("Grid Multiplier", "ATR multiplier for grid step", "Grid")
self._max_layers = self.Param("MaxLayers", 5) \
.SetDisplay("Max Layers", "Maximum grid layers", "Grid")
self._profit_multiplier = self.Param("ProfitMultiplier", 2.0) \
.SetDisplay("Profit Multiplier", "ATR multiplier for profit target", "Grid")
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Length", "EMA for trend direction", "Indicators")
self._entry_price = 0.0
self._last_grid_price = 0.0
self._grid_count = 0
self._prev_ema = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def AtrLength(self):
return self._atr_length.Value
@property
def GridMultiplier(self):
return self._grid_multiplier.Value
@property
def MaxLayers(self):
return self._max_layers.Value
@property
def ProfitMultiplier(self):
return self._profit_multiplier.Value
@property
def EmaLength(self):
return self._ema_length.Value
def OnStarted2(self, time):
super(stairs_strategy, self).OnStarted2(time)
self._entry_price = 0.0
self._last_grid_price = 0.0
self._grid_count = 0
self._prev_ema = 0.0
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
self._ema = ExponentialMovingAverage()
self._ema.Length = self.EmaLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._atr, self._ema, self.ProcessCandle).Start()
def ProcessCandle(self, candle, atr_val, ema_val):
if candle.State != CandleStates.Finished:
return
atr_v = float(atr_val)
ema_v = float(ema_val)
if atr_v <= 0 or self._prev_ema == 0:
self._prev_ema = ema_v
return
close = float(candle.ClosePrice)
grid_step = atr_v * float(self.GridMultiplier)
profit_target = atr_v * float(self.ProfitMultiplier)
# Check profit target
if self.Position > 0 and self._entry_price > 0:
if close - self._entry_price >= profit_target or close < ema_v:
self.SellMarket()
self._grid_count = 0
self._entry_price = 0.0
self._last_grid_price = 0.0
elif self.Position < 0 and self._entry_price > 0:
if self._entry_price - close >= profit_target or close > ema_v:
self.BuyMarket()
self._grid_count = 0
self._entry_price = 0.0
self._last_grid_price = 0.0
# Grid: add to winning direction
max_layers = self.MaxLayers
if self.Position > 0 and self._last_grid_price > 0 and self._grid_count < max_layers:
if close - self._last_grid_price >= grid_step:
self.BuyMarket()
self._last_grid_price = close
self._grid_count += 1
elif self.Position < 0 and self._last_grid_price > 0 and self._grid_count < max_layers:
if self._last_grid_price - close >= grid_step:
self.SellMarket()
self._last_grid_price = close
self._grid_count += 1
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_ema = ema_v
return
# Initial entry based on trend
if self.Position == 0:
ema_rising = ema_v > self._prev_ema
ema_falling = ema_v < self._prev_ema
if ema_rising and close > ema_v:
self._entry_price = close
self._last_grid_price = close
self._grid_count = 0
self.BuyMarket()
elif ema_falling and close < ema_v:
self._entry_price = close
self._last_grid_price = close
self._grid_count = 0
self.SellMarket()
self._prev_ema = ema_v
def OnReseted(self):
super(stairs_strategy, self).OnReseted()
self._entry_price = 0.0
self._last_grid_price = 0.0
self._grid_count = 0
self._prev_ema = 0.0
def CreateClone(self):
return stairs_strategy()