Фигура "Двойное дно"
Эта стратегия ищет два подряд идущих минимума примерно на одной цене, разделённые заданным количеством баров. После формирования второго дна бычья свеча подтверждает разворот.
Тестирование показывает среднегодичную доходность около 55%. Стратегию лучше запускать на фондовом рынке.
При подтверждении система покупает со стопом ниже минимумов фигуры. Такой подход нацелен на резкий отскок после исчерпания продаж.
Выход осуществляется по заранее заданному стоп-лоссу или ручным целям прибыли.
Детали
- Условия входа: Два дна формируются в пределах
SimilarityPercentпослеDistanceбаров. - Длинные/короткие позиции: Только длинные.
- Условия выхода: Цена не развивает рост или стоп-лосс.
- Стопы: Да.
- Значения по умолчанию:
Distance= 5SimilarityPercent= 2.0mCandleType= TimeSpan.FromMinutes(15)StopLossPercent= 1.0m
- Фильтры:
- Категория: Pattern
- Направление: Long
- Индикаторы: Price Action
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Внутридневной
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Да
- Уровень риска: Средний
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>
/// Double Bottom reversal strategy.
/// Detects two similar bottoms and enters long on confirmation.
/// Uses SMA for exit signal.
/// </summary>
public class DoubleBottomStrategy : Strategy
{
private readonly StrategyParam<int> _distanceParam;
private readonly StrategyParam<decimal> _similarityPercent;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _cooldownBars;
private decimal _recentLow;
private decimal _prevLow;
private int _barsSinceLow;
private int _cooldown;
/// <summary>
/// Distance between bottoms in bars.
/// </summary>
public int Distance
{
get => _distanceParam.Value;
set => _distanceParam.Value = value;
}
/// <summary>
/// Maximum percent difference between two bottoms.
/// </summary>
public decimal SimilarityPercent
{
get => _similarityPercent.Value;
set => _similarityPercent.Value = value;
}
/// <summary>
/// MA Period for exit.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Type of candles to use.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="DoubleBottomStrategy"/>.
/// </summary>
public DoubleBottomStrategy()
{
_distanceParam = Param(nameof(Distance), 20)
.SetRange(3, 100)
.SetDisplay("Distance", "Bars between bottoms", "Pattern");
_similarityPercent = Param(nameof(SimilarityPercent), 1.0m)
.SetRange(0.1m, 5.0m)
.SetDisplay("Similarity %", "Max % diff between bottoms", "Pattern");
_maPeriod = Param(nameof(MAPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Period for exit SMA", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_cooldownBars = Param(nameof(CooldownBars), 500)
.SetRange(1, 1000)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_recentLow = default;
_prevLow = default;
_barsSinceLow = default;
_cooldown = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_recentLow = 0;
_prevLow = 0;
_barsSinceLow = 0;
_cooldown = 0;
var sma = new SimpleMovingAverage { Length = MAPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldown > 0)
{
_cooldown--;
TrackLows(candle);
return;
}
// Track new lows
if (_recentLow == 0 || candle.LowPrice < _recentLow)
{
if (_recentLow > 0)
_prevLow = _recentLow;
_recentLow = candle.LowPrice;
_barsSinceLow = 0;
}
else
{
_barsSinceLow++;
}
if (Position == 0 && _prevLow > 0 && _barsSinceLow >= Distance)
{
var priceDiff = Math.Abs((_recentLow - _prevLow) / _prevLow * 100);
if (priceDiff <= SimilarityPercent && candle.ClosePrice > smaValue)
{
BuyMarket();
_cooldown = CooldownBars;
_recentLow = 0;
_prevLow = 0;
}
else if (priceDiff <= SimilarityPercent && candle.ClosePrice < smaValue)
{
SellMarket();
_cooldown = CooldownBars;
_recentLow = 0;
_prevLow = 0;
}
}
else if (Position > 0 && candle.ClosePrice < smaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
else if (Position < 0 && candle.ClosePrice > smaValue)
{
BuyMarket();
_cooldown = CooldownBars;
}
}
private void TrackLows(ICandleMessage candle)
{
if (_recentLow == 0 || candle.LowPrice < _recentLow)
{
if (_recentLow > 0)
_prevLow = _recentLow;
_recentLow = candle.LowPrice;
_barsSinceLow = 0;
}
else
{
_barsSinceLow++;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class double_bottom_strategy(Strategy):
"""
Double Bottom reversal strategy.
Detects two similar bottoms and enters long on confirmation.
Uses SMA for exit signal.
"""
def __init__(self):
super(double_bottom_strategy, self).__init__()
self._distance = self.Param("Distance", 20).SetDisplay("Distance", "Bars between bottoms", "Pattern")
self._similarity_pct = self.Param("SimilarityPercent", 1.0).SetDisplay("Similarity %", "Max % diff between bottoms", "Pattern")
self._ma_period = self.Param("MAPeriod", 20).SetDisplay("MA Period", "Period for exit SMA", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._cooldown_bars = self.Param("CooldownBars", 500).SetDisplay("Cooldown Bars", "Bars to wait between trades", "General")
self._recent_low = 0.0
self._prev_low = 0.0
self._bars_since_low = 0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(double_bottom_strategy, self).OnReseted()
self._recent_low = 0.0
self._prev_low = 0.0
self._bars_since_low = 0
self._cooldown = 0
def OnStarted2(self, time):
super(double_bottom_strategy, self).OnStarted2(time)
self._recent_low = 0.0
self._prev_low = 0.0
self._bars_since_low = 0
self._cooldown = 0
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _track_lows(self, candle):
low = float(candle.LowPrice)
if self._recent_low == 0 or low < self._recent_low:
if self._recent_low > 0:
self._prev_low = self._recent_low
self._recent_low = low
self._bars_since_low = 0
else:
self._bars_since_low += 1
def _process_candle(self, candle, sma_val):
if candle.State != CandleStates.Finished:
return
if self._cooldown > 0:
self._cooldown -= 1
self._track_lows(candle)
return
# Track new lows
low = float(candle.LowPrice)
if self._recent_low == 0 or low < self._recent_low:
if self._recent_low > 0:
self._prev_low = self._recent_low
self._recent_low = low
self._bars_since_low = 0
else:
self._bars_since_low += 1
close = float(candle.ClosePrice)
sv = float(sma_val)
cd = self._cooldown_bars.Value
dist = self._distance.Value
sim = float(self._similarity_pct.Value)
if self.Position == 0 and self._prev_low > 0 and self._bars_since_low >= dist:
price_diff = abs((self._recent_low - self._prev_low) / self._prev_low * 100.0)
if price_diff <= sim and close > sv:
self.BuyMarket()
self._cooldown = cd
self._recent_low = 0.0
self._prev_low = 0.0
elif price_diff <= sim and close < sv:
self.SellMarket()
self._cooldown = cd
self._recent_low = 0.0
self._prev_low = 0.0
elif self.Position > 0 and close < sv:
self.SellMarket()
self._cooldown = cd
elif self.Position < 0 and close > sv:
self.BuyMarket()
self._cooldown = cd
def CreateClone(self):
return double_bottom_strategy()