Live Trading Flow

NVDA EMA Z-Score
Node-RED Trader

Automated mean-reversion trading flow for NVDA using EMA z-score signals. Trades every 10 seconds during US market hours via Alpaca.

Overview

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.

Strategy Parameters

Configured in Step 1's initialization function node:

Symbol
NVDA
EMA Lambda (λ)
0.75
Variance Lambda (λvar)
0.95
Z-Score Threshold
±2.0
Trade Quantity
25 shares
Long Limit
400 shares
Short Limit
−400 shares
Account Balance
$125,000
Trade Interval
10 sec
Var Seed
0.001
Var Cap / Floor
100 / 0.05
Z Factor
1

How the Flow Works

The flow has 4 steps. Click Step 1 to initialize, then deploy — Steps 2 and 4 run automatically:

1

Initialize Strategy Parameters

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.

inject · Run once ƒ Initialize EMA strategy params
2

Trading Loop — Every 10 Seconds

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.

inject · Every 10s ƒ Market hours gate ƒ Set msg.symbol 🔌 Get NVDA quote
ƒ EMA Z-Score Engine + Signal ƒ Build market order 🔌 Submit order ƒ Update position + log 🐛 Trade Result
3

Status Dashboard

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.

inject · Check status ƒ Print strategy status 🐛 Strategy Status
4

Calculate Strategy P&L

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.

inject · Cron M–F ƒ Set msg.symbol 🔌 Position query ƒ Calculate EMA NVDA P&L

Flow Architecture

Complete visual overview of all nodes and their connections:

Step 1: Initialize (run once)
⏱ Run once
ƒ Initialize EMA strategy params
Step 2: Trading Loop (auto, every 10s)
⏱ Every 10s
ƒ Market hours gate
ƒ Set msg.symbol
🔌 Get NVDA quote
ƒ EMA Z-Score Engine + Signal
(returns null for HOLD)
ƒ Build market order
🔌 Submit order
ƒ Update position + log
🐛 Trade Result
Step 3: Status (click anytime)
⏱ Check status
ƒ Print strategy status
🐛 Strategy Status
Step 4: P&L (cron M–F)
⏱ Cron M–F
ƒ Set msg.symbol
🔌 Position query
ƒ Calculate P&L

Key Features

📐 Two-Speed EMA Engine

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.

🕐 Smart Market Hours Gate

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.

💱 Full Cost Basis Tracking

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.

🛡️ Risk Controls

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.

📊 Real-Time Status Dashboard

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.

🌐 Global P&L Tracking

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.

Prerequisites

  • Alpaca account: Paper or live trading account with API key and secret.
  • Alpaca config node: The flow uses the alpaca-account config node (ID: 1dfe729b4b3149f6). After import, open any Alpaca node and configure with your credentials or OAuth token.
  • Required palette nodes: alpaca-data-last-quote, alpaca-order, alpaca-position-query. Install the MachineTrader Alpaca palette if not already present.
  • MachineTrader: Any MachineTrader instance running Node-RED v3+.
  • Margin: A $125,000 account is recommended for the default position limits (400 shares × ~$130/share = ~$52k notional).

How to Import This Flow

  1. Copy the JSON code from the box below by clicking the "Copy to Clipboard" button.
  2. Open your MachineTrader Node-RED editor.
  3. Click the hamburger menu (☰) in the top-right corner.
  4. Select Import from the dropdown menu.
  5. Paste the JSON code into the import dialog.
  6. Click Import to add the flow to your workspace.
  7. Configure your Alpaca credentials — double-click the "Paper" config node inside any Alpaca node and enter your API key/secret or connect via OAuth.
  8. Click Deploy to activate the flow.
  9. Click the Step 1 inject node to initialize strategy parameters.
  10. The Step 2 trading loop starts automatically — it will begin trading at the next market open.

🚨 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.

Node-RED Flow JSON

Click the button to copy the complete flow to your clipboard:

EMA NVDA Trader.json
[
    {
        "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
    }
]

Customization Options

Edit the "Initialize EMA strategy params" function node (Step 1) to customize:

🎯 Change Symbol

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.

⚙️ Tune EMA Sensitivity

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.

📏 Adjust Z-Score Threshold

Raise zScoreThresh for fewer but stronger signals. Lower it for more frequent trades. Each 0.5 increase roughly halves the trade frequency.

⏱ Change Trade Interval

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.

📊 Position Sizing

Change tradeqty (shares per order), limitLong, and limitShort to control per-trade size and maximum exposure. Scale proportionally with your account size.

📅 Update Holiday Calendar

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.

Ready to Trade Automatically?

Import the NVDA EMA z-score trading flow into your MachineTrader™ instance and start trading in minutes.