
交易量反转趋势捕捉策略是一种基于异常交易量和价格行为的量化交易方法,旨在识别市场可能出现方向反转的关键时刻。该策略核心是寻找具有显著高于平均水平交易量的K线,并在确认交易量下降时,根据之前的趋势方向做出相反的交易决策。它利用了大交易量后的市场心理变化,即所谓的”中指形态”——市场通常在大量介入后会出现短期反转。该策略采用限价单进入市场,并设置基于固定点数或ATR的止损和止盈水平,同时包含时间过滤功能以避开市场低流动性时段。
该策略的核心原理基于市场异常交易量后的趋势反转现象。具体操作逻辑如下:
识别异常交易量:系统检测前一根K线是否具有显著高于平均水平的交易量。在常规交易时段(RTH),交易量需超过近期平均交易量的3倍(可调整);在盘后或特殊时段(ETH),需要超过5倍。平均交易量计算会自动排除RTH边缘时段、收盘后4-6点及周日盘前时段。
确认交易量回落:当前K线的交易量必须低于前一根异常量能K线,表明大额交易行为已经结束。
确定趋势方向:通过比较异常交易量K线之前的收盘价与SMA(简单移动平均线)的关系,确定趋势方向。
逆势入场信号:
入场执行:
风险管理:根据不同品种特性,系统提供两种止损/止盈设置方式:
时间过滤:策略可选择性地过滤RTH首尾15分钟的交易信号,并始终过滤盘后收盘时段(下午4-6点)和周日盘前时段的信号。
捕捉关键转折点:策略专注于捕捉伴随异常交易量出现的市场转折点,这些点位通常代表市场情绪的显著变化,提供较高胜率的交易机会。
精确的入场点位:通过使用限价单在异常交易量K线的高点/低点入场,确保在技术上重要的价格水平进行交易,提高入场精度。
自适应的量能识别:策略根据不同交易时段(常规交易时间vs盘后/特殊时段)动态调整异常交易量的判定标准,更符合市场实际情况。
灵活的风险管理:提供基于固定点数和ATR的止损/止盈选项,可根据不同品种的特性和波动性进行个性化设置。
智能时间过滤:自动识别并过滤低流动性和不稳定的交易时段,避免在市场开盘和收盘附近容易出现的虚假信号。
清晰的视觉反馈:策略在图表上提供直观的视觉指示,包括异常交易量K线高亮、趋势SMA线、止损止盈水平等,方便交易者监控和分析。
自动化执行:一旦满足条件,系统会自动执行限价单下单和止损止盈设置,减少人为干预并保持交易纪律。
假突破风险:异常交易量可能导致价格短期突破重要水平,但随后可能迅速回撤,造成错误信号。为缓解此风险,可考虑增加确认指标,如RSI超买/超卖确认或突破持续时间要求。
消息驱动事件影响:重大经济数据或公司公告可能导致异常交易量,但这些反应往往持续较长时间而非立即反转。建议在重要经济数据发布前后暂停策略或增加过滤条件。
市场环境变化风险:在强趋势市场中,逆势交易可能面临持续不利的价格走势。可考虑增加长期趋势过滤器,避免在强趋势环境中逆势操作。
限价单未成交风险:如果价格在下一根K线未触及设定的限价水平,交易信号可能失效。可以考虑设置最大有效期限,或在特定条件下转为市价单执行。
低流动性风险:尽管策略已包含时间过滤功能,某些品种在特定时段仍可能面临流动性不足问题。建议针对交易品种的特性调整交易时段限制。
参数优化风险:过度优化策略参数可能导致过拟合历史数据而在未来表现不佳。应确保参数在合理范围内,并通过样本外测试验证策略稳健性。
多时间周期确认:增加更高时间周期的趋势过滤器,确保在更大趋势方向上有更高的胜率。例如,可以检查日线趋势方向,只在与日线趋势一致时入场。
交易量质量评估:除了纯量能大小外,可考虑增加交易量的质量评估,如成交量加权平均价格(VWAP)偏离度,以更好地理解大交易量背后的市场行为。
动态止损策略:实现基于波动率的动态止损,随着交易向有利方向发展自动调整止损位置,锁定部分利润。例如,可使用跟踪止损或在突破关键水平后移动止损至成本价。
多品种相关性过滤:对于相关品种(如股指期货与现货、黄金与白银等),增加相关品种的确认指标可提高信号质量。当多个相关品种同时出现异常交易量和价格行为时,信号可能更可靠。
机器学习优化:通过机器学习算法分析历史数据中最成功的异常交易量模式特征,动态调整入场条件和参数。例如,可使用决策树或随机森林来预测给定异常交易量特征下的最佳行动。
波动率调整:根据市场当前波动率状态调整异常交易量的判定标准和止损/止盈水平。在高波动率环境中提高异常量能判定阈值,降低止损距离;在低波动率环境中则相反。
增加基本面过滤:在重大经济数据发布日或季度财报季节性调整策略参数或暂停交易,避免消息面干扰导致的假信号。
交易量反转趋势捕捉策略是一种注重交易量和价格行为的量化交易系统,通过识别异常交易量后的市场情绪变化来捕捉潜在的反转点。该策略在技术上明确定义了入场、出场条件和风险管理规则,并包含智能的时间过滤机制,以避开市场低质量时段。
策略的核心优势在于其对市场”中指形态”的精准捕捉——在市场参与者大量涌入并随后撤出时,往往会形成短期反转机会。通过限价单精确定位在关键价格水平,并配合合理的止损止盈管理,该策略提供了一种纪律化的交易方法。
然而,使用者应注意策略在强趋势市场中的潜在风险,以及对消息面事件的敏感性。通过增加多时间周期确认、动态调整参数和增强风险管理机制,该策略可以进一步优化其表现稳定性和适应性。
总体而言,交易量反转趋势捕捉策略为交易者提供了一种基于市场行为和心理学原理的交易系统,特别适合波动性市场和区间震荡行情。通过合理设置和持续优化,该策略有望成为交易组合中的有效工具。
/*backtest
start: 2024-05-13 00:00:00
end: 2025-05-11 08:00:00
period: 1h
basePeriod: 1h
exchanges: [{"eid":"Futures_Binance","currency":"BTC_USDT"}]
*/
//@version=5
// Strategy Title Reflects Latest Logic
strategy(title="Middle Finger Trading Strategy",
shorttitle="Middle_Finger",
overlay=true,
pyramiding=0, // Only one entry at a time
default_qty_type=strategy.percent_of_equity,
default_qty_value=1, // Trade 1% of equity
commission_value=0.04, // Example commission (adjust as needed)
commission_type=strategy.commission.percent,
initial_capital = 10000, // Example starting capital
process_orders_on_close=false // Important for limit orders to potentially fill intra-bar
)
// --- Inputs ---
// Volume Settings Group
grp_vol = "Volume Settings"
float rthHugeVolMultiplier = input.float(3.0, title="1. RTH Huge Vol. Multiplier (> Avg)", minval=1.1, step=0.1, group=grp_vol, tooltip="Multiplier for core RTH (9:45-15:44 ET)")
float ethHugeVolMultiplier = input.float(5.0, title="2. ETH/Excluded Huge Vol. Multiplier (> Avg)", minval=1.1, step=0.1, group=grp_vol, tooltip="Multiplier for ETH and first/last 15min RTH (default 5x)")
int volLookback = input.int(20, title="3. Volume SMA Lookback", minval=1, group=grp_vol, tooltip="Lookback for calculating the filtered average volume (Used ONLY for identifying the HUGE spike)")
// Removed normalVolMultiplier as it's no longer used for entry confirmation
// Trend Settings Group
grp_trend = "Trend Settings"
int trendLookback = input.int(20, title="1. Trend SMA Lookback", minval=2, group=grp_trend, tooltip="Lookback period for the Simple Moving Average used to determine the trend before the spike")
// Risk Management Group
grp_risk = "Risk Management (SL/TP)"
string nqTargetTickerId = input.string("CME:NQ1!", title="1. Target Ticker ID for Fixed NQ Points", group=grp_risk, tooltip="Specify the exact Ticker ID (e.g., CME:NQ1!, TVC:NDX) for fixed SL/TP. Found in Symbol Info.")
float nqFixedStopPoints = input.float(20.0, title="2. Fixed SL Points (for Target Ticker)", group=grp_risk, minval=0.1, step=0.1)
float nqFixedTpPoints = input.float(50.0, title="3. Fixed TP Points (for Target Ticker)", group=grp_risk, minval=0.1, step=0.1)
// General SL/TP Settings (used if NOT the target ticker)
bool useAtrStops = input.bool(true, title="4. Use ATR for SL/TP (Other Tickers)?", group=grp_risk)
int atrLookback = input.int(14, title="5. ATR Lookback", group=grp_risk, inline="atr_other")
float atrStopMultiplier = input.float(2.0, title="6. ATR SL Multiplier", group=grp_risk, inline="atr_other", minval=0.1, step=0.1)
float atrTpMultiplier = input.float(4.0, title="7. ATR TP Multiplier", group=grp_risk, inline="atr_other", minval=0.1, step=0.1)
float fixedStopPoints = input.float(100.0, title="6. Fixed SL Points (Other Tickers)", group=grp_risk, inline="fixed_other", minval=1)
float fixedTpPoints = input.float(200.0, title="7. Fixed TP Points (Other Tickers)", group=grp_risk, inline="fixed_other", minval=1)
// Time Filter Settings Group
grp_time = "Time Filter (ET)"
bool enableEntryFilterRthEdges = input.bool(true, title="1. Filter Entries First/Last 15 Min RTH (ET)?", group=grp_time, tooltip="If checked, ignores entries from 9:30-9:44 ET and 15:45-15:59 ET. Avg Vol calc *always* filters these times, 4-6PM ET, and Sun pre-6PM ET.")
string targetTimezone = "America/New_York" // Specify Eastern Time zone
// --- Time Calculation Function ---
isTimeInSession(t, tz, sessionString) =>
not na(time(timeframe.period, sessionString, tz))
// --- Time Context Functions ---
getTimeContext(t, tz) =>
h = hour(t, tz)
m = minute(t, tz)
d = dayofweek(t, tz)
// Core RTH: 9:45 AM to 15:44 PM ET (Mon-Fri)
bool isCoreRTH = d >= dayofweek.monday and d <= dayofweek.friday and
((h == 9 and m >= 45) or (h >= 10 and h <= 14) or (h == 15 and m <= 44))
// Excluded RTH Edges: 9:30-9:44 ET and 15:45-15:59 ET (Mon-Fri)
bool isExcludedRTH = d >= dayofweek.monday and d <= dayofweek.friday and
((h == 9 and m >= 30 and m <= 44) or (h == 15 and m >= 45))
// After Hours Closed: 4:00 PM to 5:59 PM ET (Mon-Fri)
bool isAfterHoursClosed = d >= dayofweek.monday and d <= dayofweek.friday and
(h >= 16 and h < 18)
// Sunday Pre-Market: Sunday before 6:00 PM ET
bool isSundayPreMarket = d == dayofweek.sunday and h < 18
// Combine ALL periods where activity should be ignored or volume excluded from avg
bool isExcludedPeriod = isExcludedRTH or isAfterHoursClosed or isSundayPreMarket
[isCoreRTH, isExcludedRTH, isAfterHoursClosed, isSundayPreMarket, isExcludedPeriod]
// --- Get Time Context for Current and Previous Bar ---
[isCurrentBarCoreRTH, isCurrentBarExcludedRTH, isCurrentBarAfterHoursClosed, isCurrentBarSundayPreMarket, isCurrentBarExcludedPeriod] = getTimeContext(time, targetTimezone)
[isPreviousBarCoreRTH, isPreviousBarExcludedRTH, isPreviousBarAfterHoursClosed, isPreviousBarSundayPreMarket, isPreviousBarExcludedPeriod] = getTimeContext(time[1], targetTimezone)
// --- Calculations ---
// Volume Averaging: Exclude RTH edges, 4-6 PM ET, and Sunday Pre-6 PM ET ALWAYS
// This average is *only* used to define the huge volume spike threshold
bool excludeCurrentVolFromAvg = isCurrentBarExcludedPeriod
float volumeForAvgCalc = excludeCurrentVolFromAvg ? na : volume
float avgVolume = ta.sma(volumeForAvgCalc, volLookback)
// Dynamic Huge Volume Multiplier: Based on *previous* bar's time (Core RTH or not)
float activeHugeVolMultiplier = isPreviousBarCoreRTH ? rthHugeVolMultiplier : ethHugeVolMultiplier
// Use avgVolume[1] as current avgVolume excludes current bar, and we compare previous volume to avg *before* it
float hugeVolThreshold = nz(avgVolume[1]) * activeHugeVolMultiplier
// --- MODIFIED Volume Conditions ---
// 1. Check if the *previous* bar had huge volume compared to its preceding average
bool isHugeVolumePrevBar = volume[1] > hugeVolThreshold and hugeVolThreshold > 0
// 2. Check if the *current* bar's volume is simply lower than the previous (huge) bar's volume
bool isVolumeLowerThanSpike = volume < volume[1]
// Trend Condition
float priceSma = ta.sma(close, trendLookback)
// Ensure trend condition uses close[2] vs sma[2] (trend state *before* the spike bar)
bool isBullishTrendBeforeSpike = close[2] > nz(priceSma[2])
bool isBearishTrendBeforeSpike = close[2] < nz(priceSma[2])
// --- Entry Time Filtering ---
// Always filter After Hours Closed and Sunday Pre-Market.
// Optionally filter RTH Edges based on input.
bool shouldFilterRthEdges = enableEntryFilterRthEdges and isCurrentBarExcludedRTH
bool isIgnoreEntryTime = shouldFilterRthEdges or isCurrentBarAfterHoursClosed or isCurrentBarSundayPreMarket
// --- MODIFIED Base Conditions ---
// Uses the simplified `isVolumeLowerThanSpike` check
bool baseLongCondition = isBearishTrendBeforeSpike and isHugeVolumePrevBar and isVolumeLowerThanSpike
bool baseShortCondition = isBullishTrendBeforeSpike and isHugeVolumePrevBar and isVolumeLowerThanSpike
// Final Conditions (Apply Time Filter)
bool finalLongCondition = baseLongCondition and not isIgnoreEntryTime
bool finalShortCondition = baseShortCondition and not isIgnoreEntryTime
// --- Stop Loss & Take Profit Calculation (Conditional Logic) ---
// This part remains the same
float atrValue = ta.atr(atrLookback)
float tickValue = syminfo.mintick
int stopLossTicks = 100 // Default fallback SL ticks
int takeProfitTicks = 200 // Default fallback TP ticks
// Check if the current symbol matches the target ticker ID
bool isTargetTicker = str.upper(syminfo.tickerid) == str.upper(nqTargetTickerId) // Case-insensitive comparison
if (isTargetTicker and tickValue > 0)
// --- Target Ticker Logic (e.g., NQ Fixed Points) ---
float ticksPerPoint = 1.0 / tickValue
stopLossTicks := math.max(1, math.round(nqFixedStopPoints * ticksPerPoint))
takeProfitTicks := math.max(1, math.round(nqFixedTpPoints * ticksPerPoint))
else if tickValue > 0 // Use only if tickValue is valid
// --- Standard Logic (Other Tickers: ATR or Fixed) ---
float stopLossDistance = useAtrStops ? atrValue * atrStopMultiplier : fixedStopPoints * tickValue
float takeProfitDistance = useAtrStops ? atrValue * atrTpMultiplier : fixedTpPoints * tickValue
// Calculate ticks, ensuring it's at least 1 tick
stopLossTicks := na(stopLossDistance) ? 100 : math.max(1, math.round(stopLossDistance / tickValue))
takeProfitTicks := na(takeProfitDistance) ? 200 : math.max(1, math.round(takeProfitDistance / tickValue))
// Final check to ensure SL/TP are not na
stopLossTicks := nz(stopLossTicks, 100)
takeProfitTicks := nz(takeProfitTicks, 200)
// --- Strategy Execution ---
// Uses Limit Orders based on previous bar's low/high - Remains the same
float limitEntryPriceLong = low[1] // Target entry at the low of the huge volume bar
float limitEntryPriceShort = high[1] // Target entry at the high of the huge volume bar
if (finalLongCondition and strategy.position_size == 0)
strategy.cancel("S") // Cancel any pending short limit order first
strategy.entry("L", strategy.long, limit = limitEntryPriceLong)
strategy.exit("L SL/TP", from_entry="L", loss=stopLossTicks, profit=takeProfitTicks)
if (finalShortCondition and strategy.position_size == 0)
strategy.cancel("L") // Cancel any pending long limit order first
strategy.entry("S", strategy.short, limit = limitEntryPriceShort)
strategy.exit("S SL/TP", from_entry="S", loss=stopLossTicks, profit=takeProfitTicks)
// --- Plotting & Visuals ---
plot(avgVolume, title="Filtered Avg Volume", color=color.new(color.blue, 60), style=plot.style_line)
// Removed the plot for the normal volume threshold as it's no longer used
// Highlight huge volume bar (previous bar that triggered the signal)
bgcolor(isHugeVolumePrevBar[1] ? color.new(color.yellow, 85) : na, title="Huge Volume Bar [-1]")
// Highlight bars excluded from volume average calculation
bgcolor(excludeCurrentVolFromAvg ? color.new(color.teal, 90) : na, title="Vol Excluded from Avg Calc")
// Highlight bars where entries are ignored due to time filters
bgcolor(isIgnoreEntryTime and (baseLongCondition or baseShortCondition) ? color.new(color.gray, 75) : na, title="Entry Time Filtered Bar")
// --- MODIFIED Highlight base conditions met ---
// Reflects the updated base conditions using isVolumeLowerThanSpike
bgcolor(baseLongCondition and not isIgnoreEntryTime ? color.new(color.green, 90) : na, title="Base Long Condition Met")
bgcolor(baseShortCondition and not isIgnoreEntryTime ? color.new(color.red, 90) : na, title="Base Short Condition Met")
plot(priceSma, title="Trend SMA", color=color.gray)
// Plot SL/TP levels for visualization - Remains the same
var float entryPrice = na
var float slLevel = na
var float tpLevel = na
if (strategy.opentrades > 0 and strategy.opentrades[1] == 0) // Just entered a trade
entryPrice := strategy.opentrades.entry_price(0)
if (strategy.position_size > 0) // Long
slLevel := entryPrice - stopLossTicks * tickValue
tpLevel := entryPrice + takeProfitTicks * tickValue
else // Short
slLevel := entryPrice + stopLossTicks * tickValue
tpLevel := entryPrice - takeProfitTicks * tickValue
else if (strategy.opentrades == 0 and strategy.opentrades[1] > 0) // Position closed
entryPrice := na
slLevel := na
tpLevel := na
else if (strategy.opentrades > 0) // Position still open
entryPrice := strategy.opentrades.entry_price(0)
if (strategy.position_size > 0) // Long
slLevel := entryPrice - stopLossTicks * tickValue
tpLevel := entryPrice + takeProfitTicks * tickValue
else // Short
slLevel := entryPrice + stopLossTicks * tickValue
tpLevel := entryPrice - takeProfitTicks * tickValue
plot(strategy.opentrades > 0 ? slLevel : na, title="Stop Loss Level", color=color.red, style=plot.style_linebr)
plot(strategy.opentrades > 0 ? tpLevel : na, title="Take Profit Level", color=color.green, style=plot.style_linebr)
// Optional Debugging Plots
// plotchar(isHugeVolumePrevBar, "HugeVol[1]", "H", location.bottom, color.yellow, size=size.tiny)
// plotchar(isVolumeLowerThanSpike, "VolLow", "v", location.bottom, color.purple, size=size.tiny) // Changed char
// plotchar(finalLongCondition, "FinalLong", "L", location.top, color.green, size=size.tiny)
// plotchar(finalShortCondition, "FinalShort", "S", location.top, color.red, size=size.tiny)
// plot(finalLongCondition ? limitEntryPriceLong : na, "Long Limit Target", color.lime, style=plot.style_circles, linewidth=2)
// plot(finalShortCondition ? limitEntryPriceShort : na, "Short Limit Target", color.fuchsia, style=plot.style_circles, linewidth=2)