Files
trading_bot_v4/n8n-stop-loss-analysis.json
mindesbunister d3c04ea9c9 feat: Position Manager persistence + order cleanup + improved stop loss
- Add Position Manager state persistence to survive restarts
  - Auto-restore open trades from database on startup
  - Save state after TP1, SL adjustments, profit locks
  - Persist to configSnapshot JSON field

- Add automatic order cancellation
  - Cancel all TP/SL orders when position fully closed
  - New cancelAllOrders() function in drift/orders.ts
  - Prevents orphaned orders after manual closes

- Improve stop loss management
  - Move SL to +0.35% after TP1 (was +0.15%)
  - Gives more breathing room for retracements
  - Still locks in half of TP1 profit

- Add database sync when Position Manager closes trades
  - Auto-update Trade record with exit data
  - Save P&L, exit reason, hold time
  - Fix analytics showing stale data

- Add trade state management functions
  - updateTradeState() for Position Manager persistence
  - getOpenTrades() for startup restoration
  - getInitializedPositionManager() for async init

- Create n8n database analytics workflows
  - Daily report workflow (automated at midnight)
  - Pattern analysis (hourly/daily performance)
  - Stop loss effectiveness analysis
  - Database analytics query workflow
  - Complete setup guide (N8N_DATABASE_SETUP.md)
2025-10-27 10:39:05 +01:00

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"
}