Estrategia PowerZone
Esta estrategia identifica bloques de órdenes "PowerZone" creados por una vela bajista seguida de velas alcistas consecutivas (y viceversa). Una ruptura por encima de la zona alcista activa una operación larga, mientras que una ruptura por debajo de la zona bajista abre una corta. Los objetivos y stops se basan en el rango de la zona.
Detalles
- Criterios de entrada:
- Largo: Vela bajista hace
Periods+1 barras seguida de Periods velas alcistas y el precio rompe por encima del máximo de la zona.
- Corto: Vela alcista hace
Periods+1 barras seguida de Periods velas bajistas y el precio rompe por debajo del mínimo de la zona.
- Largo/Corto: Ambos lados.
- Criterios de salida: Take profit y stop loss como múltiplos del rango de la zona.
- Indicadores: Ninguno.
- Valores predeterminados:
Periods = 5
Threshold = 0
UseWicks = false
Take Profit Factor = 1.5
Stop Loss Factor = 1
- Filtros:
- Categoría: Ruptura
- Dirección: Ambos
- Indicadores: Ninguno
- Stops: Sí
- Complejidad: Moderado
- Marco temporal: Cualquiera
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
- Nivel de riesgo: Moderado
Cálculo y ejecución
C# y Python usan velas terminadas. En la barra de decisión t, el ancla es t - Periods - 1; las siguientes Periods barras hasta t - 1 deben cerrar todas estrictamente por encima de la apertura para zona alcista, o por debajo para bajista. El ancla tiene color estrictamente opuesto; los dojis no califican. La vela actual no confirma la zona, que nunca se conoce antes de terminar la serie.
Exija 100 * abs(Close[t - 1] - Close del ancla) / abs(Close del ancla) >= Threshold; un cierre del ancla cero es inválido. La zona es su cuerpo [min(Open, Close), max(Open, Close)] o [Low, High] con UseWicks=true. Los extremos de confirmación no amplían la zona; la anchura debe ser positiva. Mantenga por separado las últimas zonas alcista y bajista hasta reemplazarlas, con una sola entrada aceptada por zona.
Largo requiere Close actual > máximo de zona alcista; corto, Close < mínimo de zona bajista. Igualdad mantiene la posición. Una zona nueva puede entrar en su primera vela de decisión aunque el precio ya esté fuera. Entre con Volume desde cero o invierta completamente con Volume + abs(Position), sin aumentar el mismo lado. Si ambas direcciones califican desde cero, largo tiene prioridad. Órdenes pendientes bloquean otras órdenes. No hay EMA, espera obligatoria ni otros indicadores.
Al aceptar la entrada, congele anchura * StopLossFactor y anchura * TakeProfitFactor alrededor de la ejecución real. Aumentos parciales del mismo lado usan base ponderada por volumen; reducir conserva la base e invertir la reinicia. Zonas posteriores no mueven las distancias. La protección local comprueba bid ejecutable para largos, ask para cortos y cierres de velas; cierra toda la cantidad restante a mercado, con prioridad sobre entradas en esa vela. Tocar máximos/mínimos no supone ejecuciones intrabar; saltos pueden alterar el precio ejecutado.
Valores compartidos: parámetros API TakeProfitFactor=1.5, StopLossFactor=1, CandleType de cinco minutos. El parámetro publicado es UseWicks; la propiedad C# es IsUseWicks. Periods y factores positivos; Threshold es un porcentaje no negativo. Estas definiciones precisan las reglas de umbral, límites, consumo único y base de ejecución antes no especificadas.
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()