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
140 lines
9.1 KiB
JSON
140 lines
9.1 KiB
JSON
{
|
|
"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"
|
|
}
|