Autotrade Pending Stops 策略
概述
该策略是 MetaTrader 顾问 Autotrade (barabashkakvn's edition) 的 StockSharp 版本。策略始终在当前价格上下维持两张对称的挂单:在价格上方放置 Buy Stop,在价格下方放置 Sell Stop。只要没有持仓,挂单会在每根收盘 K 线时刷新;一旦挂单被触发,系统会根据市场波动情况或绝对盈亏阈值来决定何时平仓。实现过程中严格遵循 AGENTS.md 的要求,并完全使用 StockSharp 的高级 API。
参数对照
| StockSharp 参数 | MQL5 输入 | 说明 |
|---|---|---|
IndentTicks |
InpIndent |
当前价格到挂单的距离(以价格最小跳动数表示)。 |
MinProfit |
MinProfit |
在行情趋于平静时触发平仓所需的最小浮动盈利(账户货币)。 |
ExpirationMinutes |
ExpirationMinutes |
挂单的存活时间,超时后挂单会被取消并在下一根 K 线重建。 |
AbsoluteFixation |
AbsoluteFixation |
触发强制平仓的绝对盈亏阈值(账户货币)。 |
StabilizationTicks |
InpStabilization |
前一根 K 线实体的最大允许长度,用于识别盘整行情。 |
OrderVolume |
Lots |
Buy Stop 与 Sell Stop 的下单手数。 |
CandleType |
Period() |
驱动策略的 K 线类型(默认 1 分钟)。 |
所有以“点”为单位的距离都会根据 Security.PriceStep 转换为实际价格跳动。盈亏阈值通过 Security.StepPrice 计算,以便与原始 MQL5 版本使用的账户货币结果一致。
交易流程
挂单布置
- 只处理状态为
CandleStates.Finished的完整 K 线。 - 第一根 K 线用于初始化历史数据(上一根开/收价),随后立即放置挂单。
- 当仓位为零时会清理失效引用,然后:
- 在
Close + IndentTicks * PriceStep处放置 Buy Stop; - 在
Close - IndentTicks * PriceStep处放置 Sell Stop。
- 在
- 每张挂单的到期时间均设为
CloseTime + ExpirationMinutes分钟;一旦过期便取消,并在下一根 K 线上重新创建。
仓位管理
- 当其中一张挂单成交后,会立刻取消另一张挂单,以避免在 StockSharp 的净额模型下产生对冲仓位。
- 策略保存上一根 K 线的实体长度(
|Open - Close|),用于判断市场是否进入低波动区间。 - 当持仓存在时:
- 根据
PositionAvgPrice计算当前浮动盈亏(使用PriceStep和StepPrice转换为货币单位)。 - 若浮盈超过
MinProfit且上一根 K 线实体小于StabilizationTicks * PriceStep,则以市价平仓。 - 无论波动如何,只要绝对盈亏超过
AbsoluteFixation,也会立即平仓。
- 根据
- 仓位归零后,所有剩余的挂单会被彻底移除。
其他行为
- 策略始终保持单向净头寸,
OrderVolume会同步设置策略的Volume。 - 在多数回测场景中缺乏实时买卖盘,因此挂单价格基于完成 K 线的收盘价计算。
- 下单前会检查
IsFormedAndOnlineAndAllowTrading(),确保数据已就绪且允许交易。
实现差异与注意事项
- 盈亏换算依赖
Security.PriceStep与Security.StepPrice。若交易品种未提供这些值,代码会退化为使用默认值1,需要在接入市场数据前确认配置正确。 - 原始 MQL5 版本允许对冲式的双向仓位;本移植版在挂单成交后立即撤销另一张挂单,以适配 StockSharp 的净额账户模型。
- 挂单到期时间基于 K 线的
CloseTime。如果数据源缺失该字段,需要扩展数据适配层以提供有效时间戳。 - 通过调整
CandleType可以轻松切换不同的时间框架或其他类型的蜡烛图数据。
使用建议
- 将
CandleType设置为与原始策略相同的周期,以保持交易节奏一致。 - 根据品种的最小跳动和 tick 价值调节
IndentTicks、StabilizationTicks、MinProfit与AbsoluteFixation。 - 确认账户模式(净额/对冲)。策略假设为净额模式,会在重新布置挂单前确保仓位归零。
- 在 StockSharp Designer 或 Backtester 中利用参数进行优化,以适配不同市场或交易品种。
- 关注日志输出:策略仅在收到完整数据且交易被允许时才会提交新订单。
风险提示
量化交易存在较高风险。请在历史数据上充分回测、验证参数,并确保满足券商关于挂单最小距离等限制后再用于真实账户。
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Conversion of the MQL Autotrade strategy that places symmetric stop orders around the market.
/// Pending stop entries are refreshed on every candle while no position is open.
/// Positions are closed when the market calms down or when absolute profit/loss thresholds are reached.
/// </summary>
public class AutotradePendingStopsStrategy : Strategy
{
private readonly StrategyParam<int> _indentTicks;
private readonly StrategyParam<decimal> _minProfit;
private readonly StrategyParam<int> _expirationMinutes;
private readonly StrategyParam<decimal> _absoluteFixation;
private readonly StrategyParam<int> _stabilizationTicks;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevOpen;
private decimal _prevClose;
private bool _hasPrevCandle;
private decimal _tickSize = 1m;
private decimal _tickValue = 1m;
/// <summary>
/// Distance in price steps from the current market to the pending stop entries.
/// </summary>
public int IndentTicks
{
get => _indentTicks.Value;
set => _indentTicks.Value = value;
}
/// <summary>
/// Minimal profit in account currency required to exit when price action stabilizes.
/// </summary>
public decimal MinProfit
{
get => _minProfit.Value;
set => _minProfit.Value = value;
}
/// <summary>
/// Lifetime of pending stop orders in minutes.
/// </summary>
public int ExpirationMinutes
{
get => _expirationMinutes.Value;
set => _expirationMinutes.Value = value;
}
/// <summary>
/// Absolute profit or loss that forces the position to close.
/// </summary>
public decimal AbsoluteFixation
{
get => _absoluteFixation.Value;
set => _absoluteFixation.Value = value;
}
/// <summary>
/// Maximum size of the previous candle body that is treated as consolidation.
/// </summary>
public int StabilizationTicks
{
get => _stabilizationTicks.Value;
set => _stabilizationTicks.Value = value;
}
/// <summary>
/// Order volume used for entries.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set
{
_orderVolume.Value = value;
Volume = value;
}
}
/// <summary>
/// Candle type used to drive the strategy logic.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public AutotradePendingStopsStrategy()
{
_indentTicks = Param(nameof(IndentTicks), 200)
.SetGreaterThanZero()
.SetDisplay("Indent Ticks", "Distance in ticks between price and pending stop orders", "Entries");
_minProfit = Param(nameof(MinProfit), 2m)
.SetGreaterThanZero()
.SetDisplay("Min Profit", "Minimum profit to close during low volatility", "Risk");
_expirationMinutes = Param(nameof(ExpirationMinutes), 41)
.SetGreaterThanZero()
.SetDisplay("Order Expiration", "Lifetime of pending stops in minutes", "Entries");
_absoluteFixation = Param(nameof(AbsoluteFixation), 43m)
.SetGreaterThanZero()
.SetDisplay("Absolute Fixation", "Profit or loss in currency that forces exit", "Risk");
_stabilizationTicks = Param(nameof(StabilizationTicks), 25)
.SetGreaterThanZero()
.SetDisplay("Stabilization Ticks", "Maximum candle body considered as flat market", "Exits");
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Default volume for both stop orders", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame that drives order refresh", "General");
Volume = _orderVolume.Value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Reset runtime state when the strategy is reloaded.
_prevOpen = 0m;
_prevClose = 0m;
_hasPrevCandle = false;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = _orderVolume.Value;
// Cache price step and tick value for fast profit calculations.
_tickSize = Security.PriceStep ?? 1m;
_tickValue = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? _tickSize;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Only act on completed candles to stay aligned with the original MQL logic.
if (candle.State != CandleStates.Finished)
return;
if (!_hasPrevCandle)
{
// Store the first candle so that stabilization checks have history.
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_hasPrevCandle = true;
EnsurePendingOrders(candle);
return;
}
UpdatePendingOrdersLifetime(candle);
if (Position == 0)
{
// Refresh pending orders as soon as the market is flat.
EnsurePendingOrders(candle);
}
else
{
// Manage the active position and close it when required.
ManageOpenPosition(candle);
}
// Keep the previous candle body for stabilization checks on the next bar.
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
}
private decimal _entryPrice;
private void EnsurePendingOrders(ICandleMessage candle)
{
if (!IsFormedAndOnlineAndAllowTrading())
return;
var indent = IndentTicks * _tickSize;
var buyPrice = candle.ClosePrice + indent;
var sellPrice = candle.ClosePrice - indent;
// Simulate stop-order breakout: if high breaches buy level, go long
if (candle.HighPrice >= buyPrice && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(OrderVolume);
_entryPrice = buyPrice;
}
// if low breaches sell level, go short
else if (candle.LowPrice <= sellPrice && Position >= 0)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(OrderVolume);
_entryPrice = sellPrice;
}
}
private void UpdatePendingOrdersLifetime(ICandleMessage candle)
{
// No pending orders in simplified version - nothing to expire.
}
private void ManageOpenPosition(ICandleMessage candle)
{
var entryPrice = _entryPrice;
if (entryPrice == 0)
return;
var priceDiff = Position > 0 ? candle.ClosePrice - entryPrice : entryPrice - candle.ClosePrice;
var prevBodySize = Math.Abs(_prevClose - _prevOpen);
// Exit if profitable and market consolidating, or if loss exceeds threshold
var exitByProfit = priceDiff > 0 && prevBodySize < candle.ClosePrice * 0.001m;
var exitByLoss = priceDiff < -candle.ClosePrice * 0.005m;
if (Position > 0 && (exitByProfit || exitByLoss))
{
SellMarket();
}
else if (Position < 0 && (exitByProfit || exitByLoss))
{
BuyMarket();
}
}
}
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
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class autotrade_pending_stops_strategy(Strategy):
def __init__(self):
super(autotrade_pending_stops_strategy, self).__init__()
self._indent_ticks = self.Param("IndentTicks", 200)
self._min_profit = self.Param("MinProfit", 2.0)
self._expiration_minutes = self.Param("ExpirationMinutes", 41)
self._absolute_fixation = self.Param("AbsoluteFixation", 43.0)
self._stabilization_ticks = self.Param("StabilizationTicks", 25)
self._order_volume = self.Param("OrderVolume", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._tick_size = 1.0
self._entry_price = 0.0
@property
def IndentTicks(self):
return self._indent_ticks.Value
@IndentTicks.setter
def IndentTicks(self, value):
self._indent_ticks.Value = value
@property
def MinProfit(self):
return self._min_profit.Value
@MinProfit.setter
def MinProfit(self, value):
self._min_profit.Value = value
@property
def ExpirationMinutes(self):
return self._expiration_minutes.Value
@ExpirationMinutes.setter
def ExpirationMinutes(self, value):
self._expiration_minutes.Value = value
@property
def AbsoluteFixation(self):
return self._absolute_fixation.Value
@AbsoluteFixation.setter
def AbsoluteFixation(self, value):
self._absolute_fixation.Value = value
@property
def StabilizationTicks(self):
return self._stabilization_ticks.Value
@StabilizationTicks.setter
def StabilizationTicks(self, value):
self._stabilization_ticks.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.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(autotrade_pending_stops_strategy, self).OnStarted2(time)
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._entry_price = 0.0
self.Volume = self._order_volume.Value
self._tick_size = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self._tick_size <= 0.0:
self._tick_size = 1.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_price = float(candle.OpenPrice)
if not self._has_prev_candle:
self._prev_open = open_price
self._prev_close = close
self._has_prev_candle = True
self._ensure_pending_orders(candle)
return
if self.Position == 0:
self._ensure_pending_orders(candle)
else:
self._manage_open_position(candle)
self._prev_open = open_price
self._prev_close = close
def _ensure_pending_orders(self, candle):
if not self.IsFormedAndOnlineAndAllowTrading():
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
order_vol = float(self._order_volume.Value)
indent = int(self._indent_ticks.Value) * self._tick_size
buy_price = close + indent
sell_price = close - indent
pos = float(self.Position)
if high >= buy_price and pos <= 0:
if pos < 0:
self.BuyMarket(abs(pos))
self.BuyMarket(order_vol)
self._entry_price = buy_price
elif low <= sell_price and pos >= 0:
if pos > 0:
self.SellMarket(abs(pos))
self.SellMarket(order_vol)
self._entry_price = sell_price
def _manage_open_position(self, candle):
if self._entry_price == 0.0:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0:
price_diff = close - self._entry_price
else:
price_diff = self._entry_price - close
prev_body_size = abs(self._prev_close - self._prev_open)
exit_by_profit = price_diff > 0.0 and prev_body_size < close * 0.001
exit_by_loss = price_diff < -close * 0.005
if self.Position > 0 and (exit_by_profit or exit_by_loss):
self.SellMarket()
elif self.Position < 0 and (exit_by_profit or exit_by_loss):
self.BuyMarket()
def OnReseted(self):
super(autotrade_pending_stops_strategy, self).OnReseted()
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._tick_size = 1.0
self._entry_price = 0.0
def CreateClone(self):
return autotrade_pending_stops_strategy()