Patrón de Doble Suelo (Double Bottom Pattern)
Esta estrategia basada en patrones busca dos mínimos consecutivos aproximadamente al mismo precio, separados por una distancia establecida. Después de formarse el segundo suelo, una vela alcista confirma la reversión.
Las pruebas indican un rendimiento anual promedio de aproximadamente el 55%. Funciona mejor en el mercado de acciones.
Cuando se produce la confirmación, el sistema compra con un stop por debajo de los mínimos del patrón. La configuración tiene como objetivo capturar rebotes pronunciados tras un agotamiento de la venta.
Las salidas dependen de un stop-loss predefinido o de objetivos de beneficio manuales.
Detalles
- Criterios de entrada: Dos suelos se forman dentro de
SimilarityPercentdespués deDistancebarras. - Largo/Corto: Solo largos.
- Criterios de salida: El precio falla o stop-loss.
- Stops: Sí.
- Valores predeterminados:
Distance= 5SimilarityPercent= 2.0mCandleType= TimeSpan.FromMinutes(15)StopLossPercent= 1.0m
- Filtros:
- Categoría: Patrón
- Dirección: Solo largos
- Indicadores: Price Action
- Stops: Sí
- Complejidad: Intermedio
- Marco temporal: Intradía
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: Sí
- Nivel de riesgo: Medio
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Long-only double bottom: two confirmed pivot lows followed by a bullish candle.
/// A pattern-low stop is checked against executable best bids and finished-bar lows.
/// </summary>
public class DoubleBottomStrategy : Strategy
{
private readonly StrategyParam<int> _distance;
private readonly StrategyParam<decimal> _similarityPercent;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private ICandleMessage _twoBack;
private ICandleMessage _previous;
private (int Bar, decimal Low)? _lastPivot;
private decimal? _candidateLow;
private int _candidateExpires;
private decimal? _patternStop;
private int _bar;
private Order _entryOrder;
private Order _exitOrder;
public int Distance { get => _distance.Value; set => _distance.Value = value; }
public decimal SimilarityPercent { get => _similarityPercent.Value; set => _similarityPercent.Value = value; }
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public DoubleBottomStrategy()
{
_distance = Param(nameof(Distance), 5).SetRange(3, 100)
.SetDisplay("Distance", "Minimum bars between confirmed pivot lows", "Pattern");
_similarityPercent = Param(nameof(SimilarityPercent), 2m).SetRange(0.1m, 5m)
.SetDisplay("Similarity %", "Maximum relative difference between pivot lows", "Pattern");
_stopLossPercent = Param(nameof(StopLossPercent), 1m).SetRange(0m, 99m)
.SetDisplay("Stop below lows (%)", "Percentage buffer below the lower pattern low; zero places it at the low.", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for pivot lows and bullish confirmation", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_twoBack = _previous = null;
_lastPivot = null;
_candidateLow = null;
_candidateExpires = 0;
_patternStop = null;
_bar = 0;
_entryOrder = _exitOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
var bids = new Subscription(DataType.Level1, Security);
bids.MarketData.BuildField = Level1Fields.BestBidPrice;
SubscribeLevel1(bids).Bind(ProcessBid).Start();
var candles = SubscribeCandles(CandleType);
candles.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candles);
DrawOwnTrades(area);
}
}
private static bool IsPending(Order order)
=> order is not null && order.State is not (OrderStates.Done or OrderStates.Failed);
private void ProcessBid(Level1ChangeMessage message)
{
if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid && bid > 0m)
CheckStop(bid);
}
private void CheckStop(decimal executableBid)
{
if (Position > 0m && _patternStop is decimal stop && executableBid <= stop && !IsPending(_exitOrder))
{
_exitOrder = SellMarket(Position);
// An exited pattern cannot be reused to open another position.
_twoBack = _previous = null;
_lastPivot = null;
_candidateLow = null;
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_bar++;
var older = _twoBack;
var middle = _previous;
_twoBack = middle;
_previous = candle;
if (Position > 0m)
{
// Covers missing/stale quotes or a gap; the next market order may fill beyond the stop.
CheckStop(candle.LowPrice);
return;
}
if (IsPending(_entryOrder) || IsPending(_exitOrder))
return;
if (older is not null && middle is not null &&
middle.LowPrice < older.LowPrice && middle.LowPrice <= candle.LowPrice)
{
var pivot = (Bar: _bar - 1, Low: middle.LowPrice);
if (_lastPivot is { } first && pivot.Bar - first.Bar >= Distance &&
Math.Abs(pivot.Low - first.Low) * 100m <= first.Low * SimilarityPercent)
{
_candidateLow = Math.Min(first.Low, pivot.Low);
_candidateExpires = _bar + Distance;
}
_lastPivot = pivot;
}
if (_candidateLow is not decimal low)
return;
if (_bar > _candidateExpires || candle.LowPrice < low)
{
_candidateLow = null;
return;
}
if (candle.ClosePrice <= candle.OpenPrice || !IsFormedAndOnlineAndAllowTrading())
return;
_patternStop = low * (1m - StopLossPercent / 100m);
_entryOrder = BuyMarket(Volume);
_candidateLow = null;
_lastPivot = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Level1Fields, OrderStates
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Strategies import Strategy
class double_bottom_strategy(Strategy):
"""
Long-only double bottom: two confirmed pivot lows followed by a bullish candle.
A pattern-low stop watches executable best bids and finished-bar lows.
"""
def __init__(self):
super(double_bottom_strategy, self).__init__()
self._distance = self.Param("Distance", 5).SetRange(3, 100).SetDisplay("Distance", "Minimum bars between confirmed pivot lows", "Pattern")
self._similarity_percent = self.Param("SimilarityPercent", 2.0).SetRange(0.1, 5.0).SetDisplay("Similarity %", "Maximum relative difference between pivot lows", "Pattern")
self._stop_loss_percent = self.Param("StopLossPercent", 1.0).SetRange(0.0, 99.0).SetDisplay("Stop below lows (%)", "Percentage buffer below the lower pattern low; zero places it at the low.", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Timeframe for pivot lows and bullish confirmation", "General")
self._clear_state()
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _clear_state(self):
self._two_back = None
self._previous = None
self._last_pivot = None
self._candidate_low = None
self._candidate_expires = 0
self._pattern_stop = None
self._bar = 0
self._entry_order = None
self._exit_order = None
def OnReseted(self):
super(double_bottom_strategy, self).OnReseted()
self._clear_state()
def OnStarted2(self, time):
super(double_bottom_strategy, self).OnStarted2(time)
self._clear_state()
bids = Subscription(DataType.Level1, self.Security)
bids.MarketData.BuildField = Level1Fields.BestBidPrice
self.SubscribeLevel1(bids).Bind(self._process_bid).Start()
candles = self.SubscribeCandles(self.candle_type)
candles.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawOwnTrades(area)
def _is_pending(self, order):
return order is not None and order.State not in (OrderStates.Done, OrderStates.Failed)
def _process_bid(self, message):
if message.Changes.ContainsKey(Level1Fields.BestBidPrice):
bid = message.Changes[Level1Fields.BestBidPrice]
if bid is not None and bid > Decimal(0):
self._check_stop(bid)
def _check_stop(self, executable_bid):
if self.Position > 0 and self._pattern_stop is not None and executable_bid <= self._pattern_stop and not self._is_pending(self._exit_order):
self._exit_order = self.SellMarket(self.Position)
# Never reuse an exited pattern for another entry.
self._two_back = None
self._previous = None
self._last_pivot = None
self._candidate_low = None
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._bar += 1
older = self._two_back
middle = self._previous
self._two_back = middle
self._previous = candle
if self.Position > 0:
# A bar-low fallback handles absent/stale quotes or gaps; fills can slip.
self._check_stop(candle.LowPrice)
return
if self._is_pending(self._entry_order) or self._is_pending(self._exit_order):
return
if older is not None and middle is not None and middle.LowPrice < older.LowPrice and middle.LowPrice <= candle.LowPrice:
pivot = (self._bar - 1, middle.LowPrice)
first = self._last_pivot
if first is not None and pivot[0] - first[0] >= self._distance.Value and Math.Abs(pivot[1] - first[1]) * Decimal(100) <= first[1] * Decimal(self._similarity_percent.Value):
self._candidate_low = min(first[1], pivot[1])
self._candidate_expires = self._bar + self._distance.Value
self._last_pivot = pivot
low = self._candidate_low
if low is None:
return
if self._bar > self._candidate_expires or candle.LowPrice < low:
self._candidate_low = None
return
if candle.ClosePrice <= candle.OpenPrice or not self.IsFormedAndOnlineAndAllowTrading():
return
self._pattern_stop = low * (Decimal(1) - Decimal(self._stop_loss_percent.Value) / Decimal(100))
self._entry_order = self.BuyMarket(self.Volume)
self._candidate_low = None
self._last_pivot = None
def CreateClone(self):
return double_bottom_strategy()