Cup Finder Strategy
This pattern-based strategy searches for rounded "cup" formations in price data. When price breaks out from a completed cup, it enters long or short depending on the direction.
Testing indicates an average annual return of about 47%. It works best on stocks.
The strategy buys on bullish cup breakouts and sells on bearish inverted cups. Positions are protected by a stop-loss.
Details
- Entry Criteria: Cup pattern forms and price breaks the rim.
- Long/Short: Both.
- Exit Criteria: Price reverses or hits stop-loss.
- Stops: Yes.
- Default Values:
Lookback= 150WidthPercent= 5mStopLossPercent= 1mCandleType= TimeSpan.FromMinutes(15)
- Filters:
- Category: Pattern
- Direction: Long/Short
- Indicators: Price Action
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- Seasonality: No
- Neural Networks: No
- Divergence: Yes
- Risk Level: Medium
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Cup Finder strategy.
/// The previous Lookback candles are split into thirds. A cup has rims (highest highs of the outer thirds) within WidthPercent
/// of each other and a middle third that stays below the lower rim; a close above the higher rim buys. An inverted cup has
/// troughs (lowest lows of the outer thirds) within WidthPercent of each other and a middle third that stays above the higher
/// trough; a close below the lower trough sells short. An opposite breakout reverses the position and a percent stop limits the loss.
/// </summary>
public class CupFinderStrategy : Strategy
{
private readonly StrategyParam<int> _lookback;
private readonly StrategyParam<decimal> _widthPercent;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<(decimal high, decimal low)> _bars = new();
/// <summary>
/// Candles the cup is searched in.
/// </summary>
public int Lookback
{
get => _lookback.Value;
set => _lookback.Value = value;
}
/// <summary>
/// Maximum difference between the two rims in percent.
/// </summary>
public decimal WidthPercent
{
get => _widthPercent.Value;
set => _widthPercent.Value = value;
}
/// <summary>
/// Stop loss percentage from entry price.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public CupFinderStrategy()
{
_lookback = Param(nameof(Lookback), 150)
.SetRange(3, 10000)
.SetDisplay("Lookback", "Candles the cup is searched in", "Pattern");
_widthPercent = Param(nameof(WidthPercent), 5m)
.SetNotNegative()
.SetDisplay("Width %", "Maximum difference between the two rims in percent", "Pattern");
_stopLossPercent = Param(nameof(StopLossPercent), 1m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_bars.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bars.Clear();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
StartProtection(new Unit(), StopLossPercent > 0 ? new Unit(StopLossPercent, UnitTypes.Percent) : new Unit(), useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var ready = _bars.Count >= Lookback;
var bullish = false;
var bearish = false;
if (ready)
{
var bars = _bars.ToArray();
var third = bars.Length / 3;
var left = bars.Take(third).ToArray();
var middle = bars.Skip(third).Take(bars.Length - 2 * third).ToArray();
var right = bars.Skip(bars.Length - third).ToArray();
var leftRim = left.Max(b => b.high);
var rightRim = right.Max(b => b.high);
var middleHigh = middle.Max(b => b.high);
var rim = Math.Max(leftRim, rightRim);
var isCup = rim > 0 && (rim - Math.Min(leftRim, rightRim)) / rim * 100m <= WidthPercent && middleHigh < Math.Min(leftRim, rightRim);
bullish = isCup && candle.ClosePrice > rim;
var leftTrough = left.Min(b => b.low);
var rightTrough = right.Min(b => b.low);
var middleLow = middle.Min(b => b.low);
var trough = Math.Min(leftTrough, rightTrough);
var higherTrough = Math.Max(leftTrough, rightTrough);
var isInverted = higherTrough > 0 && (higherTrough - trough) / higherTrough * 100m <= WidthPercent && middleLow > higherTrough;
bearish = isInverted && candle.ClosePrice < trough;
}
_bars.Enqueue((candle.HighPrice, candle.LowPrice));
while (_bars.Count > Lookback)
_bars.Dequeue();
if (!ready || !IsFormedAndOnlineAndAllowTrading())
return;
if (bullish && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (bearish && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from collections import deque
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class cup_finder_strategy(Strategy):
"""
Cup Finder strategy.
The previous Lookback candles are split into thirds. A cup has rims (highest highs of the outer thirds) within WidthPercent
of each other and a middle third that stays below the lower rim; a close above the higher rim buys. An inverted cup has
troughs (lowest lows of the outer thirds) within WidthPercent of each other and a middle third that stays above the higher
trough; a close below the lower trough sells short. An opposite breakout reverses the position and a percent stop limits the loss.
"""
def __init__(self):
super(cup_finder_strategy, self).__init__()
self._lookback = self.Param("Lookback", 150).SetRange(3, 10000).SetDisplay("Lookback", "Candles the cup is searched in", "Pattern")
self._width_percent = self.Param("WidthPercent", 5.0).SetNotNegative().SetDisplay("Width %", "Maximum difference between the two rims in percent", "Pattern")
self._stop_loss_percent = self.Param("StopLossPercent", 1.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._bars = deque()
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(cup_finder_strategy, self).OnReseted()
self._bars = deque()
def OnStarted2(self, time):
super(cup_finder_strategy, self).OnStarted2(time)
self._bars = deque()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
stop = float(self._stop_loss_percent.Value)
self.StartProtection(Unit(), Unit(Decimal(stop), UnitTypes.Percent) if stop > 0 else Unit(), useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
lookback = self._lookback.Value
width = float(self._width_percent.Value)
close = float(candle.ClosePrice)
ready = len(self._bars) >= lookback
bullish = False
bearish = False
if ready:
bars = list(self._bars)
third = len(bars) // 3
left = bars[:third]
middle = bars[third:len(bars) - third]
right = bars[len(bars) - third:]
left_rim = max(b[0] for b in left)
right_rim = max(b[0] for b in right)
middle_high = max(b[0] for b in middle)
rim = max(left_rim, right_rim)
is_cup = rim > 0 and (rim - min(left_rim, right_rim)) / rim * 100.0 <= width and middle_high < min(left_rim, right_rim)
bullish = is_cup and close > rim
left_trough = min(b[1] for b in left)
right_trough = min(b[1] for b in right)
middle_low = min(b[1] for b in middle)
trough = min(left_trough, right_trough)
higher_trough = max(left_trough, right_trough)
is_inverted = higher_trough > 0 and (higher_trough - trough) / higher_trough * 100.0 <= width and middle_low > higher_trough
bearish = is_inverted and close < trough
self._bars.append((float(candle.HighPrice), float(candle.LowPrice)))
while len(self._bars) > lookback:
self._bars.popleft()
if not ready or not self.IsFormedAndOnlineAndAllowTrading():
return
if bullish and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif bearish and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
def CreateClone(self):
return cup_finder_strategy()