fix(n8n): CRITICAL - Add quality score validation to old workflow path

PROBLEM:
- Trades with quality score 35 and 45 were executed (threshold: 60)
- Position opened without risk management after signal flips
- "Parse Signal" node didn't extract ATR/ADX/RSI/volumeRatio/pricePosition
- "Check Risk" node only sent symbol+direction, skipped quality validation
- "Execute Trade" node didn't forward metrics to backend

ROOT CAUSE:
n8n workflow had TWO paths:
1. NEW: Parse Signal Enhanced → Check Risk1 → Execute Trade1 (working)
2. OLD: Parse Signal → Check Risk → Execute Trade (broken)

Old path bypassed quality check because check-risk endpoint saw
hasContextMetrics=false and allowed trade without validation.

FIX:
1. Changed "Parse Signal" from 'set' to 'code' node with metric extraction
2. Updated "Check Risk" to send atr/adx/rsi/volumeRatio/pricePosition
3. Updated "Execute Trade" to forward all metrics to backend

IMPACT:
- All trades now validated against quality score threshold (60)
- Low-quality signals properly blocked before execution
- Prevents positions opening without proper risk management

Evidence from database showed 3 trades in 2 hours with scores <60:
- 10:00:31 SOL LONG - Score 35 (phantom detected)
- 09:55:30 SOL SHORT - Score 35 (executed)
- 09:35:14 SOL LONG - Score 45 (executed)

All three should have been blocked. Fix prevents future bypasses.
This commit is contained in:
mindesbunister
2025-11-04 11:18:57 +01:00
parent 8bc08955cc
commit fdbb474e68
2 changed files with 241 additions and 25 deletions

View File

@@ -19,32 +19,12 @@
},
{
"parameters": {
"fields": {
"values": [
{
"name": "rawMessage",
"stringValue": "={{ $json.body }}"
},
{
"name": "symbol",
"stringValue": "={{ $json.body.match(/\\bSOL\\b/i) ? 'SOL-PERP' : ($json.body.match(/\\bBTC\\b/i) ? 'BTC-PERP' : ($json.body.match(/\\bETH\\b/i) ? 'ETH-PERP' : 'SOL-PERP')) }}"
},
{
"name": "direction",
"stringValue": "={{ $json.body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long' }}"
},
{
"name": "timeframe",
"stringValue": "={{ $json.body.match(/\\.P\\s+(\\d+)/)?.[1] || '15' }}"
}
]
},
"options": {}
"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)/i);\nconst symbol = symbolMatch ? symbolMatch[1].toUpperCase() + '-PERP' : 'SOL-PERP';\n\nconst direction = body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long';\n\n// Updated regex to match new format: \"ETH buy 15\" (no .P)\nconst timeframeMatch = body.match(/\\b(buy|sell)\\s+(\\d+|D|W|M)\\b/i);\nconst timeframe = timeframeMatch ? timeframeMatch[2] : '5';\n\n// Parse new context metrics from enhanced format:\n// \"ETH buy 15 | ATR:1.85 | ADX:28.3 | RSI:62.5 | VOL:1.45 | POS:75.3\"\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\nreturn {\n rawMessage: body,\n symbol,\n direction,\n timeframe,\n // New context fields\n atr,\n adx,\n rsi,\n volumeRatio,\n pricePosition\n};"
},
"id": "97d5b0ad-d078-411f-8f34-c9a81d18d921",
"name": "Parse Signal",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-760,
580
@@ -91,7 +71,7 @@
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"symbol\": \"{{ $json.symbol }}\",\n \"direction\": \"{{ $json.direction }}\"\n}",
"jsonBody": "={\n \"symbol\": \"{{ $json.symbol }}\",\n \"direction\": \"{{ $json.direction }}\",\n \"atr\": {{ $json.atr || 0 }},\n \"adx\": {{ $json.adx || 0 }},\n \"rsi\": {{ $json.rsi || 0 }},\n \"volumeRatio\": {{ $json.volumeRatio || 0 }},\n \"pricePosition\": {{ $json.pricePosition || 0 }}\n}",
"options": {}
},
"id": "c1165de4-2095-4f5f-b9b1-18e76fd8c47b",
@@ -150,7 +130,7 @@
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal').item.json.timeframe }}\",\n \"signalStrength\": \"strong\"\n}",
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal').item.json.timeframe }}\",\n \"signalStrength\": \"strong\",\n \"atr\": {{ $('Parse Signal').item.json.atr }},\n \"adx\": {{ $('Parse Signal').item.json.adx }},\n \"rsi\": {{ $('Parse Signal').item.json.rsi }},\n \"volumeRatio\": {{ $('Parse Signal').item.json.volumeRatio }},\n \"pricePosition\": {{ $('Parse Signal').item.json.pricePosition }}\n}",
"options": {
"timeout": 120000
}