Created
November 10, 2025 03:32
-
-
Save vitqst/2541b692096349fc2eeb9de960bef12f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "Trading Signal Backtest", | |
| "nodes": [ | |
| { | |
| "parameters": {}, | |
| "name": "Start", | |
| "type": "n8n-nodes-base.start", | |
| "typeVersion": 1, | |
| "position": [250, 300] | |
| }, | |
| { | |
| "parameters": { | |
| "operation": "executeQuery", | |
| "query": "SELECT * FROM signals WHERE back_test_result IS NULL ORDER BY timestamp ASC", | |
| "options": {} | |
| }, | |
| "name": "Get Signals to Backtest", | |
| "type": "n8n-nodes-base.postgres", | |
| "typeVersion": 2, | |
| "position": [450, 300], | |
| "credentials": { | |
| "postgres": { | |
| "id": "YOUR_POSTGRES_CREDENTIAL_ID", | |
| "name": "PostgreSQL account" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": { | |
| "batchSize": 1, | |
| "options": {} | |
| }, | |
| "name": "Loop Over Signals", | |
| "type": "n8n-nodes-base.splitInBatches", | |
| "typeVersion": 3, | |
| "position": [650, 300] | |
| }, | |
| { | |
| "parameters": { | |
| "url": "=https://api.binance.com/api/v3/klines", | |
| "method": "GET", | |
| "queryParameters": { | |
| "parameters": [ | |
| { | |
| "name": "symbol", | |
| "value": "={{ $json.symbol.trim() }}" | |
| }, | |
| { | |
| "name": "interval", | |
| "value": "1m" | |
| }, | |
| { | |
| "name": "startTime", | |
| "value": "={{ new Date($json.timestamp).getTime() }}" | |
| }, | |
| { | |
| "name": "limit", | |
| "value": "500" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "name": "Fetch 500 Candles", | |
| "type": "n8n-nodes-base.httpRequest", | |
| "typeVersion": 4, | |
| "position": [850, 300] | |
| }, | |
| { | |
| "parameters": { | |
| "mode": "runOnceForAllItems", | |
| "jsCode": "// Get signal data from first input\nconst signal = $input.first().json;\n\n// Get candle data from HTTP request\nconst candles = $input.first().json;\n\n// Extract signal parameters\nconst direction = signal.direction.trim();\nconst entryPrice = parseFloat(signal.entry_price);\nconst stopLoss = parseFloat(signal.sl);\nconst tp1 = parseFloat(signal.tp_1);\nconst tp2 = parseFloat(signal.tp_2);\nconst tp3 = parseFloat(signal.tp_3);\nconst signalId = signal.id;\nconst symbol = signal.symbol.trim();\n\n// Initialize result object\nconst result = {\n signal_id: signalId,\n symbol: symbol,\n direction: direction,\n entry_price: entryPrice,\n sl_hit: false,\n tp1_hit: false,\n tp2_hit: false,\n tp3_hit: false,\n bars_to_sl: null,\n bars_to_tp1: null,\n bars_to_tp2: null,\n bars_to_tp3: null,\n max_profit_pct: 0,\n max_drawdown_pct: 0,\n final_result: 'OPEN',\n pnl_pct: 0,\n exit_bar: null,\n total_bars: candles.length\n};\n\n// Process each candle\nfor (let i = 0; i < candles.length; i++) {\n const candle = candles[i];\n const open = parseFloat(candle[1]);\n const high = parseFloat(candle[2]);\n const low = parseFloat(candle[3]);\n const close = parseFloat(candle[4]);\n const timestamp = candle[0];\n \n if (direction === 'LONG') {\n // Check stop loss (price goes down to low)\n if (!result.sl_hit && low <= stopLoss) {\n result.sl_hit = true;\n result.bars_to_sl = i + 1;\n result.final_result = 'STOP_LOSS';\n result.pnl_pct = ((stopLoss - entryPrice) / entryPrice) * 100;\n result.exit_bar = i + 1;\n break; // Exit on stop loss\n }\n \n // Check take profits (price goes up to high)\n if (!result.tp1_hit && high >= tp1) {\n result.tp1_hit = true;\n result.bars_to_tp1 = i + 1;\n if (!result.final_result || result.final_result === 'OPEN') {\n result.final_result = 'TP1';\n result.pnl_pct = ((tp1 - entryPrice) / entryPrice) * 100;\n result.exit_bar = i + 1;\n }\n }\n \n if (!result.tp2_hit && high >= tp2) {\n result.tp2_hit = true;\n result.bars_to_tp2 = i + 1;\n if (result.final_result === 'TP1' || result.final_result === 'OPEN') {\n result.final_result = 'TP2';\n result.pnl_pct = ((tp2 - entryPrice) / entryPrice) * 100;\n result.exit_bar = i + 1;\n }\n }\n \n if (!result.tp3_hit && high >= tp3) {\n result.tp3_hit = true;\n result.bars_to_tp3 = i + 1;\n result.final_result = 'TP3';\n result.pnl_pct = ((tp3 - entryPrice) / entryPrice) * 100;\n result.exit_bar = i + 1;\n break; // Exit on TP3\n }\n \n // Track max profit and drawdown\n const currentProfit = ((high - entryPrice) / entryPrice) * 100;\n const currentDrawdown = ((low - entryPrice) / entryPrice) * 100;\n \n if (currentProfit > result.max_profit_pct) {\n result.max_profit_pct = currentProfit;\n }\n if (currentDrawdown < result.max_drawdown_pct) {\n result.max_drawdown_pct = currentDrawdown;\n }\n \n } else if (direction === 'SHORT') {\n // Check stop loss (price goes up to high)\n if (!result.sl_hit && high >= stopLoss) {\n result.sl_hit = true;\n result.bars_to_sl = i + 1;\n result.final_result = 'STOP_LOSS';\n result.pnl_pct = ((entryPrice - stopLoss) / entryPrice) * 100;\n result.exit_bar = i + 1;\n break; // Exit on stop loss\n }\n \n // Check take profits (price goes down to low)\n if (!result.tp1_hit && low <= tp1) {\n result.tp1_hit = true;\n result.bars_to_tp1 = i + 1;\n if (!result.final_result || result.final_result === 'OPEN') {\n result.final_result = 'TP1';\n result.pnl_pct = ((entryPrice - tp1) / entryPrice) * 100;\n result.exit_bar = i + 1;\n }\n }\n \n if (!result.tp2_hit && low <= tp2) {\n result.tp2_hit = true;\n result.bars_to_tp2 = i + 1;\n if (result.final_result === 'TP1' || result.final_result === 'OPEN') {\n result.final_result = 'TP2';\n result.pnl_pct = ((entryPrice - tp2) / entryPrice) * 100;\n result.exit_bar = i + 1;\n }\n }\n \n if (!result.tp3_hit && low <= tp3) {\n result.tp3_hit = true;\n result.bars_to_tp3 = i + 1;\n result.final_result = 'TP3';\n result.pnl_pct = ((entryPrice - tp3) / entryPrice) * 100;\n result.exit_bar = i + 1;\n break; // Exit on TP3\n }\n \n // Track max profit and drawdown\n const currentProfit = ((entryPrice - low) / entryPrice) * 100;\n const currentDrawdown = ((entryPrice - high) / entryPrice) * 100;\n \n if (currentProfit > result.max_profit_pct) {\n result.max_profit_pct = currentProfit;\n }\n if (currentDrawdown < result.max_drawdown_pct) {\n result.max_drawdown_pct = currentDrawdown;\n }\n }\n}\n\n// If no exit, mark as timeout\nif (result.final_result === 'OPEN') {\n result.final_result = 'TIMEOUT';\n result.exit_bar = candles.length;\n}\n\n// Round percentages\nresult.max_profit_pct = Math.round(result.max_profit_pct * 100) / 100;\nresult.max_drawdown_pct = Math.round(result.max_drawdown_pct * 100) / 100;\nresult.pnl_pct = Math.round(result.pnl_pct * 100) / 100;\n\nreturn [{ json: result }];" | |
| }, | |
| "name": "Backtest Logic", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [1050, 300] | |
| }, | |
| { | |
| "parameters": { | |
| "operation": "executeQuery", | |
| "query": "=UPDATE signals SET back_test_result = '{{ $json }}' WHERE id = {{ $('Loop Over Signals').item.json.id }}", | |
| "options": {} | |
| }, | |
| "name": "Update Backtest Result", | |
| "type": "n8n-nodes-base.postgres", | |
| "typeVersion": 2, | |
| "position": [1250, 300], | |
| "credentials": { | |
| "postgres": { | |
| "id": "YOUR_POSTGRES_CREDENTIAL_ID", | |
| "name": "PostgreSQL account" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": {}, | |
| "name": "Loop Back", | |
| "type": "n8n-nodes-base.noOp", | |
| "typeVersion": 1, | |
| "position": [1450, 300] | |
| } | |
| ], | |
| "connections": { | |
| "Start": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Get Signals to Backtest", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Get Signals to Backtest": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Loop Over Signals", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Loop Over Signals": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Fetch 500 Candles", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Fetch 500 Candles": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Backtest Logic", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Backtest Logic": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Update Backtest Result", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Update Backtest Result": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Loop Back", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Loop Back": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Loop Over Signals", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment