Automated mean-reversion trading flow for NVDA using EMA z-score signals. Trades every 10 seconds during US market hours via Alpaca.
This Node-RED flow implements a live EMA z-score mean-reversion trading strategy on NVIDIA (NVDA). It polls the Alpaca IEX feed for the latest quote every 10 seconds, computes a two-speed Exponential Weighted Moving Average, and places real market orders when the z-score crosses the ±2.0 threshold.
The strategy buys when NVDA's price drops significantly below its EMA (z < −2.0) and sells when the price spikes above it (z > +2.0). Position limits, cost basis tracking, realized P&L, and a full trade log are all managed within flow context variables. The flow includes a market hours gate that blocks trading on weekends, holidays, and outside 9:30 AM – 4:00 PM ET.
Configured in Step 1's initialization function node:
The flow has 4 steps. Click Step 1 to initialize, then deploy — Steps 2 and 4 run automatically:
Click the inject node once to store all strategy parameters and reset state variables in flow context. Sets the EMA decay rates, position limits, trade size, z-score threshold, and the 2026 NYSE holiday calendar. State variables (ema, variance, position, avgCost, realizedPnl, tradeLog) are all reset to their defaults.
An inject node fires every 10 seconds. The market hours gate checks the current time in Eastern Time and blocks messages on weekends, holidays, and outside 9:30 AM – 4:00 PM. Passing messages get the NVDA symbol set, then the Alpaca last-quote node fetches the latest bid/ask. The EMA Z-Score Engine computes the midpoint price, updates the EMA and adaptive variance, calculates the z-score, and generates a BUY or SELL signal (or returns null for HOLD, which blocks downstream). If a signal fires, the order is built and submitted via the Alpaca order node, then position and cost basis are updated.
Click the inject node at any time to print the current strategy state to the debug sidebar: ticks processed, trades executed, current EMA value, variance, position size, average cost, realized P&L, and the most recent trade. Useful for monitoring the strategy mid-session.
Runs on a cron schedule (every minute, M–F during market hours). Queries the Alpaca position for NVDA, combines the unrealized P&L from the broker with the locally tracked realized P&L, and stores the total in global.strategyEmaNvdaGainloss and global.strategyEmaNvdaGainlosspct for use by dashboards and other flows.
Complete visual overview of all nodes and their connections:
Fast EMA (λ=0.75) tracks the price level, while a slower variance estimator (λvar=0.95, ~20-tick lookback) measures volatility. The z-score = (price − EMA) / √variance, allowing signals to fire on sudden moves before variance catches up.
Automatically blocks all trading outside US market hours (9:30 AM – 4:00 PM ET). Respects weekends and all 2026 NYSE holidays. No manual intervention needed — just deploy and let it run.
Tracks average cost and handles all position transitions: adding to longs/shorts, closing positions, and flipping from long to short (or vice versa). Realized P&L is computed on every fill.
Hard position limits of +400 / −400 shares prevent runaway exposure. Trade size is fixed at 25 shares per signal. The z-score threshold of ±2.0 ensures only high-conviction trades are taken.
Click the Step 3 inject node anytime to see: ticks processed, trades executed, current EMA, variance, position, average cost, realized P&L, and the last trade — all in the debug sidebar.
Step 4 combines Alpaca's real-time unrealized P&L with locally tracked realized P&L and stores the total in global context — ready for Node-RED Dashboard charts, tables, or external API endpoints.
alpaca-account config node (ID: 1dfe729b4b3149f6). After import, open any Alpaca node and configure with your credentials or OAuth token.alpaca-data-last-quote, alpaca-order, alpaca-position-query. Install the MachineTrader Alpaca palette if not already present.🚨 LIVE TRADING WARNING: This flow places real market orders via Alpaca when deployed. It is configured for Paper Trading by default, but double-check your Alpaca config node before deploying. Always test with paper trading before using real funds.
💡 Tip: To stop trading, either disable the "Every 10s" inject node (double-click → uncheck "Repeat") or disable the entire flow tab. The strategy state will persist in flow context until you re-initialize with Step 1.
Click the button to copy the complete flow to your clipboard:
[
{
"id": "a1b2c3d4e5f60001",
"type": "tab",
"label": "EMA NVDA Z-Score Trader",
"disabled": false,
"info": "EMA z-score mean-reversion strategy on NVDA.\nTrades every 10 seconds during market hours (9:30 AM \u2013 4:00 PM ET, M\u2013F, excluding holidays).\nUses Alpaca IEX feed for quotes and paper trading API for orders.",
"env": []
},
{
"id": "a1b2c3d4e5f60002",
"type": "comment",
"z": "a1b2c3d4e5f60001",
"name": "NVDA EMA Z-Score Trader \u2014 Click Step 1 to initialize, then Step 2 to start the 10-second trading loop.",
"info": "",
"x": 370,
"y": 40,
"wires": []
},
{
"id": "a1b2c3d4e5f60003",
"type": "comment",
"z": "a1b2c3d4e5f60001",
"name": "Step 1: Initialize strategy parameters",
"info": "",
"x": 190,
"y": 100,
"wires": []
},
{
"id": "a1b2c3d4e5f60004",
"type": "inject",
"z": "a1b2c3d4e5f60001",
"name": "Run once",
"props": [],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 120,
"y": 140,
"wires": [
[
"a1b2c3d4e5f60005"
]
]
},
{
"id": "a1b2c3d4e5f60005",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "Initialize EMA strategy params",
"func": "// \u2500\u2500 Strategy parameters \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(\"symbol\", \"NVDA\");\nflow.set(\"lambdaf\", 0.75); // EMA decay (fast)\nflow.set(\"lambdaVar\", 0.95); // Variance decay (slow)\nflow.set(\"limitLong\", 400); // max long shares\nflow.set(\"limitShort\", -400); // max short shares\nflow.set(\"tradeqty\", 25); // shares per order\nflow.set(\"balance\", 125000); // notional balance\nflow.set(\"varPrice\", 0.001); // variance seed\nflow.set(\"varCap\", 100); // variance cap\nflow.set(\"varFloor\", 0.05); // variance floor\nflow.set(\"zFact\", 1); // z-score scale factor\nflow.set(\"zScoreThresh\", 2.0); // z-score threshold\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\nflow.set(\"ema\", null); // current EMA\nflow.set(\"variance\", 0); // current variance\nflow.set(\"position\", 0); // net shares held\nflow.set(\"tickCount\", 0); // ticks processed\nflow.set(\"tradeCount\", 0); // orders filled\nflow.set(\"realizedPnl\", 0); // realized P&L\nflow.set(\"avgCost\", 0); // average cost/share\nflow.set(\"tradeLog\", []); // recent trades\n\n// US Market Holidays 2026\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 EMA NVDA strategy initialized \u2014 Symbol: NVDA | \u03bb=0.75 | \u03bb_var=0.95 | Z-thresh=2.0 | Qty=25 | Limits: +400/\u2212400\");\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 370,
"y": 140,
"wires": [
[]
]
},
{
"id": "a1b2c3d4e5f60006",
"type": "comment",
"z": "a1b2c3d4e5f60001",
"name": "Step 2: Trading loop \u2014 fires every 10 seconds during market hours",
"info": "",
"x": 290,
"y": 200,
"wires": []
},
{
"id": "a1b2c3d4e5f60007",
"type": "inject",
"z": "a1b2c3d4e5f60001",
"name": "Every 10s (M-F 9:30-16:00 ET)",
"props": [],
"repeat": "10",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 190,
"y": 260,
"wires": [
[
"a1b2c3d4e5f60008"
]
]
},
{
"id": "a1b2c3d4e5f60008",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "Market hours gate",
"func": "// Only allow messages through during market hours\n// 9:30 AM \u2013 4:00 PM ET, Monday\u2013Friday, excluding holidays\n\nvar now = new Date();\n// Convert to ET string to extract components\nvar etStr = now.toLocaleString(\"en-US\", {timeZone: \"America/New_York\"});\nvar et = new Date(etStr);\n\nvar day = et.getDay(); // 0=Sun, 6=Sat\nif (day === 0 || day === 6) { return null; }\n\n// Check holidays\nvar holidays = flow.get(\"holidays2026\") || [];\nvar yyyy = et.getFullYear();\nvar mm = String(et.getMonth() + 1).padStart(2, \"0\");\nvar dd = String(et.getDate()).padStart(2, \"0\");\nvar dateStr = yyyy + \"-\" + mm + \"-\" + dd;\nif (holidays.indexOf(dateStr) >= 0) { return null; }\n\n// Check time: 9:30 \u2013 16:00\nvar h = et.getHours();\nvar m = et.getMinutes();\nvar mins = h * 60 + m;\nif (mins < 570 || mins >= 960) { return null; } // 570 = 9:30, 960 = 16:00\n\nmsg.etTime = String(h).padStart(2,\"0\") + \":\" + String(m).padStart(2,\"0\") + \":\" + String(et.getSeconds()).padStart(2,\"0\");\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 430,
"y": 260,
"wires": [
[
"a1b2c3d4e5f60009"
]
]
},
{
"id": "a1b2c3d4e5f60009",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "Set msg.symbol",
"func": "msg.symbol = flow.get(\"symbol\") || \"NVDA\";\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 640,
"y": 260,
"wires": [
[
"a1b2c3d4e5f6000a"
]
]
},
{
"id": "a1b2c3d4e5f6000a",
"type": "alpaca-data-last-quote",
"z": "a1b2c3d4e5f60001",
"conf": "1dfe729b4b3149f6",
"symbol": "",
"name": "Get NVDA quote",
"x": 860,
"y": 260,
"wires": [
[
"a1b2c3d4e5f6000b"
]
]
},
{
"id": "a1b2c3d4e5f6000b",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "EMA Z-Score Engine + Signal",
"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// EMA Z-Score Engine \u2014 computes EMA, adaptive variance,\n// z-score, and generates BUY / SELL / HOLD signal.\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\n\n// --- Extract midpoint price from quote ---\nvar ask = Number(msg.payload.ask_price) || 0;\nvar bid = Number(msg.payload.bid_price) || 0;\nvar price = 0;\nif (ask > 0 && bid > 0) { price = (ask + bid) / 2; }\nelse if (ask > 0) { price = ask; }\nelse { price = bid; }\n\nif (price <= 0) { return null; } // no valid price\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;\n\n// --- Load state ---\nvar ema = flow.get(\"ema\");\nvar variance = flow.get(\"variance\") || 0;\nvar position = flow.get(\"position\") || 0;\nvar tickCount= (flow.get(\"tickCount\") || 0) + 1;\nflow.set(\"tickCount\", tickCount);\n\n// --- EMA + Variance update ---\nvar zScore = 0;\n\nif (ema === null || ema === undefined) {\n // First tick \u2014 seed\n ema = price;\n variance = varPrice;\n} else {\n // Update EMA (fast)\n ema = lam * ema + (1 - lam) * price;\n\n // Deviation from EMA\n var diff = price - ema;\n\n // Adaptive variance (slow decay)\n var rawVar = lamVar * variance + (1 - lamVar) * (diff * diff);\n variance = Math.max(varFloor, Math.min(rawVar, varCap));\n\n // Z-score\n var std = Math.sqrt(variance);\n zScore = zFact * diff / (std || 1e-9);\n}\n\n// Persist state\nflow.set(\"ema\", ema);\nflow.set(\"variance\", variance);\n\n// --- Signal logic ---\nvar side = null;\nvar qty = 0;\n\nif (zScore < -thresh) {\n // Buy signal \u2014 price below EMA (mean reversion long)\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 // Sell signal \u2014 price above EMA (mean reversion short)\n var maxSell = position - limShort;\n if (maxSell > 0) {\n qty = Math.min(tradeQty, maxSell);\n side = \"sell\";\n }\n}\n\nvar status = side ? (side.toUpperCase() + \" \" + qty) : \"HOLD\";\nvar zStr = (zScore >= 0 ? \"+\" : \"\") + zScore.toFixed(3);\nnode.warn(\"[\" + (msg.etTime||\"?\") + \"] #\" + tickCount + \" | $\" + price.toFixed(2) + \" | EMA $\" + ema.toFixed(2) + \" | Var \" + variance.toFixed(4) + \" | Z \" + zStr + \" | Pos \" + position + \" | \" + status);\n\n// --- Route output ---\nif (side) {\n msg.price = price;\n msg.zScore = zScore;\n msg.emaValue = ema;\n msg.tradeQty = qty;\n msg.tradeSide = side;\n return msg; // \u2192 order node\n}\n\nreturn null; // hold \u2014 no output\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 200,
"y": 360,
"wires": [
[
"a1b2c3d4e5f6000c"
]
]
},
{
"id": "a1b2c3d4e5f6000c",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "Build market order",
"func": "// Build Alpaca market order from signal\nvar symbol = flow.get(\"symbol\") || \"NVDA\";\nvar side = msg.tradeSide;\nvar qty = msg.tradeQty;\n\nvar d = Date.now();\nmsg.client_order_id = symbol + \"_ema_\" + d;\n\nmsg.payload = {\n \"symbol\": symbol,\n \"qty\": String(qty),\n \"side\": side,\n \"type\": \"market\",\n \"time_in_force\": \"day\",\n \"client_order_id\": msg.client_order_id\n};\n\nnode.warn(\"\ud83d\udce4 ORDER: \" + side.toUpperCase() + \" \" + qty + \" \" + symbol + \" @ ~$\" + msg.price.toFixed(2));\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 450,
"y": 360,
"wires": [
[
"a1b2c3d4e5f6000d"
]
]
},
{
"id": "a1b2c3d4e5f6000d",
"type": "alpaca-order",
"z": "a1b2c3d4e5f60001",
"conf": "1dfe729b4b3149f6",
"name": "Submit order",
"x": 670,
"y": 360,
"wires": [
[
"a1b2c3d4e5f6000e"
]
]
},
{
"id": "a1b2c3d4e5f6000e",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "Update position + log trade",
"func": "// Update position state after a fill\nvar side = msg.tradeSide;\nvar qty = msg.tradeQty;\nvar price = msg.price;\nvar position = flow.get(\"position\") || 0;\nvar avgCost = flow.get(\"avgCost\") || 0;\nvar realized = flow.get(\"realizedPnl\") || 0;\n\nif (side === \"buy\") {\n if (position >= 0) {\n // Adding to long\n var totalCost = avgCost * position + price * qty;\n position += qty;\n avgCost = position > 0 ? totalCost / position : 0;\n } else {\n // Covering short\n var coverQty = Math.min(qty, Math.abs(position));\n realized += coverQty * (avgCost - price);\n position += qty;\n if (position > 0) { avgCost = price; }\n else if (position === 0) { avgCost = 0; }\n }\n} else {\n // sell\n if (position <= 0) {\n // Adding to short\n var totalCostS = avgCost * Math.abs(position) + price * qty;\n position -= qty;\n avgCost = position !== 0 ? totalCostS / Math.abs(position) : 0;\n } else {\n // Closing long\n var closeQty = Math.min(qty, position);\n realized += closeQty * (price - avgCost);\n position -= qty;\n if (position < 0) { avgCost = price; }\n else if (position === 0) { avgCost = 0; }\n }\n}\n\nflow.set(\"position\", position);\nflow.set(\"avgCost\", avgCost);\nflow.set(\"realizedPnl\", realized);\n\nvar tradeCount = (flow.get(\"tradeCount\") || 0) + 1;\nflow.set(\"tradeCount\", tradeCount);\n\n// Log trade\nvar log = flow.get(\"tradeLog\") || [];\nlog.push({\n time: new Date().toISOString(),\n side: side,\n qty: qty,\n price: Number(price.toFixed(2)),\n position: position,\n realizedPnl: Number(realized.toFixed(2)),\n zScore: Number((msg.zScore || 0).toFixed(4))\n});\nif (log.length > 200) { log = log.slice(-200); }\nflow.set(\"tradeLog\", log);\n\n// Check for error response\nvar errMsg = msg.payload && msg.payload.message;\nif (errMsg) {\n node.warn(\"\u274c ORDER ERROR: \" + errMsg);\n} else {\n var sign = realized >= 0 ? \"+\" : \"\";\n node.warn(\"\u2705 FILLED: \" + side.toUpperCase() + \" \" + qty + \" NVDA @ $\" + price.toFixed(2) + \" | Pos: \" + position + \" | Realized P&L: \" + sign + \"$\" + realized.toFixed(2) + \" | Trade #\" + tradeCount);\n}\n\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 920,
"y": 360,
"wires": [
[
"a1b2c3d4e5f6000f"
]
]
},
{
"id": "a1b2c3d4e5f6000f",
"type": "debug",
"z": "a1b2c3d4e5f60001",
"name": "Trade Result",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1130,
"y": 360,
"wires": []
},
{
"id": "a1b2c3d4e5f60010",
"type": "comment",
"z": "a1b2c3d4e5f60001",
"name": "Step 3: Status dashboard \u2014 click to print current state",
"info": "",
"x": 260,
"y": 440,
"wires": []
},
{
"id": "a1b2c3d4e5f60011",
"type": "inject",
"z": "a1b2c3d4e5f60001",
"name": "Check status",
"props": [],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 130,
"y": 480,
"wires": [
[
"a1b2c3d4e5f60012"
]
]
},
{
"id": "a1b2c3d4e5f60012",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "Print strategy status",
"func": "var ema = flow.get(\"ema\");\nvar variance = flow.get(\"variance\") || 0;\nvar position = flow.get(\"position\") || 0;\nvar avgCost = flow.get(\"avgCost\") || 0;\nvar realized = flow.get(\"realizedPnl\") || 0;\nvar ticks = flow.get(\"tickCount\") || 0;\nvar trades = flow.get(\"tradeCount\") || 0;\nvar balance = flow.get(\"balance\") || 125000;\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\");\nnode.warn(\" NVDA EMA Z-Score Strategy Status\");\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\");\nnode.warn(\" Ticks processed: \" + ticks);\nnode.warn(\" Trades executed: \" + trades);\nnode.warn(\" Current EMA: \" + (ema !== null ? \"$\" + ema.toFixed(4) : \"not seeded\"));\nnode.warn(\" Variance: \" + variance.toFixed(6));\nnode.warn(\" Position: \" + position + \" shares\");\nnode.warn(\" Avg Cost: $\" + avgCost.toFixed(2));\nvar sign = realized >= 0 ? \"+\" : \"\";\nnode.warn(\" Realized P&L: \" + sign + \"$\" + realized.toFixed(2));\n\n// Trade log\nvar log = flow.get(\"tradeLog\") || [];\nnode.warn(\" Recent trades: \" + log.length + \" logged\");\nif (log.length > 0) {\n var last = log[log.length - 1];\n node.warn(\" Last trade: \" + last.side.toUpperCase() + \" \" + last.qty + \" @ $\" + last.price + \" (z=\" + last.zScore + \")\");\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\");\n\nmsg.payload = {\n ticks: ticks,\n trades: trades,\n ema: ema,\n variance: variance,\n position: position,\n avgCost: Number(avgCost.toFixed(2)),\n realizedPnl: Number(realized.toFixed(2)),\n recentTrades: log.slice(-10)\n};\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 380,
"y": 480,
"wires": [
[
"a1b2c3d4e5f60013"
]
]
},
{
"id": "a1b2c3d4e5f60013",
"type": "debug",
"z": "a1b2c3d4e5f60001",
"name": "Strategy Status",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 600,
"y": 480,
"wires": []
},
{
"id": "a1b2c3d4e5f60014",
"type": "comment",
"z": "a1b2c3d4e5f60001",
"name": "Step 4: Calculate strategy P&L (position market value + net trades). Uses global ordersPaper.",
"info": "",
"x": 360,
"y": 560,
"wires": []
},
{
"id": "a1b2c3d4e5f60015",
"type": "inject",
"z": "a1b2c3d4e5f60001",
"name": "Run Once",
"props": [],
"repeat": "",
"crontab": "*/1 4-19 * * 1,2,3,4,5",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 120,
"y": 600,
"wires": [
[
"a1b2c3d4e5f60016"
]
]
},
{
"id": "a1b2c3d4e5f60016",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "Set msg.symbol for position query",
"func": "msg.symbol = flow.get(\"symbol\") || \"NVDA\";\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 350,
"y": 600,
"wires": [
[
"a1b2c3d4e5f60017"
]
]
},
{
"id": "a1b2c3d4e5f60017",
"type": "alpaca-position-query",
"z": "a1b2c3d4e5f60001",
"conf": "1dfe729b4b3149f6",
"symbol": "",
"name": "",
"x": 590,
"y": 600,
"wires": [
[
"a1b2c3d4e5f60018"
]
]
},
{
"id": "a1b2c3d4e5f60018",
"type": "function",
"z": "a1b2c3d4e5f60001",
"name": "Calculate EMA NVDA P&L",
"func": "// Calculate strategy gain/loss from position + realized trades\nvar symbol = flow.get(\"symbol\") || \"NVDA\";\nvar balance = flow.get(\"balance\") || 125000;\nvar realizedPnl = flow.get(\"realizedPnl\") || 0;\n\n// Current position market value from Alpaca\nvar marketValue = 0;\nvar unrealizedPnl = 0;\n\nif (msg.payload && !msg.payload.message) {\n // Single position object or array\n var pos = Array.isArray(msg.payload) ? msg.payload[0] : msg.payload;\n if (pos && pos.market_value !== undefined) {\n marketValue = parseFloat(pos.market_value) || 0;\n unrealizedPnl = parseFloat(pos.unrealized_pl) || 0;\n }\n}\n\nvar totalPnl = realizedPnl + unrealizedPnl;\nvar pctPnl = (totalPnl / balance * 100).toFixed(2);\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\");\nnode.warn(\" EMA NVDA Strategy P&L\");\nnode.warn(\" Market Value: $\" + marketValue.toFixed(2));\nnode.warn(\" Unrealized P&L: $\" + unrealizedPnl.toFixed(2));\nnode.warn(\" Realized P&L: $\" + realizedPnl.toFixed(2));\nnode.warn(\" Total P&L: $\" + totalPnl.toFixed(2) + \" (\" + pctPnl + \"%)\");\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\");\n\nglobal.set(\"strategyEmaNvdaGainloss\", totalPnl.toFixed(2));\nglobal.set(\"strategyEmaNvdaGainlosspct\", pctPnl);\n\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 830,
"y": 600,
"wires": [
[]
]
},
{
"id": "a1b2c3d4e5f60019",
"type": "comment",
"z": "a1b2c3d4e5f60001",
"name": "Note: Uses Alpaca Paper config node. Set global.apiKeyLive / global.apiSecretLive if also using http-request nodes.",
"info": "",
"x": 420,
"y": 680,
"wires": []
},
{
"id": "1dfe729b4b3149f6",
"type": "alpaca-account",
"name": "Paper",
"keyId": "USE-OAUTH-OR-REPLACE",
"paper": true
}
]
Edit the "Initialize EMA strategy params" function node (Step 1) to customize:
Update flow.set("symbol", "NVDA") to any Alpaca-supported ticker. The entire flow adapts automatically — quotes, orders, and position queries will use the new symbol.
Adjust lambdaf (0→1): lower = more responsive EMA. Adjust lambdaVar to control variance adaptation speed. The two-speed design lets you dial in signal responsiveness independently from volatility tracking.
Raise zScoreThresh for fewer but stronger signals. Lower it for more frequent trades. Each 0.5 increase roughly halves the trade frequency.
Double-click the "Every 10s" inject node and change the repeat interval. Faster intervals mean more ticks but more API calls. Slower intervals reduce API usage but may miss short-lived signals.
Change tradeqty (shares per order), limitLong, and limitShort to control per-trade size and maximum exposure. Scale proportionally with your account size.
The holidays2026 array contains NYSE holidays for 2026. Update annually or add early close dates (1:00 PM ET) by adjusting the market hours gate logic.
Backtest this exact strategy using historical 1-min bars, all within Node-RED — no Python needed.
Full Python backtest with performance charts, daily P&L breakdown, and buy-and-hold comparison.
Equal-weight Magnificent 7 portfolio flow — buy AAPL, AMZN, GOOG, META, MSFT, NVDA, TSLA.
Import the NVDA EMA z-score trading flow into your MachineTrader™ instance and start trading in minutes.