- Created momentum scalper indicator for catching rapid price acceleration - ROC-based detection: 2.0% threshold over 5 bars - Volume confirmation: 2.0x spike (checks last 3 bars) - ADX filter: Requires 12+ minimum directional movement - Anti-chop filter: Blocks signals in dead markets - Debug table: Real-time metric display for troubleshooting Status: Functional but signal quality inferior to v6 HalfTrend Decision: Shelved for now, continue with proven v6 strategy File: docs/guides/MOMENTUM_INDICATOR_V1.pine (239 lines) Lessons learned: - Momentum indicators inherently noisy (40-50% WR expected) - Signals either too early (false breakouts) or too late (miss move) - Volume spike timing issue: Often lags price move by 1-2 bars - Better to optimize proven strategy than add complexity Related: Position Manager duplicate update bug fixed (awaiting verification)
239 lines
11 KiB
Plaintext
239 lines
11 KiB
Plaintext
// Momentum Scalper v1 - Complements HalfTrend v6
|
|
// Catches rapid price acceleration with volume confirmation
|
|
// Use alongside v6 for complete coverage (trend + momentum)
|
|
|
|
//@version=6
|
|
indicator("Momentum Scalper v1", overlay=true)
|
|
|
|
// ============================================================================
|
|
// INPUTS
|
|
// ============================================================================
|
|
|
|
// Momentum Detection
|
|
roc_length = input.int(5, "ROC Length (candles)", minval=1, maxval=20)
|
|
roc_threshold = input.float(2.0, "ROC Threshold %", minval=0.5, maxval=5.0, step=0.1)
|
|
|
|
// Volume Confirmation
|
|
vol_ma_length = input.int(20, "Volume MA Length", minval=5, maxval=50)
|
|
vol_spike_mult = input.float(2.0, "Volume Spike Multiplier", minval=1.2, maxval=3.0, step=0.1)
|
|
|
|
// RSI Extremes
|
|
rsi_length = input.int(14, "RSI Length", minval=5, maxval=30)
|
|
rsi_overbought = input.int(65, "RSI Overbought", minval=60, maxval=80)
|
|
rsi_oversold = input.int(35, "RSI Oversold", minval=20, maxval=40)
|
|
|
|
// Filters
|
|
min_atr_pct = input.float(0.25, "Min ATR %", minval=0.1, maxval=1.0, step=0.05)
|
|
min_adx = input.int(12, "Min ADX", minval=10, maxval=25)
|
|
|
|
// Signal Quality
|
|
enable_strict_mode = input.bool(false, "Strict Mode (higher quality)")
|
|
|
|
// ============================================================================
|
|
// INDICATORS
|
|
// ============================================================================
|
|
|
|
// Rate of Change (price acceleration)
|
|
roc = (close - close[roc_length]) / close[roc_length] * 100
|
|
|
|
// Volume Analysis (check current + last 2 bars for spike)
|
|
vol_ma = ta.sma(volume, vol_ma_length)
|
|
vol_ratio = volume / vol_ma
|
|
vol_ratio_1 = volume[1] / vol_ma[1]
|
|
vol_ratio_2 = volume[2] / vol_ma[2]
|
|
vol_spike = vol_ratio >= vol_spike_mult or vol_ratio_1 >= vol_spike_mult or vol_ratio_2 >= vol_spike_mult
|
|
|
|
// RSI (momentum extremes)
|
|
rsi = ta.rsi(close, rsi_length)
|
|
rsi_extreme_high = rsi > rsi_overbought
|
|
rsi_extreme_low = rsi < rsi_oversold
|
|
|
|
// ATR (volatility gate)
|
|
atr = ta.atr(14)
|
|
atr_pct = (atr / close) * 100
|
|
sufficient_volatility = atr_pct >= min_atr_pct
|
|
|
|
// ADX (trend strength - even momentum needs some direction)
|
|
[diPlus, diMinus, adx] = ta.dmi(14, 14)
|
|
adx_sufficient = adx >= min_adx
|
|
|
|
// Price Position in Range (avoid extremes)
|
|
highest_100 = ta.highest(high, 100)
|
|
lowest_100 = ta.lowest(low, 100)
|
|
price_position = ((close - lowest_100) / (highest_100 - lowest_100)) * 100
|
|
|
|
// Candle Analysis
|
|
red_candle = close < open
|
|
green_candle = close > open
|
|
candle_size = math.abs(close - open) / open * 100
|
|
strong_candle = candle_size > 0.15 // At least 0.15% move (relaxed from 0.3%)
|
|
|
|
// ============================================================================
|
|
// MOMENTUM SIGNALS
|
|
// ============================================================================
|
|
|
|
// SHORT SIGNAL: Sharp drop with acceleration
|
|
momentum_short_basic = roc < -roc_threshold and vol_spike and red_candle and sufficient_volatility and adx_sufficient
|
|
|
|
// Strict: Requires RSI not oversold (>40), ADX confirmation, decent candle size
|
|
momentum_short_strict = momentum_short_basic and rsi > 40 and strong_candle
|
|
|
|
// LONG SIGNAL: Sharp rally with acceleration
|
|
momentum_long_basic = roc > roc_threshold and vol_spike and green_candle and sufficient_volatility and adx_sufficient
|
|
|
|
// Strict: Requires RSI not overbought (<60), ADX confirmation, decent candle size
|
|
momentum_long_strict = momentum_long_basic and rsi < 60 and strong_candle
|
|
|
|
// Choose mode
|
|
short_signal = enable_strict_mode ? momentum_short_strict : momentum_short_basic
|
|
long_signal = enable_strict_mode ? momentum_long_strict : momentum_long_basic
|
|
|
|
// ============================================================================
|
|
// ANTI-CHOP FILTER (from v6)
|
|
// ============================================================================
|
|
|
|
// Prevent signals in dead, sideways markets
|
|
extreme_chop = adx < 10 and atr_pct < 0.25 and vol_ratio < 0.9
|
|
signal_blocked = extreme_chop
|
|
|
|
// Final signals with anti-chop
|
|
final_short = short_signal and not signal_blocked
|
|
final_long = long_signal and not signal_blocked
|
|
|
|
// ============================================================================
|
|
// VISUALIZATION
|
|
// ============================================================================
|
|
|
|
// Plot FINAL signals (green/red triangles)
|
|
plotshape(final_long, "🚀 MOMENTUM LONG", shape.triangleup, location.belowbar,
|
|
color.new(color.lime, 0), size=size.normal, text="M-LONG")
|
|
plotshape(final_short, "💥 MOMENTUM SHORT", shape.triangledown, location.abovebar,
|
|
color.new(color.red, 0), size=size.normal, text="M-SHORT")
|
|
|
|
// Plot BASIC signals (smaller dots for comparison - see what strict mode filters out)
|
|
plotshape(momentum_long_basic and not final_long, "Basic Long (filtered)", shape.circle, location.belowbar,
|
|
color.new(color.green, 70), size=size.tiny)
|
|
plotshape(momentum_short_basic and not final_short, "Basic Short (filtered)", shape.circle, location.abovebar,
|
|
color.new(color.orange, 70), size=size.tiny)
|
|
|
|
// Background color for signal zones (more visible)
|
|
bgcolor(final_long ? color.new(color.green, 85) : na, title="Long Zone")
|
|
bgcolor(final_short ? color.new(color.red, 85) : na, title="Short Zone")
|
|
bgcolor(signal_blocked ? color.new(color.orange, 92) : na, title="Chop Zone")
|
|
|
|
// Plot volume spike bars (helps see when conditions are close)
|
|
barcolor(vol_spike and sufficient_volatility ? (close > open ? color.new(color.aqua, 50) : color.new(color.purple, 50)) : na,
|
|
title="Volume Spike Candles")
|
|
|
|
// Debug info table (shows current status)
|
|
var table debug_table = table.new(position.top_right, 2, 8, bgcolor=color.new(color.black, 80), border_width=1)
|
|
if barstate.islast
|
|
table.cell(debug_table, 0, 0, "ROC", text_color=color.white, bgcolor=color.new(color.gray, 70))
|
|
table.cell(debug_table, 1, 0, str.tostring(roc, "#.##") + "%",
|
|
text_color=roc < -roc_threshold ? color.red : (roc > roc_threshold ? color.lime : color.white))
|
|
|
|
table.cell(debug_table, 0, 1, "Vol Ratio", text_color=color.white, bgcolor=color.new(color.gray, 70))
|
|
table.cell(debug_table, 1, 1, str.tostring(vol_ratio, "#.##") + "x",
|
|
text_color=vol_spike ? color.lime : color.white)
|
|
|
|
table.cell(debug_table, 0, 2, "RSI", text_color=color.white, bgcolor=color.new(color.gray, 70))
|
|
table.cell(debug_table, 1, 2, str.tostring(rsi, "#.#"),
|
|
text_color=rsi_extreme_high or rsi_extreme_low ? color.yellow : color.white)
|
|
|
|
table.cell(debug_table, 0, 3, "ADX", text_color=color.white, bgcolor=color.new(color.gray, 70))
|
|
table.cell(debug_table, 1, 3, str.tostring(adx, "#.#"),
|
|
text_color=adx_sufficient ? color.lime : color.orange)
|
|
|
|
table.cell(debug_table, 0, 4, "ATR%", text_color=color.white, bgcolor=color.new(color.gray, 70))
|
|
table.cell(debug_table, 1, 4, str.tostring(atr_pct, "#.##") + "%",
|
|
text_color=sufficient_volatility ? color.lime : color.orange)
|
|
|
|
table.cell(debug_table, 0, 5, "Price Pos", text_color=color.white, bgcolor=color.new(color.gray, 70))
|
|
table.cell(debug_table, 1, 5, str.tostring(price_position, "#") + "%", text_color=color.white)
|
|
|
|
table.cell(debug_table, 0, 6, "Status", text_color=color.white, bgcolor=color.new(color.gray, 70))
|
|
status_text = final_long ? "🚀 LONG!" : final_short ? "💥 SHORT!" : signal_blocked ? "⛔ CHOP" : "⏳ Wait"
|
|
status_color = final_long ? color.lime : final_short ? color.red : signal_blocked ? color.orange : color.gray
|
|
table.cell(debug_table, 1, 6, status_text, text_color=status_color)
|
|
|
|
table.cell(debug_table, 0, 7, "Mode", text_color=color.white, bgcolor=color.new(color.gray, 70))
|
|
table.cell(debug_table, 1, 7, enable_strict_mode ? "STRICT" : "BASIC", text_color=color.yellow)
|
|
|
|
// ============================================================================
|
|
// ALERTS FOR TRADING BOT
|
|
// ============================================================================
|
|
|
|
// Calculate metrics for webhook payload
|
|
volumeRatio = vol_ratio
|
|
pricePosition = price_position
|
|
|
|
// LONG Alert
|
|
if final_long
|
|
alert_msg = '{\n' +
|
|
'"action": "long",\n' +
|
|
'"symbol": "' + syminfo.ticker + '",\n' +
|
|
'"timeframe": "' + timeframe.period + '",\n' +
|
|
'"indicatorVersion": "v7-momentum",\n' +
|
|
'"atr": ' + str.tostring(atr_pct, "#.####") + ',\n' +
|
|
'"adx": ' + str.tostring(adx, "#.##") + ',\n' +
|
|
'"rsi": ' + str.tostring(rsi, "#.##") + ',\n' +
|
|
'"volumeRatio": ' + str.tostring(vol_ratio, "#.##") + ',\n' +
|
|
'"pricePosition": ' + str.tostring(price_position, "#.##") + ',\n' +
|
|
'"roc": ' + str.tostring(roc, "#.##") + ',\n' +
|
|
'"currentPrice": ' + str.tostring(close, "#.####") + '\n' +
|
|
'}'
|
|
alert(alert_msg, alert.freq_once_per_bar)
|
|
|
|
// SHORT Alert
|
|
if final_short
|
|
alert_msg = '{\n' +
|
|
'"action": "short",\n' +
|
|
'"symbol": "' + syminfo.ticker + '",\n' +
|
|
'"timeframe": "' + timeframe.period + '",\n' +
|
|
'"indicatorVersion": "v7-momentum",\n' +
|
|
'"atr": ' + str.tostring(atr_pct, "#.####") + ',\n' +
|
|
'"adx": ' + str.tostring(adx, "#.##") + ',\n' +
|
|
'"rsi": ' + str.tostring(rsi, "#.##") + ',\n' +
|
|
'"volumeRatio": ' + str.tostring(vol_ratio, "#.##") + ',\n' +
|
|
'"pricePosition": ' + str.tostring(price_position, "#.##") + ',\n' +
|
|
'"roc": ' + str.tostring(roc, "#.##") + ',\n' +
|
|
'"currentPrice": ' + str.tostring(close, "#.####") + '\n' +
|
|
'}'
|
|
alert(alert_msg, alert.freq_once_per_bar)
|
|
|
|
// ============================================================================
|
|
// STRATEGY NOTES
|
|
// ============================================================================
|
|
|
|
// INTENDED USE:
|
|
// - Complement HalfTrend v6 (catches what trend-following misses)
|
|
// - Use SMALLER position size (20-30% vs 100% for v6)
|
|
// - Requires STRICTER quality score (70+ vs 60+ for v6)
|
|
// - Best on 5-minute timeframe (momentum decays fast)
|
|
// - ROC 1.5%+ threshold (captures moves before trend confirmation)
|
|
// - Volume spike lookback (checks current + last 2 bars for confirmation)
|
|
//
|
|
// STRENGTHS:
|
|
// - Catches sharp moves early (first 1-2%)
|
|
// - High volume confirmation reduces false signals
|
|
// - RSI directional confirmation (not extremes - allows catching moves earlier)
|
|
// - Works in volatile markets (where v6 struggles)
|
|
//
|
|
// WEAKNESSES:
|
|
// - More false signals than trend-following
|
|
// - Requires tight stops (moves reverse quickly)
|
|
// - Needs active monitoring (not set-and-forget)
|
|
// - Performance varies by market regime
|
|
//
|
|
// QUALITY SCORE IMPACT:
|
|
// + Volume spike (1.8x+ in last 3 bars): +15 points
|
|
// + RSI directional (RSI <55 for longs, >45 for shorts): +10 points
|
|
// + Strong ROC (>1.5%): +10 points
|
|
// + Strong candle: +5 points
|
|
// - Weak ADX (<12): -15 points
|
|
// - Low ATR (<0.25%): -20 points
|
|
// - Price at extreme (>90% or <10%): -15 points
|
|
//
|
|
// Expected score range: 55-85 (vs 60-95 for v6)
|
|
// Recommended minimum: 70 (vs 60 for v6)
|