Bollinger Heikin Ashi Entry
在 Heikin Ashi 蜡烛上计算布林带反转,实际平掉一半持仓获利,再跟踪剩余仓位。
交易规则
HA 收盘为 (Open+High+Low+Close)/4;首根 HA 开盘为 (Open+Close)/2,之后为 (前 HA 开盘+前 HA 收盘)/2。HA 最高/最低含 HA 开收盘与原始高低价。布林带从第一根存档蜡烛开始使用 HA 收盘价,而非原始收盘价。十字星不属于阴或阳。触及定义为 HA low <= lower / HA high >= upper。
前面至少两根连续阴线 HA 蜡烛必须各自触及其下轨;随后阳线 HA 蜡烛收盘严格高于自身下轨时做多。颜色、上轨触及和收盘方向反向时做空。
仅空仓时以 Volume 入场。初始止损为前一根已完成原始蜡烛的低点(多)或高点(空),发单前固定。R 是加权实际入场价到止损的正距离。达到 1R 后发市价单平一半实际仓位,按 VolumeStep 向下取整。若任一半小于 MinVolume,则在目标处全平。若入场成交越过初始止损,在下一个可执行报价平仓。
只有目标单实际成交才启用跟踪;剩余仓位止损沿最新已完成原始蜡烛的低点/高点移动,绝不放宽。多头 bid 或空头 ask 在蜡烛之间触发全量止损,止损优先于目标。未完成订单阻止重复发单。较早的蜡烛极值不会触发后来创建的价位。没有固定百分比目标或隐藏等待期。
仅在指标充分预热后使用已完成的蜡烛生成入场信号。持仓持续管理至退出;未完成的市价订单会阻止重复提交。
布林带采用滚动收盘价窗口的总体标准差,包含当前已完成蜡烛。
参数
| Parameter | Default |
|---|---|
BollingerPeriod |
20 |
BollingerDeviation |
2 |
CandleType |
5m |
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>Heikin Ashi band reversal with a real half-position target and a causal trailing remainder.</summary>
public class BollingerHeikinAshiEntryStrategy : Strategy
{
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<DataType> _candleType;
private BollingerBands _bands;
private Order _pending;
private bool _haInitialized, _targetTaken, _invalidFill;
private decimal _haOpen, _haClose, _previousLow, _previousHigh;
private decimal _stop, _target, _entryCost, _entryQuantity;
private int _redTouches, _greenTouches;
/// <summary>Initializes the published Heikin Ashi and Bollinger parameters.</summary>
public BollingerHeikinAshiEntryStrategy()
{
_bollingerPeriod = Param(nameof(BollingerPeriod), 20).SetGreaterThanZero().SetOptimize(10, 50, 5)
.SetDisplay("BollingerPeriod", "Heikin Ashi close window", "Parameters");
_bollingerDeviation = Param(nameof(BollingerDeviation), 2m).SetGreaterThanZero().SetOptimize(1m, 3m, 0.5m)
.SetDisplay("BollingerDeviation", "Bollinger standard deviation multiplier", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("CandleType", "Finished signal candle interval", "Parameters");
}
/// <summary>Heikin Ashi close window.</summary>
public int BollingerPeriod { get => _bollingerPeriod.Value; set => _bollingerPeriod.Value = value; }
/// <summary>Bollinger standard deviation multiplier.</summary>
public decimal BollingerDeviation { get => _bollingerDeviation.Value; set => _bollingerDeviation.Value = value; }
/// <summary>Finished signal candle interval.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
private bool IsPending => _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed);
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_bands = null;
_pending = null;
_haInitialized = _targetTaken = _invalidFill = false;
_haOpen = _haClose = _previousLow = _previousHigh = 0m;
_stop = _target = _entryCost = _entryQuantity = 0m;
_redTouches = _greenTouches = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bands = new BollingerBands { Length = BollingerPeriod, Width = BollingerDeviation };
Indicators.Add(_bands);
var candles = SubscribeCandles(CandleType);
candles.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area is not null)
{
DrawCandles(area, candles);
DrawIndicator(area, _bands);
DrawOwnTrades(area);
}
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quote = new Subscription(DataType.Level1, Security);
quote.MarketData.BuildField = field;
SubscribeLevel1(quote).Bind(ProcessQuote).Start();
}
}
private void Submit(Sides side, decimal quantity, string reason)
{
_pending = CreateOrder(side, 0m, quantity);
_pending.Type = OrderTypes.Market;
_pending.Comment = "0554" + reason;
RegisterOrder(_pending);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var open = _haInitialized ? (_haOpen + _haClose) / 2m : (candle.OpenPrice + candle.ClosePrice) / 2m;
var close = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
_haOpen = open;
_haClose = close;
_haInitialized = true;
var input = new DecimalIndicatorValue(_bands, close, candle.OpenTime) { IsFinal = true };
var bands = (BollingerBandsValue)_bands.Process(input);
Sides? signal = null;
if (_bands.IsFormed && bands.LowBand is decimal lower && bands.UpBand is decimal upper)
{
var redValid = _redTouches >= 2;
var greenValid = _greenTouches >= 2;
if (close > open && close > lower && redValid)
signal = Sides.Buy;
else if (close < open && close < upper && greenValid)
signal = Sides.Sell;
_redTouches = close < open && Math.Min(candle.LowPrice, Math.Min(open, close)) <= lower ? _redTouches + 1 : 0;
_greenTouches = close > open && Math.Max(candle.HighPrice, Math.Max(open, close)) >= upper ? _greenTouches + 1 : 0;
}
if (Position != 0m && _targetTaken)
_stop = Position > 0m ? Math.Max(_stop, candle.LowPrice) : Math.Min(_stop, candle.HighPrice);
var setupStop = signal == Sides.Buy ? _previousLow : _previousHigh;
_previousLow = candle.LowPrice;
_previousHigh = candle.HighPrice;
if (signal is null || Position != 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
return;
_stop = setupStop;
_entryCost = _entryQuantity = 0m;
_targetTaken = _invalidFill = false;
Submit(signal.Value, Volume, "Entry");
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order.Comment == "0554Entry")
{
_entryCost += trade.Trade.Price * trade.Trade.Volume;
_entryQuantity += trade.Trade.Volume;
var entry = _entryCost / _entryQuantity;
var risk = trade.Order.Side == Sides.Buy ? entry - _stop : _stop - entry;
_invalidFill = risk <= 0m;
_target = trade.Order.Side == Sides.Buy ? entry + risk : entry - risk;
}
else if (trade.Order.Comment == "0554Target")
{
_targetTaken = true;
if (Position != 0m)
_stop = Position > 0m ? Math.Max(_stop, _previousLow) : Math.Min(_stop, _previousHigh);
}
}
private void ProcessQuote(Level1ChangeMessage quote)
{
if (Position == 0m || _entryQuantity == 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
return;
var field = Position > 0m ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
if (!quote.Changes.TryGetValue(field, out var raw) || raw is not decimal price || price <= 0m)
return;
var side = Position > 0m ? Sides.Sell : Sides.Buy;
if (_invalidFill || (Position > 0m ? price <= _stop : price >= _stop))
Submit(side, Math.Abs(Position), "Stop");
else if (!_targetTaken && (Position > 0m ? price >= _target : price <= _target))
{
var quantity = Math.Abs(Position);
var step = Security.VolumeStep ?? 1m;
var minimum = Security.MinVolume ?? step;
var half = Math.Floor(quantity / 2m / step) * step;
// A sub-lot remainder cannot be managed: take the whole position at 1R instead.
Submit(side, half >= minimum && quantity - half >= minimum ? half : quantity, "Target");
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Decimal, Math, TimeSpan, Convert, InvalidOperationException
from StockSharp.Algo.Indicators import BollingerBands, RelativeStrengthIndex, ExponentialMovingAverage, AverageTrueRange, DecimalIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription
from StockSharp.Messages import DataType, CandleStates, Level1Fields, OrderStates, OrderTypes, Sides, Unit, UnitTypes, ITickTradeMessage
class bollinger_heikin_ashi_entry_strategy(Strategy):
"""Heikin Ashi band reversal with actual partial profit and a trailing remainder."""
def __init__(self):
super(bollinger_heikin_ashi_entry_strategy, self).__init__()
self._bollingerPeriod = self.Param("BollingerPeriod", 20).SetGreaterThanZero() \
.SetDisplay("BollingerPeriod", "Heikin Ashi close window", "Parameters")
self._bollingerDeviation = self.Param("BollingerDeviation", Decimal(2)).SetGreaterThanZero() \
.SetDisplay("BollingerDeviation", "Bollinger standard deviation multiplier", "Parameters")
self._candleType = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("CandleType", "Finished signal candle interval", "Parameters")
self._clear_state()
@property
def BollingerPeriod(self):
return self._bollingerPeriod.Value
@BollingerPeriod.setter
def BollingerPeriod(self, value):
self._bollingerPeriod.Value = value
@property
def BollingerDeviation(self):
return self._bollingerDeviation.Value
@BollingerDeviation.setter
def BollingerDeviation(self, value):
self._bollingerDeviation.Value = value
@property
def CandleType(self):
return self._candleType.Value
@CandleType.setter
def CandleType(self, value):
self._candleType.Value = value
def _clear_state(self):
self._bands = None
self._pending = None
self._ha_initialized = False
self._target_taken = False
self._invalid_fill = False
self._ha_open = self._ha_close = Decimal(0)
self._previous_low = self._previous_high = Decimal(0)
self._stop = self._target = self._entry_cost = self._entry_quantity = Decimal(0)
self._red_touches = self._green_touches = 0
def OnReseted(self):
super(bollinger_heikin_ashi_entry_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(bollinger_heikin_ashi_entry_strategy, self).OnStarted2(time)
self._bands = BollingerBands()
self._bands.Length = self.BollingerPeriod
self._bands.Width = self.BollingerDeviation
self.Indicators.Add(self._bands)
candles = self.SubscribeCandles(self.CandleType)
candles.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, self._bands)
self.DrawOwnTrades(area)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quote = Subscription(DataType.Level1, self.Security)
quote.MarketData.BuildField = field
self.SubscribeLevel1(quote).Bind(self._process_quote).Start()
def _is_pending(self):
return self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed)
def _submit(self, side, quantity, reason):
self._pending = self.CreateOrder(side, Decimal(0), quantity)
self._pending.Type = OrderTypes.Market
self._pending.Comment = "0554" + reason
self.RegisterOrder(self._pending)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
ha_open = (self._ha_open + self._ha_close) / Decimal(2) if self._ha_initialized else (candle.OpenPrice + candle.ClosePrice) / Decimal(2)
ha_close = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / Decimal(4)
self._ha_open = ha_open
self._ha_close = ha_close
self._ha_initialized = True
value = DecimalIndicatorValue(self._bands, ha_close, candle.OpenTime)
value.IsFinal = True
bands = self._bands.Process(value)
signal = None
if self._bands.IsFormed and bands.LowBand is not None and bands.UpBand is not None:
red_valid = self._red_touches >= 2
green_valid = self._green_touches >= 2
if ha_close > ha_open and ha_close > bands.LowBand and red_valid:
signal = Sides.Buy
elif ha_close < ha_open and ha_close < bands.UpBand and green_valid:
signal = Sides.Sell
self._red_touches = self._red_touches + 1 if ha_close < ha_open and min(candle.LowPrice, ha_open, ha_close) <= bands.LowBand else 0
self._green_touches = self._green_touches + 1 if ha_close > ha_open and max(candle.HighPrice, ha_open, ha_close) >= bands.UpBand else 0
if self.Position != 0 and self._target_taken:
self._stop = max(self._stop, candle.LowPrice) if self.Position > 0 else min(self._stop, candle.HighPrice)
setup_stop = self._previous_low if signal == Sides.Buy else self._previous_high
self._previous_low = candle.LowPrice
self._previous_high = candle.HighPrice
if signal is None or self.Position != 0 or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return
self._stop = setup_stop
self._entry_cost = self._entry_quantity = Decimal(0)
self._target_taken = self._invalid_fill = False
self._submit(signal, self.Volume, "Entry")
def OnOwnTradeReceived(self, trade):
super(bollinger_heikin_ashi_entry_strategy, self).OnOwnTradeReceived(trade)
if trade.Order.Comment == "0554Entry":
price = ITickTradeMessage.Price.__get__(trade.Trade)
quantity = ITickTradeMessage.Volume.__get__(trade.Trade)
self._entry_cost += price * quantity
self._entry_quantity += quantity
entry = self._entry_cost / self._entry_quantity
risk = entry - self._stop if trade.Order.Side == Sides.Buy else self._stop - entry
self._invalid_fill = risk <= 0
self._target = entry + risk if trade.Order.Side == Sides.Buy else entry - risk
elif trade.Order.Comment == "0554Target":
self._target_taken = True
if self.Position != 0:
self._stop = max(self._stop, self._previous_low) if self.Position > 0 else min(self._stop, self._previous_high)
def _process_quote(self, quote):
if self.Position == 0 or self._entry_quantity == 0 or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return
field = Level1Fields.BestBidPrice if self.Position > 0 else Level1Fields.BestAskPrice
if not quote.Changes.ContainsKey(field):
return
price = Convert.ToDecimal(quote.Changes[field])
if price <= 0:
return
side = Sides.Sell if self.Position > 0 else Sides.Buy
if self._invalid_fill or (price <= self._stop if self.Position > 0 else price >= self._stop):
self._submit(side, abs(self.Position), "Stop")
elif not self._target_taken and (price >= self._target if self.Position > 0 else price <= self._target):
quantity = abs(self.Position)
step = self.Security.VolumeStep if self.Security.VolumeStep is not None else Decimal(1)
minimum = self.Security.MinVolume if self.Security.MinVolume is not None else step
half = Math.Floor(quantity / Decimal(2) / step) * step
self._submit(side, half if half >= minimum and quantity - half >= minimum else quantity, "Target")
def CreateClone(self):
return bollinger_heikin_ashi_entry_strategy()