该策略基于蜡烛图形态分析市场情绪,通过三个核心振荡器(犹豫振荡器、恐惧振荡器和贪婪振荡器)来量化市场心理。策略综合了动量和趋势指标,同时结合成交量确认,构建了一个完整的交易系统。该策略适用于希望通过市场情绪分析来识别高概率交易机会的交易者。
策略的核心是通过分析不同的蜡烛图形态来构建三个情绪振荡器: 1. 犹豫振荡器 - 通过十字星和陀螺形态来衡量市场的不确定性 2. 恐惧振荡器 - 通过流星、上吊线和看跌吞没形态来跟踪空头情绪 3. 贪婪振荡器 - 通过光头阳线、锤子线、看涨吞没和三白兵来检测多头情绪
这三个振荡器的平均值构成了蜡烛情绪指数(CEI)。当CEI突破不同阈值时触发多空交易信号,并通过成交量确认。
这是一个将技术分析与量化交易相结合的创新策略。通过系统化的情绪分析和严格的风险管理,该策略能够为交易者提供可靠的交易信号。虽然存在一定的优化空间,但策略的基本框架是稳健的,适合进一步开发和实盘应用。
/*backtest
start: 2024-03-09 18:40:00
end: 2025-02-19 08:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Binance","currency":"SOL_USDT"}]
*/
//@version=6
strategy("Candle Emotion Index Strategy", shorttitle="CEI Strategy", overlay=true)
// User Inputs
length = input.int(14, title="Lookback Period", minval=1)
dojiThreshold = input.float(0.1, title="Doji Threshold", minval=0.01, maxval=0.5)
spinningTopThreshold = input.float(0.3, title="Spinning Top Threshold", minval=0.1, maxval=0.5)
shootingStarThreshold = input.float(0.5, title="Shooting Star Threshold", minval=0.1, maxval=1.0)
hangingManThreshold = input.float(0.5, title="Hanging Man Threshold", minval=0.1, maxval=1.0)
engulfingThreshold = input.float(0.5, title="Engulfing Threshold", minval=0.1, maxval=1.0)
marubozuThreshold = input.float(0.9, title="Marubozu Threshold", minval=0.5, maxval=1.0)
hammerThreshold = input.float(0.5, title="Hammer Threshold", minval=0.1, maxval=1.0)
threeWhiteSoldiersThreshold = input.float(0.5, title="Three White Soldiers Threshold", minval=0.1, maxval=1.0)
// Volume Multiplier Input
volumeMultiplier = input.float(1.5, title="Volume Multiplier", minval=1.0)
// Cooldown Period Input
cooldownPeriod = input.int(10, title="Cooldown Period (Candles)", minval=1)
// Maximum Holding Period Inputs
maxHoldingPeriod = input.int(20, title="Maximum Holding Period (Candles)", minval=1)
lossHoldingPeriod = input.int(10, title="Loss Exit Holding Period (Candles)", minval=1)
lossThreshold = input.float(0.02, title="Loss Threshold (as % of Entry Price)", minval=0.01, maxval=1.0)
// --- Indecision Oscillator Functions ---
isDoji(open, close, high, low, threshold) =>
bodySize = math.abs(close - open)
rangeSize = high - low
bodySize / rangeSize < threshold
isSpinningTop(open, close, high, low, threshold) =>
bodySize = math.abs(close - open)
rangeSize = high - low
bodySize / rangeSize < threshold and bodySize / rangeSize >= dojiThreshold
indecisionOscillator() =>
var float dojiScore = 0.0
var float spinningTopScore = 0.0
for i = 1 to length
if isDoji(open[i], close[i], high[i], low[i], dojiThreshold)
dojiScore := dojiScore + 1.0
if isSpinningTop(open[i], close[i], high[i], low[i], spinningTopThreshold)
spinningTopScore := spinningTopScore + 1.0
dojiScore := dojiScore / length
spinningTopScore := spinningTopScore / length
(dojiScore + spinningTopScore) / 2
// --- Fear Oscillator Functions ---
isShootingStar(open, close, high, low, threshold) =>
bodySize = math.abs(close - open)
upperWick = high - math.max(open, close)
lowerWick = math.min(open, close) - low
upperWick / bodySize > threshold and lowerWick < bodySize
isHangingMan(open, close, high, low, threshold) =>
bodySize = math.abs(close - open)
upperWick = high - math.max(open, close)
lowerWick = math.min(open, close) - low
lowerWick / bodySize > threshold and upperWick < bodySize
isBearishEngulfing(open, close, openPrev, closePrev, threshold) =>
bodySize = math.abs(close - open)
prevBodySize = math.abs(closePrev - openPrev)
close < openPrev and open > closePrev and bodySize / prevBodySize > threshold
fearOscillator() =>
var float shootingStarScore = 0.0
var float hangingManScore = 0.0
var float engulfingScore = 0.0
for i = 1 to length
if isShootingStar(open[i], close[i], high[i], low[i], shootingStarThreshold)
shootingStarScore := shootingStarScore + 1.0
if isHangingMan(open[i], close[i], high[i], low[i], hangingManThreshold)
hangingManScore := hangingManScore + 1.0
if isBearishEngulfing(open[i], close[i], open[i+1], close[i+1], engulfingThreshold)
engulfingScore := engulfingScore + 1.0
shootingStarScore := shootingStarScore / length
hangingManScore := hangingManScore / length
engulfingScore := engulfingScore / length
(shootingStarScore + hangingManScore + engulfingScore) / 3
// --- Greed Oscillator Functions ---
isMarubozu(open, close, high, low, threshold) =>
bodySize = math.abs(close - open)
totalRange = high - low
bodySize / totalRange > threshold
isHammer(open, close, high, low, threshold) =>
bodySize = math.abs(close - open)
lowerWick = math.min(open, close) - low
upperWick = high - math.max(open, close)
lowerWick / bodySize > threshold and upperWick < bodySize
isBullishEngulfing(open, close, openPrev, closePrev, threshold) =>
bodySize = math.abs(close - open)
prevBodySize = math.abs(closePrev - openPrev)
close > openPrev and open < closePrev and bodySize / prevBodySize > threshold
isThreeWhiteSoldiers(open, close, openPrev, closePrev, openPrev2, closePrev2, threshold) =>
close > open and closePrev > openPrev and closePrev2 > openPrev2 and close > closePrev and closePrev > closePrev2
greedOscillator() =>
var float marubozuScore = 0.0
var float hammerScore = 0.0
var float engulfingScore = 0.0
var float soldiersScore = 0.0
for i = 1 to length
if isMarubozu(open[i], close[i], high[i], low[i], marubozuThreshold)
marubozuScore := marubozuScore + 1.0
if isHammer(open[i], close[i], high[i], low[i], hammerThreshold)
hammerScore := hammerScore + 1.0
if isBullishEngulfing(open[i], close[i], open[i+1], close[i+1], engulfingThreshold)
engulfingScore := engulfingScore + 1.0
if isThreeWhiteSoldiers(open[i], close[i], open[i+1], close[i+1], open[i+2], close[i+2], threeWhiteSoldiersThreshold)
soldiersScore := soldiersScore + 1.0
marubozuScore := marubozuScore / length
hammerScore := hammerScore / length
engulfingScore := engulfingScore / length
soldiersScore := soldiersScore / length
(marubozuScore + hammerScore + engulfingScore + soldiersScore) / 4
// --- Final Calculations ---
indecision = indecisionOscillator()
fear = fearOscillator()
greed = greedOscillator()
// Calculate the average of the three oscillators
averageOscillator = (indecision + fear + greed) / 3
// --- Combined Strategy Logic ---
var float entryPriceLong = na
var float entryPriceShort = na
var int holdingPeriodLong = 0
var int holdingPeriodShort = 0
var int cooldownCounter = 0
// Buy Signal Logic for Long and Short
longBuySignal = ta.crossover(averageOscillator, 0.1)
shortBuySignal = ta.crossover(averageOscillator, 0.2)
// Calculate average volume over the lookback period
avgVolume = ta.sma(volume, length)
// Take Profit Conditions
longTakeProfitCondition = close > open and volume > avgVolume * volumeMultiplier
shortTakeProfitCondition = close < open and volume > avgVolume * volumeMultiplier
// Buy Logic for Long Positions
if longBuySignal and strategy.position_size == 0 and cooldownCounter <= 0
entryPriceLong := close
strategy.entry("Long Entry", strategy.long)
cooldownCounter := cooldownPeriod
holdingPeriodLong := 0
// Increment holding period if in a long position
if strategy.position_size > 0
holdingPeriodLong := holdingPeriodLong + 1
// Sell Logic for Long Positions
if longTakeProfitCondition and strategy.position_size > 0 and close > entryPriceLong
strategy.close_all()
cooldownCounter := cooldownPeriod
if holdingPeriodLong >= maxHoldingPeriod and strategy.position_size > 0 and close >= entryPriceLong
strategy.close_all()
cooldownCounter := cooldownPeriod
if holdingPeriodLong >= lossHoldingPeriod and strategy.position_size > 0 and close < entryPriceLong * (1 - lossThreshold)
strategy.close_all()
cooldownCounter := cooldownPeriod
// Short Logic for Short Positions
if shortBuySignal and strategy.position_size == 0 and cooldownCounter <= 0
entryPriceShort := close
strategy.entry("Short Entry", strategy.short)
cooldownCounter := cooldownPeriod
holdingPeriodShort := 0
// Increment holding period if in a short position
if strategy.position_size < 0
holdingPeriodShort := holdingPeriodShort + 1
// Cover Logic for Short Positions
if shortTakeProfitCondition and strategy.position_size < 0 and close < entryPriceShort
strategy.close_all()
cooldownCounter := cooldownPeriod
if holdingPeriodShort >= maxHoldingPeriod and strategy.position_size < 0 and close <= entryPriceShort
strategy.close_all()
cooldownCounter := cooldownPeriod
if holdingPeriodShort >= lossHoldingPeriod and strategy.position_size < 0 and close > entryPriceShort * (1 + lossThreshold)
strategy.close_all()
cooldownCounter := cooldownPeriod
// Decrement the cooldown counter each candle
if cooldownCounter > 0
cooldownCounter := cooldownCounter - 1