Estratégia PowerZone
Esta estratégia identifica blocos de ordens "PowerZone" criados por uma vela bearish seguida de velas bullish consecutivas (e vice-versa). Um rompimento acima da zona bullish aciona uma operação comprada, enquanto uma quebra abaixo da zona bearish abre uma vendida. Alvos e stops são baseados no intervalo da zona.
Detalhes
- Critérios de entrada:
- Comprado: Vela bearish há
Periods+1 barras seguida de Periods velas bullish e o preço rompe acima do máximo da zona.
- Vendido: Vela bullish há
Periods+1 barras seguida de Periods velas bearish e o preço rompe abaixo do mínimo da zona.
- Comprado/Vendido: Ambos os lados.
- Critérios de saída: Take profit e stop loss como múltiplos do intervalo da zona.
- Indicadores: Nenhum.
- Valores padrão:
Periods = 5
Threshold = 0
UseWicks = false
Take Profit Factor = 1.5
Stop Loss Factor = 1
- Filtros:
- Categoria: Rompimento
- Direção: Ambos
- Indicadores: Nenhum
- Stops: Sim
- Complexidade: Moderado
- Período: Qualquer
- Sazonalidade: Não
- Redes neurais: Não
- Divergência: Não
- Nível de risco: Moderado
Cálculo e execução
C# e Python usam velas concluídas. Na barra de decisão t, a âncora é t - Periods - 1; as próximas Periods barras até t - 1 precisam fechar todas estritamente acima da abertura para zona de alta, ou abaixo para zona de baixa. A âncora tem cor estritamente oposta; dojis não qualificam. A vela atual não confirma a zona, que nunca é conhecida antes de concluir a sequência.
Exija 100 * abs(Close[t - 1] - Close da âncora) / abs(Close da âncora) >= Threshold; fechamento da âncora zero é inválido. A zona é seu corpo [min(Open, Close), max(Open, Close)] ou [Low, High] com UseWicks=true. Extremos da confirmação não ampliam a zona; a largura deve ser positiva. Guarde separadamente as últimas zonas de alta e baixa até substituí-las, com no máximo uma entrada aceita por zona.
Compra exige Close atual > máxima da zona de alta; venda, Close < mínima da zona de baixa. Igualdade mantém a posição. Uma zona nova pode entrar na primeira vela de decisão mesmo se o preço já estiver fora. Entre com Volume sem posição ou reverta completamente com Volume + abs(Position), sem aumentar o mesmo lado. Se ambas as direções qualificarem sem posição, compra tem prioridade. Ordens pendentes bloqueiam outras ordens. Não há EMA, espera obrigatória ou outros indicadores.
Na entrada aceita, congele largura * StopLossFactor e largura * TakeProfitFactor ao redor da execução real. Aumentos parciais do mesmo lado usam base ponderada por volume; reduzir preserva a base e reverter a reinicia. Zonas posteriores não movem as distâncias. Proteção local verifica bid executável para compras, ask para vendas e fechamentos de velas; fecha toda a quantidade restante a mercado, antes de novas entradas na mesma vela. Toques nas máximas/mínimas não pressupõem execuções intrabar; lacunas podem alterar o preço executado.
Padrões compartilhados: parâmetros API TakeProfitFactor=1.5, StopLossFactor=1, CandleType de cinco minutos. O parâmetro publicado é UseWicks; a propriedade C# é IsUseWicks. Periods e fatores positivos; Threshold é percentual não negativo. Essas definições esclarecem regras de limiar, limites, uso único e base de execução antes não 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()