feat: Add per-symbol trading controls for SOL and ETH

- Add SymbolSettings interface with enabled/positionSize/leverage fields
- Implement per-symbol ENV variables (SOLANA_*, ETHEREUM_*)
- Add SOL and ETH sections to settings UI with enable/disable toggles
- Add symbol-specific test buttons (SOL LONG/SHORT, ETH LONG/SHORT)
- Update execute and test endpoints to check symbol enabled status
- Add real-time risk/reward calculator per symbol
- Rename 'Position Sizing' to 'Global Fallback' for clarity
- Fix position manager P&L calculation for externally closed positions
- Fix zero P&L bug affecting 12 historical trades
- Add SQL scripts for recalculating historical P&L data
- Move archive TypeScript files to .archive to fix build

Defaults:
- SOL: 10 base × 10x leverage = 100 notional (profit trading)
- ETH:  base × 1x leverage =  notional (data collection)
- Global: 10 × 10x for BTC and other symbols

Configuration priority: Per-symbol ENV > Market config > Global ENV > Defaults
This commit is contained in:
mindesbunister
2025-11-03 10:28:48 +01:00
parent aa8e9f130a
commit 881a99242d
17 changed files with 1996 additions and 108 deletions

View File

@@ -12,8 +12,8 @@
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-980,
680
-1020,
660
],
"webhookId": "tradingview-bot-v4"
},
@@ -46,8 +46,8 @@
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
-780,
680
-760,
580
]
},
{
@@ -56,7 +56,6 @@
"string": [
{
"value1": "={{ $json.timeframe }}",
"operation": "equals",
"value2": "15"
}
]
@@ -67,8 +66,8 @@
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-560,
540
-760,
260
]
},
{
@@ -126,7 +125,7 @@
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-80,
-120,
660
]
},
@@ -151,7 +150,7 @@
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal Enhanced').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal Enhanced').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal Enhanced').item.json.timeframe }}\",\n \"signalStrength\": \"strong\",\n \"atr\": {{ $('Parse Signal Enhanced').item.json.atr }},\n \"adx\": {{ $('Parse Signal Enhanced').item.json.adx }},\n \"rsi\": {{ $('Parse Signal Enhanced').item.json.rsi }},\n \"volumeRatio\": {{ $('Parse Signal Enhanced').item.json.volumeRatio }},\n \"pricePosition\": {{ $('Parse Signal Enhanced').item.json.pricePosition }},\n \"qualityScore\": {{ $('Check Risk').item.json.qualityScore }}\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}",
"options": {
"timeout": 120000
}
@@ -197,7 +196,7 @@
"values": [
{
"name": "message",
"stringValue": "={{ `🟢 TRADE OPENED\n\n📊 Symbol: ${$('Parse Signal Enhanced').item.json.symbol}\n${$('Parse Signal Enhanced').item.json.direction === 'long' ? '📈' : '📉'} Direction: ${$('Parse Signal Enhanced').item.json.direction.toUpperCase()}\n\n💵 Position: $${$('Execute Trade').item.json.positionSize}\n⚡ Leverage: ${$('Execute Trade').item.json.leverage}x${$('Execute Trade').item.json.qualityScore ? `\n⭐ Quality: ${$('Execute Trade').item.json.qualityScore}/100` : ''}\n\n💰 Entry: $${$('Execute Trade').item.json.entryPrice.toFixed(4)}\n🎯 TP1: $${$('Execute Trade').item.json.takeProfit1.toFixed(4)} (${$('Execute Trade').item.json.tp1Percent}%)\n🎯 TP2: $${$('Execute Trade').item.json.takeProfit2.toFixed(4)} (${$('Execute Trade').item.json.tp2Percent}%)\n🛑 SL: $${$('Execute Trade').item.json.stopLoss.toFixed(4)} (${$('Execute Trade').item.json.stopLossPercent}%)\n\n⏰ ${$now.setZone('Europe/Berlin').toFormat('HH:mm:ss')}\n✅ Position monitored` }}"
"stringValue": "={{ `🟢 TRADE OPENED\n\n📊 Symbol: ${$('Parse Signal Enhanced').item.json.symbol}\n${$('Parse Signal Enhanced').item.json.direction === 'long' ? '📈' : '📉'} Direction: ${$('Parse Signal Enhanced').item.json.direction.toUpperCase()}\n\n💵 Position: $${$('Execute Trade1').item.json.positionSize}\n⚡ Leverage: ${$('Execute Trade1').item.json.leverage}x${$('Execute Trade1').item.json.qualityScore ? `\n\n⭐ Quality: ${$('Execute Trade1').item.json.qualityScore}/100` : ''}\n\n💰 Entry: $${$('Execute Trade1').item.json.entryPrice.toFixed(4)}\n🎯 TP1: $${$('Execute Trade1').item.json.takeProfit1.toFixed(4)} (${$('Execute Trade1').item.json.tp1Percent}%)\n🎯 TP2: $${$('Execute Trade1').item.json.takeProfit2.toFixed(4)} (${$('Execute Trade1').item.json.tp2Percent}%)\n🛑 SL: $${$('Execute Trade1').item.json.stopLoss.toFixed(4)} (${$('Execute Trade1').item.json.stopLossPercent}%)\n\n⏰ ${$now.setZone('Europe/Berlin').toFormat('HH:mm:ss')}\n✅ Position monitored` }}"
}
]
},
@@ -218,7 +217,7 @@
"values": [
{
"name": "message",
"stringValue": "🔴 TRADE FAILED\\n\\n{{ $('Parse Signal').item.json.rawMessage }}\\n\\n❌ Error: {{ $json.error || $json.message }}\\n⏰ {{ $now.setZone('Europe/Berlin').toFormat('HH:mm') }}"
"stringValue": "=🔴 TRADE FAILED\n\n{{ $('Parse Signal Enhanced')').item.json.rawMessage }}\n\n❌ Error: {{ $json.error || $json.message }}\n⏰ {{ $now.setZone('Europe/Berlin').toFormat('HH:mm') }}"
}
]
},
@@ -235,15 +234,11 @@
},
{
"parameters": {
"mode": "manual",
"duplicateItem": false,
"assignments": {
"assignments": [
"fields": {
"values": [
{
"id": "risk_message",
"name": "message",
"value": "={{ '⚠️ TRADE BLOCKED\\n\\n' + $('Parse Signal Enhanced').item.json.rawMessage + '\\n\\n🛑 Reason: ' + $json.reason + '\\n📋 Details: ' + ($json.details || 'N/A') + '\\n\\n📊 Quality Score: ' + ($json.qualityScore || 'N/A') + '/100' + ($json.qualityReasons && $json.qualityReasons.length > 0 ? '\\n⚠ Issues:\\n • ' + $json.qualityReasons.join('\\n • ') : '') + '\\n\\n⏰ ' + $now.setZone('Europe/Berlin').toFormat('HH:mm:ss') }}",
"type": "string"
"stringValue": "={{ '⚠️ TRADE BLOCKED\\n\\n' + $('Parse Signal Enhanced').item.json.rawMessage + '\\n\\n🛑 Reason: ' + $json.reason + '\\n📋 Details: ' + ($json.details || 'N/A') + '\\n\\n📊 Quality Score: ' + ($json.qualityScore || 'N/A') + '/100' + ($json.qualityReasons && $json.qualityReasons.length > 0 ? '\\n⚠ Issues:\\n • ' + $json.qualityReasons.join('\\n • ') : '') + '\\n\\n⏰ ' + $now.setZone('Europe/Berlin').toFormat('HH:mm:ss') }}"
}
]
},
@@ -252,7 +247,7 @@
"id": "da462967-0548-4d57-a6de-cb783c96ac07",
"name": "Format Risk",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"typeVersion": 3.2,
"position": [
60,
760
@@ -333,7 +328,6 @@
"string": [
{
"value1": "={{ $json.timeframe }}",
"operation": "equals",
"value2": "5"
}
]
@@ -344,9 +338,148 @@
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-560,
800
-500,
660
]
},
{
"parameters": {
"chatId": "579304651",
"text": "={{ $json.rawMessage }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "a63da8d2-e255-4933-9fba-a1999d2da31e",
"name": "Trend Signal",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [
-260,
860
],
"credentials": {
"telegramApi": {
"id": "Csk5cg4HtaSqP5jJ",
"name": "Telegram account"
}
}
},
{
"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)/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": "81f28bc7-c96a-4021-acac-242e993d9d98",
"name": "Parse Signal Enhanced",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-740,
860
]
},
{
"parameters": {
"chatId": "579304651",
"text": "={{ $json.rawMessage }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "fa3a214c-9f30-4f2d-b727-68ba1877e4c8",
"name": "Trend Signal1",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [
-500,
860
],
"credentials": {
"telegramApi": {
"id": "Csk5cg4HtaSqP5jJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "http://10.0.0.48:3001/api/trading/execute",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer 2a344f0149442c857fb56c038c0c7d1b113883b830bec792c76f1e0efa15d6bb"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal Enhanced').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal Enhanced').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal Enhanced').item.json.timeframe }}\",\n \"signalStrength\": \"strong\",\n \"atr\": {{ $('Parse Signal Enhanced').item.json.atr }},\n \"adx\": {{ $('Parse Signal Enhanced').item.json.adx }},\n \"rsi\": {{ $('Parse Signal Enhanced').item.json.rsi }},\n \"volumeRatio\": {{ $('Parse Signal Enhanced').item.json.volumeRatio }},\n \"pricePosition\": {{ $('Parse Signal Enhanced').item.json.pricePosition }},\n \"qualityScore\": {{ $input.first().json.qualityScore }}\n}",
"options": {
"timeout": 120000
}
},
"id": "9902ecc4-53b0-402f-8db9-6248e6077740",
"name": "Execute Trade1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
60,
240
],
"credentials": {
"httpHeaderAuth": {
"id": "MATuNdkZclq5ISbr",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "http://10.0.0.48:3001/api/trading/check-risk",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer 2a344f0149442c857fb56c038c0c7d1b113883b830bec792c76f1e0efa15d6bb"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"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": "55671044-c7c8-4566-b271-9369a1c43158",
"name": "Check Risk1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
-280,
240
],
"credentials": {
"httpHeaderAuth": {
"id": "MATuNdkZclq5ISbr",
"name": "Header Auth account"
}
}
}
],
"pinData": {},
@@ -355,7 +488,7 @@
"main": [
[
{
"node": "Parse Signal",
"node": "Parse Signal Enhanced",
"type": "main",
"index": 0
}
@@ -365,11 +498,6 @@
"Parse Signal": {
"main": [
[
{
"node": "15min Chart Only?",
"type": "main",
"index": 0
},
{
"node": "5min Chart Only?1",
"type": "main",
@@ -393,7 +521,7 @@
"main": [
[
{
"node": "Execute Trade",
"node": "Execute Trade1",
"type": "main",
"index": 0
}
@@ -469,22 +597,51 @@
]
]
},
"15min Chart Only?": {
"5min Chart Only?1": {
"main": [
[
{
"node": "Check Risk",
"node": "Check Risk1",
"type": "main",
"index": 0
}
],
[
{
"node": "Trend Signal",
"type": "main",
"index": 0
}
]
]
},
"5min Chart Only?1": {
"Parse Signal Enhanced": {
"main": [
[
{
"node": "Check Risk",
"node": "5min Chart Only?1",
"type": "main",
"index": 0
}
]
]
},
"Execute Trade1": {
"main": [
[
{
"node": "Trade Success?",
"type": "main",
"index": 0
}
]
]
},
"Check Risk1": {
"main": [
[
{
"node": "Risk Passed?",
"type": "main",
"index": 0
}
@@ -496,7 +653,7 @@
"settings": {
"executionOrder": "v1"
},
"versionId": "1376fb3b-08fb-4d96-a038-371249d36eda",
"versionId": "1ec420e9-a965-48bd-8f29-e912aa569431",
"id": "gUDqTiHyHSfRUXv6",
"meta": {
"instanceId": "e766d4f0b5def8ee8cb8561cd9d2b9ba7733e1907990b6987bca40175f82c379"