双底形态 (Double Bottom Pattern)
侦测两次相近低点形成的反转形态。
测试表明年均收益约为 55%,该策略在股票市场表现最佳。
第二个低点后出现看涨蜡烛确认后买入。
详情
- 入场条件: Two bottoms form within
SimilarityPercentafterDistancebars. - 多空方向: Long only.
- 出场条件: Price fails or stop-loss.
- 止损: Yes.
- 默认值:
Distance= 5SimilarityPercent= 2.0mCandleType= TimeSpan.FromMinutes(15)StopLossPercent= 1.0m
- 过滤器:
- 类别: Pattern
- 方向: Long
- 指标: Price Action
- 止损: Yes
- 复杂度: Intermediate
- 时间框架: Intraday
- 季节性: No
- 神经网络: No
- 背离: Yes
- 风险等级: Medium
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()