Skip to content

Instantly share code, notes, and snippets.

@vitqst
Created November 10, 2025 03:32
Show Gist options
  • Select an option

  • Save vitqst/2541b692096349fc2eeb9de960bef12f to your computer and use it in GitHub Desktop.

Select an option

Save vitqst/2541b692096349fc2eeb9de960bef12f to your computer and use it in GitHub Desktop.
{
"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