受注安定化戦略
概要
注文安定化戦略は、MetaTrader エキスパート アドバイザー hjueiisyx8lp2o379e_www_forex-instruments_info.mq4 の転換です。元のロボットは、現在の価格付近で 2 つの逆指値注文を出し、ブレイクアウトを待ちます。ポジションがオープンされると、システムは最近のローソク体を監視して価格行動が停滞(「安定化」)したかどうかを判断し、市場の勢いが失われたとき、または事前に定義された利益しきい値に達したときに取引を終了します。
この C# ポートは、高レベルの StockSharp API を使用して同じロジックを維持します。生のティックではなく完成したローソク足に依存するため、バックテストやライブ取引中の動作が決定的になります。
取引ルール
- オープンポジションやアクティブな注文がない場合、この戦略は市場より上に買いストップを、市場より下に売りストップを送信します。距離は MetaTrader ポイント (通常は 1 ピップに等しい) で測定されます。
- 逆指値注文が実行された場合:
- 約定された注文により、
OrderVolumeロットのポジションがオープンされます。 - 反対方向のブレイクアウトをキャッチするために、反対のストップ注文が保留されたままになります。
- 約定された注文により、
- ポジションがオープンしている間、戦略は直近に終了した 2 つのローソク足の実体サイズをチェックします。
- 最新のローソク足本体が
StabilizationPointsより小さく、変動利益がProfitThresholdより高い場合、ポジションはクローズされ、反対の未決注文はキャンセルされます。 - 2 つの連続するローソク足が
StabilizationPointsより小さい場合、現在の利益に関係なく取引は終了します。 - 利益が
AbsoluteFixationに達した場合、取引は直ちに終了します。
- 最新のローソク足本体が
- 保留中の注文は、値がゼロ (無期限) に設定されていない限り、
ExpirationMinutesの後にキャンセルされ、再作成されます。
パラメーター
| 名前 | 説明 | デフォルト |
|---|---|---|
OrderVolume |
両方のストップエントリーに使用されるロットの取引量。 | 0.1 |
OrderDistancePoints |
現在の終値と各逆指値注文の間の距離。MetaTrader ポイントで表されます。 | 20 |
ProfitThreshold |
安定化によるエグジットが許可される前に必要な最低変動利益(口座通貨)。 | -2 |
AbsoluteFixation |
即時退出を強制する利益レベル(口座通貨)。 | 30 |
StabilizationPoints |
市場が横ばいであることを示すローソク足の最大サイズ(ポイント)。 | 25 |
ExpirationMinutes |
保留中のストップ注文の存続期間 (分単位)。 0 は有効期限を無効にします。 |
20 |
CandleType |
安定化の評価に使用されるローソクのタイプ (デフォルトは 5 分の時間枠)。 | TimeFrame(5m) |
変換メモ
- 元のエキスパートアドバイザーはチャートティックに基づいて操作していました。このポートは完成したキャンドルのみを評価し、再現可能なバックテストを確保しながらロジックを維持します。
- MetaTrader 個の「ポイント」は StockSharp
PriceStepにマッピングされます。商品に価格ステップがない場合、1のステップが想定されます。 - 利益は、価格変動をアカウント通貨に換算するために
PriceStepとStepPriceを使用して概算されます。 - すべてのコード コメントは英語で書き直され、パラメーターのメタデータにはグループ化されたわかりやすい説明が含まれています。
使用法
- 戦略を StockSharp ソリューションに追加し、必要なセキュリティとポートフォリオを割り当てます。
- パラメータ、特にローソク足の時間枠とポイント単位の距離を機器の特性に合わせて設定します。
- 戦略を開始します。ペアのストップ注文を発行し、上記の安定化ロジックに従ってポジションを管理します。
さらなるアイデア
- 応答性とノイズ フィルターのバランスをとるために、さまざまなキャンドル間隔を試してください。
- この戦略とボラティリティ フィルター (ATR、Bollinger バンド) を組み合わせて、非常に静かなセッションでの取引を回避します。
- 絶対利益目標に近づいたら、トレーリング ストップまたは部分的なポジション エグジットを使用してロジックを拡張します。
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Paired breakout stop orders with stabilization-based exits.
/// </summary>
public class OrderStabilizationStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<decimal> _orderDistancePoints;
private readonly StrategyParam<decimal> _profitThreshold;
private readonly StrategyParam<decimal> _absoluteFixation;
private readonly StrategyParam<decimal> _stabilizationPoints;
private readonly StrategyParam<int> _expirationMinutes;
private readonly StrategyParam<DataType> _candleType;
private Order _buyStop;
private Order _sellStop;
private DateTime? _createdAt;
private decimal _filledPosition;
private decimal _entryValue;
private decimal _previousBody;
private bool _hasPreviousBody;
public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
public decimal OrderDistancePoints { get => _orderDistancePoints.Value; set => _orderDistancePoints.Value = value; }
public decimal ProfitThreshold { get => _profitThreshold.Value; set => _profitThreshold.Value = value; }
public decimal AbsoluteFixation { get => _absoluteFixation.Value; set => _absoluteFixation.Value = value; }
public decimal StabilizationPoints { get => _stabilizationPoints.Value; set => _stabilizationPoints.Value = value; }
public int ExpirationMinutes { get => _expirationMinutes.Value; set => _expirationMinutes.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public OrderStabilizationStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero()
.SetDisplay("Order Volume", "Trade volume in lots used for both stop entries", "Trading");
_orderDistancePoints = Param(nameof(OrderDistancePoints), 20m).SetGreaterThanZero()
.SetDisplay("Order Distance", "Distance between the current close price and each stop order, in points", "Trading");
_profitThreshold = Param(nameof(ProfitThreshold), -2m)
.SetDisplay("Profit Threshold", "Minimum floating profit (account currency) required before an exit triggered by stabilization is allowed", "Exit");
_absoluteFixation = Param(nameof(AbsoluteFixation), 30m).SetNotNegative()
.SetDisplay("Absolute Fixation", "Profit level (account currency) that forces an immediate exit", "Exit");
_stabilizationPoints = Param(nameof(StabilizationPoints), 25m).SetGreaterThanZero()
.SetDisplay("Stabilization", "Maximum candle body size (points) that signals a flat market", "Exit");
_expirationMinutes = Param(nameof(ExpirationMinutes), 20).SetNotNegative()
.SetDisplay("Expiration", "Lifetime of pending stop orders in minutes, 0 disables expiration", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle type used to evaluate stabilization", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Ticks)];
protected override void OnReseted()
{
base.OnReseted();
_buyStop = null;
_sellStop = null;
_createdAt = null;
_filledPosition = 0m;
_entryValue = 0m;
_previousBody = 0m;
_hasPreviousBody = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Stop orders are matched against trade prints, so they fire between candle closes.
Subscribe(new Subscription(DataType.Ticks, Security));
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var point = Security?.PriceStep ?? 1m;
if (point <= 0m)
point = 1m;
var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
if (Position != 0m && IsExitDue(candle.ClosePrice, body, point))
{
CancelIfActive(_buyStop);
CancelIfActive(_sellStop);
Flatten();
}
else if ((Position == 0m && !IsWorking(_buyStop) && !IsWorking(_sellStop)) || IsExpired(candle.OpenTime))
{
ArmStops(candle.ClosePrice, candle.OpenTime, point);
}
_previousBody = body;
_hasPreviousBody = true;
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var price = trade.Trade.Price;
var volume = trade.Trade.Volume;
var signed = trade.Order.Side == Sides.Buy ? volume : -volume;
var previous = _filledPosition;
_filledPosition += signed;
if (_filledPosition == 0m)
_entryValue = 0m;
else if (previous == 0m || Math.Sign(previous) == Math.Sign(signed))
_entryValue += price * volume;
else if (Math.Sign(_filledPosition) != Math.Sign(previous))
_entryValue = price * Math.Abs(_filledPosition);
else
_entryValue -= _entryValue / Math.Abs(previous) * volume;
}
private bool IsExitDue(decimal price, decimal body, decimal point)
{
var bodyLimit = StabilizationPoints * point;
var pnl = FloatingPnL(price, point);
var oneSmall = body <= bodyLimit;
var twoSmall = oneSmall && _hasPreviousBody && _previousBody <= bodyLimit;
return (oneSmall && pnl > ProfitThreshold) ||
twoSmall ||
(AbsoluteFixation > 0m && pnl >= AbsoluteFixation);
}
private bool IsExpired(DateTime time)
=> ExpirationMinutes > 0 &&
_createdAt is DateTime created &&
time - created >= TimeSpan.FromMinutes(ExpirationMinutes);
// Flat: both stops are placed around the close. With a position open only the pending
// opposite stop is moved; the side that opened the position is never placed again.
private void ArmStops(decimal center, DateTime time, decimal point)
{
var distance = OrderDistancePoints * point;
if (Position == 0m || (Position < 0m && IsWorking(_buyStop)))
_buyStop = RenewStop(_buyStop, Sides.Buy, center + distance);
if (Position == 0m || (Position > 0m && IsWorking(_sellStop)))
_sellStop = RenewStop(_sellStop, Sides.Sell, center - distance);
_createdAt = IsWorking(_buyStop) || IsWorking(_sellStop) ? time : null;
}
private Order RenewStop(Order order, Sides side, decimal activationPrice)
{
if (order is { State: OrderStates.Active })
CancelOrder(order);
else if (IsWorking(order))
return order;
var stop = new Order
{
Security = Security,
Portfolio = Portfolio,
Side = side,
Volume = OrderVolume,
Type = OrderTypes.Conditional,
Condition = new StopOrderCondition { ActivationPrice = activationPrice },
};
RegisterOrder(stop);
return stop;
}
private static bool IsWorking(Order order)
=> order is not null && order.State is not (OrderStates.Done or OrderStates.Failed);
private void CancelIfActive(Order order)
{
if (order is { State: OrderStates.Active })
CancelOrder(order);
}
private decimal FloatingPnL(decimal price, decimal point)
{
if (Position == 0m || _filledPosition == 0m)
return 0m;
var entryPrice = _entryValue / Math.Abs(_filledPosition);
var direction = Position > 0m ? 1m : -1m;
var move = (price - entryPrice) * direction;
var stepPrice = Security?.StepPrice ?? 0m;
return stepPrice > 0m
? move / point * stepPrice * Math.Abs(Position)
: move * (Security?.Multiplier ?? 1m) * Math.Abs(Position);
}
private void Flatten()
{
if (Position > 0m)
SellMarket(Math.Abs(Position));
else if (Position < 0m)
BuyMarket(Math.Abs(Position));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.MatchingEngine")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Sides, OrderTypes, OrderStates, ITickTradeMessage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.MatchingEngine import StopOrderCondition
# ExecutionMessage implements tick Price/Volume explicitly; access the declared interface.
_tick_price = clr.GetClrType(ITickTradeMessage).GetProperty("Price")
_tick_volume = clr.GetClrType(ITickTradeMessage).GetProperty("Volume")
class order_stabilization_strategy(Strategy):
def __init__(self):
super(order_stabilization_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 0.1).SetGreaterThanZero() \
.SetDisplay("Order Volume", "Trade volume in lots used for both stop entries", "Trading")
self._order_distance = self.Param("OrderDistancePoints", 20.0).SetGreaterThanZero() \
.SetDisplay("Order Distance", "Distance between the current close price and each stop order, in points", "Trading")
self._profit_threshold = self.Param("ProfitThreshold", -2.0) \
.SetDisplay("Profit Threshold", "Minimum floating profit (account currency) required before an exit triggered by stabilization is allowed", "Exit")
self._absolute_fixation = self.Param("AbsoluteFixation", 30.0).SetNotNegative() \
.SetDisplay("Absolute Fixation", "Profit level (account currency) that forces an immediate exit", "Exit")
self._stabilization_points = self.Param("StabilizationPoints", 25.0).SetGreaterThanZero() \
.SetDisplay("Stabilization", "Maximum candle body size (points) that signals a flat market", "Exit")
self._expiration_minutes = self.Param("ExpirationMinutes", 20).SetNotNegative() \
.SetDisplay("Expiration", "Lifetime of pending stop orders in minutes, 0 disables expiration", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candle type used to evaluate stabilization", "General")
self._buy_stop = None
self._sell_stop = None
self._created_at = None
self._filled_position = Decimal.Zero
self._entry_value = Decimal.Zero
self._previous_body = Decimal.Zero
self._has_previous_body = False
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value), (self.Security, DataType.Ticks)]
def OnReseted(self):
super(order_stabilization_strategy, self).OnReseted()
self._buy_stop = None
self._sell_stop = None
self._created_at = None
self._filled_position = Decimal.Zero
self._entry_value = Decimal.Zero
self._previous_body = Decimal.Zero
self._has_previous_body = False
def OnStarted2(self, time):
super(order_stabilization_strategy, self).OnStarted2(time)
# Stop orders are matched against trade prints, so they fire between candle closes.
self.Subscribe(Subscription(DataType.Ticks, self.Security))
self.SubscribeCandles(self._candle_type.Value).Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
point = self.Security.PriceStep if self.Security is not None and self.Security.PriceStep is not None else Decimal.One
if point <= 0:
point = Decimal.One
body = Math.Abs(candle.ClosePrice - candle.OpenPrice)
if self.Position != 0 and self._is_exit_due(candle.ClosePrice, body, point):
self._cancel_if_active(self._buy_stop)
self._cancel_if_active(self._sell_stop)
self._flatten()
elif (self.Position == 0 and not self._is_working(self._buy_stop) and not self._is_working(self._sell_stop)) or \
self._is_expired(candle.OpenTime):
self._arm_stops(candle.ClosePrice, candle.OpenTime, point)
self._previous_body = body
self._has_previous_body = True
def OnOwnTradeReceived(self, trade):
super(order_stabilization_strategy, self).OnOwnTradeReceived(trade)
price = _tick_price.GetValue(trade.Trade)
volume = _tick_volume.GetValue(trade.Trade)
signed = volume if trade.Order.Side == Sides.Buy else -volume
previous = self._filled_position
self._filled_position += signed
if self._filled_position == 0:
self._entry_value = Decimal.Zero
elif previous == 0 or Math.Sign(previous) == Math.Sign(signed):
self._entry_value += price * volume
elif Math.Sign(self._filled_position) != Math.Sign(previous):
self._entry_value = price * Math.Abs(self._filled_position)
else:
self._entry_value -= self._entry_value / Math.Abs(previous) * volume
def _is_exit_due(self, price, body, point):
body_limit = Decimal(self._stabilization_points.Value) * point
pnl = self._floating_pnl(price, point)
one_small = body <= body_limit
two_small = one_small and self._has_previous_body and self._previous_body <= body_limit
absolute_fixation = Decimal(self._absolute_fixation.Value)
return (one_small and pnl > Decimal(self._profit_threshold.Value)) or \
two_small or \
(absolute_fixation > 0 and pnl >= absolute_fixation)
def _is_expired(self, time):
minutes = int(self._expiration_minutes.Value)
return minutes > 0 and \
self._created_at is not None and \
time - self._created_at >= TimeSpan.FromMinutes(minutes)
# Flat: both stops are placed around the close. With a position open only the pending
# opposite stop is moved; the side that opened the position is never placed again.
def _arm_stops(self, center, time, point):
distance = Decimal(self._order_distance.Value) * point
if self.Position == 0 or (self.Position < 0 and self._is_working(self._buy_stop)):
self._buy_stop = self._renew_stop(self._buy_stop, Sides.Buy, center + distance)
if self.Position == 0 or (self.Position > 0 and self._is_working(self._sell_stop)):
self._sell_stop = self._renew_stop(self._sell_stop, Sides.Sell, center - distance)
self._created_at = time if self._is_working(self._buy_stop) or self._is_working(self._sell_stop) else None
def _renew_stop(self, order, side, activation_price):
if order is not None and order.State == OrderStates.Active:
self.CancelOrder(order)
elif self._is_working(order):
return order
stop = Order()
stop.Security = self.Security
stop.Portfolio = self.Portfolio
stop.Side = side
stop.Volume = Decimal(self._order_volume.Value)
stop.Type = OrderTypes.Conditional
condition = StopOrderCondition()
condition.ActivationPrice = activation_price
stop.Condition = condition
self.RegisterOrder(stop)
return stop
def _is_working(self, order):
return order is not None and order.State != OrderStates.Done and order.State != OrderStates.Failed
def _cancel_if_active(self, order):
if order is not None and order.State == OrderStates.Active:
self.CancelOrder(order)
def _floating_pnl(self, price, point):
if self.Position == 0 or self._filled_position == 0:
return Decimal.Zero
entry_price = self._entry_value / Math.Abs(self._filled_position)
direction = Decimal.One if self.Position > 0 else Decimal.MinusOne
move = (price - entry_price) * direction
step_price = self.Security.StepPrice if self.Security is not None and self.Security.StepPrice is not None else Decimal.Zero
if step_price > 0:
return move / point * step_price * Math.Abs(self.Position)
multiplier = self.Security.Multiplier if self.Security is not None and self.Security.Multiplier is not None else Decimal.One
return move * multiplier * Math.Abs(self.Position)
def _flatten(self):
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
def CreateClone(self):
return order_stabilization_strategy()