using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
public enum LbsMoneyMode
{
FixedLot,
RiskPercent,
}
/// <summary>
/// Previous-candle breakout strategy with paired conditional stops and Level1 trailing.
/// </summary>
public class LbsStrategy : Strategy
{
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<LbsMoneyMode> _moneyMode;
private readonly StrategyParam<decimal> _volumeOrRisk;
private readonly StrategyParam<int> _hour1;
private readonly StrategyParam<int> _hour2;
private readonly StrategyParam<int> _hour3;
private readonly StrategyParam<DataType> _candleType;
private decimal? _bid;
private decimal? _ask;
private Order _buyPending;
private Order _sellPending;
private Order _protectiveStop;
private decimal _entryPrice;
public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
public int TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
public LbsMoneyMode MoneyMode { get => _moneyMode.Value; set => _moneyMode.Value = value; }
public decimal VolumeOrRisk { get => _volumeOrRisk.Value; set => _volumeOrRisk.Value = value; }
public int Hour1 { get => _hour1.Value; set => _hour1.Value = value; }
public int Hour2 { get => _hour2.Value; set => _hour2.Value = value; }
public int Hour3 { get => _hour3.Value; set => _hour3.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public LbsStrategy()
{
_stopLossPips = Param(nameof(StopLossPips), 50).SetNotNegative();
_trailingStopPips = Param(nameof(TrailingStopPips), 5).SetNotNegative();
_trailingStepPips = Param(nameof(TrailingStepPips), 15).SetNotNegative();
_moneyMode = Param(nameof(MoneyMode), LbsMoneyMode.FixedLot);
_volumeOrRisk = Param(nameof(VolumeOrRisk), 1m).SetGreaterThanZero();
_hour1 = Param(nameof(Hour1), 10).SetRange(0, 23);
_hour2 = Param(nameof(Hour2), 11).SetRange(0, 23);
_hour3 = Param(nameof(Hour3), 12).SetRange(0, 23);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
_bid = null;
_ask = null;
_buyPending = null;
_sellPending = null;
_protectiveStop = null;
_entryPrice = 0m;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = Level1Fields.BestAskPrice;
SubscribeLevel1(quotes).Bind(ProcessLevel1).Start();
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || Position != 0m)
return;
var barEnd = CandleType.Arg is TimeSpan timeFrame ? candle.OpenTime + timeFrame : candle.CloseTime;
if (!IsTradingHour(barEnd.Hour) || _bid is not decimal bid || _ask is not decimal ask)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
CancelEntryStops();
var point = GetPoint();
var (buyPrice, sellPrice) = CalculateBreakoutLevels(
candle.HighPrice, candle.LowPrice, bid, ask, point);
var volume = CalculateVolume(point);
if (volume <= 0m)
return;
_buyPending = CreateStopOrder(Sides.Buy, buyPrice, volume, "LBS buy breakout");
_sellPending = CreateStopOrder(Sides.Sell, sellPrice, volume, "LBS sell breakout");
RegisterOrder(_buyPending);
RegisterOrder(_sellPending);
}
private void ProcessLevel1(Level1ChangeMessage message)
{
if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid && bid > 0m)
_bid = bid;
if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask && ask > 0m)
_ask = ask;
if (Position == 0m || _protectiveStop is null || TrailingStopPips <= 0 || TrailingStepPips <= 0)
return;
var point = GetPoint();
var trail = TrailingStopPips * point;
var step = TrailingStepPips * point;
if (Position > 0m && _bid is decimal longPrice)
{
if (longPrice - _entryPrice < trail + step)
return;
var candidate = longPrice - trail;
var current = GetActivation(_protectiveStop);
if (candidate >= current + step)
ReplaceProtectiveStop(Sides.Sell, candidate);
}
else if (Position < 0m && _ask is decimal shortPrice)
{
if (_entryPrice - shortPrice < trail + step)
return;
var candidate = shortPrice + trail;
var current = GetActivation(_protectiveStop);
if (candidate <= current - step)
ReplaceProtectiveStop(Sides.Buy, candidate);
}
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order is null || trade.Trade is null)
return;
if (IsSameOrder(trade.Order, _buyPending) || IsSameOrder(trade.Order, _sellPending))
{
var filledSide = trade.Order.Side;
_entryPrice = trade.Trade.Price;
if (filledSide == Sides.Buy)
CancelIfActive(_sellPending);
else
CancelIfActive(_buyPending);
_buyPending = null;
_sellPending = null;
if (StopLossPips > 0 && Position != 0m)
{
var point = GetPoint();
var stop = filledSide == Sides.Buy
? _entryPrice - StopLossPips * point
: _entryPrice + StopLossPips * point;
ReplaceProtectiveStop(filledSide.Invert(), stop);
}
return;
}
if (IsSameOrder(trade.Order, _protectiveStop) && Position == 0m)
{
_protectiveStop = null;
_entryPrice = 0m;
}
}
private bool IsTradingHour(int hour)
=> (Hour1 != 0 && hour == Hour1) ||
(Hour2 != 0 && hour == Hour2) ||
(Hour3 != 0 && hour == Hour3);
private decimal CalculateVolume(decimal point)
{
if (MoneyMode == LbsMoneyMode.FixedLot)
return NormalizeVolume(VolumeOrRisk);
if (StopLossPips <= 0)
return 0m;
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance <= 0m)
return 0m;
var riskMoney = balance * VolumeOrRisk / 100m;
var stepPrice = Security?.StepPrice ?? 0m;
var lossPerUnit = stepPrice > 0m
? StopLossPips * stepPrice
: StopLossPips * point * (Security?.Multiplier ?? 1m);
return lossPerUnit > 0m ? NormalizeVolume(riskMoney / lossPerUnit) : 0m;
}
private Order CreateStopOrder(Sides side, decimal activationPrice, decimal volume, string comment)
=> new()
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Conditional,
Condition = new StopOrderCondition { ActivationPrice = Security.ShrinkPrice(activationPrice, ShrinkRules.Auto) },
Side = side,
Volume = volume,
Comment = comment,
};
private void ReplaceProtectiveStop(Sides side, decimal activationPrice)
{
var old = _protectiveStop;
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
var replacement = CreateStopOrder(side, activationPrice, volume, "LBS protective stop");
if (old is { State: OrderStates.Active })
ReRegisterOrder(old, replacement);
else
RegisterOrder(replacement);
_protectiveStop = replacement;
}
private void CancelEntryStops()
{
CancelIfActive(_buyPending);
CancelIfActive(_sellPending);
_buyPending = null;
_sellPending = null;
}
private void CancelIfActive(Order order)
{
if (order is { State: OrderStates.Active })
CancelOrder(order);
}
private static bool IsSameOrder(Order left, Order right)
=> left is not null && right is not null &&
(ReferenceEquals(left, right) || left.TransactionId == right.TransactionId);
private static decimal GetActivation(Order order)
=> order?.Condition is StopOrderCondition stop ? stop.ActivationPrice ?? 0m : 0m;
private decimal NormalizeVolume(decimal volume)
{
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.VolumeStep is decimal step && step > 0m)
volume = Math.Floor(volume / step) * step;
return volume;
}
private decimal GetPoint()
{
var point = Security?.PriceStep ?? 0m;
return point > 0m ? point : 0.0001m;
}
internal static (decimal buy, decimal sell) CalculateBreakoutLevels(
decimal candleHigh,
decimal candleLow,
decimal bid,
decimal ask,
decimal priceStep)
{
var spread = Math.Max(0m, ask - bid);
var buffer = Math.Max(3m * spread, 10m * priceStep);
return (Math.Max(candleHigh, ask + buffer), Math.Min(candleLow, bid - buffer));
}
}
import clr
import math
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, Level1Fields, Sides, OrderTypes, OrderStates, ShrinkRules
from StockSharp.Algo import TraderHelper
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.MatchingEngine import StopOrderCondition
from StockSharp.Algo.Strategies import Strategy
class lbs_strategy(Strategy):
FIXED_LOT = 0
RISK_PERCENT = 1
def __init__(self):
super(lbs_strategy, self).__init__()
self._stop_loss = self.Param("StopLossPips", 50).SetNotNegative()
self._trailing_stop = self.Param("TrailingStopPips", 5).SetNotNegative()
self._trailing_step = self.Param("TrailingStepPips", 15).SetNotNegative()
self._money_mode = self.Param("MoneyMode", self.FIXED_LOT)
self._volume_or_risk = self.Param("VolumeOrRisk", 1.0).SetGreaterThanZero()
self._hour1 = self.Param("Hour1", 10).SetRange(0, 23)
self._hour2 = self.Param("Hour2", 11).SetRange(0, 23)
self._hour3 = self.Param("Hour3", 12).SetRange(0, 23)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._bid = None
self._ask = None
self._buy_pending = None
self._sell_pending = None
self._protective_stop = None
self._entry_price = 0.0
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value), (self.Security, DataType.Level1)]
def OnReseted(self):
super(lbs_strategy, self).OnReseted()
self._bid = None
self._ask = None
self._buy_pending = None
self._sell_pending = None
self._protective_stop = None
self._entry_price = 0.0
def OnStarted2(self, time):
super(lbs_strategy, self).OnStarted2(time)
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = Level1Fields.BestAskPrice
self.SubscribeLevel1(quotes).Bind(self._process_level1).Start()
self.SubscribeCandles(self._candle_type.Value).Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished or self.Position != 0:
return
arg = self._candle_type.Value.Arg
bar_end = candle.OpenTime + arg if isinstance(arg, TimeSpan) else candle.CloseTime
if not self._is_trading_hour(bar_end.Hour) or self._bid is None or self._ask is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
self._cancel_entry_stops()
point = self._point()
buy_price, sell_price = self.calculate_breakout_levels(
float(candle.HighPrice), float(candle.LowPrice), self._bid, self._ask, point)
volume = self._calculate_volume(point)
if volume <= 0:
return
self._buy_pending = self._create_stop(Sides.Buy, buy_price, volume, "LBS buy breakout")
self._sell_pending = self._create_stop(Sides.Sell, sell_price, volume, "LBS sell breakout")
self.RegisterOrder(self._buy_pending)
self.RegisterOrder(self._sell_pending)
def _process_level1(self, message):
bid = message.Changes[Level1Fields.BestBidPrice] if message.Changes.ContainsKey(Level1Fields.BestBidPrice) else None
ask = message.Changes[Level1Fields.BestAskPrice] if message.Changes.ContainsKey(Level1Fields.BestAskPrice) else None
if bid is not None and float(bid) > 0:
self._bid = float(bid)
if ask is not None and float(ask) > 0:
self._ask = float(ask)
if (self.Position == 0 or self._protective_stop is None or
int(self._trailing_stop.Value) <= 0 or int(self._trailing_step.Value) <= 0):
return
point = self._point()
trail = int(self._trailing_stop.Value) * point
step = int(self._trailing_step.Value) * point
current = self._activation(self._protective_stop)
if self.Position > 0 and self._bid is not None:
if self._bid - self._entry_price < trail + step:
return
candidate = self._bid - trail
if candidate >= current + step:
self._replace_protective(Sides.Sell, candidate)
elif self.Position < 0 and self._ask is not None:
if self._entry_price - self._ask < trail + step:
return
candidate = self._ask + trail
if candidate <= current - step:
self._replace_protective(Sides.Buy, candidate)
def OnOwnTradeReceived(self, trade):
super(lbs_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Order is None or trade.Trade is None:
return
if self._same(trade.Order, self._buy_pending) or self._same(trade.Order, self._sell_pending):
side = trade.Order.Side
self._entry_price = float(trade.Trade.TradePrice)
if side == Sides.Buy:
self._cancel_if_active(self._sell_pending)
else:
self._cancel_if_active(self._buy_pending)
self._buy_pending = None
self._sell_pending = None
sl = int(self._stop_loss.Value)
if sl > 0 and self.Position != 0:
point = self._point()
stop = self._entry_price - sl * point if side == Sides.Buy else self._entry_price + sl * point
self._replace_protective(Sides.Sell if side == Sides.Buy else Sides.Buy, stop)
return
if self._same(trade.Order, self._protective_stop) and self.Position == 0:
self._protective_stop = None
self._entry_price = 0.0
def _is_trading_hour(self, hour):
return ((int(self._hour1.Value) != 0 and hour == int(self._hour1.Value)) or
(int(self._hour2.Value) != 0 and hour == int(self._hour2.Value)) or
(int(self._hour3.Value) != 0 and hour == int(self._hour3.Value)))
def _calculate_volume(self, point):
if int(self._money_mode.Value) == self.FIXED_LOT:
return self._normalize_volume(float(self._volume_or_risk.Value))
sl = int(self._stop_loss.Value)
if sl <= 0:
return 0.0
balance = 0.0
if self.Portfolio is not None:
value = self.Portfolio.CurrentValue if self.Portfolio.CurrentValue is not None else self.Portfolio.BeginValue
balance = float(value) if value is not None else 0.0
if balance <= 0:
return 0.0
risk_money = balance * float(self._volume_or_risk.Value) / 100.0
step_price = float(self.Security.StepPrice) if self.Security is not None and self.Security.StepPrice is not None else 0.0
multiplier = float(self.Security.Multiplier) if self.Security is not None and self.Security.Multiplier is not None else 1.0
loss_per_unit = sl * step_price if step_price > 0 else sl * point * multiplier
return self._normalize_volume(risk_money / loss_per_unit) if loss_per_unit > 0 else 0.0
def _create_stop(self, side, activation, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Conditional
condition = StopOrderCondition()
condition.ActivationPrice = TraderHelper.ShrinkPrice(self.Security, Decimal(activation), ShrinkRules.Auto)
order.Condition = condition
order.Side = side
order.Volume = volume
order.Comment = comment
return order
def _replace_protective(self, side, activation):
volume = abs(float(self.Position))
if volume <= 0:
return
replacement = self._create_stop(side, activation, volume, "LBS protective stop")
old = self._protective_stop
if old is not None and old.State == OrderStates.Active:
self.ReRegisterOrder(old, replacement)
else:
self.RegisterOrder(replacement)
self._protective_stop = replacement
def _cancel_entry_stops(self):
self._cancel_if_active(self._buy_pending)
self._cancel_if_active(self._sell_pending)
self._buy_pending = None
self._sell_pending = None
def _cancel_if_active(self, order):
if order is not None and order.State == OrderStates.Active:
self.CancelOrder(order)
@staticmethod
def _same(left, right):
return left is not None and right is not None and (
left is right or left.TransactionId == right.TransactionId)
@staticmethod
def _activation(order):
if order is None or order.Condition is None:
return 0.0
return float(order.Condition.ActivationPrice) if order.Condition.ActivationPrice is not None else 0.0
def _normalize_volume(self, volume):
if self.Security is not None:
if self.Security.MaxVolume is not None and float(self.Security.MaxVolume) > 0:
volume = min(volume, float(self.Security.MaxVolume))
if self.Security.MinVolume is not None and float(self.Security.MinVolume) > 0:
volume = max(volume, float(self.Security.MinVolume))
if self.Security.VolumeStep is not None and float(self.Security.VolumeStep) > 0:
step = float(self.Security.VolumeStep)
volume = math.floor(volume / step) * step
return volume
def _point(self):
point = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0001
return point if point > 0 else 0.0001
@staticmethod
def calculate_breakout_levels(candle_high, candle_low, bid, ask, price_step):
spread = max(0.0, ask - bid)
buffer = max(3.0 * spread, 10.0 * price_step)
return max(candle_high, ask + buffer), min(candle_low, bid - buffer)
def CreateClone(self):
return lbs_strategy()