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()