From d3c04ea9c9c10acebb671c40b7c1a1d392a6eaf6 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Mon, 27 Oct 2025 10:39:05 +0100 Subject: [PATCH] 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) --- N8N_DATABASE_SETUP.md | 317 +++++++++++++++++++++++++++++++ app/api/trading/execute/route.ts | 4 +- lib/database/trades.ts | 73 +++++++ lib/drift/orders.ts | 71 +++++++ lib/trading/position-manager.ts | 143 +++++++++++++- n8n-daily-report.json | 171 +++++++++++++++++ n8n-database-analytics.json | 73 +++++++ n8n-pattern-analysis.json | 139 ++++++++++++++ n8n-stop-loss-analysis.json | 139 ++++++++++++++ 9 files changed, 1122 insertions(+), 8 deletions(-) create mode 100644 N8N_DATABASE_SETUP.md create mode 100644 n8n-daily-report.json create mode 100644 n8n-database-analytics.json create mode 100644 n8n-pattern-analysis.json create mode 100644 n8n-stop-loss-analysis.json diff --git a/N8N_DATABASE_SETUP.md b/N8N_DATABASE_SETUP.md new file mode 100644 index 0000000..b12167a --- /dev/null +++ b/N8N_DATABASE_SETUP.md @@ -0,0 +1,317 @@ +# n8n Database Integration Setup Guide + +## Overview +This guide shows you how to connect your n8n instance to the Trading Bot v4 PostgreSQL database for automated analysis and insights. + +## Database Connection Details + +⚠️ **IMPORTANT:** n8n is on a **different Docker network** than the trading bot postgres. You MUST use the host machine IP or localhost. + +### ✅ CORRECT Connection (n8n is on different network) +``` +Type: PostgreSQL +Host: host.docker.internal (or your machine's IP like 172.18.0.1) +Port: 5432 +Database: trading_bot_v4 +User: postgres +Password: postgres +SSL Mode: disable +``` + +### Alternative: Use localhost with host networking +If `host.docker.internal` doesn't work, find your docker network gateway: +```bash +docker inspect n8n --format '{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}' +# Result: 172.18.0.1 (use this as Host) +``` + +### Network Details (for reference) +- **n8n network:** `compose_files_default` (172.18.0.0/16) +- **Trading bot network:** `traderv4_trading-net` (172.28.0.0/16) +- **PostgreSQL container:** `trading-bot-postgres` on traderv4_trading-net (172.28.0.2) +- **PostgreSQL exposed port:** 5432 → localhost:5432 + +Since they're on different networks, use the **host machine as bridge**. + +## Setup Steps + +### 1. Access n8n +Open your browser and navigate to: +``` +http://localhost:8098 +``` + +### 2. Create PostgreSQL Credential +1. Click on **Credentials** in the left sidebar +2. Click **Add Credential** +3. Search for **Postgres** and select it +4. Fill in the connection details (see above) +5. Name it: **Trading Bot Database** +6. Click **Test Connection** to verify +7. Click **Save** + +### 3. Import Workflows +Four pre-built workflow templates are ready in your workspace: + +#### A. Database Analytics (n8n-database-analytics.json) +**Purpose:** Query and analyze closed trades with statistical calculations + +**Features:** +- Fetches last 100 closed trades +- Calculates win rate, P&L, profit factor +- Breaks down by symbol, direction, and exit reason +- Identifies best performing setups + +**To import:** +1. Go to **Workflows** → **Add Workflow** +2. Click **...** menu → **Import from File** +3. Select `n8n-database-analytics.json` +4. Update PostgreSQL node to use "Trading Bot Database" credential +5. Click **Save** +6. Click **Execute Workflow** to test + +#### B. Daily Trading Report (n8n-daily-report.json) +**Purpose:** Automated daily summary at midnight (stores in DailyStats table) + +**Features:** +- Runs automatically at 00:05 every day +- Calculates yesterday's performance +- Breaks down by symbol +- Stores in DailyStats table for historical tracking +- Calculates win rate, profit factor, avg hold time + +**To import:** +1. Import workflow from file +2. Update both PostgreSQL nodes with "Trading Bot Database" credential +3. **Activate** the workflow (toggle in top right) +4. Will run automatically at midnight + +#### C. Pattern Analysis (n8n-pattern-analysis.json) +**Purpose:** Discover which times/conditions produce best results + +**Features:** +- **Hourly Analysis:** Which hours have best win rate +- **Daily Analysis:** Which days perform best +- **Hold Time Analysis:** Optimal position duration +- Generates actionable recommendations + +**Example insights:** +- "Focus trading around 14:00-16:00 (75% win rate)" +- "Trade more on Tuesday, avoid Friday" +- "Target exits in 15-30 min range" + +**To import:** +1. Import workflow +2. Update all 3 PostgreSQL nodes with credential +3. Run manually to see insights + +#### D. Stop Loss Analysis (n8n-stop-loss-analysis.json) +**Purpose:** Optimize stop loss distances and understand exit patterns + +**Features:** +- Exit reason breakdown (stopped out vs targets hit) +- Stop distance effectiveness (tight vs wide stops) +- Symbol-specific stop performance +- Calculates profit factor (avg win / avg loss) +- Recommendations for stop optimization + +**Example insights:** +- "⚠️ High stop hit rate - consider wider stops" +- "💡 Normal (1-1.5%) stops perform 45% better than tight stops" +- "✅ Risk/reward ratio is positive" + +**To import:** +1. Import workflow +2. Update all 3 PostgreSQL nodes with credential +3. Run manually to analyze + +## Database Schema Reference + +### Trade Table (Main table) +Key fields for analysis: +```sql +id, symbol, direction, entryPrice, exitPrice, quantity, +notionalSize, realizedPnL, realizedPnLPercent, +entryTime, exitTime, holdTimeSeconds, +stopLossPrice, takeProfitPrice1, takeProfitPrice2, +exitReason, status, isTestTrade +``` + +### Common Queries + +#### Get all closed trades (last 30 days) +```sql +SELECT * FROM "Trade" +WHERE status = 'closed' + AND "isTestTrade" = false + AND "entryTime" >= NOW() - INTERVAL '30 days' +ORDER BY "entryTime" DESC; +``` + +#### Calculate win rate +```sql +SELECT + COUNT(*) as total_trades, + COUNT(CASE WHEN "realizedPnL" > 0 THEN 1 END) as wins, + ROUND(COUNT(CASE WHEN "realizedPnL" > 0 THEN 1 END)::numeric / COUNT(*) * 100, 2) as win_rate +FROM "Trade" +WHERE status = 'closed' AND "isTestTrade" = false; +``` + +#### Best performing symbols +```sql +SELECT + symbol, + COUNT(*) as trades, + SUM("realizedPnL") as total_pnl, + AVG("realizedPnL") as avg_pnl +FROM "Trade" +WHERE status = 'closed' AND "isTestTrade" = false +GROUP BY symbol +ORDER BY total_pnl DESC; +``` + +## Workflow Automation Ideas + +### 1. Performance Alerts +**Trigger:** Schedule (every 6 hours) +**Query:** Check if win rate drops below 50% in last 24h +**Action:** Send Telegram notification to pause trading + +### 2. Best Setup Detector +**Trigger:** Manual or daily +**Query:** Find symbol + direction + time combinations with >70% win rate +**Action:** Save insights to config for bot to prioritize + +### 3. Drawdown Monitor +**Trigger:** After each trade (webhook) +**Query:** Calculate rolling 10-trade P&L +**Action:** Auto-reduce position size if in drawdown + +### 4. Exit Optimization +**Trigger:** Weekly +**Query:** Compare TP1 vs TP2 hit rates and P&L +**Action:** Recommend adjustment to TP levels + +## Connecting Workflows to Trading Bot + +### Webhook from Trading Bot to n8n +In your n8n workflow: +1. Add **Webhook** trigger node +2. Set HTTP Method: POST +3. Note the webhook URL: `http://localhost:8098/webhook/your-unique-id` + +In trading bot code (e.g., after trade closes): +```typescript +// Send trade data to n8n for analysis +await fetch('http://localhost:8098/webhook/your-unique-id', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tradeId: trade.id, + symbol: trade.symbol, + pnl: trade.realizedPnL, + exitReason: trade.exitReason + }) +}); +``` + +### Update Bot Config from n8n +In your n8n workflow (after analysis): +1. Add **HTTP Request** node +2. URL: `http://trading-bot-v4:3000/api/settings` +3. Method: POST +4. Body: Updated config based on analysis + +Example - adjust stop loss based on analysis: +```json +{ + "STOP_LOSS_PERCENT": 1.5, + "USE_DUAL_STOPS": true +} +``` + +## Advanced Use Cases + +### Machine Learning Integration +Use n8n to: +1. Export trade data to CSV +2. Send to Python ML service via HTTP +3. Receive predictions +4. Update bot configuration + +### Multi-Timeframe Analysis +Create workflow that: +1. Queries trades by hour/day/week +2. Identifies patterns at each timeframe +3. Generates trading schedule recommendations + +### Risk Management Automation +Build workflow that: +1. Monitors account balance +2. Calculates daily/weekly profit target +3. Auto-pauses bot after hitting target +4. Resumes trading next day/week + +## Troubleshooting + +### Connection Refused +- Verify PostgreSQL container is running: `docker ps | grep postgres` +- Check port mapping: `docker port trading-bot-postgres` +- Ensure n8n and postgres are on same Docker network + +### Query Timeouts +- Add indexes to frequently queried columns +- Limit result sets with `LIMIT` clause +- Use date range filters to reduce dataset + +### Empty Results +- Check `isTestTrade` filter (you may want to include test trades for testing) +- Verify date range in queries +- Ensure trades have been closed (`status = 'closed'`) + +## Testing Your Setup + +### Quick Test Query +In n8n, create a workflow with: +1. Manual Trigger +2. PostgreSQL node with query: +```sql +SELECT COUNT(*) as total_trades, + MAX("entryTime") as last_trade +FROM "Trade"; +``` +3. Execute and verify results + +### Verify Test Trade Flag +```sql +SELECT + "isTestTrade", + COUNT(*) as count +FROM "Trade" +GROUP BY "isTestTrade"; +``` + +Expected output: +- `false`: Production trades +- `true`: Test trades from /api/trading/test-db + +## Next Steps + +1. **Import all 4 workflows** and test each one +2. **Activate the Daily Report** workflow for automated tracking +3. **Run Pattern Analysis** to discover your best trading times +4. **Run Stop Loss Analysis** to optimize risk management +5. **Create custom workflows** based on your specific needs + +## Support + +If you encounter issues: +1. Check n8n logs: `docker logs n8n` +2. Check postgres logs: `docker logs trading-bot-postgres` +3. Test database connection from host: `psql -h localhost -p 5432 -U postgres -d trading_bot_v4` +4. Verify bot is writing to database: Check `/analytics` page in web UI + +--- + +**Pro Tip:** Start with the Pattern Analysis workflow to understand your trading patterns, then use those insights to create automated optimization workflows! diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index b401dd8..ac82853 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -10,7 +10,7 @@ import { initializeDriftService } from '@/lib/drift/client' import { openPosition, placeExitOrders } from '@/lib/drift/orders' import { normalizeTradingViewSymbol } from '@/config/trading' import { getMergedConfig } from '@/config/trading' -import { getPositionManager, ActiveTrade } from '@/lib/trading/position-manager' +import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager' import { createTrade } from '@/lib/database/trades' export interface ExecuteTradeRequest { @@ -209,7 +209,7 @@ export async function POST(request: NextRequest): Promise 0) { + console.log(`✅ Cancelled ${cancelResult.cancelledCount} orders`) + } + } + return { success: true, transactionSignature: txSig, @@ -515,6 +524,68 @@ export async function closePosition( } } +/** + * Cancel all open orders for a specific market + */ +export async function cancelAllOrders( + symbol: string +): Promise<{ success: boolean; cancelledCount?: number; error?: string }> { + try { + console.log(`🗑️ Cancelling all orders for ${symbol}...`) + + const driftService = getDriftService() + const driftClient = driftService.getClient() + const marketConfig = getMarketConfig(symbol) + + const isDryRun = process.env.DRY_RUN === 'true' + if (isDryRun) { + console.log('🧪 DRY RUN: Simulating order cancellation') + return { success: true, cancelledCount: 0 } + } + + // Get user account to check for orders + const userAccount = driftClient.getUserAccount() + if (!userAccount) { + throw new Error('User account not found') + } + + // Filter orders for this market + const ordersToCancel = userAccount.orders.filter( + (order: any) => + order.marketIndex === marketConfig.driftMarketIndex && + order.status === 0 // 0 = Open status + ) + + if (ordersToCancel.length === 0) { + console.log('✅ No open orders to cancel') + return { success: true, cancelledCount: 0 } + } + + console.log(`📋 Found ${ordersToCancel.length} open orders to cancel`) + + // Cancel all orders for this market + const txSig = await driftClient.cancelOrders( + undefined, // Cancel by market type + marketConfig.driftMarketIndex, + undefined // No specific direction filter + ) + + console.log(`✅ Orders cancelled! Transaction: ${txSig}`) + + return { + success: true, + cancelledCount: ordersToCancel.length, + } + + } catch (error) { + console.error('❌ Failed to cancel orders:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} + /** * Close entire position for a market */ diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 1c94f19..bbeb43b 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -8,6 +8,7 @@ import { getDriftService } from '../drift/client' import { closePosition } from '../drift/orders' import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor' import { getMergedConfig, TradingConfig } from '../../config/trading' +import { updateTradeExit, updateTradeState, getOpenTrades } from '../database/trades' export interface ActiveTrade { id: string @@ -46,7 +47,7 @@ export interface ActiveTrade { export interface ExitResult { success: boolean - reason: 'TP1' | 'TP2' | 'SL' | 'emergency' | 'manual' | 'error' + reason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'emergency' | 'manual' | 'error' closePrice?: number closedSize?: number realizedPnL?: number @@ -58,12 +59,74 @@ export class PositionManager { private activeTrades: Map = new Map() private config: TradingConfig private isMonitoring: boolean = false + private initialized: boolean = false constructor(config?: Partial) { this.config = getMergedConfig(config) console.log('✅ Position manager created') } + /** + * Initialize and restore active trades from database + */ + async initialize(): Promise { + if (this.initialized) { + return + } + + console.log('🔄 Restoring active trades from database...') + + try { + const openTrades = await getOpenTrades() + + for (const dbTrade of openTrades) { + // Extract Position Manager state from configSnapshot + const pmState = (dbTrade.configSnapshot as any)?.positionManagerState + + // Reconstruct ActiveTrade object + const activeTrade: ActiveTrade = { + id: dbTrade.id, + positionId: dbTrade.positionId, + symbol: dbTrade.symbol, + direction: dbTrade.direction as 'long' | 'short', + entryPrice: dbTrade.entryPrice, + entryTime: dbTrade.entryTime.getTime(), + positionSize: dbTrade.positionSizeUSD, + leverage: dbTrade.leverage, + stopLossPrice: pmState?.stopLossPrice ?? dbTrade.stopLossPrice, + tp1Price: dbTrade.takeProfit1Price, + tp2Price: dbTrade.takeProfit2Price, + emergencyStopPrice: dbTrade.stopLossPrice * (dbTrade.direction === 'long' ? 0.98 : 1.02), + currentSize: pmState?.currentSize ?? dbTrade.positionSizeUSD, + tp1Hit: pmState?.tp1Hit ?? false, + slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false, + slMovedToProfit: pmState?.slMovedToProfit ?? false, + realizedPnL: pmState?.realizedPnL ?? 0, + unrealizedPnL: pmState?.unrealizedPnL ?? 0, + peakPnL: pmState?.peakPnL ?? 0, + priceCheckCount: 0, + lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice, + lastUpdateTime: Date.now(), + } + + this.activeTrades.set(activeTrade.id, activeTrade) + console.log(`✅ Restored trade: ${activeTrade.symbol} ${activeTrade.direction} at $${activeTrade.entryPrice}`) + } + + if (this.activeTrades.size > 0) { + console.log(`🎯 Restored ${this.activeTrades.size} active trades`) + await this.startMonitoring() + } else { + console.log('✅ No active trades to restore') + } + + } catch (error) { + console.error('❌ Failed to restore active trades:', error) + } + + this.initialized = true + } + /** * Add a new trade to monitor */ @@ -72,6 +135,9 @@ export class PositionManager { this.activeTrades.set(trade.id, trade) + // Save initial state to database + await this.saveTradeState(trade) + console.log(`✅ Trade added. Active trades: ${this.activeTrades.size}`) // Start monitoring if not already running @@ -238,17 +304,20 @@ export class PositionManager { console.log(`🎉 TP1 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) await this.executeExit(trade, 50, 'TP1', currentPrice) - // Move SL to breakeven + // Move SL to secure profit after TP1 trade.tp1Hit = true trade.currentSize = trade.positionSize * 0.5 trade.stopLossPrice = this.calculatePrice( trade.entryPrice, - 0.15, // +0.15% to cover fees + 0.35, // +0.35% to secure profit and avoid stop-out on retracement trade.direction ) trade.slMovedToBreakeven = true - console.log(`🔒 SL moved to breakeven: ${trade.stopLossPrice.toFixed(4)}`) + console.log(`🔒 SL moved to +0.35% (half of TP1): ${trade.stopLossPrice.toFixed(4)}`) + + // Save state after TP1 + await this.saveTradeState(trade) return } @@ -268,6 +337,9 @@ export class PositionManager { trade.slMovedToProfit = true console.log(`🎯 SL moved to +${this.config.profitLockPercent}%: ${trade.stopLossPrice.toFixed(4)}`) + + // Save state after profit lock + await this.saveTradeState(trade) } // 5. Take profit 2 (remaining 50%) @@ -305,8 +377,29 @@ export class PositionManager { if (percentToClose >= 100) { // Full close - remove from monitoring trade.realizedPnL += result.realizedPnL || 0 - this.removeTrade(trade.id) + // Save to database (only for valid exit reasons) + if (reason !== 'error') { + try { + const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000) + await updateTradeExit({ + positionId: trade.positionId, + exitPrice: result.closePrice || currentPrice, + exitReason: reason as 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency', + realizedPnL: trade.realizedPnL, + exitOrderTx: result.transactionSignature || 'MANUAL_CLOSE', + holdTimeSeconds, + maxDrawdown: 0, // TODO: Track this + maxGain: trade.peakPnL, + }) + console.log('💾 Trade saved to database') + } catch (dbError) { + console.error('❌ Failed to save trade exit to database:', dbError) + // Don't fail the close if database fails + } + } + + this.removeTrade(trade.id) console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`) } else { // Partial close (TP1) @@ -316,7 +409,6 @@ export class PositionManager { console.log(`✅ 50% closed | Realized: $${result.realizedPnL?.toFixed(2)} | Remaining: ${trade.currentSize}`) } - // TODO: Save to database // TODO: Send notification } catch (error) { @@ -404,6 +496,29 @@ export class PositionManager { console.log('✅ All positions closed') } + /** + * Save trade state to database (for persistence across restarts) + */ + private async saveTradeState(trade: ActiveTrade): Promise { + try { + await updateTradeState({ + positionId: trade.positionId, + currentSize: trade.currentSize, + tp1Hit: trade.tp1Hit, + slMovedToBreakeven: trade.slMovedToBreakeven, + slMovedToProfit: trade.slMovedToProfit, + stopLossPrice: trade.stopLossPrice, + realizedPnL: trade.realizedPnL, + unrealizedPnL: trade.unrealizedPnL, + peakPnL: trade.peakPnL, + lastPrice: trade.lastPrice, + }) + } catch (error) { + console.error('❌ Failed to save trade state:', error) + // Don't throw - state save is non-critical + } + } + /** * Get monitoring status */ @@ -426,10 +541,26 @@ export class PositionManager { // Singleton instance let positionManagerInstance: PositionManager | null = null +let initPromise: Promise | null = null export function getPositionManager(): PositionManager { if (!positionManagerInstance) { positionManagerInstance = new PositionManager() + + // Initialize asynchronously (restore trades from database) + if (!initPromise) { + initPromise = positionManagerInstance.initialize().catch(error => { + console.error('❌ Failed to initialize Position Manager:', error) + }) + } } return positionManagerInstance } + +export async function getInitializedPositionManager(): Promise { + const manager = getPositionManager() + if (initPromise) { + await initPromise + } + return manager +} diff --git a/n8n-daily-report.json b/n8n-daily-report.json new file mode 100644 index 0000000..2d97d07 --- /dev/null +++ b/n8n-daily-report.json @@ -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" +} diff --git a/n8n-database-analytics.json b/n8n-database-analytics.json new file mode 100644 index 0000000..d627bec --- /dev/null +++ b/n8n-database-analytics.json @@ -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" +} diff --git a/n8n-pattern-analysis.json b/n8n-pattern-analysis.json new file mode 100644 index 0000000..86dcd96 --- /dev/null +++ b/n8n-pattern-analysis.json @@ -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" +} diff --git a/n8n-stop-loss-analysis.json b/n8n-stop-loss-analysis.json new file mode 100644 index 0000000..abda0f1 --- /dev/null +++ b/n8n-stop-loss-analysis.json @@ -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" +}