PowerZone Strategie
Diese Strategie identifiziert "PowerZone"-Orderblöcke, die durch eine bearishe Kerze gefolgt von aufeinanderfolgenden bullischen Kerzen entstehen (und umgekehrt). Ein Ausbruch über die Bull-Zone löst einen Long-Trade aus, während ein Einbruch unter die Bear-Zone eine Short-Position eröffnet. Ziele und Stops basieren auf dem Bereich der Zone.
Details
- Einstiegskriterien:
- Long: Bearishe Kerze vor
Periods+1 Bars gefolgt von Periods bullischen Kerzen und Preis bricht über das Zonenhoch.
- Short: Bullische Kerze vor
Periods+1 Bars gefolgt von Periods bearishen Kerzen und Preis bricht unter das Zonentief.
- Long/Short: Beide Seiten.
- Ausstiegskriterien: Take-Profit und Stop-Loss als Vielfache des Zonenbereichs.
- Indikatoren: Keine.
- Standardwerte:
Periods = 5
Threshold = 0
UseWicks = false
Take Profit Factor = 1.5
Stop Loss Factor = 1
- Filter:
- Kategorie: Ausbruch
- Richtung: Beide
- Indikatoren: Keine
- Stops: Ja
- Komplexität: Moderat
- Zeitrahmen: Beliebig
- Saisonalität: Nein
- Neuronale Netze: Nein
- Divergenz: Nein
- Risikolevel: Moderat
Berechnung und Ausführung
C# und Python verwenden abgeschlossene Kerzen. Auf Entscheidungsbar t ist der Anker t - Periods - 1; die folgenden Periods Bars bis t - 1 müssen alle strikt über ihrer Eröffnung schließen für eine Bull-Zone, oder darunter für eine Bear-Zone. Der Anker hat die strikt entgegengesetzte Farbe; Dojis zählen nicht. Die aktuelle Entscheidungsbar bestätigt die Zone nicht. Vor Abschluss der Bestätigung ist die Zone unbekannt.
Erforderlich ist 100 * abs(Close[t - 1] - Anker-Close) / abs(Anker-Close) >= Threshold; Anker-Close null ist ungültig. Die Zone besteht aus dem Ankerkörper [min(Open, Close), max(Open, Close)] oder bei UseWicks=true aus [Low, High]. Bestätigungsextreme vergrößern sie nicht; die Breite muss positiv sein. Letzte Bull- und Bear-Zone bleiben getrennt bis zur Ersetzung bestehen, mit höchstens einem akzeptierten Einstieg je Zone.
Long verlangt aktueller Close > Bull-Zonenhoch, Short Close < Bear-Zonentief. Gleichheit hält die Position. Eine neue Zone kann auf ihrer ersten Entscheidungsbar einsteigen, auch wenn der Preis bereits außerhalb liegt. Aus einer Nullposition Volume eröffnen oder mit Volume + abs(Position) vollständig umkehren; keine gleichgerichteten Zusätze. Sind beide Richtungen aus null gültig, hat Long Vorrang. Ausstehende Orders sperren weitere Orders. Keine EMA, Wartefrist oder anderen Indikatoren.
Beim akzeptierten Einstieg werden Zonenbreite * StopLossFactor und Zonenbreite * TakeProfitFactor eingefroren. Basis ist die tatsächliche Ausführung, bei gleichgerichteten Teilerhöhungen volumengewichtet; Reduzierungen behalten sie, Umkehrungen setzen sie zurück. Spätere Zonen bewegen die Abstände nicht. Lokaler Schutz prüft ausführbaren Long-Bid, Short-Ask und abgeschlossene Kerzenschlusskurse und schließt die gesamte Restposition per Marktorder, vor neuen Einstiegen auf derselben Bar. Hoch-/Tiefberührungen sind keine angenommenen Intrabar-Ausführungen; Kurslücken können den Ausführungspreis ändern.
Gemeinsame Standardwerte: API-Parameter TakeProfitFactor=1.5, StopLossFactor=1, fünfminütiger CandleType. Veröffentlichter Parametername ist UseWicks, C#-Eigenschaft IsUseWicks. Periods und Faktoren positiv; Threshold ist ein nichtnegativer Prozentsatz. Diese Definitionen präzisieren vorher nicht festgelegte Schwellen-, Grenz-, Verbrauchs- und Ausführungsbasisregeln.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Trades confirmed candle order-block zones with optional wick boundaries,
/// percent movement filtering and frozen zone-range exits from actual fills.
/// </summary>
public class PowerZoneStrategy : Strategy
{
private readonly StrategyParam<int> _periods;
private readonly StrategyParam<decimal> _threshold;
private readonly StrategyParam<bool> _isUseWicks;
private readonly StrategyParam<decimal> _takeProfitFactor;
private readonly StrategyParam<decimal> _stopLossFactor;
private readonly StrategyParam<DataType> _candleType;
private readonly List<(decimal open, decimal high, decimal low, decimal close)> _bars = [];
private decimal? _bullHigh;
private decimal? _bullLow;
private decimal? _bearHigh;
private decimal? _bearLow;
private bool _isBullUsed;
private bool _isBearUsed;
private Order _pendingOrder;
private decimal _filledPosition;
private decimal _entryPrice;
private decimal _stopDistance;
private decimal _takeDistance;
/// <summary>Initializes five-bar confirmations, body zones, zero movement threshold and range-based 1/1.5 exits.</summary>
public PowerZoneStrategy()
{
_periods = Param(nameof(Periods), 5).SetGreaterThanZero()
.SetDisplay("Periods", "Completed same-color confirmation candles after the opposite anchor", "Zones");
_threshold = Param(nameof(Threshold), 0m).SetRange(0m, decimal.MaxValue)
.SetDisplay("Threshold", "Minimum absolute percent move from anchor close to final confirmation close", "Zones");
_isUseWicks = Param("UseWicks", false)
.SetDisplay("Use Wicks", "Use anchor high/low instead of body boundaries", "Zones");
_takeProfitFactor = Param(nameof(TakeProfitFactor), 1.5m).SetGreaterThanZero()
.SetDisplay("Take Profit Factor", "Frozen entry-zone range multiple above/below actual fill basis", "Protection");
_stopLossFactor = Param(nameof(StopLossFactor), 1m).SetGreaterThanZero()
.SetDisplay("Stop Loss Factor", "Frozen entry-zone range multiple below/above actual fill basis", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Finished zone confirmations and subsequent breakout decisions", "General");
OrderRegistering += order => _pendingOrder = order;
OwnTradeReceived += ObserveFill;
}
/// <summary>Number of strict same-color confirmation bars after the opposite anchor candle.</summary>
public int Periods { get => _periods.Value; set => _periods.Value = value; }
/// <summary>Inclusive minimum absolute anchor-to-confirmation close movement in percent.</summary>
public decimal Threshold { get => _threshold.Value; set => _threshold.Value = value; }
/// <summary>Use anchor wicks; the published strategy parameter remains named UseWicks.</summary>
public bool IsUseWicks { get => _isUseWicks.Value; set => _isUseWicks.Value = value; }
/// <summary>Positive take distance as a multiple of the accepted zone range.</summary>
public decimal TakeProfitFactor { get => _takeProfitFactor.Value; set => _takeProfitFactor.Value = value; }
/// <summary>Positive stop distance as a multiple of the accepted zone range.</summary>
public decimal StopLossFactor { get => _stopLossFactor.Value; set => _stopLossFactor.Value = value; }
/// <summary>Base candle type; five minutes by default.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
private void ResetState()
{
_bars.Clear();
_bullHigh = _bullLow = _bearHigh = _bearLow = null;
_isBullUsed = _isBearUsed = false;
_pendingOrder = null;
_filledPosition = _entryPrice = _stopDistance = _takeDistance = 0m;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Periods <= 0 || Threshold < 0m || TakeProfitFactor <= 0m || StopLossFactor <= 0m)
throw new ArgumentException("Periods and risk factors must be positive; movement threshold cannot be negative.");
ResetState();
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
}
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private bool IsPending => _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);
private void Submit(Sides side, decimal volume, string comment)
=> RegisterOrder(new Order { Security = Security, Portfolio = Portfolio, Type = OrderTypes.Market, Side = side, Volume = volume, Comment = comment });
private void ObserveFill(Subscription subscription, MyTrade trade)
{
var previous = _filledPosition;
var signed = trade.Order.Side == Sides.Buy ? trade.Trade.Volume : -trade.Trade.Volume;
_filledPosition += signed;
if (_filledPosition == 0m)
_entryPrice = 0m;
else if (previous == 0m || Math.Sign(_filledPosition) != Math.Sign(previous))
_entryPrice = trade.Trade.Price;
else if (Math.Sign(signed) == Math.Sign(previous))
_entryPrice = (_entryPrice * Math.Abs(previous) + trade.Trade.Price * Math.Abs(signed)) / Math.Abs(_filledPosition);
}
private bool TryRiskExit(decimal price)
{
if (Position == 0m || _entryPrice <= 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
return false;
var profit = (price - _entryPrice) * Math.Sign(Position);
var comment = profit <= -_stopDistance ? "PowerZone stop loss" : profit >= _takeDistance ? "PowerZone take profit" : null;
if (comment is null)
return false;
Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), comment);
return true;
}
private void ObserveQuote(Level1ChangeMessage quote)
{
var field = Position > 0m ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
if (quote.TryGetDecimal(field) is decimal price && price > 0m)
TryRiskExit(price);
}
private void ConfirmZones()
{
if (_bars.Count < checked(Periods + 2))
return;
var anchor = _bars[0];
var bull = anchor.close < anchor.open;
var bear = anchor.close > anchor.open;
for (var i = 1; i < _bars.Count - 1; i++)
{
bull &= _bars[i].close > _bars[i].open;
bear &= _bars[i].close < _bars[i].open;
}
if ((!bull && !bear) || anchor.close == 0m
|| 100m * Math.Abs(_bars[^2].close - anchor.close) / Math.Abs(anchor.close) < Threshold)
return;
var high = IsUseWicks ? anchor.high : Math.Max(anchor.open, anchor.close);
var low = IsUseWicks ? anchor.low : Math.Min(anchor.open, anchor.close);
if (high <= low)
return;
if (bull)
{
_bullHigh = high;
_bullLow = low;
_isBullUsed = false;
}
else
{
_bearHigh = high;
_bearLow = low;
_isBearUsed = false;
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_bars.Add((candle.OpenPrice, candle.HighPrice, candle.LowPrice, candle.ClosePrice));
if (_bars.Count > checked(Periods + 2))
_bars.RemoveAt(0);
ConfirmZones();
if (TryRiskExit(candle.ClosePrice) || IsPending || !IsFormedAndOnlineAndAllowTrading())
return;
Sides? side = null;
var width = 0m;
if (!_isBullUsed && _bullHigh is decimal bullHigh && _bullLow is decimal bullLow
&& candle.ClosePrice > bullHigh && Position <= 0m)
{
side = Sides.Buy;
width = bullHigh - bullLow;
_isBullUsed = true;
}
else if (!_isBearUsed && _bearHigh is decimal bearHigh && _bearLow is decimal bearLow
&& candle.ClosePrice < bearLow && Position >= 0m)
{
side = Sides.Sell;
width = bearHigh - bearLow;
_isBearUsed = true;
}
if (side is Sides direction)
{
_stopDistance = width * StopLossFactor;
_takeDistance = width * TakeProfitFactor;
Submit(direction, Volume + Math.Abs(Position), "PowerZone breakout entry");
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import ArgumentException, Decimal, Math, TimeSpan
from StockSharp.Messages import CandleStates, DataType, ITickTradeMessage, Level1Fields, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class power_zone_strategy(Strategy):
def __init__(self):
super(power_zone_strategy, self).__init__()
self._periods = self.Param("Periods", 5).SetGreaterThanZero() \
.SetDisplay("Periods", "Completed same-color confirmation candles after the opposite anchor", "Zones")
self._threshold = self.Param("Threshold", Decimal.Zero).SetRange(Decimal.Zero, Decimal.MaxValue) \
.SetDisplay("Threshold", "Minimum absolute percent move from anchor close to final confirmation close", "Zones")
self._use_wicks = self.Param("UseWicks", False) \
.SetDisplay("Use Wicks", "Use anchor high/low instead of body boundaries", "Zones")
self._take_profit_factor = self.Param("TakeProfitFactor", Decimal.Parse("1.5")).SetGreaterThanZero() \
.SetDisplay("Take Profit Factor", "Frozen entry-zone range multiple above/below actual fill basis", "Protection")
self._stop_loss_factor = self.Param("StopLossFactor", Decimal(1)).SetGreaterThanZero() \
.SetDisplay("Stop Loss Factor", "Frozen entry-zone range multiple below/above actual fill basis", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Finished zone confirmations and subsequent breakout decisions", "General")
tick_type = clr.GetClrType(ITickTradeMessage)
self._tick_price = tick_type.GetProperty("Price")
self._tick_volume = tick_type.GetProperty("Volume")
self._reset_state()
self.OrderRegistering += self._observe_order
self.OwnTradeReceived += self._observe_fill
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def _reset_state(self):
self._bars = []
self._bull = self._bear = None
self._bull_used = self._bear_used = False
self._pending_order = None
self._filled_position = self._entry_price = self._stop_distance = self._take_distance = Decimal.Zero
def OnReseted(self):
super(power_zone_strategy, self).OnReseted()
self._reset_state()
def _observe_order(self, order):
self._pending_order = order
def OnStarted2(self, time):
super(power_zone_strategy, self).OnStarted2(time)
if self._periods.Value <= 0 or self._threshold.Value < 0 \
or self._take_profit_factor.Value <= 0 or self._stop_loss_factor.Value <= 0:
raise ArgumentException("Periods and risk factors must be positive; movement threshold cannot be negative.")
self._reset_state()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _is_pending(self):
order = self._pending_order
return order is not None and order.State != OrderStates.Done and order.State != OrderStates.Failed
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
order.Comment = comment
self.RegisterOrder(order)
def _observe_fill(self, subscription, trade):
previous = self._filled_position
price = self._tick_price.GetValue(trade.Trade)
volume = self._tick_volume.GetValue(trade.Trade)
signed = volume if trade.Order.Side == Sides.Buy else -volume
self._filled_position += signed
if self._filled_position == 0:
self._entry_price = Decimal.Zero
elif previous == 0 or Math.Sign(self._filled_position) != Math.Sign(previous):
self._entry_price = price
elif Math.Sign(signed) == Math.Sign(previous):
self._entry_price = (self._entry_price * abs(previous) + price * abs(signed)) / abs(self._filled_position)
def _try_risk_exit(self, price):
if self.Position == 0 or self._entry_price <= 0 or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return False
profit = (price - self._entry_price) * Math.Sign(self.Position)
comment = "PowerZone stop loss" if profit <= -self._stop_distance \
else "PowerZone take profit" if profit >= self._take_distance else None
if comment is None:
return False
self._submit(Sides.Sell if self.Position > 0 else Sides.Buy, abs(self.Position), comment)
return True
def _observe_quote(self, quote):
field = Level1Fields.BestBidPrice if self.Position > 0 else Level1Fields.BestAskPrice
if quote.Changes.ContainsKey(field) and quote.Changes[field] > 0:
self._try_risk_exit(quote.Changes[field])
def _confirm_zones(self):
if len(self._bars) < self._periods.Value + 2:
return
anchor = self._bars[0]
confirmation = self._bars[1:-1]
bull = anchor[3] < anchor[0] and all(bar[3] > bar[0] for bar in confirmation)
bear = anchor[3] > anchor[0] and all(bar[3] < bar[0] for bar in confirmation)
if not bull and not bear or anchor[3] == 0 \
or Decimal(100) * abs(confirmation[-1][3] - anchor[3]) / abs(anchor[3]) < self._threshold.Value:
return
high = anchor[1] if self._use_wicks.Value else max(anchor[0], anchor[3])
low = anchor[2] if self._use_wicks.Value else min(anchor[0], anchor[3])
if high <= low:
return
if bull:
self._bull = (high, low)
self._bull_used = False
else:
self._bear = (high, low)
self._bear_used = False
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
self._bars.append((candle.OpenPrice, candle.HighPrice, candle.LowPrice, candle.ClosePrice))
if len(self._bars) > self._periods.Value + 2:
self._bars.pop(0)
self._confirm_zones()
if self._try_risk_exit(candle.ClosePrice) or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return
side = None
width = Decimal.Zero
if not self._bull_used and self._bull is not None and candle.ClosePrice > self._bull[0] and self.Position <= 0:
side = Sides.Buy
width = self._bull[0] - self._bull[1]
self._bull_used = True
elif not self._bear_used and self._bear is not None and candle.ClosePrice < self._bear[1] and self.Position >= 0:
side = Sides.Sell
width = self._bear[0] - self._bear[1]
self._bear_used = True
if side is not None:
self._stop_distance = width * self._stop_loss_factor.Value
self._take_distance = width * self._take_profit_factor.Value
self._submit(side, self.Volume + abs(self.Position), "PowerZone breakout entry")
def CreateClone(self):
return power_zone_strategy()