LBS 戦略
LBS 戦略 は MetaTrader 5 エキスパートアドバイザー「LBS (barabashkakvn's edition)」の直接変換です。元のシステムは設定可能な取引ウィンドウ中に前のローソク足のブレイクアウトを監視し、両方の極値にストップ注文を配置します。StockSharp ポートは高レベル API(SubscribeCandles、SubscribeLevel1、BuyStop/SellStop)を使用して同じトレード管理ルールを維持します。
取引ロジック
- ストラテジーは選択した時間軸(
CandleType)の完了したローソク足を監視します。 - ローソク足のクローズ時間が有効な取引時間(
Hour1、Hour2、Hour3)のいずれかと一致すると、アルゴリズムはブレイクアウトレベルを計算します:- バイストップはローソク足の高値と現在のアスクにフリーズバッファを加えた値の大きい方に配置されます。
- セルストップはローソク足の安値と現在のビッドからバッファを差し引いた値の小さい方に配置されます。
- バッファは MetaTrader の
SYMBOL_TRADE_FREEZE_LEVELフォールバック(スプレッドの3倍、ただし10ピップ未満にはなりません)を再現します。
- ポジションが開かれると、反対の保留注文は即座にキャンセルされます。これは MQL エキスパートの
DeleteAllPendingOrdersルーティンと同様です。 - 初期ストップロス価格は
StopLossPipsに従って添付されます。オプションのトレーリングロジック(TrailingStopPipsとTrailingStepPips)は、浮動利益が設定された閾値を超えるとストップを移動させます。 - ストラテジーがオンライン状態で、ポジションが開いておらず、有効な Level1 クォートが利用可能な場合のみ注文が送信されます。
資金管理
MoneyMode は元のエキスパートの Lot/Risk スイッチを反映します:
- FixedLot –
VolumeOrRiskパラメーターは絶対的な取引量として解釈されます。 - RiskPercent – ストラテジーは
VolumeOrRiskをポートフォリオ価値の割合に変換します。リスク額をエントリー価格と保護ストップ間の距離(価格ステップ単位)で割って注文量を算出します。このモードを使用する場合、ストップロスを有効にする必要があります。そうでなければ注文はスキップされます。
すべてのボリュームはブローカーの拒否を避けるために、楽器の最小、最大、ステップ制約に正規化されます。
パラメーター
| 名前 | デフォルト | 説明 |
|---|---|---|
StopLossPips |
50 | ピップ単位の固定ストップまでの距離。ゼロは初期ストップとトレーリングモジュールの両方を無効にします。 |
TrailingStopPips |
5 | ピップ単位のトレーリングストップ距離。ゼロはトレーリングを無効にします。 |
TrailingStepPips |
15 | トレーリングストップが移動する前に必要な追加利益(ピップ単位)。トレーリングが有効な場合は正の値が必要です。 |
MoneyMode |
FixedLot |
固定ボリュームとリスクパーセントのサイジングを選択します。 |
VolumeOrRisk |
1.0 | FixedLot モードでのロットサイズ、または RiskPercent モードでのリスク割合。 |
Hour1 |
10 | 最初の取引時間。無効にするには 0 に設定します。 |
Hour2 |
11 | 2番目の取引時間。無効にするには 0 に設定します。 |
Hour3 |
12 | 3番目の取引時間。無効にするには 0 に設定します。 |
CandleType |
1時間の時間軸 | ブレイクアウト検出に使用されるローソク足シリーズ。MetaTrader のチャート時間軸を反映するように調整します。 |
注意事項
- 時間の比較にはローソク足のクローズ時間が使用されます。これは MetaTrader の
TimeCurrent()が次のバーの始まりと等しくなる瞬間に対応します。 - フリーズ/ストップレベルの近似により、ストップ注文が現在の bid/ask から10ピップ未満にならないことが保証され、最も一般的な MetaTrader エラーを防ぎます。
- トレーリングストップはすべての Level1 ティックで更新されるため、元のエキスパートのティック駆動
OnTickハンドラーに近い動作が確保されます。 - リスクベースのサイジングは
Portfolio.CurrentValueが利用可能な場合に使用し、そうでない場合はPortfolio.BeginValueにフォールバックします。
使用上のヒント
- ストラテジーを楽器に接続し、MetaTrader で使用したのと同じ時間軸を選択します。
- 取引したいセッションに応じて取引時間を設定します(
0に設定するとそのスロットが無効になります)。 - 自動スケーリングを希望する場合は
RiskPercentモードを選択し、StopLossPipsが正であることを確認してください。 - 固定ロット取引の場合は
MoneyModeをFixedLotのままにし、VolumeOrRiskを希望するサイズに設定します。 - ストラテジーを開始します。次の設定された時間に2つの保留注文を配置し、保護ストップを自動的に維持します。
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()