LBS 策略
LBS 策略 是 MetaTrader 5 顾问 "LBS (barabashkakvn's edition)" 的 StockSharp 版本。原始程序在指定的交易时段内监测前一根 K 线的高低点,并在突破时放置止损买入/卖出单。本移植使用 StockSharp 的高级 API(SubscribeCandles、SubscribeLevel1、BuyStop/SellStop 等)实现同样的交易逻辑和资金管理规则。
交易流程
- 策略订阅所选时间框 (
CandleType) 的收盘蜡烛。 - 当蜡烛的收盘时间等于任意启用的交易小时 (
Hour1、Hour2、Hour3) 时,会计算突破价格:- Buy Stop 下单价取蜡烛最高价与当前卖价加“冻结”缓冲区中的较大值。
- Sell Stop 下单价取蜡烛最低价与当前买价减“冻结”缓冲区中的较小值。
- 缓冲区模拟 MetaTrader 的
SYMBOL_TRADE_FREEZE_LEVEL行为:取三倍点差,但不低于十个点。
- 当任一方向成交后,另一张挂单立即撤销,对应原版中的
DeleteAllPendingOrders。 StopLossPips定义初始止损;若启用TrailingStopPips与TrailingStepPips,当浮动盈利超过两者之和时,止损会沿趋势移动。- 仅在策略在线、没有持仓且有有效的 Level1 报价时才会发送订单。
资金管理
MoneyMode 参数对应原始顾问的“固定手数 / 风险百分比”开关:
- FixedLot:
VolumeOrRisk视为固定下单手数。 - RiskPercent:
VolumeOrRisk视为账户权益百分比。策略将风险金额除以入场价与止损价之间的距离(以价格步长计)来计算下单手数。使用该模式时必须启用止损,否则不会发送订单。
策略会按照交易品种的最小手数、步长和最大手数限制对计算结果进行归一化,避免被经纪商拒单。
参数列表
| 参数 | 默认值 | 说明 |
|---|---|---|
StopLossPips |
50 | 固定止损距离(点)。设为 0 可关闭初始止损与追踪逻辑。 |
TrailingStopPips |
5 | 追踪止损距离(点)。设为 0 可禁用追踪。 |
TrailingStepPips |
15 | 移动止损前所需的额外盈利(点)。启用追踪时必须大于 0。 |
MoneyMode |
FixedLot |
资金管理模式:固定手数或风险百分比。 |
VolumeOrRisk |
1.0 | 在 FixedLot 模式下为手数,在 RiskPercent 模式下为风险百分比。 |
Hour1 |
10 | 第一个交易小时,设为 0 表示关闭。 |
Hour2 |
11 | 第二个交易小时,设为 0 表示关闭。 |
Hour3 |
12 | 第三个交易小时,设为 0 表示关闭。 |
CandleType |
1 小时 | 计算突破所用的蜡烛时间框。请与 MetaTrader 图表保持一致。 |
实现细节
- 通过蜡烛收盘时间来判断交易时段,对应 MetaTrader 中新柱形成时的
TimeCurrent()。 - 冻结/止损缓冲不低于十个点,可避免因经纪商限制导致的下单失败。
- 追踪止损在每个 Level1 价格变动时更新,以模拟原始
OnTick回调的行为。 - 风险百分比模式优先使用
Portfolio.CurrentValue,若不可用则回退到Portfolio.BeginValue。
使用建议
- 选择目标品种和蜡烛时间框,使其与 MetaTrader 设置一致。
- 设定需要交易的小时段,输入
0即可关闭对应时间窗。 - 若需按权益比例下单,将
MoneyMode改为RiskPercent,并确保StopLossPips为正数。 - 若使用固定手数,保持
MoneyMode = FixedLot,并把VolumeOrRisk设置为期望手数。 - 启动策略后,在下一次符合条件的交易小时会自动挂出双向突破单,并根据规则维护保护性止损。
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()