Import this Node-RED flow into your MachineTrader™ instance to backtest the NVDA EMA z-score mean-reversion strategy using historical 1-minute bars from Alpaca's IEX feed.
This Node-RED flow implements the complete NVDA EMA z-score backtest entirely within MachineTrader's visual flow editor — no Python required. It fetches historical 1-minute bars from Alpaca, runs the full EMA engine simulation in JavaScript function nodes, and outputs detailed results including daily P&L, trade logs, and buy-and-hold comparison.
The flow uses paginated API requests to retrieve all 1-minute bars from January 2, 2026 to the present, then processes them through the identical two-speed EWMA engine used by the live trader — fast EMA (λ=0.75) for the signal, slow variance (λ=0.95) for the z-score denominator.
These parameters are set in Step 1 and match the live EMA NVDA trader exactly:
The flow has 3 sequential steps. Click each inject node in order:
Sets all strategy parameters (lambdaf, lambdaVar, zScoreThresh, tradeqty, etc.) and resets all backtest state variables (bt_ema, bt_variance, bt_position, bt_trades, bt_dailyPnl) to their initial values. Also stores the 2026 NYSE holiday calendar for market-hours filtering.
Builds a paginated Alpaca Data API request for 1-minute bars starting from 2026-01-02T14:30:00Z. Each page returns up to 10,000 bars. The accumulator node collects bars and loops back through a 250ms delay for the next page. Once all bars are fetched (no more next_page_token), the complete dataset is sent to the simulation node which runs the full EMA z-score engine over every bar — computing signals, filling simulated trades, tracking position/cost basis, and building daily P&L records.
Reads the simulation results stored in flow context and outputs a formatted report to the debug sidebar: daily P&L with 📈/📉 emojis, the last 20 trades with side/price/z-score/position, and a full summary box with total P&L, ending balance, buy-and-hold comparison, and win/loss day counts. Results are also stored in global context variables for use by dashboards or other flows.
Visual overview of all nodes and their connections:
The simulation node implements a two-speed Exponential Weighted Moving Average: a fast EMA (λ=0.75) tracks price, while a slow variance estimator (λvar=0.95, ~20-bar lookback) measures volatility. The z-score = (price − EMA) / √variance. This allows z-scores to spike on sudden moves before variance catches up.
Alpaca returns at most 10,000 bars per request. The accumulator node checks for a next_page_token in the response and loops back (with a 250ms rate-limit delay) to fetch the next page. This ensures the full backtest period is covered regardless of how many bars exist.
The simulation tracks average cost basis and handles all transitions: adding to longs, adding to shorts, closing longs with sells, closing shorts with buys, and flipping from long to short (or vice versa). Realized P&L is computed at each fill.
Results include: total/realized/unrealized P&L, win/loss days, bars processed, trading days, buy-and-hold comparison, daily open/close/high/low, per-day trade counts, position at EOD, and last 200 individual trades with timestamps, z-scores, and running P&L.
Each bar's timestamp is converted to Eastern Time and filtered to 9:30 AM – 4:00 PM on weekdays only. The 2026 NYSE holiday calendar (New Year's, MLK Day, Presidents' Day, Good Friday, Memorial Day, July 4th, Labor Day, Thanksgiving, Christmas) is respected.
Results are stored in global.backtestEmaNvdaResults, global.backtestEmaNvdaDaily, and global.backtestEmaNvdaTrades for easy access by Node-RED dashboards, other flows, or HTTP endpoints.
global.apiKeyLive and global.apiSecretLive in your Node-RED global context (or use a flow that injects them on startup). These are used for the HTTP request node headers.data.alpaca.markets for historical bars (available on all Alpaca plans).global.set("apiKeyLive", "YOUR_KEY") and global.set("apiSecretLive", "YOUR_SECRET").⚠️ Important: This flow does NOT place any live orders — it is a simulation only. All trades are computed in-memory against historical data. However, it does make API calls to Alpaca's data endpoint to fetch bars. Ensure your API keys have data access permissions.
⏱ Note: Step 2 may take 30–60 seconds to complete depending on how many trading days are in the backtest window. Watch the debug sidebar for page-by-page progress updates and the final simulation summary.
Click the button to copy the complete flow configuration to your clipboard:
[
{
"id": "b2c3d4e5f6a70001",
"type": "tab",
"label": "Backtest EMA NVDA Z-Score",
"disabled": false,
"info": "Backtests the NVDA EMA z-score mean-reversion strategy using historical 1-min bars from Alpaca (IEX feed).\nClick inject nodes in order: Step 1 \u2192 Step 2 \u2192 Step 3.",
"env": []
},
{
"id": "b2c3d4e5f6a70002",
"type": "comment",
"z": "b2c3d4e5f6a70001",
"name": "Backtest: EMA z-score strategy on NVDA starting 1/2/2026. Click Inject nodes in order 1 \u2192 2 \u2192 3.",
"info": "",
"x": 350,
"y": 40,
"wires": []
},
{
"id": "b2c3d4e5f6a70003",
"type": "comment",
"z": "b2c3d4e5f6a70001",
"name": "Step 1: Initialize strategy parameters & backtest state",
"info": "",
"x": 260,
"y": 100,
"wires": []
},
{
"id": "b2c3d4e5f6a70004",
"type": "inject",
"z": "b2c3d4e5f6a70001",
"name": "Run once",
"props": [],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 120,
"y": 140,
"wires": [
[
"b2c3d4e5f6a70005"
]
]
},
{
"id": "b2c3d4e5f6a70005",
"type": "function",
"z": "b2c3d4e5f6a70001",
"name": "Initialize backtest params",
"func": "// \u2500\u2500 Strategy parameters (must match live trader) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nflow.set(\"symbol\", \"NVDA\");\nflow.set(\"lambdaf\", 0.75);\nflow.set(\"lambdaVar\", 0.95);\nflow.set(\"limitLong\", 400);\nflow.set(\"limitShort\", -400);\nflow.set(\"tradeqty\", 25);\nflow.set(\"balance\", 125000);\nflow.set(\"varPrice\", 0.001);\nflow.set(\"varCap\", 100);\nflow.set(\"varFloor\", 0.05);\nflow.set(\"zFact\", 1);\nflow.set(\"zScoreThresh\", 2.0);\n\n// \u2500\u2500 Backtest config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Backtest start: 1/2/2026 9:30 AM ET = 14:30 UTC\nflow.set(\"backtestStartRFC\", \"2026-01-02T14:30:00Z\");\n\n// \u2500\u2500 State (reset) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nflow.set(\"bt_ema\", null);\nflow.set(\"bt_variance\", 0);\nflow.set(\"bt_position\", 0);\nflow.set(\"bt_avgCost\", 0);\nflow.set(\"bt_realizedPnl\",0);\nflow.set(\"bt_trades\", []);\nflow.set(\"bt_dailyPnl\", {});\nflow.set(\"bt_barCount\", 0);\nflow.set(\"bt_buyCount\", 0);\nflow.set(\"bt_sellCount\", 0);\nflow.set(\"bt_firstPrice\", null);\nflow.set(\"bt_lastPrice\", null);\nflow.set(\"bt_allBars\", []);\n\nflow.set(\"holidays2026\", [\n \"2026-01-01\",\"2026-01-19\",\"2026-02-16\",\"2026-04-03\",\n \"2026-05-25\",\"2026-07-03\",\"2026-09-07\",\"2026-11-26\",\"2026-12-25\"\n]);\n\nnode.warn(\"\u2705 Backtest initialized \u2014 NVDA EMA z-score | \u03bb=0.75 | \u03bb_var=0.95 | Z-thresh=2.0 | Start: 1/2/2026\");\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 370,
"y": 140,
"wires": [
[]
]
},
{
"id": "b2c3d4e5f6a70006",
"type": "comment",
"z": "b2c3d4e5f6a70001",
"name": "Step 2: Fetch all historical 1-min bars (paginated) and run simulation",
"info": "",
"x": 310,
"y": 220,
"wires": []
},
{
"id": "b2c3d4e5f6a70007",
"type": "inject",
"z": "b2c3d4e5f6a70001",
"name": "Run once",
"props": [],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 120,
"y": 260,
"wires": [
[
"b2c3d4e5f6a70008"
]
]
},
{
"id": "b2c3d4e5f6a70008",
"type": "function",
"z": "b2c3d4e5f6a70001",
"name": "Build first bars request",
"func": "// Build paginated 1-min bars request for NVDA\nvar symbol = flow.get(\"symbol\") || \"NVDA\";\nvar startRFC = flow.get(\"backtestStartRFC\");\n\n// End = now\nvar now = new Date();\nvar endRFC = now.toISOString();\n\nflow.set(\"bt_endRFC\", endRFC);\nflow.set(\"bt_allBars\", []);\nflow.set(\"bt_pageCount\", 0);\n\nvar params = [\n \"timeframe=1Min\",\n \"start=\" + encodeURIComponent(startRFC),\n \"end=\" + encodeURIComponent(endRFC),\n \"feed=iex\",\n \"adjustment=split\",\n \"limit=10000\"\n].join(\"&\");\n\nmsg.url = \"https://data.alpaca.markets/v2/stocks/\" + symbol + \"/bars?\" + params;\nmsg.method = \"GET\";\nmsg.apiKeyId = global.get(\"apiKeyLive\") || \"\";\nmsg.apiSecretKey = global.get(\"apiSecretLive\") || \"\";\n\nnode.warn(\"\ud83d\udcca Fetching 1-min bars for \" + symbol + \" from \" + startRFC + \" ...\");\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 340,
"y": 260,
"wires": [
[
"b2c3d4e5f6a70009"
]
]
},
{
"id": "b2c3d4e5f6a70009",
"type": "http request",
"z": "b2c3d4e5f6a70001",
"name": "Alpaca Historical Bars",
"method": "use",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [
{
"keyType": "other",
"keyValue": "APCA-API-KEY-ID",
"valueType": "msg",
"valueValue": "apiKeyId"
},
{
"keyType": "other",
"keyValue": "APCA-API-SECRET-KEY",
"valueType": "msg",
"valueValue": "apiSecretKey"
},
{
"keyType": "other",
"keyValue": "accept",
"valueType": "other",
"valueValue": "application/json"
}
],
"x": 580,
"y": 260,
"wires": [
[
"b2c3d4e5f6a7000a"
]
]
},
{
"id": "b2c3d4e5f6a7000a",
"type": "function",
"z": "b2c3d4e5f6a70001",
"name": "Accumulate bars + paginate",
"func": "// Accumulate fetched bars and handle pagination\nvar bars = (msg.payload && msg.payload.bars) || [];\nvar nextPageToken = msg.payload ? msg.payload.next_page_token : null;\nvar allBars = flow.get(\"bt_allBars\") || [];\nvar pageCount = (flow.get(\"bt_pageCount\") || 0) + 1;\n\nallBars = allBars.concat(bars);\nflow.set(\"bt_allBars\", allBars);\nflow.set(\"bt_pageCount\", pageCount);\n\nnode.warn(\" Page \" + pageCount + \": fetched \" + bars.length + \" bars (total: \" + allBars.length + \")\");\n\nif (nextPageToken) {\n // More pages \u2014 build next request\n var symbol = flow.get(\"symbol\") || \"NVDA\";\n var startRFC = flow.get(\"backtestStartRFC\");\n var endRFC = flow.get(\"bt_endRFC\");\n\n var params = [\n \"timeframe=1Min\",\n \"start=\" + encodeURIComponent(startRFC),\n \"end=\" + encodeURIComponent(endRFC),\n \"feed=iex\",\n \"adjustment=split\",\n \"limit=10000\",\n \"page_token=\" + nextPageToken\n ].join(\"&\");\n\n msg.url = \"https://data.alpaca.markets/v2/stocks/\" + symbol + \"/bars?\" + params;\n msg.method = \"GET\";\n msg.apiKeyId = global.get(\"apiKeyLive\") || \"\";\n msg.apiSecretKey = global.get(\"apiSecretLive\") || \"\";\n\n // Send to output 1 (loop back to http request)\n return [msg, null];\n} else {\n // Done fetching \u2014 send to output 2 (simulation)\n node.warn(\"\u2705 All bars fetched: \" + allBars.length + \" total across \" + pageCount + \" pages\");\n msg.payload = allBars;\n return [null, msg];\n}\n",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 840,
"y": 260,
"wires": [
[
"b2c3d4e5f6a7000b"
],
[
"b2c3d4e5f6a7000c"
]
]
},
{
"id": "b2c3d4e5f6a7000b",
"type": "delay",
"z": "b2c3d4e5f6a70001",
"name": "250ms delay",
"pauseType": "delay",
"timeout": "250",
"timeoutUnits": "milliseconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 580,
"y": 320,
"wires": [
[
"b2c3d4e5f6a70009"
]
]
},
{
"id": "b2c3d4e5f6a7000c",
"type": "function",
"z": "b2c3d4e5f6a70001",
"name": "Run EMA simulation on all bars",
"func": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Run the full EMA z-score backtest simulation over all bars.\n// Identical logic to the live trader EMA engine.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvar bars = msg.payload || [];\nif (bars.length === 0) {\n node.warn(\"\u274c No bars to simulate.\");\n return null;\n}\n\n// Load parameters\nvar lam = flow.get(\"lambdaf\") || 0.75;\nvar lamVar = flow.get(\"lambdaVar\") || 0.95;\nvar varFloor = flow.get(\"varFloor\") || 0.05;\nvar varCap = flow.get(\"varCap\") || 100;\nvar varPrice = flow.get(\"varPrice\") || 0.001;\nvar zFact = flow.get(\"zFact\") || 1;\nvar thresh = flow.get(\"zScoreThresh\") || 2.0;\nvar tradeQty = flow.get(\"tradeqty\") || 25;\nvar limLong = flow.get(\"limitLong\") || 400;\nvar limShort = flow.get(\"limitShort\") || -400;\nvar balance = flow.get(\"balance\") || 125000;\nvar holidays = flow.get(\"holidays2026\") || [];\n\n// Engine state\nvar ema = null;\nvar variance = 0;\n\n// Position state\nvar position = 0;\nvar avgCost = 0;\nvar realizedPnl = 0;\n\n// Counters\nvar barCount = 0;\nvar buyCount = 0;\nvar sellCount = 0;\nvar firstPrice = null;\nvar lastPrice = null;\n\n// Results\nvar trades = [];\nvar dailyPnl = {};\n\n// Helper: check trading day\nfunction isTradingDay(dateStr) {\n var d = new Date(dateStr);\n var day = d.getDay();\n if (day === 0 || day === 6) return false;\n // Check holidays by formatted date\n var yyyy = d.getFullYear();\n var mm = String(d.getMonth() + 1).padStart(2, \"0\");\n var dd2 = String(d.getDate()).padStart(2, \"0\");\n var ds = yyyy + \"-\" + mm + \"-\" + dd2;\n return holidays.indexOf(ds) < 0;\n}\n\nnode.warn(\"Running simulation on \" + bars.length + \" bars...\");\n\nfor (var i = 0; i < bars.length; i++) {\n var bar = bars[i];\n var ts = bar.t || \"\";\n\n // Parse timestamp (Alpaca returns ISO 8601)\n var barDate = new Date(ts);\n if (isNaN(barDate.getTime())) continue;\n\n // Convert to ET\n var etStr = barDate.toLocaleString(\"en-US\", {timeZone: \"America/New_York\"});\n var et = new Date(etStr);\n var h = et.getHours();\n var m = et.getMinutes();\n var mins = h * 60 + m;\n\n // Only 9:30 \u2013 16:00 ET\n if (mins < 570 || mins >= 960) continue;\n\n // Trading day check\n var yyyy = et.getFullYear();\n var mm = String(et.getMonth() + 1).padStart(2, \"0\");\n var dd2 = String(et.getDate()).padStart(2, \"0\");\n var dateKey = yyyy + \"-\" + mm + \"-\" + dd2;\n if (holidays.indexOf(dateKey) >= 0) continue;\n if (et.getDay() === 0 || et.getDay() === 6) continue;\n\n // Use VWAP as price\n var price = bar.vw || bar.c || 0;\n if (price <= 0) continue;\n\n barCount++;\n if (firstPrice === null) firstPrice = price;\n lastPrice = price;\n\n // --- EMA Engine ---\n var zScore = 0;\n if (ema === null) {\n ema = price;\n variance = varPrice;\n } else {\n ema = lam * ema + (1 - lam) * price;\n var diff = price - ema;\n var rawVar = lamVar * variance + (1 - lamVar) * (diff * diff);\n variance = Math.max(varFloor, Math.min(rawVar, varCap));\n var std = Math.sqrt(variance);\n zScore = zFact * diff / (std || 1e-9);\n }\n\n // --- Signal ---\n var side = null;\n var qty = 0;\n if (zScore < -thresh) {\n var maxBuy = limLong - position;\n if (maxBuy > 0) {\n qty = Math.min(tradeQty, maxBuy);\n side = \"buy\";\n }\n } else if (zScore > thresh) {\n var maxSell = position - limShort;\n if (maxSell > 0) {\n qty = Math.min(tradeQty, maxSell);\n side = \"sell\";\n }\n }\n\n // --- Fill ---\n if (side) {\n if (side === \"buy\") {\n if (position >= 0) {\n var tc = avgCost * position + price * qty;\n position += qty;\n avgCost = position > 0 ? tc / position : 0;\n } else {\n var cov = Math.min(qty, Math.abs(position));\n realizedPnl += cov * (avgCost - price);\n position += qty;\n if (position > 0) avgCost = price;\n else if (position === 0) avgCost = 0;\n }\n buyCount++;\n } else {\n if (position <= 0) {\n var tcs = avgCost * Math.abs(position) + price * qty;\n position -= qty;\n avgCost = position !== 0 ? tcs / Math.abs(position) : 0;\n } else {\n var cl = Math.min(qty, position);\n realizedPnl += cl * (price - avgCost);\n position -= qty;\n if (position < 0) avgCost = price;\n else if (position === 0) avgCost = 0;\n }\n sellCount++;\n }\n trades.push({\n side: side, qty: qty,\n price: Number(price.toFixed(2)),\n position: position,\n realizedPnl: Number(realizedPnl.toFixed(2)),\n zScore: Number(zScore.toFixed(4)),\n timestamp: ts\n });\n }\n\n // --- Unrealized P&L ---\n var unrealized = 0;\n if (position > 0) {\n unrealized = position * (price - avgCost);\n } else if (position < 0) {\n unrealized = Math.abs(position) * (avgCost - price);\n }\n var totalPnl = realizedPnl + unrealized;\n\n // --- Daily tracking ---\n if (!dailyPnl[dateKey]) {\n dailyPnl[dateKey] = {\n date: dateKey,\n open_price: Number(price.toFixed(2)),\n close_price: Number(price.toFixed(2)),\n high_price: Number(price.toFixed(2)),\n low_price: Number(price.toFixed(2)),\n trades: 0, buys: 0, sells: 0,\n end_position: 0, realized_pnl: 0, total_pnl: 0\n };\n }\n var day = dailyPnl[dateKey];\n day.close_price = Number(price.toFixed(2));\n if (price > day.high_price) day.high_price = Number(price.toFixed(2));\n if (price < day.low_price) day.low_price = Number(price.toFixed(2));\n if (side) {\n day.trades++;\n if (side === \"buy\") day.buys++;\n else day.sells++;\n }\n day.end_position = position;\n day.realized_pnl = Number(realizedPnl.toFixed(2));\n day.total_pnl = Number(totalPnl.toFixed(2));\n}\n\n// --- Final mark-to-market ---\nvar finalUnrealized = 0;\nif (lastPrice && position > 0) {\n finalUnrealized = position * (lastPrice - avgCost);\n} else if (lastPrice && position < 0) {\n finalUnrealized = Math.abs(position) * (avgCost - lastPrice);\n}\nvar finalTotalPnl = realizedPnl + finalUnrealized;\nvar finalPct = (finalTotalPnl / balance * 100);\n\n// Buy & hold\nvar bhPnl = 0;\nvar bhPct = 0;\nif (firstPrice && lastPrice) {\n var bhShares = balance / firstPrice;\n bhPnl = bhShares * (lastPrice - firstPrice);\n bhPct = (lastPrice - firstPrice) / firstPrice * 100;\n}\n\n// Daily results sorted\nvar dailyResults = Object.values(dailyPnl).sort(function(a,b) {\n return a.date.localeCompare(b.date);\n});\n\n// Win/loss days\nvar winDays = 0, lossDays = 0, prevPnl = 0;\ndailyResults.forEach(function(d) {\n var delta = d.total_pnl - prevPnl;\n if (delta > 0) winDays++;\n else if (delta < 0) lossDays++;\n prevPnl = d.total_pnl;\n});\n\nvar totalTrades = buyCount + sellCount;\n\n// Store in flow\nflow.set(\"bt_results\", {\n barsProcessed: barCount,\n tradingDays: dailyResults.length,\n totalTrades: totalTrades,\n buys: buyCount,\n sells: sellCount,\n finalPosition: position,\n firstPrice: firstPrice ? Number(firstPrice.toFixed(2)) : null,\n lastPrice: lastPrice ? Number(lastPrice.toFixed(2)) : null,\n realizedPnl: Number(realizedPnl.toFixed(2)),\n unrealizedPnl: Number(finalUnrealized.toFixed(2)),\n totalPnl: Number(finalTotalPnl.toFixed(2)),\n totalPnlPct: Number(finalPct.toFixed(2)),\n endingBalance: Number((balance + finalTotalPnl).toFixed(2)),\n buyHoldPnl: Number(bhPnl.toFixed(2)),\n buyHoldPct: Number(bhPct.toFixed(2)),\n winDays: winDays,\n lossDays: lossDays\n});\nflow.set(\"bt_dailyResults\", dailyResults);\nflow.set(\"bt_tradeLog\", trades.slice(-200));\n\nnode.warn(\"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\");\nnode.warn(\" NVDA EMA Z-SCORE BACKTEST COMPLETE\");\nnode.warn(\"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\");\nnode.warn(\" Bars: \" + barCount + \" | Days: \" + dailyResults.length);\nnode.warn(\" Trades: \" + totalTrades + \" (\" + buyCount + \" buys, \" + sellCount + \" sells)\");\nnode.warn(\" Position: \" + position + \" shares\");\nnode.warn(\" First: $\" + (firstPrice||0).toFixed(2) + \" | Last: $\" + (lastPrice||0).toFixed(2));\nvar sign = finalTotalPnl >= 0 ? \"+\" : \"\";\nnode.warn(\" Realized P&L: \" + sign + \"$\" + realizedPnl.toFixed(2));\nnode.warn(\" Unrealized P&L: \" + (finalUnrealized >= 0 ? \"+\" : \"\") + \"$\" + finalUnrealized.toFixed(2));\nnode.warn(\" Total P&L: \" + sign + \"$\" + finalTotalPnl.toFixed(2) + \" (\" + sign + finalPct.toFixed(2) + \"%)\");\nnode.warn(\" Ending Balance: $\" + (balance + finalTotalPnl).toFixed(2));\nnode.warn(\" Buy & Hold: \" + (bhPnl >= 0 ? \"+\" : \"\") + \"$\" + bhPnl.toFixed(2) + \" (\" + (bhPct >= 0 ? \"+\" : \"\") + bhPct.toFixed(2) + \"%)\");\nnode.warn(\" Win/Loss Days: \" + winDays + \" / \" + lossDays);\nnode.warn(\"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\");\n\nmsg.payload = flow.get(\"bt_results\");\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 190,
"y": 420,
"wires": [
[
"b2c3d4e5f6a7000d"
]
]
},
{
"id": "b2c3d4e5f6a7000d",
"type": "debug",
"z": "b2c3d4e5f6a70001",
"name": "Backtest Summary",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 430,
"y": 420,
"wires": []
},
{
"id": "b2c3d4e5f6a7000e",
"type": "comment",
"z": "b2c3d4e5f6a70001",
"name": "Step 3: Display detailed results (daily P&L, trade log)",
"info": "",
"x": 270,
"y": 500,
"wires": []
},
{
"id": "b2c3d4e5f6a7000f",
"type": "inject",
"z": "b2c3d4e5f6a70001",
"name": "Run once",
"props": [],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 120,
"y": 540,
"wires": [
[
"b2c3d4e5f6a70010"
]
]
},
{
"id": "b2c3d4e5f6a70010",
"type": "function",
"z": "b2c3d4e5f6a70001",
"name": "Display detailed backtest report",
"func": "var results = flow.get(\"bt_results\");\nvar daily = flow.get(\"bt_dailyResults\") || [];\nvar trades = flow.get(\"bt_tradeLog\") || [];\nvar balance = flow.get(\"balance\") || 125000;\n\nif (!results) {\n node.warn(\"\u274c No backtest results. Run Steps 1 and 2 first.\");\n return null;\n}\n\n// \u2500\u2500 Summary \u2500\u2500\nnode.warn(\"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\");\nnode.warn(\"\u2551 NVDA EMA Z-SCORE BACKTEST \u2014 FULL REPORT \u2551\");\nnode.warn(\"\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\");\nnode.warn(\"\u2551 Balance: $\" + balance.toLocaleString());\nnode.warn(\"\u2551 Bars: \" + results.barsProcessed.toLocaleString());\nnode.warn(\"\u2551 Trading Days: \" + results.tradingDays);\nnode.warn(\"\u2551 Total Trades: \" + results.totalTrades + \" (\" + results.buys + \"B / \" + results.sells + \"S)\");\nnode.warn(\"\u2551 Final Pos: \" + results.finalPosition + \" shares\");\nvar s = results.totalPnl >= 0 ? \"+\" : \"\";\nnode.warn(\"\u2551 Total P&L: \" + s + \"$\" + results.totalPnl.toFixed(2) + \" (\" + s + results.totalPnlPct.toFixed(2) + \"%)\");\nnode.warn(\"\u2551 End Balance: $\" + results.endingBalance.toFixed(2));\nnode.warn(\"\u2551 Win/Loss: \" + results.winDays + \"W / \" + results.lossDays + \"L\");\nnode.warn(\"\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\");\n\n// \u2500\u2500 Daily P&L \u2500\u2500\nnode.warn(\"\");\nnode.warn(\"\u2500\u2500 Daily P&L \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\");\ndaily.forEach(function(d) {\n var sign = d.total_pnl >= 0 ? \"+\" : \"\";\n var emoji = d.total_pnl >= 0 ? \"\ud83d\udcc8\" : \"\ud83d\udcc9\";\n node.warn(emoji + \" \" + d.date + \" | O:$\" + d.open_price.toFixed(2) + \" C:$\" + d.close_price.toFixed(2) + \" | Trades: \" + d.trades + \" | Pos: \" + d.end_position + \" | P&L: \" + sign + \"$\" + d.total_pnl.toFixed(2));\n});\n\n// \u2500\u2500 Last 20 trades \u2500\u2500\nnode.warn(\"\");\nnode.warn(\"\u2500\u2500 Recent Trades (last 20) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\");\nvar recent = trades.slice(-20);\nrecent.forEach(function(t) {\n var ts = t.timestamp ? t.timestamp.substring(0, 19) : \"\";\n var emoji = t.side === \"buy\" ? \"\ud83d\udfe2\" : \"\ud83d\udd34\";\n node.warn(emoji + \" \" + ts + \" | \" + t.side.toUpperCase() + \" \" + t.qty + \" @ $\" + t.price.toFixed(2) + \" | z=\" + t.zScore.toFixed(3) + \" | Pos: \" + t.position + \" | Realized: $\" + t.realizedPnl.toFixed(2));\n});\n\n// \u2500\u2500 Store in global \u2500\u2500\nglobal.set(\"backtestEmaNvdaResults\", results);\nglobal.set(\"backtestEmaNvdaDaily\", daily);\nglobal.set(\"backtestEmaNvdaTrades\", trades);\n\nmsg.payload = {\n summary: results,\n dailyResults: daily,\n trades: recent\n};\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 380,
"y": 540,
"wires": [
[
"b2c3d4e5f6a70011"
]
]
},
{
"id": "b2c3d4e5f6a70011",
"type": "debug",
"z": "b2c3d4e5f6a70001",
"name": "Full Report",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 610,
"y": 540,
"wires": []
},
{
"id": "b2c3d4e5f6a70012",
"type": "comment",
"z": "b2c3d4e5f6a70001",
"name": "Note: Set global.apiKeyLive & global.apiSecretLive for historical data requests. Simulation output \u2192 2nd wire of accumulator node.",
"info": "",
"x": 430,
"y": 620,
"wires": []
},
{
"id": "b2c3d4e5f6a70013",
"type": "comment",
"z": "b2c3d4e5f6a70001",
"name": "\u2193 Simulation runs here after all bars are fetched (output 2 of accumulator)",
"info": "",
"x": 310,
"y": 380,
"wires": []
},
{
"id": "1dfe729b4b3149f6",
"type": "alpaca-account",
"name": "Paper",
"keyId": "USE-OAUTH-OR-REPLACE",
"paper": true
}
]
Edit the "Initialize backtest params" function node in Step 1 to customize:
Update flow.set("symbol", "NVDA") to any Alpaca-supported ticker to backtest the same EMA strategy on a different stock.
Adjust lambdaf (0→1): lower values make the EMA more responsive. Adjust lambdaVar to control how quickly variance adapts (higher = slower adaptation = more signal spikes).
Raise zScoreThresh for fewer but higher-conviction trades. Lower it for more frequent signals. The default of 2.0 corresponds to ~2 standard deviations.
Modify backtestStartRFC to an earlier or later date. Use RFC 3339 format in UTC, e.g., "2025-06-01T13:30:00Z" for June 1, 2025 at 9:30 AM ET.
Change tradeqty (shares per trade), limitLong, and limitShort to control trade size and maximum exposure in either direction.
Add a ui_template node after Step 3 to render results on a Node-RED Dashboard. The global context vars provide all the data you need for charts and tables.
View the Python version of this backtest with full performance analysis charts and results.
Equal-weight Magnificent 7 portfolio trading flow for AAPL, AMZN, GOOG, META, MSFT, NVDA, TSLA.
Node-RED backtest for the Magnificent 7 buy-and-hold portfolio from January 2025.
Import the flow into your MachineTrader™ instance and backtest the NVDA EMA z-score strategy in minutes.