在 GitHub 上查看

PowerZone 策略

该策略识别“PowerZone”订单块:先出现一根看跌K线,随后连续出现若干根看涨K线(反之亦然)。价格向上突破牛区高点买入,向下跌破熊区低点卖出。止盈与止损根据区间大小计算。

细节

  • 入场条件:
    • 多头:Periods+1根之前的看跌K线,之后Periods根看涨K线,并且价格突破区间上沿。
    • 空头:Periods+1根之前的看涨K线,之后Periods根看跌K线,并且价格跌破区间下沿。
  • 多空方向:双向。
  • 出场条件:止盈和止损为区间宽度的倍数。
  • 指标:无。
  • 默认值:
    • Periods = 5
    • Threshold = 0
    • UseWicks = false
    • Take Profit Factor = 1.5
    • Stop 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");
		}
	}
}