Auf GitHub ansehen

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");
		}
	}
}