PowerZone 策略
该策略识别“PowerZone”订单块:先出现一根看跌K线,随后连续出现若干根看涨K线(反之亦然)。价格向上突破牛区高点买入,向下跌破熊区低点卖出。止盈与止损根据区间大小计算。
细节
- 入场条件:
- 多头:
Periods+1根之前的看跌K线,之后Periods根看涨K线,并且价格突破区间上沿。 - 空头:
Periods+1根之前的看涨K线,之后Periods根看跌K线,并且价格跌破区间下沿。
- 多头:
- 多空方向:双向。
- 出场条件:止盈和止损为区间宽度的倍数。
- 指标:无。
- 默认值:
Periods= 5Threshold= 0UseWicks= falseTake Profit Factor= 1.5Stop Loss Factor= 1
- 过滤器:
- 类别:突破
- 方向:双向
- 指标:无
- 止损:有
- 复杂度:中等
- 时间框架:任意
- 季节性:无
- 神经网络:无
- 背离:无
- 风险等级:中等
计算与执行
C# 和 Python 使用已完成K线。在决策K线 t,基准K线为 t - Periods - 1,其后至 t - 1 的 Periods 根K线必须全部严格收涨才形成牛区,或全部严格收跌才形成熊区。基准K线颜色严格相反,十字星不合格。当前决策K线不参与确认,因此不会在确认完成前使用区域。
要求 100 * abs(Close[t - 1] - 基准 Close) / abs(基准 Close) >= Threshold,基准收盘价不能为零。区域只取基准K线的实体 [min(Open, Close), max(Open, Close)],UseWicks=true 时取其 [Low, High]。确认K线的极值不扩大区域,区域宽度必须为正。分别保留最近牛区和熊区直到替换,每个区域最多接受一次入场。
做多要求当前 Close > 牛区高点,做空要求 Close < 熊区低点,同价保持仓位。新确认区域在第一个决策K线即可入场,即使价格已经越过边界。空仓入场 Volume,反向仓位用 Volume + abs(Position) 完全反转,不在同一方向加仓。空仓时两方向同时合格则优先做多。有未完成订单时不再下单,没有 EMA、冷却期或其他指标。
接受入场时固定 区域宽度 * StopLossFactor 和 区域宽度 * TakeProfitFactor,以实际成交价为基准;同侧部分增仓使用成交量加权均价,减仓保留基准,反转重置。后续区域不改变已有仓位的保护距离。本地保护检查多仓可执行 bid、空仓 ask 和已完成K线收盘价,用市价单平掉全部剩余仓位,同一K线上优先于新入场。不把最高/最低价触及视为盘中成交;跳空可导致实际成交价不同。
上述默认值两语言相同:API 参数 TakeProfitFactor=1.5、StopLossFactor=1、CandleType 为五分钟。已发布参数名为 UseWicks,C# 属性名为 IsUseWicks。Periods 和倍数必须为正,Threshold 是非负百分比。这些定义明确了原简介未规定的阈值、边界、一次性使用和成交基准规则。
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()