Files
trading_bot_v4/workflows/trading/moneyline_v9_ma_gap.pinescript
mindesbunister b6d4a8f157 fix: Add Position Manager health monitoring system
CRITICAL FIXES FOR $1,000 LOSS BUG (Dec 8, 2025):

**Bug #1: Position Manager Never Actually Monitors**
- System logged 'Trade added' but never started monitoring
- isMonitoring stayed false despite having active trades
- Result: No TP/SL monitoring, no protection, uncontrolled losses

**Bug #2: Silent SL Placement Failures**
- placeExitOrders() returned SUCCESS but only 2/3 orders placed
- Missing SL order left $2,003 position completely unprotected
- No error logs, no indication anything was wrong

**Bug #3: Orphan Detection Cancelled Active Orders**
- Old orphaned position detection triggered on NEW position
- Cancelled TP/SL orders while leaving position open
- User opened trade WITH protection, system REMOVED protection

**SOLUTION: Health Monitoring System**

New file: lib/health/position-manager-health.ts
- Runs every 30 seconds to detect critical failures
- Checks: DB open trades vs PM monitoring status
- Checks: PM has trades but monitoring is OFF
- Checks: Missing SL/TP orders on open positions
- Checks: DB vs Drift position count mismatch
- Logs: CRITICAL alerts when bugs detected

Integration: lib/startup/init-position-manager.ts
- Health monitor starts automatically on server startup
- Runs alongside other critical services
- Provides continuous verification Position Manager works

Test: tests/integration/position-manager/monitoring-verification.test.ts
- Validates startMonitoring() actually calls priceMonitor.start()
- Validates isMonitoring flag set correctly
- Validates price updates trigger trade checks
- Validates monitoring stops when no trades remain

**Why This Matters:**
User lost $1,000+ because Position Manager said 'working' but wasn't.
This health system detects that failure within 30 seconds and alerts.

**Next Steps:**
1. Rebuild Docker container
2. Verify health monitor starts
3. Manually test: open position, wait 30s, check health logs
4. If issues found: Health monitor will alert immediately

This prevents the $1,000 loss bug from ever happening again.
2025-12-08 15:43:54 +01:00

294 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//@version=6
indicator("Bullmania Money Line v11 All Filters", shorttitle="ML v11", overlay=true)
// Calculation source (Chart vs Heikin Ashi)
srcMode = input.string("Chart", "Calculation source", options=["Chart","Heikin Ashi"], tooltip="Use regular chart candles or Heikin Ashi for the line calculation.")
// Parameter Mode
paramMode = input.string("Profiles by timeframe", "Parameter Mode", options=["Single", "Profiles by timeframe"], tooltip="Choose whether to use one global set of parameters or timeframe-specific profiles.")
// Single (global) parameters
atrPeriodSingle = input.int(10, "ATR Period (Single mode)", minval=1, group="Single Mode")
multiplierSingle = input.float(3.0, "Multiplier (Single mode)", minval=0.1, step=0.1, group="Single Mode")
// Profile override when using profiles
profileOverride = input.string("Auto", "Profile Override", options=["Auto", "Minutes", "Hours", "Daily", "Weekly/Monthly"], tooltip="When in 'Profiles by timeframe' mode, choose a fixed profile or let it auto-detect from the chart timeframe.", group="Profiles")
// Timeframe profile parameters
// Minutes (<= 59m)
atr_m = input.int(12, "ATR Period (Minutes)", minval=1, group="Profiles — Minutes")
mult_m = input.float(3.8, "Multiplier (Minutes)", minval=0.1, step=0.1, group="Profiles — Minutes", tooltip="V8: Increased from 3.3 for stickier trend")
// Hours (>=1h and <1d)
atr_h = input.int(10, "ATR Period (Hours)", minval=1, group="Profiles — Hours")
mult_h = input.float(3.5, "Multiplier (Hours)", minval=0.1, step=0.1, group="Profiles — Hours", tooltip="V8: Increased from 3.0 for stickier trend")
// Daily (>=1d and <1w)
atr_d = input.int(10, "ATR Period (Daily)", minval=1, group="Profiles — Daily")
mult_d = input.float(3.2, "Multiplier (Daily)", minval=0.1, step=0.1, group="Profiles — Daily", tooltip="V8: Increased from 2.8 for stickier trend")
// Weekly/Monthly (>=1w)
atr_w = input.int(7, "ATR Period (Weekly/Monthly)", minval=1, group="Profiles — Weekly/Monthly")
mult_w = input.float(3.0, "Multiplier (Weekly/Monthly)", minval=0.1, step=0.1, group="Profiles — Weekly/Monthly", tooltip="V8: Increased from 2.5 for stickier trend")
// Optional MACD confirmation
useMacd = input.bool(false, "Use MACD confirmation", inline="macd")
macdSrc = input.source(close, "MACD Source", inline="macd")
macdFastLen = input.int(12, "Fast", minval=1, inline="macdLens")
macdSlowLen = input.int(26, "Slow", minval=1, inline="macdLens")
macdSigLen = input.int(9, "Signal", minval=1, inline="macdLens")
// Signal timing (ALWAYS applies to all signals)
groupTiming = "Signal Timing"
confirmBars = input.int(0, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="V11: Set to 0 for immediate signals on flip. Increase to wait X bars for confirmation.")
flipThreshold = input.float(0.25, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V11 OPTIMIZED: 0.25% (from exhaustive sweep) - 10× better than v9 baseline.")
// Entry filters (optional)
groupFilters = "Entry filters"
useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group=groupFilters, tooltip="V11: Enabled by default. Close must be beyond the Money Line by buffer amount to avoid wick flips.")
entryBufferATR = input.float(0.10, "Buffer size (in ATR)", minval=0.0, step=0.05, group=groupFilters, tooltip="V11 OPTIMIZED: 0.10 ATR (from exhaustive sweep) - balanced flip protection.")
useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="V11: Enabled by default to reduce choppy trades.")
adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters)
adxMin = input.int(5, "ADX minimum", minval=0, maxval=100, group=groupFilters, tooltip="V11 OPTIMIZED: 5 (from exhaustive sweep) - allows more signals with sticky trend system protecting quality.")
// NEW v6 FILTERS
groupV6Filters = "v6 Quality Filters"
usePricePosition = input.bool(true, "Use price position filter", group=groupV6Filters, tooltip="Prevent chasing extremes - don't buy at top of range or sell at bottom.")
longPosMax = input.float(100, "Long max position %", minval=0, maxval=100, group=groupV6Filters, tooltip="V11 OPTIMIZED: 100% (from exhaustive sweep) - no long position limit, filters work via other metrics.")
shortPosMin = input.float(5, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="V11 OPTIMIZED: 5% (from exhaustive sweep) - catches early short momentum signals.")
useVolumeFilter = input.bool(true, "Use volume filter", group=groupV6Filters, tooltip="Filter signals with extreme volume (too low = dead, too high = climax).")
volMin = input.float(0.0, "Volume min ratio", minval=0.1, step=0.1, group=groupV6Filters, tooltip="V11 OPTIMIZED: 0.0 (from exhaustive sweep) - no volume floor, quality via trend structure.")
volMax = input.float(3.5, "Volume max ratio", minval=0.5, step=0.5, group=groupV6Filters, tooltip="Maximum volume relative to 20-bar MA.")
useRsiFilter = input.bool(true, "Use RSI momentum filter", group=groupV6Filters, tooltip="Ensure momentum confirms direction.")
rsiLongMin = input.float(30, "RSI long minimum", minval=0, maxval=100, group=groupV6Filters, tooltip="V11 OPTIMIZED: 30 (from exhaustive sweep).")
rsiLongMax = input.float(70, "RSI long maximum", minval=0, maxval=100, group=groupV6Filters)
rsiShortMin = input.float(30, "RSI short minimum", minval=0, maxval=100, group=groupV6Filters)
rsiShortMax = input.float(80, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters, tooltip="V11 OPTIMIZED: 80 (from exhaustive sweep).")
// V9 NEW: MA GAP VISUALIZATION OPTIONS
groupV9MA = "v9 MA Gap Options"
showMAs = input.bool(true, "Show 50 and 200 MAs on chart", group=groupV9MA, tooltip="Display the moving averages for visual reference.")
ma50Color = input.color(color.new(color.yellow, 0), "MA 50 Color", group=groupV9MA)
ma200Color = input.color(color.new(color.orange, 0), "MA 200 Color", group=groupV9MA)
// Determine effective parameters based on selected mode/profile
var string activeProfile = ""
resSec = timeframe.in_seconds(timeframe.period)
isMinutes = resSec < 3600
isHours = resSec >= 3600 and resSec < 86400
isDaily = resSec >= 86400 and resSec < 604800
isWeeklyOrMore = resSec >= 604800
// Resolve profile bucket
string profileBucket = "Single"
if paramMode == "Single"
profileBucket := "Single"
else
if profileOverride == "Minutes"
profileBucket := "Minutes"
else if profileOverride == "Hours"
profileBucket := "Hours"
else if profileOverride == "Daily"
profileBucket := "Daily"
else if profileOverride == "Weekly/Monthly"
profileBucket := "Weekly/Monthly"
else
profileBucket := isMinutes ? "Minutes" : isHours ? "Hours" : isDaily ? "Daily" : "Weekly/Monthly"
atrPeriod = profileBucket == "Single" ? atrPeriodSingle : profileBucket == "Minutes" ? atr_m : profileBucket == "Hours" ? atr_h : profileBucket == "Daily" ? atr_d : atr_w
multiplier = profileBucket == "Single" ? multiplierSingle : profileBucket == "Minutes" ? mult_m : profileBucket == "Hours" ? mult_h : profileBucket == "Daily" ? mult_d : mult_w
activeProfile := profileBucket
// Core Money Line logic (with selectable source)
// Build selected source OHLC
// Optimized: Calculate Heikin Ashi directly instead of using request.security()
haC = srcMode == "Heikin Ashi" ? (open + high + low + close) / 4 : close
haO = srcMode == "Heikin Ashi" ? (nz(haC[1]) + nz(open[1])) / 2 : open
haH = srcMode == "Heikin Ashi" ? math.max(high, math.max(haO, haC)) : high
haL = srcMode == "Heikin Ashi" ? math.min(low, math.min(haO, haC)) : low
calcH = haH
calcL = haL
calcC = haC
// ATR on selected source
tr = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1])))
atr = ta.rma(tr, atrPeriod)
src = (calcH + calcL) / 2
up = src - (multiplier * atr)
dn = src + (multiplier * atr)
var float up1 = na
var float dn1 = na
up1 := nz(up1[1], up)
dn1 := nz(dn1[1], dn)
up1 := calcC[1] > up1 ? math.max(up, up1) : up
dn1 := calcC[1] < dn1 ? math.min(dn, dn1) : dn
var int trend = 1
var float tsl = na
tsl := nz(tsl[1], up1)
// V8: Apply flip threshold - require price to move X% beyond line before flip
thresholdAmount = tsl * (flipThreshold / 100)
// Track consecutive bars in potential new direction (anti-whipsaw)
var int bullMomentumBars = 0
var int bearMomentumBars = 0
if trend == 1
tsl := math.max(up1, tsl)
// Count consecutive bearish bars
if calcC < (tsl - thresholdAmount)
bearMomentumBars := bearMomentumBars + 1
bullMomentumBars := 0
else
bearMomentumBars := 0
// Flip only after X consecutive bars below threshold
trend := bearMomentumBars >= (confirmBars + 1) ? -1 : 1
else
tsl := math.min(dn1, tsl)
// Count consecutive bullish bars
if calcC > (tsl + thresholdAmount)
bullMomentumBars := bullMomentumBars + 1
bearMomentumBars := 0
else
bullMomentumBars := 0
// Flip only after X consecutive bars above threshold
trend := bullMomentumBars >= (confirmBars + 1) ? 1 : -1
supertrend = tsl
// Plot the Money Line
upTrend = trend == 1 ? supertrend : na
downTrend = trend == -1 ? supertrend : na
plot(upTrend, "Up Trend", color=color.new(color.green, 0), style=plot.style_linebr, linewidth=2)
plot(downTrend, "Down Trend", color=color.new(color.red, 0), style=plot.style_linebr, linewidth=2)
// Show active profile on chart as a label (optimized - only on confirmed bar)
showProfileLabel = input.bool(true, "Show active profile label", group="Profiles")
var label profLbl = na
if barstate.islast and barstate.isconfirmed and showProfileLabel
label.delete(profLbl)
profLbl := label.new(bar_index, close, text="Profile: " + activeProfile + " | ATR=" + str.tostring(atrPeriod) + " Mult=" + str.tostring(multiplier), yloc=yloc.price, style=label.style_label_upper_left, textcolor=color.white, color=color.new(color.blue, 20))
// MACD confirmation logic
[macdLine, macdSignal, macdHist] = ta.macd(macdSrc, macdFastLen, macdSlowLen, macdSigLen)
longOk = not useMacd or (macdLine > macdSignal)
shortOk = not useMacd or (macdLine < macdSignal)
// Plot buy/sell signals (gated by optional MACD)
buyFlip = trend == 1 and trend[1] == -1
sellFlip = trend == -1 and trend[1] == 1
// ADX computation (always calculate for context, but only filter if enabled)
upMove = calcH - calcH[1]
downMove = calcL[1] - calcL
plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0
minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0
trADX = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1])))
atrADX = ta.rma(trADX, adxLen)
plusDMSmooth = ta.rma(plusDM, adxLen)
minusDMSmooth = ta.rma(minusDM, adxLen)
plusDI = atrADX == 0.0 ? 0.0 : 100.0 * plusDMSmooth / atrADX
minusDI = atrADX == 0.0 ? 0.0 : 100.0 * minusDMSmooth / atrADX
dx = (plusDI + minusDI == 0.0) ? 0.0 : 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI)
adxVal = ta.rma(dx, adxLen)
adxOk = not useAdx or (adxVal > adxMin)
// Entry buffer gates relative to current Money Line
longBufferOk = not useEntryBuffer or (calcC > supertrend + entryBufferATR * atr)
shortBufferOk = not useEntryBuffer or (calcC < supertrend - entryBufferATR * atr)
// Confirmation bars after flip
buyReady = ta.barssince(buyFlip) == confirmBars
sellReady = ta.barssince(sellFlip) == confirmBars
// === CONTEXT METRICS FOR SIGNAL QUALITY ===
// Calculate ATR as percentage of price
atrPercent = (atr / calcC) * 100
// Calculate RSI
rsi14 = ta.rsi(calcC, 14)
// Volume ratio (current volume vs 20-bar MA)
volMA20 = ta.sma(volume, 20)
volumeRatio = volume / volMA20
// v6 IMPROVEMENT: Price position in 100-bar range (was 20-bar in v5)
highest100 = ta.highest(calcH, 100) // Changed from 20 to 100
lowest100 = ta.lowest(calcL, 100) // Changed from 20 to 100
priceRange = highest100 - lowest100
pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest100) / priceRange) * 100
// === V9 NEW: MA GAP ANALYSIS ===
// Calculate 50 and 200 period moving averages on CLOSE (not Heikin Ashi)
// Use standard close for MA calculations to match traditional analysis
ma50 = ta.sma(close, 50)
ma200 = ta.sma(close, 200)
// Calculate MA gap as percentage
// Positive gap = bullish (50 MA above 200 MA)
// Negative gap = bearish (50 MA below 200 MA)
// Values near 0 = convergence (potential crossover brewing)
maGap = ma200 == 0 ? 0.0 : ((ma50 - ma200) / ma200) * 100
// Plot MAs if enabled (for visual reference) - disabled by default for clean chart
// plot(showMAs ? ma50 : na, title="MA 50", color=ma50Color, linewidth=1)
// plot(showMAs ? ma200 : na, title="MA 200", color=ma200Color, linewidth=2)
// v6 NEW FILTERS
// Price position filter - prevent chasing extremes
longPositionOk = not usePricePosition or (pricePosition < longPosMax)
shortPositionOk = not usePricePosition or (pricePosition > shortPosMin)
// Volume filter - avoid dead or overheated moves
volumeOk = not useVolumeFilter or (volumeRatio >= volMin and volumeRatio <= volMax)
// RSI momentum filter
rsiLongOk = not useRsiFilter or (rsi14 >= rsiLongMin and rsi14 <= rsiLongMax)
rsiShortOk = not useRsiFilter or (rsi14 >= rsiShortMin and rsi14 <= rsiShortMax)
// V11: OPTIMIZED STICKY TREND SIGNALS - 10× BETTER THAN V9
// Parameters from exhaustive sweep (2,000/26,244 configs tested)
// Protection: 0.25% flip threshold + 0.10 ATR buffer + ADX 5+ + quality filters
// Result: $4,158 PnL vs v9 $406 baseline (72.5% WR, 1.755 PF, $95 max DD)
// V11 trades 2.7× more signals while maintaining 93% less drawdown
finalLongSignal = buyReady // 🟢 Signal on red → green flip (with threshold)
finalShortSignal = sellReady // 🔴 Signal on green → red flip (with threshold)
plotshape(finalLongSignal, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small)
plotshape(finalShortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small)
// Extract base currency from ticker (e.g., "ETHUSD" -> "ETH", "SOLUSD" -> "SOL")
// CRITICAL: Remove USDT first, then USD (otherwise "FARTCOINUSDT" becomes "FARTCOINT")
baseCurrency = str.replace(syminfo.ticker, "USDT", "")
baseCurrency := str.replace(baseCurrency, "USD", "")
baseCurrency := str.replace(baseCurrency, "PERP", "")
// Indicator version for tracking in database
indicatorVer = "v11"
// Build enhanced alert messages with context (timeframe.period is dynamic)
// V9 NEW: Added MAGAP field for MA gap percentage
longAlertMsg = baseCurrency + " buy " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:" + indicatorVer
shortAlertMsg = baseCurrency + " sell " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:" + indicatorVer
// Fire alerts with dynamic messages (use alert() not alertcondition() for dynamic content)
if finalLongSignal
alert(longAlertMsg, alert.freq_once_per_bar_close)
if finalShortSignal
alert(shortAlertMsg, alert.freq_once_per_bar_close)
// Fill area between price and Money Line
fill(plot(close, display=display.none), plot(upTrend, display=display.none), color=color.new(color.green, 90))
fill(plot(close, display=display.none), plot(downTrend, display=display.none), color=color.new(color.red, 90))