在 GitHub 上查看
Breakeven Trailing Stop Tick 策略
概览
- 根据 MetaTrader 专家顾问
e_Breakeven_v4 改写的基于 tick 的追踪止损管理器。
- 当价格远离开仓价时,自动上移或下移虚拟止损,从而把仓位锁定在保本并继续跟随趋势。
- 价格触及追踪水平时以市价平掉全部仓位,复现原始 EA 的“保本 + 步长”逻辑。
- 内置可选的演示模式,在回测时随机开仓,便于观察追踪效果而无需额外信号源。
工作流程
- 订阅逐笔成交数据(
DataType.Ticks),模拟 MQL5 中的 OnTick 回调。
- 只要持仓盈利并超过
TrailingStop + TrailingStep(单位:点),就把追踪止损移动到更接近当前价格的位置。
- 多头:若价格上涨超过阈值,则把止损放在
当前价格 - TrailingStop。
- 空头:若价格下跌超过阈值,则把止损放在
当前价格 + TrailingStop。
- 一旦最新成交价触碰追踪水平,立即按市价平仓并重置状态。
- 当标的报价精度为 3 或 5 位小数时,将最小价位变动乘以 10,把 point 转换成 pip,与 MQL5 的处理保持一致。
- 开启演示模式时,策略在空仓状态收到新的 tick 后会根据随机结果开多或开空,仓位大小取自
Volume。
参数
| 名称 |
说明 |
默认值 |
备注 |
TrailingStopPips |
当前价格与追踪止损之间的 pip 距离。 |
10 |
设为 0 可完全关闭追踪功能。 |
TrailingStepPips |
每次继续移动止损前需要的额外 pip 距离。 |
1 |
当启用追踪时必须大于 0,与原 EA 的校验一致。 |
EnableDemoEntries |
是否在测试时随机开仓。 |
false |
设为 true 时,在空仓的 tick 上抛硬币决定方向。 |
仓位管理
- 默认不主动开仓,仅对外部或人工仓位执行追踪;若开启演示模式,则使用随机信号。
- 多、空两种方向都采用相同的追踪规则,可处理任意手数。
- 使用虚拟止损,通过市价平仓来执行,不依赖券商的真实止损订单。
- 可作为其他策略的“保护层”,只负责管理止损,不干预开仓逻辑。
使用提示
- 推荐在提供逐笔成交数据的市场中使用,以便追踪能即时响应。
- 使用演示模式时,请确保
Volume 与期望的下单手数一致。
- pip 转换假设标的是外汇类品种(3 或 5 位小数),其他品种可根据需要调整默认参数。
- 触发条件采用首个穿越止损的 tick,与 MQL5 版本的即时修改并平仓行为相符。
与原始 MQL5 EA 的差异
- StockSharp 策略通过虚拟止损和市价平仓来实现保护,不直接修改经纪商侧的止损订单。
- MetaTrader 测试器中的随机开仓被改造成可配置的
EnableDemoEntries 参数。
- 点值转换通过
Security.PriceStep 与小数位统计实现,而非使用 Symbol().Digits()。
- 所有代码注释与日志均改为英文,满足仓库的统一要求。
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>
/// Trailing stop manager that moves stops to breakeven and beyond once price advances.
/// Designed to trail any manually opened position using pip based distances.
/// </summary>
public class BreakevenTrailingStopTickStrategy : Strategy
{
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<bool> _enableDemoEntries;
private readonly StrategyParam<DataType> _candleType;
private decimal _pointValue;
private decimal? _longStopPrice;
private decimal? _shortStopPrice;
private bool _exitOrderPending;
private decimal _entryPrice;
private DateTimeOffset? _lastDemoEntryTime;
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Trailing step in pips before the stop is moved again.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Enable random demo entries to showcase the trailing behaviour in testing.
/// </summary>
public bool EnableDemoEntries
{
get => _enableDemoEntries.Value;
set => _enableDemoEntries.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="BreakevenTrailingStopTickStrategy"/>.
/// </summary>
public BreakevenTrailingStopTickStrategy()
{
_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
.SetNotNegative()
.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Trailing")
.SetOptimize(5m, 30m, 5m);
_trailingStepPips = Param(nameof(TrailingStepPips), 1m)
.SetNotNegative()
.SetDisplay("Trailing Step", "Additional pips required before stop moves again", "Trailing")
.SetOptimize(0.5m, 5m, 0.5m);
_enableDemoEntries = Param(nameof(EnableDemoEntries), true)
.SetDisplay("Enable Demo Entries", "Automatically open random trades in testing", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for candles", "General");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pointValue = 0m;
_longStopPrice = null;
_shortStopPrice = null;
_exitOrderPending = false;
_lastDemoEntryTime = null;
_entryPrice = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");
_pointValue = CalculateAdjustedPoint();
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var price = candle.ClosePrice;
if (EnableDemoEntries)
TryCreateDemoEntry(candle, price);
if (Position == 0)
{
ResetTrailingState();
return;
}
if (TrailingStopPips <= 0m || _pointValue <= 0m)
return;
if (Position > 0)
UpdateLongTrailing(price);
else if (Position < 0)
UpdateShortTrailing(price);
}
private void TryCreateDemoEntry(ICandleMessage candle, decimal price)
{
if (Position != 0 || _exitOrderPending)
return;
var serverTime = candle.CloseTime;
if (_lastDemoEntryTime.HasValue && (serverTime - _lastDemoEntryTime.Value).TotalMinutes < 30)
return;
var volume = Volume;
if (volume <= 0m)
return;
if (Random.Shared.NextDouble() < 0.5)
{
BuyMarket(volume);
_entryPrice = price;
}
else
{
SellMarket(volume);
_entryPrice = price;
}
_lastDemoEntryTime = serverTime;
}
private void UpdateLongTrailing(decimal currentPrice)
{
var entryPrice = _entryPrice;
if (entryPrice <= 0m)
return;
var stopOffset = TrailingStopPips * _pointValue;
var stepOffset = TrailingStepPips * _pointValue;
if (stopOffset <= 0m)
return;
var activationOffset = stopOffset + stepOffset;
if (currentPrice - entryPrice <= activationOffset)
return;
var threshold = currentPrice - activationOffset;
if (!_longStopPrice.HasValue || _longStopPrice.Value < threshold)
{
var newStop = currentPrice - stopOffset;
if (newStop > 0m)
{
_longStopPrice = newStop;
// log($"Long trailing stop moved to {newStop}.");
}
}
if (_longStopPrice.HasValue && currentPrice <= _longStopPrice.Value)
ExitLongPosition();
}
private void UpdateShortTrailing(decimal currentPrice)
{
var entryPrice = _entryPrice;
if (entryPrice <= 0m)
return;
var stopOffset = TrailingStopPips * _pointValue;
var stepOffset = TrailingStepPips * _pointValue;
if (stopOffset <= 0m)
return;
var activationOffset = stopOffset + stepOffset;
if (entryPrice - currentPrice <= activationOffset)
return;
var threshold = currentPrice + activationOffset;
if (!_shortStopPrice.HasValue || _shortStopPrice.Value > threshold)
{
var newStop = currentPrice + stopOffset;
_shortStopPrice = newStop;
// log($"Short trailing stop moved to {newStop}.");
}
if (_shortStopPrice.HasValue && currentPrice >= _shortStopPrice.Value)
ExitShortPosition();
}
private void ExitLongPosition()
{
if (_exitOrderPending)
return;
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
SellMarket(volume);
_exitOrderPending = true;
// log("Long position closed by trailing stop.");
}
private void ExitShortPosition()
{
if (_exitOrderPending)
return;
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
BuyMarket(volume);
_exitOrderPending = true;
// log("Short position closed by trailing stop.");
}
private void ResetTrailingState()
{
_longStopPrice = null;
_shortStopPrice = null;
_exitOrderPending = false;
_entryPrice = 0m;
}
private decimal CalculateAdjustedPoint()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
var decimals = CountDecimals(step);
return decimals is 3 or 5 ? step * 10m : step;
}
private static int CountDecimals(decimal value)
{
value = Math.Abs(value);
var decimals = 0;
while (value != Math.Truncate(value) && decimals < 10)
{
value *= 10m;
decimals++;
}
return decimals;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class breakeven_trailing_stop_tick_strategy(Strategy):
def __init__(self):
super(breakeven_trailing_stop_tick_strategy, self).__init__()
self._trailing_stop_pips = self.Param("TrailingStopPips", 10.0)
self._trailing_step_pips = self.Param("TrailingStepPips", 1.0)
self._enable_demo_entries = self.Param("EnableDemoEntries", True)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._point_value = 0.0
self._long_stop_price = None
self._short_stop_price = None
self._exit_order_pending = False
self._entry_price = 0.0
self._last_demo_entry_time = None
self._candle_count = 0
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(breakeven_trailing_stop_tick_strategy, self).OnStarted2(time)
self._point_value = self._calculate_adjusted_point()
self._candle_count = 0
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
self._candle_count += 1
if self._enable_demo_entries.Value:
self._try_create_demo_entry(candle, price)
if self.Position == 0:
self._reset_trailing_state()
return
if self._trailing_stop_pips.Value <= 0 or self._point_value <= 0:
return
if self.Position > 0:
self._update_long_trailing(price)
elif self.Position < 0:
self._update_short_trailing(price)
def _try_create_demo_entry(self, candle, price):
if self.Position != 0 or self._exit_order_pending:
return
server_time = candle.CloseTime
if self._last_demo_entry_time is not None and (server_time - self._last_demo_entry_time).TotalMinutes < 30:
return
volume = float(self.Volume)
if volume <= 0:
return
# Use candle count parity as deterministic pseudo-random for demo entries
if self._candle_count % 2 == 0:
self.BuyMarket(volume)
self._entry_price = price
else:
self.SellMarket(volume)
self._entry_price = price
self._last_demo_entry_time = server_time
def _update_long_trailing(self, current_price):
entry_price = self._entry_price
if entry_price <= 0:
return
stop_offset = self._trailing_stop_pips.Value * self._point_value
step_offset = self._trailing_step_pips.Value * self._point_value
if stop_offset <= 0:
return
activation_offset = stop_offset + step_offset
if current_price - entry_price <= activation_offset:
return
threshold = current_price - activation_offset
if self._long_stop_price is None or self._long_stop_price < threshold:
new_stop = current_price - stop_offset
if new_stop > 0:
self._long_stop_price = new_stop
if self._long_stop_price is not None and current_price <= self._long_stop_price:
self._exit_long_position()
def _update_short_trailing(self, current_price):
entry_price = self._entry_price
if entry_price <= 0:
return
stop_offset = self._trailing_stop_pips.Value * self._point_value
step_offset = self._trailing_step_pips.Value * self._point_value
if stop_offset <= 0:
return
activation_offset = stop_offset + step_offset
if entry_price - current_price <= activation_offset:
return
threshold = current_price + activation_offset
if self._short_stop_price is None or self._short_stop_price > threshold:
new_stop = current_price + stop_offset
self._short_stop_price = new_stop
if self._short_stop_price is not None and current_price >= self._short_stop_price:
self._exit_short_position()
def _exit_long_position(self):
if self._exit_order_pending:
return
volume = abs(self.Position)
if volume <= 0:
return
self.SellMarket(volume)
self._exit_order_pending = True
def _exit_short_position(self):
if self._exit_order_pending:
return
volume = abs(self.Position)
if volume <= 0:
return
self.BuyMarket(volume)
self._exit_order_pending = True
def _reset_trailing_state(self):
self._long_stop_price = None
self._short_stop_price = None
self._exit_order_pending = False
self._entry_price = 0.0
def _calculate_adjusted_point(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if step <= 0:
return 1.0
decimals = self._count_decimals(step)
return step * 10.0 if decimals == 3 or decimals == 5 else step
def _count_decimals(self, value):
value = abs(value)
decimals = 0
while value != int(value) and decimals < 10:
value *= 10.0
decimals += 1
return decimals
def OnReseted(self):
super(breakeven_trailing_stop_tick_strategy, self).OnReseted()
self._point_value = 0.0
self._long_stop_price = None
self._short_stop_price = None
self._exit_order_pending = False
self._entry_price = 0.0
self._last_demo_entry_time = None
self._candle_count = 0
def CreateClone(self):
return breakeven_trailing_stop_tick_strategy()