feat: Deploy HA auto-failover with database promotion

- Enhanced DNS failover monitor on secondary (72.62.39.24)
- Auto-promotes database: pg_ctl promote on failover
- Creates DEMOTED flag on primary via SSH (split-brain protection)
- Telegram notifications with database promotion status
- Startup safety script ready (integration pending)
- 90-second automatic recovery vs 10-30 min manual
- Zero-cost 95% enterprise HA benefit

Status: DEPLOYED and MONITORING (14:52 CET)
Next: Controlled failover test during maintenance
This commit is contained in:
mindesbunister
2025-12-12 15:54:03 +01:00
parent 7ff5c5b3a4
commit d637aac2d7
25 changed files with 1071 additions and 170 deletions

View File

@@ -52,7 +52,7 @@ alertcondition(true, title="1min Market Data")
// Build dynamic payload and emit an alert once per bar close
alertPayload = '{' +
'"action": "market_data_1min",' +
'"symbol": "{{ticker}}",' +
'"symbol": "' + syminfo.ticker + '",' +
'"timeframe": "1",' +
'"atr": ' + str.tostring(atr, "#.########") + ',' +
'"adx": ' + str.tostring(adxVal, "#.########") + ',' +
@@ -60,8 +60,8 @@ alertPayload = '{' +
'"volumeRatio": ' + str.tostring(volumeRatio, "#.########") + ',' +
'"pricePosition": ' + str.tostring(pricePosition, "#.########") + ',' +
'"currentPrice": ' + str.tostring(close, "#.########") + ',' +
'"timestamp": "{{timenow}}",' +
'"exchange": "{{exchange}}",' +
'"timestamp": "' + str.tostring(time, "yyyy-MM-dd'T'HH:mm:ss'Z'") + '",' +
'"exchange": "' + (syminfo.prefix != '' ? syminfo.prefix : 'UNKNOWN') + '",' +
'"indicatorVersion": "v9"' +
'}'

View File

@@ -19,7 +19,7 @@
},
{
"parameters": {
"jsCode": "// Get the body - it might be a string or nested in an object\nlet body = $json.body || $json.query?.body || JSON.stringify($json);\n\n// If body is an object, stringify it\nif (typeof body === 'object') {\n body = JSON.stringify(body);\n}\n\n// Parse basic signal (existing logic)\nconst symbolMatch = body.match(/\\b(SOL|BTC|ETH)\\b/i);\nconst symbol = symbolMatch ? symbolMatch[1].toUpperCase() + '-PERP' : 'SOL-PERP';\n\nconst direction = body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long';\n\n// Enhanced timeframe extraction supporting multiple formats:\n// - \"buy 5\" → \"5\"\n// - \"buy 15\" → \"15\"\n// - \"buy 60\" or \"buy 1h\" → \"60\"\n// - \"buy 240\" or \"buy 4h\" → \"240\"\n// - \"buy D\" or \"buy 1d\" → \"D\"\n// - \"buy W\" → \"W\"\nconst timeframeMatch = body.match(/\\b(buy|sell)\\s+(\\d+|D|W|M|1h|4h|1d)\\b/i);\nlet timeframe = '5'; // Default to 5min\n\nif (timeframeMatch) {\n const tf = timeframeMatch[2];\n // Convert hour/day notation to minutes\n if (tf === '1h' || tf === '60') {\n timeframe = '60';\n } else if (tf === '4h' || tf === '240') {\n timeframe = '240';\n } else if (tf === '1d' || tf.toUpperCase() === 'D') {\n timeframe = 'D';\n } else if (tf.toUpperCase() === 'W') {\n timeframe = 'W';\n } else if (tf.toUpperCase() === 'M') {\n timeframe = 'M';\n } else {\n timeframe = tf;\n }\n}\n\n// Parse new context metrics from enhanced format:\n// \"SOLT.P buy 15 | ATR:0.65 | ADX:14.3 | RSI:51.3 | VOL:0.87 | POS:59.3 | IND:v8\"\nconst atrMatch = body.match(/ATR:([\\d.]+)/);\nconst atr = atrMatch ? parseFloat(atrMatch[1]) : 0;\n\nconst adxMatch = body.match(/ADX:([\\d.]+)/);\nconst adx = adxMatch ? parseFloat(adxMatch[1]) : 0;\n\nconst rsiMatch = body.match(/RSI:([\\d.]+)/);\nconst rsi = rsiMatch ? parseFloat(rsiMatch[1]) : 0;\n\nconst volumeMatch = body.match(/VOL:([\\d.]+)/);\nconst volumeRatio = volumeMatch ? parseFloat(volumeMatch[1]) : 0;\n\nconst pricePositionMatch = body.match(/POS:([\\d.]+)/);\nconst pricePosition = pricePositionMatch ? parseFloat(pricePositionMatch[1]) : 0;\n\n// Parse indicator version (optional, backward compatible)\nconst indicatorVersionMatch = body.match(/IND:(v\\d+)/i);\nconst indicatorVersion = indicatorVersionMatch ? indicatorVersionMatch[1] : 'v8';\n\nreturn {\n rawMessage: body,\n symbol,\n direction,\n timeframe,\n // Context fields\n atr,\n adx,\n rsi,\n volumeRatio,\n pricePosition,\n // Version tracking (defaults to v8 for backward compatibility)\n indicatorVersion\n};"
"jsCode": "// Get the body - it might be a string or nested in an object\nlet body = $json.body || $json.query?.body || JSON.stringify($json);\n\n// If body is an object, stringify it\nif (typeof body === 'object') {\n body = JSON.stringify(body);\n}\n\n// Forward market data payloads directly to /api/trading/market-data and stop workflow\ntry {\n const parsed = JSON.parse(body);\n const action = parsed?.action?.toLowerCase?.() || '';\n if (action.startsWith('market_data')) {\n await fetch('http://10.0.0.48:3001/api/trading/market-data', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(parsed),\n });\n return []; // Halt further nodes (no Telegram/risk)\n }\n} catch (err) {\n // Body isn't JSON, keep processing\n}\nif (/market_data/i.test(body)) {\n // Fallback: drop unknown market_data text payloads\n return [];\n}\n\n// Parse basic signal (existing logic)\nconst symbolMatch = body.match(/\\b(SOL|BTC|ETH)\\b/i);\nconst symbol = symbolMatch ? symbolMatch[1].toUpperCase() + '-PERP' : 'SOL-PERP';\n\nconst direction = body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long';\n\n// Enhanced timeframe extraction supporting multiple formats:\n// - \"buy 5\" → \"5\"\n// - \"buy 15\" → \"15\"\n// - \"buy 60\" or \"buy 1h\" → \"60\"\n// - \"buy 240\" or \"buy 4h\" → \"240\"\n// - \"buy D\" or \"buy 1d\" → \"D\"\n// - \"buy W\" → \"W\"\nconst timeframeMatch = body.match(/\\b(buy|sell)\\s+(\\d+|D|W|M|1h|4h|1d)\\b/i);\nlet timeframe = '5'; // Default to 5min\n\nif (timeframeMatch) {\n const tf = timeframeMatch[2];\n // Convert hour/day notation to minutes\n if (tf === '1h' || tf === '60') {\n timeframe = '60';\n } else if (tf === '4h' || tf === '240') {\n timeframe = '240';\n } else if (tf === '1d' || tf.toUpperCase() === 'D') {\n timeframe = 'D';\n } else if (tf.toUpperCase() === 'W') {\n timeframe = 'W';\n } else if (tf.toUpperCase() === 'M') {\n timeframe = 'M';\n } else {\n timeframe = tf;\n }\n}\n\n// Parse new context metrics from enhanced format:\n// \"SOLT.P buy 15 | ATR:0.65 | ADX:14.3 | RSI:51.3 | VOL:0.87 | POS:59.3 | IND:v8\"\nconst atrMatch = body.match(/ATR:([\\d.]+)/);\nconst atr = atrMatch ? parseFloat(atrMatch[1]) : 0;\n\nconst adxMatch = body.match(/ADX:([\\d.]+)/);\nconst adx = adxMatch ? parseFloat(adxMatch[1]) : 0;\n\nconst rsiMatch = body.match(/RSI:([\\d.]+)/);\nconst rsi = rsiMatch ? parseFloat(rsiMatch[1]) : 0;\n\nconst volumeMatch = body.match(/VOL:([\\d.]+)/);\nconst volumeRatio = volumeMatch ? parseFloat(volumeMatch[1]) : 0;\n\nconst pricePositionMatch = body.match(/POS:([\\d.]+)/);\nconst pricePosition = pricePositionMatch ? parseFloat(pricePositionMatch[1]) : 0;\n\n// Parse indicator version (optional, backward compatible)\nconst indicatorVersionMatch = body.match(/IND:(v\\d+)/i);\nconst indicatorVersion = indicatorVersionMatch ? indicatorVersionMatch[1] : 'v8';\n\nreturn {\n rawMessage: body,\n symbol,\n direction,\n timeframe,\n // Context fields\n atr,\n adx,\n rsi,\n volumeRatio,\n pricePosition,\n // Version tracking (defaults to v8 for backward compatibility)\n indicatorVersion\n};"
},
"id": "97d5b0ad-d078-411f-8f34-c9a81d18d921",
"name": "Parse Signal Enhanced",

View File

@@ -0,0 +1,102 @@
{
"name": "Market Data Forwarder",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "market-data-1min",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-market-data",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [400, 300],
"webhookId": "market-data-1min"
},
{
"parameters": {
"jsCode": "// Parse incoming JSON payload from TradingView\nlet payload;\n\ntry {\n // TradingView sends the alert message as plain text body\n // n8n may receive it as $json.body (string) or already parsed\n if (typeof $json.body === 'string') {\n payload = JSON.parse($json.body);\n } else if ($json.action) {\n // Already parsed JSON\n payload = $json;\n } else if (typeof $json === 'string') {\n payload = JSON.parse($json);\n } else {\n // Assume body is the whole thing\n payload = $json;\n }\n} catch (e) {\n console.error('Failed to parse JSON:', e);\n return { error: 'Invalid JSON', raw: $json };\n}\n\n// Validate required fields\nif (!payload.action || !payload.action.includes('market_data')) {\n return { error: 'Not a market data payload', action: payload.action };\n}\n\nreturn payload;"
},
"id": "parse-json",
"name": "Parse JSON",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [600, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://10.0.0.48:3001/api/trading/market-data",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json }}",
"options": {}
},
"id": "http-request",
"name": "Forward to Bot",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [800, 300]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}"
},
"id": "respond",
"name": "Respond",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [1000, 300]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Parse JSON",
"type": "main",
"index": 0
}
]
]
},
"Parse JSON": {
"main": [
[
{
"node": "Forward to Bot",
"type": "main",
"index": 0
}
]
]
},
"Forward to Bot": {
"main": [
[
{
"node": "Respond",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}

View File

@@ -2,21 +2,29 @@
indicator("Money Line - 1min Data Feed (OPTIMIZED)", overlay=false)
// ==========================================
// PURPOSE: Send ONLY essential market data every 1 minute (price + ADX)
// OPTIMIZED (Dec 4, 2025): Reduced from 8 metrics to 2 metrics (75% smaller payload)
//
// WHY: Systems only use currentPrice and ADX from 1-minute data:
// - Price: Smart Validation Queue price confirmation
// - ADX: Adaptive trailing stop (Phase 7.3) + Revenge system validation
// - Removed: ATR, RSI, volumeRatio, pricePosition, maGap, volume (NOT used)
//
// PURPOSE: Send 1-minute market data for Smart Validation Queue price confirms
// OPTIMIZED (Dec 4, 2025): Keep payload minimal but include fields the webhook expects
//
// WHY: Smart Validation Queue needs fresh price every minute to detect +0.15%/+0.3% moves
// - Required fields for /api/trading/market-data: action, symbol, timeframe, currentPrice, adx
// - Optional fields (sent for compatibility): atr, rsi, volumeRatio, pricePosition, timestamp
//
// USAGE: Create alert on indicator with "alert() function calls"
// WEBHOOK: https://flow.egonetix.de/webhook/tradingview-bot-v4 (SAME as trading signals)
// FORMAT: Uses trading signal format with timeframe="1" (gets filtered like 15min/1H/Daily)
// FORMAT: JSON payload with action="market_data_1min" and timeframe="1"
// ==========================================
// Calculate ONLY what we actually use
[diPlus, diMinus, adx] = ta.dmi(14, 14) // ADX for adaptive trailing + revenge validation
atrVal = ta.atr(14)
rsiVal = ta.rsi(close, 14)
smaVol20 = ta.sma(volume, 20)
volRatio = na(smaVol20) or smaVol20 == 0 ? 1.0 : volume / smaVol20
rangeHigh = ta.highest(high, 100)
rangeLow = ta.lowest(low, 100)
pricePos = rangeHigh == rangeLow ? 50.0 : (close - rangeLow) / (rangeHigh - rangeLow) * 100
volRatioVal = nz(volRatio, 1.0)
pricePosVal = nz(pricePos, 50.0)
// Display ADX (visual confirmation)
plot(adx, "ADX", color=color.blue, linewidth=2)
@@ -24,12 +32,15 @@ plot(close, "Price", color=color.white, linewidth=1)
hline(20, "ADX 20", color=color.gray, linestyle=hline.style_dashed)
hline(25, "ADX 25", color=color.orange, linestyle=hline.style_dashed)
// Build OPTIMIZED message - ONLY price + ADX (75% smaller than old format)
// Direction doesn't matter - bot filters by timeframe before executing
// This follows same pattern as 15min/1H/Daily data collection
// CRITICAL: Include @ price format so n8n parser extracts signalPrice correctly
// CRITICAL (Dec 7, 2025): Use syminfo.ticker for multi-asset support (SOL, FARTCOIN, etc.)
jsonMessage = syminfo.ticker + ' buy 1 @ ' + str.tostring(close) + ' | ADX:' + str.tostring(adx) + ' | IND:v9'
// Build JSON payload for the market-data webhook (consumed by /api/trading/market-data)
jsonMessage = '{"action":"market_data_1min","symbol":"' + syminfo.ticker + '",' +
'"timeframe":"1","currentPrice":' + str.tostring(close) +
',"adx":' + str.tostring(adx) +
',"atr":' + str.tostring(atrVal) +
',"rsi":' + str.tostring(rsiVal) +
',"volumeRatio":' + str.tostring(volRatioVal) +
',"pricePosition":' + str.tostring(pricePosVal) +
',"timestamp":' + str.tostring(timenow) + '}'
// Send alert every bar close (every 1 minute on 1min chart)
if barstate.isconfirmed