Two Per Bar Strategie
Überblick
Der ursprüngliche MetaTrader-Experte "Two PerBar" eröffnet zu Beginn jedes neuen Balkens eine Long- und eine Short-Position, schließt den gesamten Korb auf dem nächsten Balken und wendet optional einen martingalähnlichen Volumenmultiplikator an. Der StockSharp-Port hält denselben Rhythmus, indem er beide gehedgten Beine explizit verfolgt und einmal pro abgeschlossener Kerze reagiert. Alle Orders werden über die High-Level-API erstellt und respektieren die Instrument-Metadaten (Preisschritt, Volumenschritt und Min/Max-Lot-Beschränkungen).
Handelszyklus
- Neue Kerze erkennen – die Strategie abonniert die konfigurierte Kerzenserie über
SubscribeCandles. Wenn die Kerze mit State == CandleStates.Finished eintrifft, hat ein neuer Balken begonnen und der Zyklus läuft.
- Take-Profit-Treffer auswerten – jedes gespeicherte Bein trägt seinen eigenen Einstiegspreis und Take-Profit-Level. Wenn das Hoch oder Tief der abgeschlossenen Kerze diesen Level berührt, wird das Bein sofort mit einer Marktorder geschlossen und aus der Verfolgungsliste entfernt.
- Zwangsliquidation von Überbleibseln – alle Beine, die den Take-Profit-Scan überlebt haben, werden vor dem Öffnen des nächsten Paares zum Markt liquidiert. Dies spiegelt den MetaTrader-Code wider, der
PositionClose bei jedem Balkenöffnen aufruft.
- Nächste Losgröße bestimmen –
- Wenn ein vorheriger Zyklus noch offene Beine hatte, wird das größte Volumen unter ihnen mit
VolumeMultiplier multipliziert.
- Wenn der Korb flach endete (zum Beispiel, beide Beine haben ihren Take-Profit getroffen), wird der Zyklus auf
InitialVolume zurückgesetzt.
PrepareVolume normalisiert das Kandidaten-Lot, indem es auf zwei Dezimalstellen rundet, es an den Instrument-VolumeStep anpasst, gegen das Börsen-MinVolume prüft, und schließlich auf InitialVolume zurücksetzt, wenn es entweder das benutzerdefinierte MaxVolume oder das Security.MaxVolume übersteigt.
- Standardwerte aktualisieren – das berechnete Lot wird in
_lastCycleVolume gespeichert und in Strategy.Volume geschrieben, damit Hilfsmethoden denselben Betrag wiederverwenden.
- Ein neues gehedgtes Paar öffnen –
BuyMarket(volume) eröffnet das Long-Bein und SellMarket(volume) das Short-Bein. Jedes Bein merkt sich den Schlusskurs der abgeschlossenen Kerze und den absoluten Take-Profit-Level (entry ± TakeProfitPoints * pointSize). Ein null oder negativer TakeProfitPoints deaktiviert den Take-Profit und nur der Zwangsliquidationsschritt schließt den Korb.
Das Ergebnis ist ein perpetueller Straddle: jede Kerze beginnt mit einem Long+Short-Paar, beide werden während des Balkens auf Gewinnziele überprüft, und alles ist flach vor dem nächsten Zyklus.
Geldmanagement und Schutz
- Martingalähnliche Skalierung –
VolumeMultiplier repliziert den MetaTrader-Multiplikator. Wenn ein Bein bis zum Zwangsliquidationsschritt überlebt, verwendet der nächste Zyklus die Größe des schwersten Beins multipliziert mit diesem Wert. Ein abgeschlossener profitabler Zyklus (beide Beine über Take-Profit geschlossen) setzt das Lot auf InitialVolume zurück.
- Volumendeckelung –
MaxVolume ist eine harte Obergrenze, die das Lot auf InitialVolume zurückzwingt, sobald der Multiplikator es überschreiten würde. Dasselbe Reset passiert, wenn das Instrument ein engeres Security.MaxVolume meldet.
- Börsenkonformität – alle Volumina werden an das
Security.VolumeStep angepasst und abgelehnt, wenn sie unter MinVolume fallen. Das Setzen von InitialVolume auf eine handelbare Größe garantiert, dass der Reset-Pfad immer gültig bleibt.
- Punktberechnung – der Take-Profit-Offset verwendet
Security.PriceStep (oder MinPriceStep als Fallback). Instrumente ohne definierten Schritt deaktivieren den Take-Profit effektiv, da der berechnete Offset null ist.
Parameter
| Name |
Typ |
Standard |
Beschreibung |
CandleType |
DataType |
1-Minuten-Zeitrahmen |
Primärer Zeitrahmen, der den einmal-pro-Balken-Workflow auslöst. |
InitialVolume |
decimal |
1 |
Losgröße beim Start eines neuen Zyklus ohne überlebende Beine. |
VolumeMultiplier |
decimal |
2 |
Multiplikator auf das größte überlebende Bein aus dem vorherigen Zyklus. |
MaxVolume |
decimal |
10 |
Maximale erlaubte Losgröße vor dem Zurücksetzen auf InitialVolume. |
TakeProfitPoints |
int |
50 |
Abstand in Preispunkten für das Take-Profit-Ziel pro Bein. 0 deaktiviert den Take-Profit und verlässt sich ausschließlich auf die Balkenschluss-Liquidation. |
Implementierungshinweise und Unterschiede
- Gehedgte Beine werden manuell in
_legs verfolgt, damit die Strategie über individuelle Long/Short-Exposures nachdenken kann, obwohl StockSharp nur die Nettoposition meldet.
- Anstatt sich auf einzelne Ticks zu verlassen, prüft die Take-Profit-Logik den Hoch/Tief-Bereich der abgeschlossenen Kerze. Dies hält die Implementierung deterministisch, während sie dem ursprünglichen "pro Balken"-Verhalten treu bleibt.
- Die MetaTrader-Slippage- und Magic-Number-Einstellungen sind nicht exponiert; StockSharp verarbeitet die Order-Routing-Details, und die Strategie läuft im Portfolio der übergeordneten Strategieinstanz.
- Die Orderplatzierung verwendet die
Strategy-Hilfsmethoden (BuyMarket, SellMarket) ohne Indikatoren direkt zu Strategy.Indicators hinzuzufügen, gemäß den Repository-Richtlinien.
Verwendungstipps
- Passen Sie
InitialVolume an den Lot-Schritt des Instruments an, bevor Sie die Strategie starten. Der Konstruktor versucht nicht, Ihre Eingabe automatisch zu runden.
- Wenn das Instrument einen sehr kleinen Preisschritt hat, erwägen Sie,
TakeProfitPoints zu reduzieren; andernfalls kann der berechnete Take-Profit unrealistisch weit entfernt liegen.
- Da die Strategie gleichzeitig Aufträge in entgegengesetzte Richtungen öffnet, führen Sie sie auf Konnektoren/Börsen aus, die gehedgte Positionen erlauben. In Umgebungen, die Positionen sofort netten, spiegelt die
_legs-Liste noch die beabsichtigte Logik wider, aber das tatsächliche Broker-Verhalten kann abweichen.
- Fügen Sie die Strategie einem Chart hinzu, um Kerzen und ausgeführte Trades zu visualisieren (
DrawCandles + DrawOwnTrades sind in OnStarted aktiviert).
namespace StockSharp.Samples.Strategies;
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;
/// <summary>
/// Strategy that opens a hedged pair of market orders on every new bar.
/// </summary>
public class TwoPerBarStrategy : Strategy
{
private sealed class HedgeLeg
{
public bool IsLong;
public decimal Volume;
public decimal EntryPrice;
public decimal? TakeProfitPrice;
}
private readonly List<HedgeLeg> _legs = new();
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<decimal> _volumeMultiplier;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<int> _takeProfitPoints;
private decimal _pointSize;
private decimal _lastCycleVolume;
/// <summary>
/// Initializes a new instance of <see cref="TwoPerBarStrategy"/>.
/// </summary>
public TwoPerBarStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe used to detect new bars.", "General");
_initialVolume = Param(nameof(InitialVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Initial Volume", "Lot size used when no previous positions exist.", "Trading")
;
_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Volume Multiplier", "Factor applied to the heaviest remaining leg after closing a cycle.", "Trading")
;
_maxVolume = Param(nameof(MaxVolume), 10m)
.SetGreaterThanZero()
.SetDisplay("Maximum Volume", "Upper limit for the calculated lot size before resetting to the initial value.", "Risk")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 50)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Distance to the take profit expressed in instrument points.", "Risk")
;
}
/// <summary>
/// Candle type that drives the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Base lot size for a fresh cycle.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Multiplier applied to the previous maximum lot size.
/// </summary>
public decimal VolumeMultiplier
{
get => _volumeMultiplier.Value;
set => _volumeMultiplier.Value = value;
}
/// <summary>
/// Hard limit for the calculated lot size.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Take profit distance in price points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_legs.Clear();
_lastCycleVolume = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointSize = CalculatePointSize();
_lastCycleVolume = PrepareVolume(InitialVolume);
if (_lastCycleVolume > 0m)
Volume = _lastCycleVolume;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
CheckTakeProfitHits(candle);
var hadLegs = _legs.Count > 0;
var maxVolume = 0m;
for (var i = 0; i < _legs.Count; i++)
{
var leg = _legs[i];
if (leg.Volume > maxVolume)
maxVolume = leg.Volume;
}
if (_legs.Count > 0)
CloseAllLegs();
var nextVolume = hadLegs ? maxVolume * VolumeMultiplier : InitialVolume;
nextVolume = PrepareVolume(nextVolume);
if (nextVolume <= 0m)
return;
_lastCycleVolume = nextVolume;
Volume = nextVolume;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var offset = TakeProfitPoints > 0 && _pointSize > 0m ? TakeProfitPoints * _pointSize : 0m;
OpenHedgePair(candle.ClosePrice, offset);
}
private void CheckTakeProfitHits(ICandleMessage candle)
{
if (TakeProfitPoints <= 0)
return;
for (var i = _legs.Count - 1; i >= 0; i--)
{
var leg = _legs[i];
var target = leg.TakeProfitPrice;
if (target is null)
continue;
if (leg.IsLong)
{
if (candle.HighPrice >= target.Value)
{
SellMarket(leg.Volume);
_legs.RemoveAt(i);
}
}
else
{
if (candle.LowPrice <= target.Value)
{
BuyMarket(leg.Volume);
_legs.RemoveAt(i);
}
}
}
}
private void CloseAllLegs()
{
for (var i = _legs.Count - 1; i >= 0; i--)
{
var leg = _legs[i];
if (leg.IsLong)
SellMarket(leg.Volume);
else
BuyMarket(leg.Volume);
}
_legs.Clear();
}
private void OpenHedgePair(decimal entryPrice, decimal takeProfitOffset)
{
var volume = _lastCycleVolume;
if (volume <= 0m)
return;
var longOrder = BuyMarket(volume);
if (longOrder is not null)
{
_legs.Add(new HedgeLeg
{
IsLong = true,
Volume = volume,
EntryPrice = entryPrice,
TakeProfitPrice = takeProfitOffset > 0m ? entryPrice + takeProfitOffset : null
});
}
var shortOrder = SellMarket(volume);
if (shortOrder is not null)
{
_legs.Add(new HedgeLeg
{
IsLong = false,
Volume = volume,
EntryPrice = entryPrice,
TakeProfitPrice = takeProfitOffset > 0m ? entryPrice - takeProfitOffset : null
});
}
}
private decimal PrepareVolume(decimal candidate)
{
if (candidate <= 0m)
return 0m;
if (ShouldResetVolume(candidate))
candidate = InitialVolume;
var normalized = NormalizeVolume(candidate);
if (normalized <= 0m)
return 0m;
if (ShouldResetVolume(normalized))
normalized = NormalizeVolume(InitialVolume);
return normalized;
}
private bool ShouldResetVolume(decimal volume)
{
if (volume <= 0m)
return false;
if (MaxVolume > 0m && volume > MaxVolume)
return true;
var security = Security;
var maxFromSecurity = security?.MaxVolume;
return maxFromSecurity != null && volume > maxFromSecurity.Value;
}
private decimal NormalizeVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var normalized = decimal.Round(volume, 2, MidpointRounding.ToZero);
var security = Security;
if (security != null)
{
var step = security.VolumeStep ?? 0m;
if (step > 0m)
normalized = step * Math.Floor(normalized / step);
var min = security.MinVolume ?? 0m;
if (min > 0m && normalized < min)
return 0m;
var max = security.MaxVolume;
if (max != null && normalized > max.Value)
normalized = max.Value;
}
return normalized > 0m ? normalized : 0m;
}
private decimal CalculatePointSize()
{
var security = Security;
if (security?.PriceStep is decimal step && step > 0m)
return step;
return 0m;
}
}
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
from datatype_extensions import *
from indicator_extensions import *
class two_per_bar_strategy(Strategy):
"""Hedged pair strategy: opens buy+sell on each bar with TP management and volume multiplier."""
def __init__(self):
super(two_per_bar_strategy, self).__init__()
self._tp_points = self.Param("TakeProfitPoints", 50).SetNotNegative().SetDisplay("Take Profit", "TP in points", "Risk")
self._vol_mult = self.Param("VolumeMultiplier", 2).SetGreaterThanZero().SetDisplay("Volume Multiplier", "Multiplier after cycle", "Trading")
self._max_vol = self.Param("MaxVolume", 10).SetGreaterThanZero().SetDisplay("Max Volume", "Upper limit for lot size", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(two_per_bar_strategy, self).OnReseted()
self._legs = []
self._last_cycle_vol = 1.0
self._bar_count = 0
def OnStarted2(self, time):
super(two_per_bar_strategy, self).OnStarted2(time)
self._legs = []
self._last_cycle_vol = 1.0
self._bar_count = 0
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
self._bar_count += 1
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
tp_pts = self._tp_points.Value
# Check TP hits
if tp_pts > 0:
i = len(self._legs) - 1
while i >= 0:
leg = self._legs[i]
if leg['is_long'] and leg['tp'] is not None and high >= leg['tp']:
self.SellMarket()
self._legs.pop(i)
elif not leg['is_long'] and leg['tp'] is not None and low <= leg['tp']:
self.BuyMarket()
self._legs.pop(i)
i -= 1
# Close remaining legs
had_legs = len(self._legs) > 0
max_vol = 0.0
for leg in self._legs:
if leg['vol'] > max_vol:
max_vol = leg['vol']
if len(self._legs) > 0:
for leg in reversed(self._legs):
if leg['is_long']:
self.SellMarket()
else:
self.BuyMarket()
self._legs = []
# Calculate next volume
if had_legs:
next_vol = max_vol * self._vol_mult.Value
else:
next_vol = 1.0
max_v = self._max_vol.Value
if max_v > 0 and next_vol > max_v:
next_vol = 1.0
self._last_cycle_vol = next_vol
# Skip first few bars to gather data
if self._bar_count < 3:
return
# Open hedged pair
tp_offset = tp_pts if tp_pts > 0 else 0
self.BuyMarket()
self._legs.append({'is_long': True, 'vol': next_vol, 'entry': close, 'tp': close + tp_offset if tp_offset > 0 else None})
self.SellMarket()
self._legs.append({'is_long': False, 'vol': next_vol, 'entry': close, 'tp': close - tp_offset if tp_offset > 0 else None})
def CreateClone(self):
return two_per_bar_strategy()