Estrategia de Acumulación Wyckoff
La Acumulación Wyckoff describe una fase de base donde los grandes intereses construyen posiciones silenciosamente después de una caída. El volumen y la acción del precio forman una serie de pruebas del soporte seguidas de mínimos más altos, sugiriendo una demanda creciente.
Las pruebas indican un rendimiento anual promedio de aproximadamente 61%. Funciona mejor en el mercado de criptomonedas.
Esta estrategia entra largo cuando el precio rompe el rango de acumulación, esperando una nueva tendencia alcista impulsada por esas compras anteriores.
Un stop de protección se coloca justo por debajo de la base para limitar pérdidas si el rompimiento falla.
Detalles
- Criterios de entrada: señal de indicador
- Largo/Corto: Ambos
- Criterios de salida: stop-loss o señal opuesta
- Stops: Sí, basados en porcentaje
- Valores predeterminados:
CandleType= 15 minuteStopLoss= 2%
- Filtros:
- Categoría: Seguimiento de tendencia
- Dirección: Ambos
- Indicadores: Volume, Price
- Stops: Sí
- Complejidad: Intermedio
- Marco temporal: Intradía
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
- Nivel de riesgo: Medio
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 Accumulation pattern.
/// Detects a selling climax followed by sideways accumulation and a spring,
/// then enters long on the markup phase.
/// </summary>
public class WyckoffAccumulationStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _rangePeriod;
private readonly StrategyParam<int> _sidewaysThreshold;
private readonly StrategyParam<int> _cooldownBars;
private SimpleMovingAverage _ma;
private Highest _highest;
private Lowest _lowest;
private enum WyckoffPhase
{
None,
Accumulation,
Spring,
Markup
}
private WyckoffPhase _phase;
private decimal _rangeHigh;
private decimal _rangeLow;
private int _narrowCount;
private decimal _entryPrice;
private decimal _prevMa;
private decimal _prevClose;
private int _cooldown;
/// <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 for range detection.
/// </summary>
public int RangePeriod
{
get => _rangePeriod.Value;
set => _rangePeriod.Value = value;
}
/// <summary>
/// Number of narrow-range candles to confirm accumulation.
/// </summary>
public int SidewaysThreshold
{
get => _sidewaysThreshold.Value;
set => _sidewaysThreshold.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public WyckoffAccumulationStrategy()
{
_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);
_sidewaysThreshold = Param(nameof(SidewaysThreshold), 3)
.SetDisplay("Sideways Threshold", "Narrow candles to confirm accumulation", "Logic")
.SetRange(2, 10);
_cooldownBars = Param(nameof(CooldownBars), 65)
.SetDisplay("Cooldown Bars", "Bars between trades", "General")
.SetRange(10, 500);
}
/// <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;
_phase = WyckoffPhase.None;
_rangeHigh = 0;
_rangeLow = 0;
_narrowCount = 0;
_entryPrice = 0;
_prevMa = 0;
_prevClose = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_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 = ma;
_prevClose = close;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_prevMa = ma;
_prevClose = close;
// Still update phase tracking for exit logic
if (Position > 0)
{
// Exit: price crosses below MA
if (close < ma && _prevClose >= _prevMa)
{
SellMarket();
_phase = WyckoffPhase.None;
_narrowCount = 0;
_cooldown = CooldownBars;
}
}
else if (Position < 0)
{
// Exit short: price crosses above MA
if (close > ma && _prevClose <= _prevMa)
{
BuyMarket();
_phase = WyckoffPhase.None;
_narrowCount = 0;
_cooldown = CooldownBars;
}
}
_prevMa = ma;
_prevClose = close;
return;
}
var candleRange = candle.HighPrice - candle.LowPrice;
var isNarrow = candleRange < range * 0.4m;
var isBullish = close > candle.OpenPrice;
var isBearish = close < candle.OpenPrice;
// Wyckoff accumulation detection (simplified)
switch (_phase)
{
case WyckoffPhase.None:
// Look for price near or below the range low (selling climax area)
if (close <= lowest + range * 0.2m && isBearish)
{
_phase = WyckoffPhase.Accumulation;
_rangeLow = lowest;
_rangeHigh = highest;
_narrowCount = 0;
}
// Also detect distribution (price near range high for shorts)
else if (close >= highest - range * 0.2m && isBullish)
{
_phase = WyckoffPhase.Accumulation;
_rangeLow = lowest;
_rangeHigh = highest;
_narrowCount = -1; // negative to track distribution
}
break;
case WyckoffPhase.Accumulation:
if (_narrowCount >= 0)
{
// Accumulation (long setup): count narrow-range candles
if (isNarrow)
_narrowCount++;
if (_narrowCount >= SidewaysThreshold)
{
_phase = WyckoffPhase.Spring;
}
// Reset if price breaks significantly above range
else if (close > _rangeHigh + range * 0.1m)
{
_phase = WyckoffPhase.None;
_narrowCount = 0;
}
}
else
{
// Distribution (short setup): count narrow-range candles
if (isNarrow)
_narrowCount--;
if (_narrowCount <= -SidewaysThreshold)
{
_phase = WyckoffPhase.Spring;
}
// Reset if price breaks significantly below range
else if (close < _rangeLow - range * 0.1m)
{
_phase = WyckoffPhase.None;
_narrowCount = 0;
}
}
break;
case WyckoffPhase.Spring:
if (_narrowCount > 0)
{
// Long spring: price dips below range low then closes back above
if (candle.LowPrice < _rangeLow && close > _rangeLow)
{
_phase = WyckoffPhase.Markup;
}
// Or: bullish candle near support with close above MA
else if (isBullish && close > ma && close > _rangeLow)
{
_phase = WyckoffPhase.Markup;
}
}
else
{
// Short spring (upthrust): price spikes above range high then closes back below
if (candle.HighPrice > _rangeHigh && close < _rangeHigh)
{
_phase = WyckoffPhase.Markup;
}
// Or: bearish candle near resistance with close below MA
else if (isBearish && close < ma && close < _rangeHigh)
{
_phase = WyckoffPhase.Markup;
}
}
break;
case WyckoffPhase.Markup:
if (Position == 0)
{
if (_narrowCount > 0)
{
// Enter long on markup
if (isBullish && close > ma)
{
BuyMarket();
_entryPrice = close;
_cooldown = CooldownBars;
_phase = WyckoffPhase.None;
_narrowCount = 0;
}
}
else
{
// Enter short on markdown
if (isBearish && close < ma)
{
SellMarket();
_entryPrice = close;
_cooldown = CooldownBars;
_phase = WyckoffPhase.None;
_narrowCount = 0;
}
}
}
break;
}
// Exit logic for open positions
if (Position > 0)
{
// Exit long: price crosses below MA
if (close < ma && _prevClose >= _prevMa)
{
SellMarket();
_phase = WyckoffPhase.None;
_narrowCount = 0;
_cooldown = CooldownBars;
}
}
else if (Position < 0)
{
// Exit short: price crosses above MA
if (close > ma && _prevClose <= _prevMa)
{
BuyMarket();
_phase = WyckoffPhase.None;
_narrowCount = 0;
_cooldown = CooldownBars;
}
}
_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
# Wyckoff phases
PHASE_NONE = 0
PHASE_ACCUMULATION = 1
PHASE_SPRING = 2
PHASE_MARKUP = 3
class wyckoff_accumulation_strategy(Strategy):
"""
Strategy based on Wyckoff Accumulation pattern.
Detects a selling climax followed by sideways accumulation and a spring,
then enters long on the markup phase.
"""
def __init__(self):
super(wyckoff_accumulation_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._sideways_threshold = self.Param("SidewaysThreshold", 3).SetDisplay("Sideways Threshold", "Narrow candles to confirm accumulation", "Logic")
self._cooldown_bars = self.Param("CooldownBars", 65).SetDisplay("Cooldown Bars", "Bars between trades", "General")
self._phase = PHASE_NONE
self._range_high = 0.0
self._range_low = 0.0
self._narrow_count = 0
self._entry_price = 0.0
self._prev_ma = 0.0
self._prev_close = 0.0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(wyckoff_accumulation_strategy, self).OnReseted()
self._phase = PHASE_NONE
self._range_high = 0.0
self._range_low = 0.0
self._narrow_count = 0
self._entry_price = 0.0
self._prev_ma = 0.0
self._prev_close = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(wyckoff_accumulation_strategy, self).OnStarted2(time)
self._phase = PHASE_NONE
self._range_high = 0.0
self._range_low = 0.0
self._narrow_count = 0
self._entry_price = 0.0
self._prev_ma = 0.0
self._prev_close = 0.0
self._cooldown = 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:
self._prev_ma = ma
self._prev_close = close
return
cd = self._cooldown_bars.Value
sw_thresh = self._sideways_threshold.Value
if self._cooldown > 0:
self._cooldown -= 1
self._prev_ma = ma
self._prev_close = close
# Still check exit logic during cooldown
if self.Position > 0:
if close < ma and self._prev_close >= self._prev_ma:
self.SellMarket()
self._phase = PHASE_NONE
self._narrow_count = 0
self._cooldown = cd
elif self.Position < 0:
if close > ma and self._prev_close <= self._prev_ma:
self.BuyMarket()
self._phase = PHASE_NONE
self._narrow_count = 0
self._cooldown = cd
self._prev_ma = ma
self._prev_close = close
return
candle_range = float(candle.HighPrice) - float(candle.LowPrice)
is_narrow = candle_range < rng * 0.4
is_bullish = candle.ClosePrice > candle.OpenPrice
is_bearish = candle.ClosePrice < candle.OpenPrice
# Wyckoff accumulation detection (simplified)
if self._phase == PHASE_NONE:
# Look for price near or below the range low (selling climax area)
if close <= lowest + rng * 0.2 and is_bearish:
self._phase = PHASE_ACCUMULATION
self._range_low = lowest
self._range_high = highest
self._narrow_count = 0
# Also detect distribution (price near range high for shorts)
elif close >= highest - rng * 0.2 and is_bullish:
self._phase = PHASE_ACCUMULATION
self._range_low = lowest
self._range_high = highest
self._narrow_count = -1 # negative to track distribution
elif self._phase == PHASE_ACCUMULATION:
if self._narrow_count >= 0:
# Accumulation (long setup): count narrow-range candles
if is_narrow:
self._narrow_count += 1
if self._narrow_count >= sw_thresh:
self._phase = PHASE_SPRING
elif close > self._range_high + rng * 0.1:
self._phase = PHASE_NONE
self._narrow_count = 0
else:
# Distribution (short setup): count narrow-range candles
if is_narrow:
self._narrow_count -= 1
if self._narrow_count <= -sw_thresh:
self._phase = PHASE_SPRING
elif close < self._range_low - rng * 0.1:
self._phase = PHASE_NONE
self._narrow_count = 0
elif self._phase == PHASE_SPRING:
if self._narrow_count > 0:
# Long spring: price dips below range low then closes back above
if float(candle.LowPrice) < self._range_low and close > self._range_low:
self._phase = PHASE_MARKUP
# Or: bullish candle near support with close above MA
elif is_bullish and close > ma and close > self._range_low:
self._phase = PHASE_MARKUP
else:
# Short spring (upthrust): price spikes above range high then closes back below
if float(candle.HighPrice) > self._range_high and close < self._range_high:
self._phase = PHASE_MARKUP
# Or: bearish candle near resistance with close below MA
elif is_bearish and close < ma and close < self._range_high:
self._phase = PHASE_MARKUP
elif self._phase == PHASE_MARKUP:
if self.Position == 0:
if self._narrow_count > 0:
# Enter long on markup
if is_bullish and close > ma:
self.BuyMarket()
self._entry_price = close
self._cooldown = cd
self._phase = PHASE_NONE
self._narrow_count = 0
else:
# Enter short on markdown
if is_bearish and close < ma:
self.SellMarket()
self._entry_price = close
self._cooldown = cd
self._phase = PHASE_NONE
self._narrow_count = 0
# Exit logic for open positions
if self.Position > 0:
# Exit long: price crosses below MA
if close < ma and self._prev_close >= self._prev_ma:
self.SellMarket()
self._phase = PHASE_NONE
self._narrow_count = 0
self._cooldown = cd
elif self.Position < 0:
# Exit short: price crosses above MA
if close > ma and self._prev_close <= self._prev_ma:
self.BuyMarket()
self._phase = PHASE_NONE
self._narrow_count = 0
self._cooldown = cd
self._prev_ma = ma
self._prev_close = close
def CreateClone(self):
return wyckoff_accumulation_strategy()