PowerZone 戦略
この戦略は、弱気のローソク足に続く連続した強気のローソク足(またはその逆)によって形成される"PowerZone"オーダーブロックを識別します。強気ゾーンを上回るブレイクアウトがロングトレードを発動し、弱気ゾーンを下回る崩落がショートポジションを開きます。ターゲットとストップはゾーンのレンジに基づきます。
詳細
- エントリー条件:
- ロング:
Periods+1本前の弱気ローソク足に続くPeriods本の強気ローソク足とゾーン高値を上抜ける価格。 - ショート:
Periods+1本前の強気ローソク足に続くPeriods本の弱気ローソク足とゾーン安値を下抜ける価格。
- ロング:
- ロング/ショート: 両方。
- エグジット条件: ゾーンレンジの倍数によるテイクプロフィットとストップロス。
- インジケーター: なし。
- デフォルト値:
Periods= 5Threshold= 0UseWicks= falseTake Profit Factor= 1.5Stop Loss Factor= 1
- フィルター:
- カテゴリ: ブレイクアウト
- 方向: 両方
- インジケーター: なし
- Stops: はい
- 複雑さ: 中程度
- 時間軸: 任意
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中程度
計算と執行
C# と Python は確定足を使います。判断足 t の基準足は t - Periods - 1 です。その後の t - 1 までの Periods 本がすべて始値より厳密に高く引ければ強気ゾーン、低く引ければ弱気ゾーンになります。基準足は厳密に逆の色で、同値の足は対象外です。現在の判断足は確認に含めず、確認の完了前にゾーンを利用しません。
100 * abs(Close[t - 1] - 基準 Close) / abs(基準 Close) >= Threshold を要求し、基準終値0は無効です。ゾーンは基準足の実体 [min(Open, Close), max(Open, Close)]、UseWicks=true なら [Low, High] です。確認足の極値でゾーンを拡大せず、幅は正とします。最新の強気・弱気ゾーンを別々に更新まで保持し、各ゾーンは受理したエントリー1回だけに使います。
ロングは現在の Close > 強気ゾーン高値、ショートは Close < 弱気ゾーン安値 を要求し、同値は維持します。新ゾーンは価格がすでに境界外でも最初の判断足で入れます。ポジションなしでは Volume、反対ポジションでは Volume + abs(Position) で完全反転し、同方向の追加はしません。両方向が空の状態で適格ならロング優先です。未完了注文中は次の注文を禁止します。EMA、待機時間、その他の指標は使いません。
エントリー受理時に ゾーン幅 * StopLossFactor と ゾーン幅 * TakeProfitFactor を固定し、実際の約定価格を基準にします。同方向の部分追加は出来高加重平均、縮小は基準保持、反転はリセットです。後のゾーンは距離を動かしません。ローカル保護はロングの実行可能 bid、ショートの ask、確定足の終値を確認し、全残量を成行決済します。同じ足の新規エントリーより保護を優先します。高値・安値への接触を足内約定と想定せず、ギャップで約定価格が変わる場合があります。
共通標準値:API パラメーター TakeProfitFactor=1.5、StopLossFactor=1、CandleType は5分。公開パラメーター名は 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()