Wyckoff Distribution Strategy
Wyckoff Distribution is a topping phase characterized by heavy selling into rallies and tests of resistance. Volume often expands on down moves and contracts on bounces, suggesting large interests are liquidating positions.
Testing indicates an average annual return of about 64%. It performs best in the forex market.
This strategy sells short when price breaks down from the distribution range, anticipating a sustained decline.
A stop just above the range protects against false breakouts, and positions close if price returns to the top of the structure.
Details
- Entry Criteria: indicator signal
- Long/Short: Both
- Exit Criteria: stop-loss or opposite signal
- Stops: Yes, percent based
- Default Values:
CandleType= 15 minuteStopLoss= 2%
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Volume, Price
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: 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>
/// Strategy based on Wyckoff Distribution pattern.
/// Detects narrowing ranges near extremes (distribution/accumulation),
/// then enters on upthrust/spring confirmation with MA filter.
/// Uses bar-based cooldown to control trade frequency.
/// </summary>
public class WyckoffDistributionStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _rangePeriod;
private readonly StrategyParam<int> _cooldownBars;
private SimpleMovingAverage _ma;
private Highest _highest;
private Lowest _lowest;
private decimal _prevMa;
private decimal _prevClose;
private int _narrowCount;
private int _barsSinceEntry;
private decimal _entryPrice;
private int _holdBars;
/// <summary>
/// Candle type and timeframe.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// MA period.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Highest/Lowest period.
/// </summary>
public int RangePeriod
{
get => _rangePeriod.Value;
set => _rangePeriod.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public WyckoffDistributionStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_maPeriod = Param(nameof(MaPeriod), 20)
.SetDisplay("MA Period", "SMA period", "Indicators")
.SetRange(10, 50);
_rangePeriod = Param(nameof(RangePeriod), 20)
.SetDisplay("Range Period", "Highest/Lowest period", "Indicators")
.SetRange(10, 50);
_cooldownBars = Param(nameof(CooldownBars), 800)
.SetDisplay("Cooldown Bars", "Bars between trades", "General")
.SetRange(10, 2000);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ma = default;
_highest = default;
_lowest = default;
_prevMa = 0;
_prevClose = 0;
_narrowCount = 0;
_barsSinceEntry = 0;
_entryPrice = 0;
_holdBars = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_barsSinceEntry = CooldownBars; // allow immediate first trade
_ma = new SimpleMovingAverage { Length = MaPeriod };
_highest = new Highest { Length = RangePeriod };
_lowest = new Lowest { Length = RangePeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ma, _highest, _lowest, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal ma, decimal highest, decimal lowest)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
var range = highest - lowest;
if (range <= 0 || _prevMa == 0)
{
_prevMa = ma;
_prevClose = close;
return;
}
_barsSinceEntry++;
var candleRange = candle.HighPrice - candle.LowPrice;
var isNarrow = candleRange < range * 0.35m;
// Track consecutive narrow-range candles
if (isNarrow)
_narrowCount++;
else
_narrowCount = 0;
// Exit logic: hold for minimum bars, then exit on MA cross
if (Position != 0 && _holdBars > 0)
{
_holdBars--;
}
if (Position > 0 && _holdBars == 0)
{
if (close < ma)
{
SellMarket();
_barsSinceEntry = 0;
}
}
else if (Position < 0 && _holdBars == 0)
{
if (close > ma)
{
BuyMarket();
_barsSinceEntry = 0;
}
}
// Entry logic: only when no position and sufficient cooldown
if (Position == 0 && _barsSinceEntry >= CooldownBars && _narrowCount >= 2)
{
var nearTop = close > lowest + range * 0.55m;
var nearBottom = close < highest - range * 0.55m;
// Upthrust (short): price near top after consolidation, bearish candle below MA
if (nearTop && close < candle.OpenPrice && close < ma)
{
SellMarket();
_entryPrice = close;
_barsSinceEntry = 0;
_narrowCount = 0;
_holdBars = 20;
}
// Spring (long): price near bottom after consolidation, bullish candle above MA
else if (nearBottom && close > candle.OpenPrice && close > ma)
{
BuyMarket();
_entryPrice = close;
_barsSinceEntry = 0;
_narrowCount = 0;
_holdBars = 20;
}
}
_prevMa = ma;
_prevClose = close;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class wyckoff_distribution_strategy(Strategy):
"""
Strategy based on Wyckoff Distribution pattern.
Detects narrowing ranges near extremes (distribution/accumulation),
then enters on upthrust/spring confirmation with MA filter.
Uses bar-based cooldown to control trade frequency.
"""
def __init__(self):
super(wyckoff_distribution_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candle timeframe", "General")
self._ma_period = self.Param("MaPeriod", 20).SetDisplay("MA Period", "SMA period", "Indicators")
self._range_period = self.Param("RangePeriod", 20).SetDisplay("Range Period", "Highest/Lowest period", "Indicators")
self._cooldown_bars = self.Param("CooldownBars", 800).SetDisplay("Cooldown Bars", "Bars between trades", "General")
self._prev_ma = 0.0
self._prev_close = 0.0
self._narrow_count = 0
self._bars_since_entry = 0
self._entry_price = 0.0
self._hold_bars = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(wyckoff_distribution_strategy, self).OnReseted()
self._prev_ma = 0.0
self._prev_close = 0.0
self._narrow_count = 0
self._bars_since_entry = 0
self._entry_price = 0.0
self._hold_bars = 0
def OnStarted2(self, time):
super(wyckoff_distribution_strategy, self).OnStarted2(time)
self._bars_since_entry = self._cooldown_bars.Value # allow immediate first trade
self._prev_ma = 0.0
self._prev_close = 0.0
self._narrow_count = 0
self._entry_price = 0.0
self._hold_bars = 0
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
highest = Highest()
highest.Length = self._range_period.Value
lowest = Lowest()
lowest.Length = self._range_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, highest, lowest, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ma_val, highest_val, lowest_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
ma = float(ma_val)
highest = float(highest_val)
lowest = float(lowest_val)
rng = highest - lowest
if rng <= 0 or self._prev_ma == 0:
self._prev_ma = ma
self._prev_close = close
return
self._bars_since_entry += 1
candle_range = float(candle.HighPrice) - float(candle.LowPrice)
is_narrow = candle_range < rng * 0.35
# Track consecutive narrow-range candles
if is_narrow:
self._narrow_count += 1
else:
self._narrow_count = 0
cd = self._cooldown_bars.Value
# Exit logic: hold for minimum bars, then exit on MA cross
if self.Position != 0 and self._hold_bars > 0:
self._hold_bars -= 1
if self.Position > 0 and self._hold_bars == 0:
if close < ma:
self.SellMarket()
self._bars_since_entry = 0
elif self.Position < 0 and self._hold_bars == 0:
if close > ma:
self.BuyMarket()
self._bars_since_entry = 0
# Entry logic: only when no position and sufficient cooldown
if self.Position == 0 and self._bars_since_entry >= cd and self._narrow_count >= 2:
near_top = close > lowest + rng * 0.55
near_bottom = close < highest - rng * 0.55
# Upthrust (short): price near top after consolidation, bearish candle below MA
if near_top and candle.ClosePrice < candle.OpenPrice and close < ma:
self.SellMarket()
self._entry_price = close
self._bars_since_entry = 0
self._narrow_count = 0
self._hold_bars = 20
# Spring (long): price near bottom after consolidation, bullish candle above MA
elif near_bottom and candle.ClosePrice > candle.OpenPrice and close > ma:
self.BuyMarket()
self._entry_price = close
self._bars_since_entry = 0
self._narrow_count = 0
self._hold_bars = 20
self._prev_ma = ma
self._prev_close = close
def CreateClone(self):
return wyckoff_distribution_strategy()