波动收缩形态 (Volatility Contraction Pattern)
寻找一系列逐渐收窄的价格区间, 为爆发行情蓄势。
测试表明年均收益约为 166%,该策略在股票市场表现最佳。
突破区间高点或低点时入场。
详情
- 入场条件: Range contraction then breakout of recent high/low.
- 多空方向: Both directions.
- 出场条件: Price crosses MA or stop.
- 止损: Yes.
- 默认值:
MAPeriod= 20LookbackPeriod= 20CandleType= TimeSpan.FromMinutes(5)
- 过滤器:
- 类别: Breakout
- 方向: Both
- 指标: Range, MA
- 止损: Yes
- 复杂度: Intermediate
- 时间框架: Intraday
- 季节性: No
- 神经网络: No
- 背离: No
- 风险等级: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Volatility Contraction Pattern using prior rolling High/Low range breakouts.
/// Exits on an adverse price/SMA crossing or actual-fill percent protection.
/// </summary>
public class VcpStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<int> _contractionBars;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private decimal? _previousHigh;
private decimal _previousLow;
private int _contractionCount;
private decimal? _previousClose;
private decimal _previousMean;
private Order _pendingOrder;
public int MAPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
public int LookbackPeriod { get => _lookbackPeriod.Value; set => _lookbackPeriod.Value = value; }
public int ContractionBars { get => _contractionBars.Value; set => _contractionBars.Value = value; }
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public VcpStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
.SetOptimize(10, 50, 10);
_lookbackPeriod = Param(nameof(LookbackPeriod), 20).SetGreaterThanZero()
.SetDisplay("Lookback Period", "Rolling High/Low range length", "Indicators");
_contractionBars = Param(nameof(ContractionBars), 3).SetGreaterThanZero()
.SetDisplay("Contractions", "Strict range reductions without an intervening expansion; equal widths are neutral.", "Entry");
_stopLossPercent = Param(nameof(StopLossPercent), 2m).SetNotNegative()
.SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
OrderRegistering += order => _pendingOrder = order;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
private void ResetPattern()
{
_previousHigh = null;
_previousLow = default;
_contractionCount = 0;
_previousClose = null;
_previousMean = default;
_pendingOrder = null;
}
protected override void OnReseted()
{
base.OnReseted();
ResetPattern();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetPattern();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var highest = new Highest { Length = LookbackPeriod };
var lowest = new Lowest { Length = LookbackPeriod };
var sma = new SimpleMovingAverage { Length = MAPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(highest, lowest, sma, ProcessCandle, false).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, highest);
DrawIndicator(area, lowest);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue highestValue, IIndicatorValue lowestValue, IIndicatorValue smaValue)
{
if (candle.State != CandleStates.Finished || !highestValue.Indicator.IsFormed || !lowestValue.Indicator.IsFormed)
return;
// The setup and breakout boundaries belong to the PRIOR completed channel.
var upper = _previousHigh;
var lower = _previousLow;
var contracted = _contractionCount >= ContractionBars;
var high = highestValue.GetValue<decimal>();
var low = lowestValue.GetValue<decimal>();
if (upper is decimal previousHigh)
{
var change = high - low - (previousHigh - lower);
if (change < 0m) _contractionCount++;
else if (change > 0m) _contractionCount = 0;
// Equal width preserves, but does not add to, the contraction sequence.
}
_previousHigh = high;
_previousLow = low;
if (!smaValue.Indicator.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
var mean = smaValue.GetValue<decimal>();
var upwardCross = _previousClose is decimal up && up <= _previousMean && close > mean;
var downwardCross = _previousClose is decimal down && down >= _previousMean && close < mean;
var seeded = _previousClose.HasValue;
_previousClose = close;
_previousMean = mean;
if (!seeded || upper is not decimal previousUpper
|| _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && downwardCross)
SellMarket(Position);
else if (Position < 0m && upwardCross)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && contracted)
{
if (close > previousUpper)
{
_contractionCount = 0;
BuyMarket(Volume);
}
else if (close < lower)
{
_contractionCount = 0;
SellMarket(Volume);
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, OrderStates, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import Highest, Lowest, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class vcp_strategy(Strategy):
"""
Volatility Contraction Pattern using prior rolling High/Low range breakouts.
Exits on an adverse price/SMA crossing or actual-fill percent protection.
"""
def __init__(self):
super(vcp_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
self._lookback_period = self.Param("LookbackPeriod", 20).SetGreaterThanZero().SetDisplay("Lookback Period", "Rolling High/Low range length", "Indicators")
self._contraction_bars = self.Param("ContractionBars", 3).SetGreaterThanZero().SetDisplay("Contractions", "Strict range reductions without an intervening expansion; equal widths are neutral.", "Entry")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_pattern()
self.OrderRegistering += self._track_pending
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def _reset_pattern(self):
self._previous_high = None
self._previous_low = Decimal.Zero
self._contraction_count = 0
self._previous_close = None
self._previous_mean = Decimal.Zero
self._pending_order = None
def OnReseted(self):
super(vcp_strategy, self).OnReseted()
self._reset_pattern()
def OnStarted2(self, time):
super(vcp_strategy, self).OnStarted2(time)
self._reset_pattern()
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
highest = Highest()
highest.Length = self._lookback_period.Value
lowest = Lowest()
lowest.Length = self._lookback_period.Value
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(highest, lowest, sma, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, highest)
self.DrawIndicator(area, lowest)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between finished candles.
pass
def _process_candle(self, candle, highest_value, lowest_value, sma_value):
if candle.State != CandleStates.Finished or not highest_value.Indicator.IsFormed or not lowest_value.Indicator.IsFormed:
return
# Use the PRIOR completed channel, before the current breakout can expand it.
upper = self._previous_high
lower = self._previous_low
contracted = self._contraction_count >= self._contraction_bars.Value
high = highest_value.GetValue[Decimal](None)
low = lowest_value.GetValue[Decimal](None)
if upper is not None:
change = high - low - (upper - lower)
if change < 0:
self._contraction_count += 1
elif change > 0:
self._contraction_count = 0
# Equal width is neutral, not another contraction or an invalidation.
self._previous_high = high
self._previous_low = low
if not sma_value.Indicator.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
close = candle.ClosePrice
mean = sma_value.GetValue[Decimal](None)
seeded = self._previous_close is not None
upward_cross = seeded and self._previous_close <= self._previous_mean and close > mean
downward_cross = seeded and self._previous_close >= self._previous_mean and close < mean
self._previous_close = close
self._previous_mean = mean
if not seeded or upper is None or (self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position > 0 and downward_cross:
self.SellMarket(self.Position)
elif self.Position < 0 and upward_cross:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and contracted:
if close > upper:
self._contraction_count = 0
self.BuyMarket(self.Volume)
elif close < lower:
self._contraction_count = 0
self.SellMarket(self.Volume)
def CreateClone(self):
return vcp_strategy()