烛影比例策略
概述
烛影比例策略 是 MetaTrader 专家顾问 Candle shadow percent 的移植版本。策略会寻找上下影线达到可调比例的 K 线:出现长上影线时开空,出现长下影线时开多。交易方向与原始算法一致,同时保留了风险管理流程。
转换说明
- 原策略依赖自定义指标。StockSharp 版本直接根据收盘完成的 K 线计算影线与实体比例,无需外部指标。
- 点值通过
Security.PriceStep计算。请根据交易品种调整StopLossPips、TakeProfitPips与MinBodyPips。 - 资金管理按照 MetaTrader 中
CMoneyFixedMargin的思想实现:使用账户当前权益的一定百分比除以止损距离得到下单数量。
K 线筛选条件
满足以下条件的 K 线才会触发信号:
- 绝对实体长度不少于
MinBodyPips * Security.PriceStep。 - 对应影线长度为正值。
- 影线与实体的比例满足阈值逻辑:
- 上影线(做空):当
TopShadowIsMinimum = true时要求(High − max(Open, Close)) / Body * 100 ≥ TopShadowPercent;反之要求该比例小于或等于阈值。 - 下影线(做多):当
LowerShadowIsMinimum = true时要求(min(Open, Close) − Low) / Body * 100 ≥ LowerShadowPercent;反之要求该比例小于或等于阈值。
- 上影线(做空):当
- 如果同一根 K 线同时满足多空条件,策略仅保留比例更大的方向,避免重复下单。
入场规则
- 空头:当出现有效的上影线信号且当前为空仓或持有多单时执行。若持有多单会自动反手,并立即设置止损止盈。
- 多头:当出现有效的下影线信号且当前为空仓或持有空单时执行。若持有空单会先平仓再开多。
出场规则
- 止损:距离入场价
StopLossPips * Security.PriceStep。多单止损位于entry − stopDistance,空单止损位于entry + stopDistance。 - 止盈:距离入场价
TakeProfitPips * Security.PriceStep。当TakeProfitPips = 0时停用止盈,仅依靠止损或反向信号离场。 - 策略只在 K 线收盘后评估。如果收盘 K 线触及止损或止盈,持仓会在下一次处理时关闭。
仓位控制
- 每笔交易风险 =
Portfolio.CurrentValue * (RiskPercent / 100)。若账户权益不可用,则回退到策略配置的默认手数。 - 下单数量 = 风险金额 / 止损距离。若需要反手,会额外加上当前仓位的绝对值,确保完全对冲原仓位,这与原 MQL 实现一致。
参数说明
| 参数 | 含义 |
|---|---|
CandleType |
订阅的 K 线数据类型或周期。 |
StopLossPips |
以点/跳动计的止损距离,必须大于 0。 |
TakeProfitPips |
以点/跳动计的止盈距离,0 表示不开启止盈。 |
RiskPercent |
每笔交易承担的账户百分比风险。 |
MinBodyPips |
触发信号所需的最小实体长度(点/跳动)。 |
EnableTopShadow |
是否启用基于上影线的做空信号。 |
TopShadowPercent |
上影线与实体比例的阈值。 |
TopShadowIsMinimum |
true 表示比例需大于等于阈值,false 表示比例需小于等于阈值。 |
EnableLowerShadow |
是否启用基于下影线的做多信号。 |
LowerShadowPercent |
下影线与实体比例的阈值。 |
LowerShadowIsMinimum |
控制下影线阈值是最小值条件还是最大值条件。 |
使用建议
- 可先使用原 EA 相似的周期(如 5 分钟),再根据品种微调点数参数。
- 如果噪声过多,可适当提高
MinBodyPips;若希望捕捉更细小的反转,可降低该值。 - 需要更多过滤条件时,可在
OnStarted中绑定额外指标。 - 在真实账户前请先于模拟环境验证跳动值与风险设置是否正确。
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()