GitHub で見る
ブレークイーブン・トレーリングストップ・ティック戦略
概要
- MetaTraderのエキスパートアドバイザー
e_Breakeven_v4から変換されたティックベースのトレーリングストップマネージャーです。
- 価格がエントリーから十分に離れたら仮想ストップロスを移動させるために、すべての取引ティックを監視します。
- トレーリングレベルに達したときにロングまたはショートポジションを市場で閉じ、元のEAのブレークイーブン+ステップの動作を再現します。
- 外部シグナルソースなしでトレーリングロジックをデモンストレーションするために、テスト中にランダムにポジションを開くオプションのデモモードを含みます。
仕組み
- 戦略はMQL5で使用される
OnTickコールバックをエミュレートするために取引ティック(DataType.Ticks)をサブスクライブします。
- ポジションが存在し、トレーリングストップ(pips単位)とトレーリングステップを合わせた値が超過されると、ストップレベルが価格に近い方へシフトされます。
- ロングポジションの場合、エントリーからの動きが
トレーリングストップ + トレーリングステップを超えると、ストップは現在価格 - トレーリングストップに置かれます。
- ショートポジションの場合、価格が同じ距離だけ下方に動いたとき、ストップは
現在価格 + トレーリングストップに置かれます。
- ライブ価格が保存されたストップレベルに触れるかクロスすると、戦略はポジション全体を市場で出て、トレーリング状態をリセットします。
- 内部pip変換は、インストゥルメントが3桁または5桁の小数点を持つ場合にブローカーの価格ステップに10を掛け、MQL5のポイントからpipへの調整に対応します。
- デモモードが有効な場合、戦略は前のエントリーが閉じられた後に新しいティックが来るたびに(設定された
Volumeを使用して)ランダムにロングまたはショート取引を開きます。
パラメーター
| 名前 |
説明 |
デフォルト |
注意 |
TrailingStopPips |
現在価格とトレーリングストップの間のpips距離。 |
10 |
0に設定するとトレーリングを完全に無効化します。 |
TrailingStepPips |
ストップが再び進む前に必要な追加pip距離。 |
1 |
トレーリングストップがアクティブな場合はゼロより大きくなければならず、EAの検証ルールを再現します。 |
EnableDemoEntries |
外部シグナルなしでバックテスト用のランダムエントリーを有効にします。 |
false |
trueの場合、戦略はフラット中の各ティックで方向を決めるためにコインを投げます。 |
ポジション管理ルール
EnableDemoEntriesがtrueに設定されていない限り、戦略はポジションを自分では開きません。
- トレーリングはロングとショートポジションに対して対称的で、どのボリュームサイズでも機能します。
- ストップレベルは内部的(仮想)に管理され、市場エグジットで強制され、すべてのコネクターでサポートされない可能性のある明示的なストップ注文を避けます。
- 任意の手動取引または外部戦略がエントリーを提供できます;このコンポーネントはトレーリングストップのみを管理します。
使用上の注意
- トレーリングが即座に反応するために取引ティックを提供するインストゥルメントで最もよく機能します。
- デモモードを使用する場合は、
Volumeが受信ポジションに対応するロットサイズに設定されていることを確認してください。
- pip変換は、3桁または5桁の小数を持つシンボルがポイントをpipsに変換するために×10の乗数を必要とするFXスタイルの価格設定を前提としています。
- エグジットは保存されたストップ価格をクロスする最初のティックでトリガーされ、MQLロジックからの即時修正およびクローズフローに対応します。
元のMQL5エキスパートとの違い
- StockSharpの戦略は通常戦略ロジックを通じてエグジットを管理するため、ブローカー側のストップロス注文を変更する代わりに仮想ストップと市場エグジットを使用します。
- MetaTraderテスターのランダムエントリーブロックを設定可能な
EnableDemoEntriesフラグに置き換えます。
Symbol().Digits()の代わりにSecurity.PriceStepと小数カウントを使用してポイントからpipへのロジックを変換します。
- リポジトリガイドラインに従い、すべてのコメントとロギングは英語になっています。
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()