chore: Organize workspace structure - move docs, workflows, scripts to subdirectories

Organization:
- Created docs/ with setup/, guides/, history/ subdirectories
- Created workflows/ with trading/, analytics/, telegram/, archive/ subdirectories
- Created scripts/ with docker/, setup/, testing/ subdirectories
- Created tests/ for TypeScript test files
- Created archive/ for unused reference files

Moved files:
- 17 documentation files → docs/
- 16 workflow JSON files → workflows/
- 10 shell scripts → scripts/
- 4 test files → tests/
- 5 unused files → archive/

Updated:
- README.md with new file structure and documentation paths

Deleted:
- data/ (empty directory)
- screenshots/ (empty directory)

Critical files remain in root:
- telegram_command_bot.py (active bot - used by Dockerfile)
- watch-restart.sh (systemd service dependency)
- All Dockerfiles and docker-compose files
- All environment files

Validation:
 Containers running (trading-bot-v4, telegram-trade-bot, postgres)
 API responding (positions endpoint tested)
 Telegram bot functional (/status command tested)
 All critical files present in root

No code changes - purely organizational.
System continues running without interruption.

Recovery: git revert HEAD or git reset --hard cleanup-before
This commit is contained in:
mindesbunister
2025-10-27 12:59:25 +01:00
parent f8f289232a
commit 14d5de2c64
48 changed files with 37 additions and 14 deletions

View File

@@ -0,0 +1,171 @@
{
"name": "Daily Trading Report",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 0,
"triggerAtMinute": 5
}
]
}
},
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"name": "Every Day at Midnight",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [240, 300]
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Get yesterday's trading activity\nSELECT \n COUNT(*) as total_trades,\n COUNT(CASE WHEN \"realizedPnL\" > 0 THEN 1 END) as winning_trades,\n COUNT(CASE WHEN \"realizedPnL\" < 0 THEN 1 END) as losing_trades,\n SUM(\"realizedPnL\") as total_pnl,\n AVG(\"realizedPnL\") as avg_pnl,\n MAX(\"realizedPnL\") as best_trade,\n MIN(\"realizedPnL\") as worst_trade,\n AVG(\"holdTimeSeconds\") / 60 as avg_hold_time_minutes\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\n AND \"exitTime\" >= CURRENT_DATE - INTERVAL '1 day'\n AND \"exitTime\" < CURRENT_DATE;"
},
"id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"name": "Query Yesterday Stats",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 300],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Get breakdown by symbol\nSELECT \n symbol,\n COUNT(*) as trades,\n SUM(\"realizedPnL\") as pnl\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\n AND \"exitTime\" >= CURRENT_DATE - INTERVAL '1 day'\n AND \"exitTime\" < CURRENT_DATE\nGROUP BY symbol\nORDER BY pnl DESC;"
},
"id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"name": "Query Symbol Breakdown",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 500],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"operation": "insert",
"schema": {
"__rl": true,
"value": "public",
"mode": "list",
"cachedResultName": "public"
},
"table": {
"__rl": true,
"value": "DailyStats",
"mode": "list",
"cachedResultName": "DailyStats"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"date": "={{ $json.date }}",
"tradesCount": "={{ $json.total_trades }}",
"winningTrades": "={{ $json.winning_trades }}",
"losingTrades": "={{ $json.losing_trades }}",
"totalPnL": "={{ $json.total_pnl }}",
"totalPnLPercent": "0",
"winRate": "={{ $json.win_rate }}",
"avgWin": "={{ $json.avg_win }}",
"avgLoss": "={{ $json.avg_loss }}",
"profitFactor": "={{ $json.profit_factor }}",
"maxDrawdown": "0",
"sharpeRatio": "0"
}
}
},
"id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"name": "Save Daily Stats",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [900, 300],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"jsCode": "const stats = $('Query Yesterday Stats').first().json;\nconst symbols = $('Query Symbol Breakdown').all();\n\nconst winRate = stats.total_trades > 0 ? (stats.winning_trades / stats.total_trades) * 100 : 0;\nconst avgWin = stats.winning_trades > 0 ? stats.total_pnl / stats.winning_trades : 0;\nconst avgLoss = stats.losing_trades > 0 ? Math.abs(stats.total_pnl / stats.losing_trades) : 0;\nconst profitFactor = avgLoss !== 0 ? avgWin / avgLoss : 0;\n\nconst yesterday = new Date();\nyesterday.setDate(yesterday.getDate() - 1);\nyesterday.setHours(0, 0, 0, 0);\n\nreturn [{\n json: {\n date: yesterday.toISOString(),\n total_trades: parseInt(stats.total_trades) || 0,\n winning_trades: parseInt(stats.winning_trades) || 0,\n losing_trades: parseInt(stats.losing_trades) || 0,\n total_pnl: parseFloat(stats.total_pnl) || 0,\n win_rate: winRate,\n avg_win: avgWin,\n avg_loss: avgLoss,\n profit_factor: profitFactor,\n symbols: symbols.map(s => s.json)\n }\n}];"
},
"id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
"name": "Process Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [680, 300]
}
],
"connections": {
"Every Day at Midnight": {
"main": [
[
{
"node": "Query Yesterday Stats",
"type": "main",
"index": 0
},
{
"node": "Query Symbol Breakdown",
"type": "main",
"index": 0
}
]
]
},
"Query Yesterday Stats": {
"main": [
[
{
"node": "Process Data",
"type": "main",
"index": 0
}
]
]
},
"Query Symbol Breakdown": {
"main": [
[
{
"node": "Process Data",
"type": "main",
"index": 0
}
]
]
},
"Process Data": {
"main": [
[
{
"node": "Save Daily Stats",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 0,
"updatedAt": "2025-10-27T00:00:00.000Z",
"versionId": "1"
}

View File

@@ -0,0 +1,73 @@
{
"name": "Trading Database Analytics",
"nodes": [
{
"parameters": {},
"id": "7c2dbef4-8f5f-4c0e-9f3c-4e5c5d8f2a1b",
"name": "When clicking 'Test workflow'",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Get all closed trades with performance metrics\nSELECT \n id,\n symbol,\n direction,\n \"entryPrice\",\n \"exitPrice\",\n \"positionSizeUSD\",\n leverage,\n \"realizedPnL\",\n \"realizedPnLPercent\",\n \"holdTimeSeconds\",\n \"exitReason\",\n \"entryTime\",\n \"exitTime\",\n \"isTestTrade\",\n CASE \n WHEN \"realizedPnL\" > 0 THEN 'WIN'\n WHEN \"realizedPnL\" < 0 THEN 'LOSS'\n ELSE 'BREAKEVEN'\n END as trade_result\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\nORDER BY \"exitTime\" DESC\nLIMIT 100;"
},
"id": "3f9c1d7b-2e4a-4c8f-9b1e-6d8e5c3f2a4b",
"name": "Query Closed Trades",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 300],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"jsCode": "// Calculate trading statistics from closed trades\nconst trades = $input.all();\n\nif (trades.length === 0) {\n return [{\n json: {\n message: 'No closed trades found',\n totalTrades: 0\n }\n }];\n}\n\nconst wins = trades.filter(t => t.json.trade_result === 'WIN');\nconst losses = trades.filter(t => t.json.trade_result === 'LOSS');\n\nconst totalPnL = trades.reduce((sum, t) => sum + parseFloat(t.json.realizedPnL || 0), 0);\nconst avgWin = wins.length > 0 ? wins.reduce((sum, t) => sum + parseFloat(t.json.realizedPnL), 0) / wins.length : 0;\nconst avgLoss = losses.length > 0 ? losses.reduce((sum, t) => sum + parseFloat(t.json.realizedPnL), 0) / losses.length : 0;\nconst winRate = (wins.length / trades.length) * 100;\nconst profitFactor = avgLoss !== 0 ? Math.abs(avgWin / avgLoss) : 0;\n\n// Calculate average hold time\nconst avgHoldTime = trades.reduce((sum, t) => sum + parseInt(t.json.holdTimeSeconds || 0), 0) / trades.length;\n\n// Group by symbol\nconst bySymbol = {};\ntrades.forEach(t => {\n const symbol = t.json.symbol;\n if (!bySymbol[symbol]) {\n bySymbol[symbol] = { trades: 0, wins: 0, pnl: 0 };\n }\n bySymbol[symbol].trades++;\n if (t.json.trade_result === 'WIN') bySymbol[symbol].wins++;\n bySymbol[symbol].pnl += parseFloat(t.json.realizedPnL || 0);\n});\n\n// Group by direction\nconst longs = trades.filter(t => t.json.direction === 'long');\nconst shorts = trades.filter(t => t.json.direction === 'short');\n\nconst longWins = longs.filter(t => t.json.trade_result === 'WIN').length;\nconst shortWins = shorts.filter(t => t.json.trade_result === 'WIN').length;\n\n// Group by exit reason\nconst exitReasons = {};\ntrades.forEach(t => {\n const reason = t.json.exitReason || 'unknown';\n if (!exitReasons[reason]) {\n exitReasons[reason] = { count: 0, pnl: 0 };\n }\n exitReasons[reason].count++;\n exitReasons[reason].pnl += parseFloat(t.json.realizedPnL || 0);\n});\n\nreturn [{\n json: {\n summary: {\n totalTrades: trades.length,\n winningTrades: wins.length,\n losingTrades: losses.length,\n winRate: winRate.toFixed(2) + '%',\n totalPnL: '$' + totalPnL.toFixed(2),\n avgWin: '$' + avgWin.toFixed(2),\n avgLoss: '$' + avgLoss.toFixed(2),\n profitFactor: profitFactor.toFixed(2),\n avgHoldTimeMinutes: (avgHoldTime / 60).toFixed(1)\n },\n bySymbol,\n byDirection: {\n long: {\n total: longs.length,\n wins: longWins,\n winRate: longs.length > 0 ? ((longWins / longs.length) * 100).toFixed(2) + '%' : '0%'\n },\n short: {\n total: shorts.length,\n wins: shortWins,\n winRate: shorts.length > 0 ? ((shortWins / shorts.length) * 100).toFixed(2) + '%' : '0%'\n }\n },\n exitReasons,\n timestamp: new Date().toISOString()\n }\n}];"
},
"id": "8a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
"name": "Calculate Statistics",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [680, 300]
}
],
"connections": {
"When clicking 'Test workflow'": {
"main": [
[
{
"node": "Query Closed Trades",
"type": "main",
"index": 0
}
]
]
},
"Query Closed Trades": {
"main": [
[
{
"node": "Calculate Statistics",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 0,
"updatedAt": "2025-10-27T00:00:00.000Z",
"versionId": "1"
}

View File

@@ -0,0 +1,139 @@
{
"name": "Pattern Analysis - Win Rate by Hour",
"nodes": [
{
"parameters": {},
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Win rate analysis by hour of day\nSELECT \n EXTRACT(HOUR FROM \"entryTime\") as hour,\n COUNT(*) as total_trades,\n COUNT(CASE WHEN \"realizedPnL\" > 0 THEN 1 END) as wins,\n COUNT(CASE WHEN \"realizedPnL\" < 0 THEN 1 END) as losses,\n ROUND(COUNT(CASE WHEN \"realizedPnL\" > 0 THEN 1 END)::numeric / NULLIF(COUNT(*), 0) * 100, 2) as win_rate_pct,\n ROUND(SUM(\"realizedPnL\")::numeric, 2) as total_pnl,\n ROUND(AVG(\"realizedPnL\")::numeric, 4) as avg_pnl\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\n AND \"entryTime\" >= NOW() - INTERVAL '30 days'\nGROUP BY EXTRACT(HOUR FROM \"entryTime\")\nORDER BY hour;"
},
"id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"name": "Query Hourly Performance",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 300],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Win rate by day of week\nSELECT \n EXTRACT(DOW FROM \"entryTime\") as day_of_week,\n CASE EXTRACT(DOW FROM \"entryTime\")\n WHEN 0 THEN 'Sunday'\n WHEN 1 THEN 'Monday'\n WHEN 2 THEN 'Tuesday'\n WHEN 3 THEN 'Wednesday'\n WHEN 4 THEN 'Thursday'\n WHEN 5 THEN 'Friday'\n WHEN 6 THEN 'Saturday'\n END as day_name,\n COUNT(*) as total_trades,\n COUNT(CASE WHEN \"realizedPnL\" > 0 THEN 1 END) as wins,\n ROUND(COUNT(CASE WHEN \"realizedPnL\" > 0 THEN 1 END)::numeric / NULLIF(COUNT(*), 0) * 100, 2) as win_rate_pct,\n ROUND(SUM(\"realizedPnL\")::numeric, 2) as total_pnl\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\n AND \"entryTime\" >= NOW() - INTERVAL '30 days'\nGROUP BY EXTRACT(DOW FROM \"entryTime\")\nORDER BY day_of_week;"
},
"id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"name": "Query Daily Performance",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 500],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Hold time vs profitability\nSELECT \n CASE \n WHEN \"holdTimeSeconds\" < 300 THEN '0-5 min'\n WHEN \"holdTimeSeconds\" < 900 THEN '5-15 min'\n WHEN \"holdTimeSeconds\" < 1800 THEN '15-30 min'\n WHEN \"holdTimeSeconds\" < 3600 THEN '30-60 min'\n WHEN \"holdTimeSeconds\" < 7200 THEN '1-2 hours'\n ELSE '2+ hours'\n END as hold_time_bucket,\n COUNT(*) as trades,\n COUNT(CASE WHEN \"realizedPnL\" > 0 THEN 1 END) as wins,\n ROUND(COUNT(CASE WHEN \"realizedPnL\" > 0 THEN 1 END)::numeric / NULLIF(COUNT(*), 0) * 100, 2) as win_rate_pct,\n ROUND(AVG(\"realizedPnL\")::numeric, 4) as avg_pnl,\n ROUND(SUM(\"realizedPnL\")::numeric, 2) as total_pnl\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\n AND \"holdTimeSeconds\" IS NOT NULL\n AND \"entryTime\" >= NOW() - INTERVAL '30 days'\nGROUP BY \n CASE \n WHEN \"holdTimeSeconds\" < 300 THEN '0-5 min'\n WHEN \"holdTimeSeconds\" < 900 THEN '5-15 min'\n WHEN \"holdTimeSeconds\" < 1800 THEN '15-30 min'\n WHEN \"holdTimeSeconds\" < 3600 THEN '30-60 min'\n WHEN \"holdTimeSeconds\" < 7200 THEN '1-2 hours'\n ELSE '2+ hours'\n END\nORDER BY \n CASE \n WHEN hold_time_bucket = '0-5 min' THEN 1\n WHEN hold_time_bucket = '5-15 min' THEN 2\n WHEN hold_time_bucket = '15-30 min' THEN 3\n WHEN hold_time_bucket = '30-60 min' THEN 4\n WHEN hold_time_bucket = '1-2 hours' THEN 5\n ELSE 6\n END;"
},
"id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"name": "Query Hold Time Analysis",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 700],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"jsCode": "const hourly = $('Query Hourly Performance').all().map(item => item.json);\nconst daily = $('Query Daily Performance').all().map(item => item.json);\nconst holdTime = $('Query Hold Time Analysis').all().map(item => item.json);\n\n// Find best and worst hours\nconst sortedHours = [...hourly].sort((a, b) => b.win_rate_pct - a.win_rate_pct);\nconst bestHours = sortedHours.slice(0, 3);\nconst worstHours = sortedHours.slice(-3).reverse();\n\n// Find best day\nconst sortedDays = [...daily].sort((a, b) => b.win_rate_pct - a.win_rate_pct);\nconst bestDay = sortedDays[0];\nconst worstDay = sortedDays[sortedDays.length - 1];\n\n// Find optimal hold time\nconst sortedHoldTime = [...holdTime].sort((a, b) => b.avg_pnl - a.avg_pnl);\nconst optimalHoldTime = sortedHoldTime[0];\n\n// Generate insights\nconst insights = {\n hourly: {\n bestHours: bestHours.map(h => `${h.hour}:00 (${h.win_rate_pct}% win rate, ${h.total_trades} trades)`),\n worstHours: worstHours.map(h => `${h.hour}:00 (${h.win_rate_pct}% win rate, ${h.total_trades} trades)`),\n recommendation: bestHours.length > 0 ? `Focus trading around ${bestHours[0].hour}:00-${bestHours[2].hour}:00` : 'Need more data'\n },\n daily: {\n bestDay: bestDay ? `${bestDay.day_name} (${bestDay.win_rate_pct}% win rate)` : 'N/A',\n worstDay: worstDay ? `${worstDay.day_name} (${worstDay.win_rate_pct}% win rate)` : 'N/A',\n recommendation: bestDay && worstDay ? `Trade more on ${bestDay.day_name}, avoid ${worstDay.day_name}` : 'Need more data'\n },\n holdTime: {\n optimal: optimalHoldTime ? `${optimalHoldTime.hold_time_bucket} (avg P&L: $${optimalHoldTime.avg_pnl})` : 'N/A',\n recommendation: optimalHoldTime ? `Target exits in ${optimalHoldTime.hold_time_bucket} range` : 'Need more data'\n },\n rawData: {\n hourly,\n daily,\n holdTime\n }\n};\n\nreturn [{ json: insights }];"
},
"id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
"name": "Generate Insights",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [680, 300]
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Query Hourly Performance",
"type": "main",
"index": 0
},
{
"node": "Query Daily Performance",
"type": "main",
"index": 0
},
{
"node": "Query Hold Time Analysis",
"type": "main",
"index": 0
}
]
]
},
"Query Hourly Performance": {
"main": [
[
{
"node": "Generate Insights",
"type": "main",
"index": 0
}
]
]
},
"Query Daily Performance": {
"main": [
[
{
"node": "Generate Insights",
"type": "main",
"index": 0
}
]
]
},
"Query Hold Time Analysis": {
"main": [
[
{
"node": "Generate Insights",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 0,
"updatedAt": "2025-10-27T00:00:00.000Z",
"versionId": "1"
}

View File

@@ -0,0 +1,139 @@
{
"name": "Stop Loss Analysis - Which Stops Get Hit",
"nodes": [
{
"parameters": {},
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Exit reason breakdown\nSELECT \n \"exitReason\",\n COUNT(*) as count,\n ROUND(COUNT(*)::numeric / (SELECT COUNT(*) FROM \"Trade\" WHERE status = 'closed' AND \"isTestTrade\" = false) * 100, 2) as percentage,\n ROUND(AVG(\"realizedPnL\")::numeric, 4) as avg_pnl,\n ROUND(SUM(\"realizedPnL\")::numeric, 2) as total_pnl,\n ROUND(AVG(\"holdTimeSeconds\")::numeric / 60, 1) as avg_hold_minutes\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\n AND \"entryTime\" >= NOW() - INTERVAL '30 days'\nGROUP BY \"exitReason\"\nORDER BY count DESC;"
},
"id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"name": "Query Exit Reasons",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 300],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Stop loss effectiveness by distance\nSELECT \n CASE \n WHEN ABS((\"stopLossPrice\" - \"entryPrice\") / \"entryPrice\" * 100) < 0.5 THEN 'Very Tight (< 0.5%)'\n WHEN ABS((\"stopLossPrice\" - \"entryPrice\") / \"entryPrice\" * 100) < 1.0 THEN 'Tight (0.5-1%)'\n WHEN ABS((\"stopLossPrice\" - \"entryPrice\") / \"entryPrice\" * 100) < 1.5 THEN 'Normal (1-1.5%)'\n WHEN ABS((\"stopLossPrice\" - \"entryPrice\") / \"entryPrice\" * 100) < 2.0 THEN 'Wide (1.5-2%)'\n ELSE 'Very Wide (> 2%)'\n END as sl_distance,\n COUNT(*) as total_trades,\n COUNT(CASE WHEN \"exitReason\" LIKE '%stop%' THEN 1 END) as stopped_out,\n ROUND(COUNT(CASE WHEN \"exitReason\" LIKE '%stop%' THEN 1 END)::numeric / NULLIF(COUNT(*), 0) * 100, 2) as stop_hit_rate,\n ROUND(AVG(\"realizedPnL\")::numeric, 4) as avg_pnl\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\n AND \"stopLossPrice\" IS NOT NULL\n AND \"entryPrice\" > 0\n AND \"entryTime\" >= NOW() - INTERVAL '30 days'\nGROUP BY \n CASE \n WHEN ABS((\"stopLossPrice\" - \"entryPrice\") / \"entryPrice\" * 100) < 0.5 THEN 'Very Tight (< 0.5%)'\n WHEN ABS((\"stopLossPrice\" - \"entryPrice\") / \"entryPrice\" * 100) < 1.0 THEN 'Tight (0.5-1%)'\n WHEN ABS((\"stopLossPrice\" - \"entryPrice\") / \"entryPrice\" * 100) < 1.5 THEN 'Normal (1-1.5%)'\n WHEN ABS((\"stopLossPrice\" - \"entryPrice\") / \"entryPrice\" * 100) < 2.0 THEN 'Wide (1.5-2%)'\n ELSE 'Very Wide (> 2%)'\n END\nORDER BY \n CASE \n WHEN sl_distance = 'Very Tight (< 0.5%)' THEN 1\n WHEN sl_distance = 'Tight (0.5-1%)' THEN 2\n WHEN sl_distance = 'Normal (1-1.5%)' THEN 3\n WHEN sl_distance = 'Wide (1.5-2%)' THEN 4\n ELSE 5\n END;"
},
"id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"name": "Query Stop Distance Analysis",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 500],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Trades that got stopped out vs those that hit targets\nSELECT \n direction,\n symbol,\n COUNT(CASE WHEN \"exitReason\" LIKE '%stop%' THEN 1 END) as stopped_out,\n COUNT(CASE WHEN \"exitReason\" LIKE '%target%' OR \"exitReason\" LIKE '%TP%' THEN 1 END) as hit_targets,\n COUNT(CASE WHEN \"exitReason\" LIKE '%manual%' THEN 1 END) as manual_exits,\n ROUND(AVG(CASE WHEN \"exitReason\" LIKE '%stop%' THEN \"realizedPnL\" END)::numeric, 4) as avg_stop_loss,\n ROUND(AVG(CASE WHEN \"exitReason\" LIKE '%target%' OR \"exitReason\" LIKE '%TP%' THEN \"realizedPnL\" END)::numeric, 4) as avg_target_win\nFROM \"Trade\"\nWHERE status = 'closed'\n AND \"isTestTrade\" = false\n AND \"entryTime\" >= NOW() - INTERVAL '30 days'\nGROUP BY direction, symbol\nORDER BY stopped_out DESC;"
},
"id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"name": "Query Stop vs Target by Symbol",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [460, 700],
"credentials": {
"postgres": {
"id": "1",
"name": "Trading Bot Database"
}
}
},
{
"parameters": {
"jsCode": "const exitReasons = $('Query Exit Reasons').all().map(item => item.json);\nconst stopDistances = $('Query Stop Distance Analysis').all().map(item => item.json);\nconst symbolBreakdown = $('Query Stop vs Target by Symbol').all().map(item => item.json);\n\n// Calculate stop loss hit rate\nconst stopLossExits = exitReasons.filter(r => r.exitReason && r.exitReason.toLowerCase().includes('stop'));\nconst totalStopLosses = stopLossExits.reduce((sum, r) => sum + parseInt(r.count), 0);\nconst totalTrades = exitReasons.reduce((sum, r) => sum + parseInt(r.count), 0);\nconst stopHitRate = totalTrades > 0 ? (totalStopLosses / totalTrades * 100).toFixed(2) : 0;\n\n// Find optimal stop distance\nconst sortedByPnL = [...stopDistances].sort((a, b) => parseFloat(b.avg_pnl) - parseFloat(a.avg_pnl));\nconst optimalDistance = sortedByPnL[0];\nconst worstDistance = sortedByPnL[sortedByPnL.length - 1];\n\n// Calculate profit factor (avg win / avg loss)\nconst avgStopLoss = exitReasons\n .filter(r => r.exitReason && r.exitReason.toLowerCase().includes('stop'))\n .reduce((sum, r) => sum + parseFloat(r.avg_pnl) * parseInt(r.count), 0) / totalStopLosses;\n\nconst targetExits = exitReasons.filter(r => \n r.exitReason && (r.exitReason.toLowerCase().includes('target') || r.exitReason.toLowerCase().includes('tp'))\n);\nconst totalTargets = targetExits.reduce((sum, r) => sum + parseInt(r.count), 0);\nconst avgTarget = targetExits.reduce((sum, r) => sum + parseFloat(r.avg_pnl) * parseInt(r.count), 0) / totalTargets;\n\nconst insights = {\n summary: {\n stopHitRate: `${stopHitRate}%`,\n totalStopLosses,\n avgStopLoss: `$${avgStopLoss.toFixed(4)}`,\n avgTargetWin: `$${avgTarget.toFixed(4)}`,\n profitFactor: avgStopLoss !== 0 ? (Math.abs(avgTarget / avgStopLoss)).toFixed(2) : 'N/A'\n },\n optimalStopDistance: {\n distance: optimalDistance?.sl_distance || 'N/A',\n avgPnL: optimalDistance ? `$${optimalDistance.avg_pnl}` : 'N/A',\n stopHitRate: optimalDistance ? `${optimalDistance.stop_hit_rate}%` : 'N/A',\n recommendation: optimalDistance ? `Use ${optimalDistance.sl_distance} stop losses` : 'Need more data'\n },\n worstStopDistance: {\n distance: worstDistance?.sl_distance || 'N/A',\n avgPnL: worstDistance ? `$${worstDistance.avg_pnl}` : 'N/A',\n stopHitRate: worstDistance ? `${worstDistance.stop_hit_rate}%` : 'N/A'\n },\n recommendations: [\n stopHitRate > 50 ? '⚠️ High stop hit rate - consider wider stops or better entries' : '✅ Stop hit rate is acceptable',\n optimalDistance && worstDistance ? `💡 ${optimalDistance.sl_distance} performs ${((parseFloat(optimalDistance.avg_pnl) - parseFloat(worstDistance.avg_pnl)) / Math.abs(parseFloat(worstDistance.avg_pnl)) * 100).toFixed(1)}% better than ${worstDistance.sl_distance}` : '',\n Math.abs(avgStopLoss) > Math.abs(avgTarget) ? '⚠️ Average losses exceed average wins - adjust risk/reward' : '✅ Risk/reward ratio is positive'\n ].filter(Boolean),\n rawData: {\n exitReasons,\n stopDistances,\n symbolBreakdown\n }\n};\n\nreturn [{ json: insights }];"
},
"id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
"name": "Analyze Stop Effectiveness",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [680, 300]
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Query Exit Reasons",
"type": "main",
"index": 0
},
{
"node": "Query Stop Distance Analysis",
"type": "main",
"index": 0
},
{
"node": "Query Stop vs Target by Symbol",
"type": "main",
"index": 0
}
]
]
},
"Query Exit Reasons": {
"main": [
[
{
"node": "Analyze Stop Effectiveness",
"type": "main",
"index": 0
}
]
]
},
"Query Stop Distance Analysis": {
"main": [
[
{
"node": "Analyze Stop Effectiveness",
"type": "main",
"index": 0
}
]
]
},
"Query Stop vs Target by Symbol": {
"main": [
[
{
"node": "Analyze Stop Effectiveness",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 0,
"updatedAt": "2025-10-27T00:00:00.000Z",
"versionId": "1"
}