Strategie Locker
Gitterbasierte Hedging-Strategie, die Long- und Short-Marktorders abwechselt, um schwebende Verluste zu sichern und einen kleinen prozentualen Gewinn auf dem Kontosaldo zu erzielen.
Handelslogik
- Öffnet die erste Long-Position mit dem konfigurierten Startvolumen, sobald die erste Kerze schließt.
- Verfolgt jeden nachfolgenden Einstieg und führt ein internes Hauptbuch der Kauf- und Verkaufsbeine, um den kombinierten unrealisierten und realisierten Gewinn zu schätzen.
- Wenn die Anzahl der aktiven Beine acht erreicht, schließt die Strategie das früheste verfügbare Kauf-/Verkaufspaar, um das Engagement unter Kontrolle zu halten, bevor auf dieser Kerze weitere Aktionen ausgeführt werden.
- Wenn der kombinierte Gewinn über den Zielprozentsatz des Portfoliowerts steigt, schließt sie alle verbleibenden Positionen und setzt den internen Zustand zurück.
- Wenn der kombinierte Gewinn unter das negative Ziel fällt, misst sie den Abstand zwischen dem letzten Einstiegspreis und dem aktuellen Marktpreis. Wenn sich der Preis um den konfigurierten Schritt nach oben bewegt hat, wird ein neues Short-Bein hinzugefügt; wenn sich der Preis um denselben Abstand nach unten bewegt hat, wird ein neues Long-Bein hinzugefügt.
- Jeder Abschluss verwendet Marktorders in der entgegengesetzten Richtung des aufgezeichneten Einstiegs, damit die Absicherung sofort neutralisiert wird.
Parameter
- Profit % – Prozentsatz des aktuellen Portfoliowerts, der vor dem Glätten des Buches gesichert werden soll.
- Start Volume – Menge für den allerersten Long-Einstieg, der das Gitter startet.
- Step Volume – Menge für jede Hedging-Order, sobald die Verlustschwelle überschritten wird.
- Step Points – Anzahl der Preisschritte zwischen Gitterebenen; multipliziert mit dem Preisschritt des Instruments zur Berechnung des tatsächlichen Preisabstands.
- Enable Automation – Hauptschalter, der alle Handelslogik bei Deaktivierung pausiert.
- Candle Type – Kerzenserie, die zur Auslösung der Entscheidungslogik bei jeder abgeschlossenen Bar verwendet wird.
Die Konvertierung repliziert die ursprüngliche MetaTrader-Expertenlogik und passt dabei die Orderplatzierung an die StockSharp-High-Level-API an, während der detaillierte Handelszustand innerhalb der Strategie gespeichert wird, sodass die Gewinnberechnung der MQL-Version entspricht.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
public class LockerStrategy : Strategy
{
private readonly struct PositionEntry
{
public PositionEntry(Sides side, decimal price, decimal volume)
{
Side = side;
Price = price;
Volume = volume;
}
public Sides Side { get; }
public decimal Price { get; }
public decimal Volume { get; }
}
private readonly StrategyParam<decimal> _profitTargetPercent;
private readonly StrategyParam<decimal> _startVolume;
private readonly StrategyParam<decimal> _stepVolume;
private readonly StrategyParam<decimal> _stepPoints;
private readonly StrategyParam<bool> _enableAutomation;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maxOpenPositions;
private readonly List<PositionEntry> _entries = new();
private decimal _realizedPnL;
private decimal _lastEntryPrice;
private Sides? _lastEntrySide;
private int _cooldown;
public decimal ProfitTargetPercent { get => _profitTargetPercent.Value; set => _profitTargetPercent.Value = value; }
public decimal StartVolume { get => _startVolume.Value; set => _startVolume.Value = value; }
public decimal StepVolume { get => _stepVolume.Value; set => _stepVolume.Value = value; }
public decimal StepPoints { get => _stepPoints.Value; set => _stepPoints.Value = value; }
public bool EnableAutomation { get => _enableAutomation.Value; set => _enableAutomation.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int MaxOpenPositions { get => _maxOpenPositions.Value; set => _maxOpenPositions.Value = value; }
public LockerStrategy()
{
_profitTargetPercent = Param(nameof(ProfitTargetPercent), 0.001m)
.SetGreaterThanZero()
.SetDisplay("Profit %", "Target profit percent of balance", "General")
;
_startVolume = Param(nameof(StartVolume), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Start Volume", "Initial trade volume", "General")
;
_stepVolume = Param(nameof(StepVolume), 0.2m)
.SetGreaterThanZero()
.SetDisplay("Step Volume", "Volume for subsequent trades", "General")
;
_stepPoints = Param(nameof(StepPoints), 15000m)
.SetGreaterThanZero()
.SetDisplay("Step Points", "Number of price steps between new trades", "General")
;
_enableAutomation = Param(nameof(EnableAutomation), true)
.SetDisplay("Enable Automation", "Allow the strategy to place trades", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles for processing", "Data");
_maxOpenPositions = Param(nameof(MaxOpenPositions), 2)
.SetGreaterThanZero()
.SetDisplay("Max Open Positions", "Maximum number of hedged legs allowed", "Risk")
;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_entries.Clear();
_realizedPnL = 0m;
_lastEntryPrice = 0m;
_lastEntrySide = null;
_cooldown = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
SubscribeCandles(CandleType).Bind(Process).Start();
}
private void Process(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!EnableAutomation)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var closePrice = candle.ClosePrice;
// Use the candle close as a proxy for bid/ask because we operate on finished bars.
var bid = closePrice;
var ask = closePrice;
var currentProfit = _realizedPnL + CalculateUnrealizedProfit(bid, ask);
var openCount = _entries.Count;
if (openCount == 0)
{
// Start the grid with an initial buy order.
OpenPosition(Sides.Buy, StartVolume, ask);
return;
}
if (openCount >= MaxOpenPositions && TryClosePair(bid, ask))
{
// Reduce exposure when too many hedged orders are active.
return;
}
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
if (portfolioValue <= 0m)
portfolioValue = 1000000m;
var targetProfit = portfolioValue * ProfitTargetPercent;
if (targetProfit > 0m && currentProfit >= targetProfit)
{
// Target reached, flatten the book.
CloseAllPositions(bid, ask);
_cooldown = 20;
return;
}
if (targetProfit <= 0m)
return;
if (currentProfit <= -targetProfit)
{
var lastPrice = _lastEntryPrice;
if (lastPrice == 0m)
return;
var stepDistance = GetStepDistance();
if (stepDistance <= 0m)
return;
// Add a hedging order whenever price travels far enough from the latest entry.
if (ask > lastPrice + stepDistance)
OpenPosition(Sides.Sell, StepVolume, ask);
else if (bid < lastPrice - stepDistance)
OpenPosition(Sides.Buy, StepVolume, bid);
}
}
private decimal CalculateUnrealizedProfit(decimal bid, decimal ask)
{
var profit = 0m;
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
var exitPrice = entry.Side == Sides.Buy ? bid : ask;
var direction = entry.Side == Sides.Buy ? 1m : -1m;
profit += (exitPrice - entry.Price) * direction * entry.Volume;
}
return profit;
}
private bool TryClosePair(decimal bid, decimal ask)
{
var buyIndex = -1;
var sellIndex = -1;
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
if (entry.Side == Sides.Buy && buyIndex == -1)
buyIndex = i;
else if (entry.Side == Sides.Sell && sellIndex == -1)
sellIndex = i;
if (buyIndex != -1 && sellIndex != -1)
break;
}
if (buyIndex == -1 || sellIndex == -1)
return false;
if (buyIndex > sellIndex)
{
CloseEntry(buyIndex, bid, ask);
CloseEntry(sellIndex, bid, ask);
}
else
{
CloseEntry(sellIndex, bid, ask);
CloseEntry(buyIndex, bid, ask);
}
UpdateLastEntry();
return true;
}
private void CloseAllPositions(decimal bid, decimal ask)
{
while (_entries.Count > 0)
{
CloseEntry(_entries.Count - 1, bid, ask);
}
UpdateLastEntry();
}
private void CloseEntry(int index, decimal bid, decimal ask)
{
if (index < 0 || index >= _entries.Count)
return;
var entry = _entries[index];
var exitPrice = entry.Side == Sides.Buy ? bid : ask;
var direction = entry.Side == Sides.Buy ? Sides.Sell : Sides.Buy;
// Send the offsetting market order to neutralize the entry.
if (direction == Sides.Sell)
SellMarket();
else
BuyMarket();
var pnl = (exitPrice - entry.Price) * (entry.Side == Sides.Buy ? 1m : -1m) * entry.Volume;
_realizedPnL += pnl;
try { _entries.RemoveAt(index); } catch { }
}
private void OpenPosition(Sides side, decimal volume, decimal price)
{
if (volume <= 0m)
return;
if (side == Sides.Buy)
BuyMarket();
else
SellMarket();
_entries.Add(new PositionEntry(side, price, volume));
_lastEntryPrice = price;
_lastEntrySide = side;
}
private decimal GetStepDistance()
{
var priceStep = Security?.PriceStep ?? 0m;
return priceStep > 0m ? StepPoints * priceStep : StepPoints;
}
private void UpdateLastEntry()
{
if (_entries.Count == 0)
{
_lastEntryPrice = 0m;
_lastEntrySide = null;
return;
}
var entry = _entries[_entries.Count - 1];
_lastEntryPrice = entry.Price;
_lastEntrySide = entry.Side;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class locker_strategy(Strategy):
"""Hedging grid locker: opens initial position, hedges on drawdown, closes at profit target."""
def __init__(self):
super(locker_strategy, self).__init__()
self._profit_target_percent = self.Param("ProfitTargetPercent", 0.001) \
.SetGreaterThanZero() \
.SetDisplay("Profit %", "Target profit percent of balance", "General")
self._start_volume = self.Param("StartVolume", 0.5) \
.SetGreaterThanZero() \
.SetDisplay("Start Volume", "Initial trade volume", "General")
self._step_volume = self.Param("StepVolume", 0.2) \
.SetGreaterThanZero() \
.SetDisplay("Step Volume", "Volume for subsequent trades", "General")
self._step_points = self.Param("StepPoints", 15000.0) \
.SetGreaterThanZero() \
.SetDisplay("Step Points", "Number of price steps between new trades", "General")
self._enable_automation = self.Param("EnableAutomation", True) \
.SetDisplay("Enable Automation", "Allow the strategy to place trades", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles for processing", "Data")
self._max_open_positions = self.Param("MaxOpenPositions", 2) \
.SetGreaterThanZero() \
.SetDisplay("Max Open Positions", "Maximum number of hedged legs allowed", "Risk")
# entries: list of (side, price, volume) tuples; side='buy' or 'sell'
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
@property
def ProfitTargetPercent(self):
return float(self._profit_target_percent.Value)
@property
def StartVolume(self):
return float(self._start_volume.Value)
@property
def StepVolume(self):
return float(self._step_volume.Value)
@property
def StepPoints(self):
return float(self._step_points.Value)
@property
def EnableAutomation(self):
return self._enable_automation.Value
@property
def CandleType(self):
return self._candle_type.Value
@property
def MaxOpenPositions(self):
return int(self._max_open_positions.Value)
def OnStarted2(self, time):
super(locker_strategy, self).OnStarted2(time)
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self.EnableAutomation:
return
if self._cooldown > 0:
self._cooldown -= 1
return
close_price = float(candle.ClosePrice)
bid = close_price
ask = close_price
current_profit = self._realized_pnl + self._calc_unrealized(bid, ask)
open_count = len(self._entries)
if open_count == 0:
self._open_position('buy', self.StartVolume, ask)
return
if open_count >= self.MaxOpenPositions and self._try_close_pair(bid, ask):
return
portfolio_value = 1000000.0
if self.Portfolio is not None and self.Portfolio.CurrentValue is not None:
pv = float(self.Portfolio.CurrentValue)
if pv > 0:
portfolio_value = pv
target_profit = portfolio_value * self.ProfitTargetPercent
if target_profit > 0 and current_profit >= target_profit:
self._close_all(bid, ask)
self._cooldown = 20
return
if target_profit <= 0:
return
if current_profit <= -target_profit:
last_price = self._last_entry_price
if last_price == 0:
return
step_distance = self._get_step_distance()
if step_distance <= 0:
return
if ask > last_price + step_distance:
self._open_position('sell', self.StepVolume, ask)
elif bid < last_price - step_distance:
self._open_position('buy', self.StepVolume, bid)
def _calc_unrealized(self, bid, ask):
profit = 0.0
for side, price, volume in self._entries:
exit_price = bid if side == 'buy' else ask
direction = 1.0 if side == 'buy' else -1.0
profit += (exit_price - price) * direction * volume
return profit
def _try_close_pair(self, bid, ask):
buy_index = -1
sell_index = -1
for i in range(len(self._entries)):
side = self._entries[i][0]
if side == 'buy' and buy_index == -1:
buy_index = i
elif side == 'sell' and sell_index == -1:
sell_index = i
if buy_index != -1 and sell_index != -1:
break
if buy_index == -1 or sell_index == -1:
return False
if buy_index > sell_index:
self._close_entry(buy_index, bid, ask)
self._close_entry(sell_index, bid, ask)
else:
self._close_entry(sell_index, bid, ask)
self._close_entry(buy_index, bid, ask)
self._update_last_entry()
return True
def _close_all(self, bid, ask):
while len(self._entries) > 0:
self._close_entry(len(self._entries) - 1, bid, ask)
self._update_last_entry()
def _close_entry(self, index, bid, ask):
if index < 0 or index >= len(self._entries):
return
side, price, volume = self._entries[index]
exit_price = bid if side == 'buy' else ask
if side == 'buy':
self.SellMarket()
else:
self.BuyMarket()
direction = 1.0 if side == 'buy' else -1.0
pnl = (exit_price - price) * direction * volume
self._realized_pnl += pnl
self._entries.pop(index)
def _open_position(self, side, volume, price):
if volume <= 0:
return
if side == 'buy':
self.BuyMarket()
else:
self.SellMarket()
self._entries.append((side, price, volume))
self._last_entry_price = price
self._last_entry_side = side
def _get_step_distance(self):
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.0
if price_step > 0:
return self.StepPoints * price_step
return self.StepPoints
def _update_last_entry(self):
if len(self._entries) == 0:
self._last_entry_price = 0.0
self._last_entry_side = None
return
side, price, volume = self._entries[-1]
self._last_entry_price = price
self._last_entry_side = side
def OnReseted(self):
super(locker_strategy, self).OnReseted()
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
def CreateClone(self):
return locker_strategy()