using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Candle shadow percent strategy converted from MetaTrader.
/// Trades when a candle shows an extended wick compared to its body.
/// Position size is derived from risk percentage and stop distance.
/// </summary>
public class CandleShadowPercentStrategy : Strategy
{
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _minBodyPips;
private readonly StrategyParam<bool> _enableTopShadow;
private readonly StrategyParam<decimal> _topShadowPercent;
private readonly StrategyParam<bool> _topShadowIsMinimum;
private readonly StrategyParam<bool> _enableLowerShadow;
private readonly StrategyParam<decimal> _lowerShadowPercent;
private readonly StrategyParam<bool> _lowerShadowIsMinimum;
private readonly StrategyParam<DataType> _candleType;
private decimal? _longStop;
private decimal? _longTake;
private decimal? _shortStop;
private decimal? _shortTake;
private decimal? _entryPrice;
/// <summary>
/// Stop loss in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Risk percentage per trade.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Minimum body size in pips to evaluate shadows.
/// </summary>
public int MinBodyPips
{
get => _minBodyPips.Value;
set => _minBodyPips.Value = value;
}
/// <summary>
/// Enables signals based on the top shadow.
/// </summary>
public bool EnableTopShadow
{
get => _enableTopShadow.Value;
set => _enableTopShadow.Value = value;
}
/// <summary>
/// Threshold for the top shadow as a percentage of the body.
/// </summary>
public decimal TopShadowPercent
{
get => _topShadowPercent.Value;
set => _topShadowPercent.Value = value;
}
/// <summary>
/// If true the top shadow percentage acts as a minimum threshold.
/// </summary>
public bool TopShadowIsMinimum
{
get => _topShadowIsMinimum.Value;
set => _topShadowIsMinimum.Value = value;
}
/// <summary>
/// Enables signals based on the lower shadow.
/// </summary>
public bool EnableLowerShadow
{
get => _enableLowerShadow.Value;
set => _enableLowerShadow.Value = value;
}
/// <summary>
/// Threshold for the lower shadow as a percentage of the body.
/// </summary>
public decimal LowerShadowPercent
{
get => _lowerShadowPercent.Value;
set => _lowerShadowPercent.Value = value;
}
/// <summary>
/// If true the lower shadow percentage acts as a minimum threshold.
/// </summary>
public bool LowerShadowIsMinimum
{
get => _lowerShadowIsMinimum.Value;
set => _lowerShadowIsMinimum.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="CandleShadowPercentStrategy"/>.
/// </summary>
public CandleShadowPercentStrategy()
{
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetGreaterThanZero()
.SetDisplay("Stop Loss", "Stop loss distance in pips", "Risk")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetNotNegative()
.SetDisplay("Take Profit", "Take profit distance in pips", "Risk")
;
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Risk percentage per trade", "Risk")
;
_minBodyPips = Param(nameof(MinBodyPips), 300)
.SetGreaterThanZero()
.SetDisplay("Minimum Body", "Minimum candle body size in pips", "Pattern")
;
_enableTopShadow = Param(nameof(EnableTopShadow), true)
.SetDisplay("Use Top Shadow", "Enable sell signals from upper wicks", "Pattern");
_topShadowPercent = Param(nameof(TopShadowPercent), 30m)
.SetNotNegative()
.SetDisplay("Top Shadow %", "Upper wick percentage threshold", "Pattern")
;
_topShadowIsMinimum = Param(nameof(TopShadowIsMinimum), true)
.SetDisplay("Top Shadow Uses Min", "If true the threshold is treated as a minimum", "Pattern");
_enableLowerShadow = Param(nameof(EnableLowerShadow), true)
.SetDisplay("Use Lower Shadow", "Enable buy signals from lower wicks", "Pattern");
_lowerShadowPercent = Param(nameof(LowerShadowPercent), 80m)
.SetNotNegative()
.SetDisplay("Lower Shadow %", "Lower wick percentage threshold", "Pattern")
;
_lowerShadowIsMinimum = Param(nameof(LowerShadowIsMinimum), true)
.SetDisplay("Lower Shadow Uses Min", "If true the threshold is treated as a minimum", "Pattern");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for pattern detection", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longStop = null;
_longTake = null;
_shortStop = null;
_shortTake = null;
_entryPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ManageOpenPosition(candle);
var pipSize = GetPipSize();
var minBody = MinBodyPips * pipSize;
var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
if (body < minBody || body <= 0m)
return;
var upperShadow = candle.HighPrice - Math.Max(candle.OpenPrice, candle.ClosePrice);
var lowerShadow = Math.Min(candle.OpenPrice, candle.ClosePrice) - candle.LowPrice;
var topRatio = body > 0m ? upperShadow / body * 100m : 0m;
var lowerRatio = body > 0m ? lowerShadow / body * 100m : 0m;
var topSignal = EnableTopShadow && upperShadow > 0m && CheckThreshold(topRatio, TopShadowPercent, TopShadowIsMinimum);
var lowerSignal = EnableLowerShadow && lowerShadow > 0m && CheckThreshold(lowerRatio, LowerShadowPercent, LowerShadowIsMinimum);
if (topSignal && lowerSignal)
{
if (topRatio > lowerRatio)
lowerSignal = false;
else
topSignal = false;
}
if (topSignal && Position <= 0)
{
EnterShort(candle, pipSize);
}
else if (lowerSignal && Position >= 0)
{
EnterLong(candle, pipSize);
}
}
private void ManageOpenPosition(ICandleMessage candle)
{
if (Position > 0)
{
var stopHit = _longStop.HasValue && candle.LowPrice <= _longStop.Value;
var takeHit = _longTake.HasValue && candle.HighPrice >= _longTake.Value;
if (stopHit || takeHit)
{
SellMarket();
this.LogInfo($"Closing long at {candle.ClosePrice}. Stop hit: {stopHit}, Take hit: {takeHit}");
_longStop = null;
_longTake = null;
_entryPrice = null;
}
}
else if (Position < 0)
{
var stopHit = _shortStop.HasValue && candle.HighPrice >= _shortStop.Value;
var takeHit = _shortTake.HasValue && candle.LowPrice <= _shortTake.Value;
if (stopHit || takeHit)
{
BuyMarket();
this.LogInfo($"Closing short at {candle.ClosePrice}. Stop hit: {stopHit}, Take hit: {takeHit}");
_shortStop = null;
_shortTake = null;
_entryPrice = null;
}
}
}
private void EnterLong(ICandleMessage candle, decimal pipSize)
{
var stopDistance = StopLossPips * pipSize;
if (stopDistance <= 0m)
return;
var takeDistance = TakeProfitPips * pipSize;
var entryPrice = candle.ClosePrice;
var stopPrice = entryPrice - stopDistance;
var takePrice = takeDistance > 0m ? entryPrice + takeDistance : (decimal?)null;
BuyMarket();
_longStop = stopPrice;
_longTake = takePrice;
_shortStop = null;
_shortTake = null;
_entryPrice = entryPrice;
this.LogInfo($"Entered long at {entryPrice}. Stop {stopPrice}, Take {(takePrice.HasValue ? takePrice.Value.ToString() : "n/a")}");
}
private void EnterShort(ICandleMessage candle, decimal pipSize)
{
var stopDistance = StopLossPips * pipSize;
if (stopDistance <= 0m)
return;
var takeDistance = TakeProfitPips * pipSize;
var entryPrice = candle.ClosePrice;
var stopPrice = entryPrice + stopDistance;
var takePrice = takeDistance > 0m ? entryPrice - takeDistance : (decimal?)null;
SellMarket();
_shortStop = stopPrice;
_shortTake = takePrice;
_longStop = null;
_longTake = null;
_entryPrice = entryPrice;
this.LogInfo($"Entered short at {entryPrice}. Stop {stopPrice}, Take {(takePrice.HasValue ? takePrice.Value.ToString() : "n/a")}");
}
private decimal CalculatePositionSize(decimal stopDistance)
{
var defaultVolume = Volume > 0m ? Volume : 1m;
var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (portfolioValue <= 0m)
return defaultVolume;
var riskAmount = portfolioValue * (RiskPercent / 100m);
if (riskAmount <= 0m || stopDistance <= 0m)
return defaultVolume;
var size = riskAmount / stopDistance;
return size > 0m ? size : defaultVolume;
}
private static bool CheckThreshold(decimal ratio, decimal threshold, bool isMinimum)
{
return isMinimum ? ratio >= threshold : ratio <= threshold;
}
private decimal GetPipSize()
{
return Security?.PriceStep ?? 1m;
}
}
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.Strategies import Strategy
class candle_shadow_percent_strategy(Strategy):
"""
Candle shadow percent strategy.
Trades when a candle shows an extended wick compared to its body.
"""
def __init__(self):
super(candle_shadow_percent_strategy, self).__init__()
self._stop_loss_pips = self.Param("StopLossPips", 50) \
.SetDisplay("Stop Loss", "Stop loss distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 50) \
.SetDisplay("Take Profit", "Take profit distance in pips", "Risk")
self._risk_percent = self.Param("RiskPercent", 5.0) \
.SetDisplay("Risk %", "Risk percentage per trade", "Risk")
self._min_body_pips = self.Param("MinBodyPips", 300) \
.SetDisplay("Minimum Body", "Minimum candle body size in pips", "Pattern")
self._enable_top_shadow = self.Param("EnableTopShadow", True) \
.SetDisplay("Use Top Shadow", "Enable sell signals from upper wicks", "Pattern")
self._top_shadow_percent = self.Param("TopShadowPercent", 30.0) \
.SetDisplay("Top Shadow %", "Upper wick percentage threshold", "Pattern")
self._top_shadow_is_minimum = self.Param("TopShadowIsMinimum", True) \
.SetDisplay("Top Shadow Uses Min", "If true threshold is treated as minimum", "Pattern")
self._enable_lower_shadow = self.Param("EnableLowerShadow", True) \
.SetDisplay("Use Lower Shadow", "Enable buy signals from lower wicks", "Pattern")
self._lower_shadow_percent = self.Param("LowerShadowPercent", 80.0) \
.SetDisplay("Lower Shadow %", "Lower wick percentage threshold", "Pattern")
self._lower_shadow_is_minimum = self.Param("LowerShadowIsMinimum", True) \
.SetDisplay("Lower Shadow Uses Min", "If true threshold is treated as minimum", "Pattern")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe used for pattern detection", "Data")
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
self._entry_price = None
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(candle_shadow_percent_strategy, self).OnReseted()
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
self._entry_price = None
def OnStarted2(self, time):
super(candle_shadow_percent_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _get_pip_size(self):
return self.Security.PriceStep if self.Security.PriceStep is not None else 1.0
def _check_threshold(self, ratio, threshold, is_minimum):
return ratio >= threshold if is_minimum else ratio <= threshold
def on_process(self, candle):
if candle.State != CandleStates.Finished:
return
# Manage open position
self._manage_open_position(candle)
pip_size = self._get_pip_size()
min_body = self._min_body_pips.Value * pip_size
body = abs(candle.ClosePrice - candle.OpenPrice)
if body < min_body or body <= 0:
return
upper_shadow = candle.HighPrice - max(candle.OpenPrice, candle.ClosePrice)
lower_shadow = min(candle.OpenPrice, candle.ClosePrice) - candle.LowPrice
top_ratio = upper_shadow / body * 100.0 if body > 0 else 0.0
lower_ratio = lower_shadow / body * 100.0 if body > 0 else 0.0
top_signal = (self._enable_top_shadow.Value and upper_shadow > 0
and self._check_threshold(top_ratio, self._top_shadow_percent.Value, self._top_shadow_is_minimum.Value))
lower_signal = (self._enable_lower_shadow.Value and lower_shadow > 0
and self._check_threshold(lower_ratio, self._lower_shadow_percent.Value, self._lower_shadow_is_minimum.Value))
if top_signal and lower_signal:
if top_ratio > lower_ratio:
lower_signal = False
else:
top_signal = False
if top_signal and self.Position <= 0:
self._enter_short(candle, pip_size)
elif lower_signal and self.Position >= 0:
self._enter_long(candle, pip_size)
def _manage_open_position(self, candle):
if self.Position > 0:
stop_hit = self._long_stop is not None and candle.LowPrice <= self._long_stop
take_hit = self._long_take is not None and candle.HighPrice >= self._long_take
if stop_hit or take_hit:
self.SellMarket()
self._long_stop = None
self._long_take = None
self._entry_price = None
elif self.Position < 0:
stop_hit = self._short_stop is not None and candle.HighPrice >= self._short_stop
take_hit = self._short_take is not None and candle.LowPrice <= self._short_take
if stop_hit or take_hit:
self.BuyMarket()
self._short_stop = None
self._short_take = None
self._entry_price = None
def _enter_long(self, candle, pip_size):
stop_distance = self._stop_loss_pips.Value * pip_size
if stop_distance <= 0:
return
take_distance = self._take_profit_pips.Value * pip_size
entry_price = float(candle.ClosePrice)
stop_price = entry_price - stop_distance
take_price = entry_price + take_distance if take_distance > 0 else None
self.BuyMarket()
self._long_stop = stop_price
self._long_take = take_price
self._short_stop = None
self._short_take = None
self._entry_price = entry_price
def _enter_short(self, candle, pip_size):
stop_distance = self._stop_loss_pips.Value * pip_size
if stop_distance <= 0:
return
take_distance = self._take_profit_pips.Value * pip_size
entry_price = float(candle.ClosePrice)
stop_price = entry_price + stop_distance
take_price = entry_price - take_distance if take_distance > 0 else None
self.SellMarket()
self._short_stop = stop_price
self._short_take = take_price
self._long_stop = None
self._long_take = None
self._entry_price = entry_price
def CreateClone(self):
return candle_shadow_percent_strategy()