{
  "id": "GbNvmnke6PJsrUTB",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Daily Financial News & Movers Email Digest",
  "tags": [],
  "nodes": [
    {
      "id": "node-0001",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        48,
        160
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "node-0002",
      "name": "Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        496,
        256
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1k6mDOKB0B6Wt2OOvaAo7oTxpn7Zt-XeiLA0NZlM6VFY/edit#gid=0",
          "cachedResultName": "input"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1k6mDOKB0B6Wt2OOvaAo7oTxpn7Zt-XeiLA0NZlM6VFY",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1k6mDOKB0B6Wt2OOvaAo7oTxpn7Zt-XeiLA0NZlM6VFY/edit?usp=drivesdk",
          "cachedResultName": "stocks"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "node-0003",
      "name": "HTTP Request - EOD",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        720,
        256
      ],
      "parameters": {
        "url": "={{ 'https://eodhd.com/api/eod/' + $json.ticker + '.US' }}",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "api_token",
              "value": "={{ $('\u2699\ufe0f Config').first().json.api_token }}"
            },
            {
              "name": "fmt",
              "value": "json"
            },
            {
              "name": "order",
              "value": "d"
            },
            {
              "name": "from",
              "value": "={{ new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] }}"
            },
            {
              "name": "to",
              "value": "={{ new Date().toISOString().split('T')[0] }}"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "node-0004",
      "name": "HTTP Request - News",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        944,
        256
      ],
      "parameters": {
        "url": "https://eodhd.com/api/news",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "s",
              "value": "={{ $('Google Sheets').item.json.ticker + '.US' }}"
            },
            {
              "name": "api_token",
              "value": "={{ $('\u2699\ufe0f Config').first().json.api_token }}"
            },
            {
              "name": "fmt",
              "value": "json"
            },
            {
              "name": "limit",
              "value": "3"
            },
            {
              "name": "from",
              "value": "={{ new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] }}"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "node-0005",
      "name": "Build Email HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        256
      ],
      "parameters": {
        "jsCode": "const gsItems  = $('Google Sheets').all();\nconst eodAll   = $('HTTP Request - EOD').all();\nconst newsAll  = $('HTTP Request - News').all();\n\n// Helper: handles pairedItem as object, array, or number\nfunction getPairedIdx(item) {\n  const pi = item.pairedItem;\n  if (pi === null || pi === undefined) return 0;\n  if (Array.isArray(pi)) return pi[0]?.item ?? 0;\n  if (typeof pi === 'object') return pi.item ?? 0;\n  return Number(pi) || 0;\n}\n\n// Group split items back by ticker using pairedItem index\nconst eodByIdx  = {};\nconst newsByIdx = {};\n\neodAll.forEach(item => {\n  const idx = getPairedIdx(item);\n  if (!eodByIdx[idx]) eodByIdx[idx] = [];\n  eodByIdx[idx].push(item.json);\n});\n\nnewsAll.forEach(item => {\n  const idx = getPairedIdx(item);\n  if (!newsByIdx[idx]) newsByIdx[idx] = [];\n  newsByIdx[idx].push(item.json);\n});\n\nconst oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\nconst results = [];\nconst failedTickers = [];\n\ngsItems.forEach((gsItem, idx) => {\n  const ticker = (gsItem.json.ticker || '').toString().trim().toUpperCase();\n  if (!ticker) return;\n\n  const eodData  = eodByIdx[idx]  || [];\n  const newsData = newsByIdx[idx] || [];\n\n  const eodFailed  = eodData.length === 0  || eodData[0]?.error  !== undefined;\n  const newsFailed = newsData.length === 0 || newsData[0]?.error !== undefined;\n\n  if (eodFailed && newsFailed) { failedTickers.push(ticker); return; }\n\n  // Price & change\n  const sorted = [...eodData].sort((a, b) => new Date(b.date) - new Date(a.date));\n  let price = null, changePct = null;\n  if (sorted.length >= 2) {\n    price = sorted[0].close;\n    const prev = sorted[1].close;\n    changePct = prev !== 0 ? ((price - prev) / prev) * 100 : null;\n  } else if (sorted.length === 1) {\n    price = sorted[0].close;\n  }\n\n  // News \u2014 filter to last 7 days\n  const news = newsData\n    .filter(a => a.date && new Date(a.date) >= oneWeekAgo)\n    .slice(0, 5)\n    .map(a => {\n      const polarity = a.sentiment?.polarity ?? null;\n      const sentimentEmoji  = polarity === null ? '' : polarity > 0.6 ? '\ud83d\ude0a' : polarity < 0.4 ? '\ud83d\ude1f' : '\ud83d\ude10';\n      const sentimentLabel  = polarity === null ? '' : polarity > 0.6 ? 'Positivo' : polarity < 0.4 ? 'Negativo' : 'Neutral';\n      const sentimentColor  = polarity === null ? '#718096' : polarity > 0.6 ? '#276749' : polarity < 0.4 ? '#c53030' : '#b7791f';\n      return {\n        title: a.title || 'Untitled',\n        date:  a.date  || '',\n        url:   a.link  || a.url || '#',\n        summary: (a.content || a.description || '').toString().substring(0, 200),\n        tags: (a.tags || []).slice(0, 3),\n        sentimentEmoji, sentimentLabel, sentimentColor\n      };\n    });\n\n  if (eodFailed) failedTickers.push(ticker);\n  results.push({ ticker, price, changePct, news, error: eodFailed });\n});\n\n// Classify movers\nconst withChange = results.filter(r => r.changePct !== null);\nwithChange.sort((a, b) => Math.abs(b.changePct) - Math.abs(a.changePct));\n\n// \u2500\u2500 Build HTML \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst today = new Date().toLocaleDateString('en-US', { weekday:'long', year:'numeric', month:'long', day:'numeric' });\nconst fmtPrice = v => v != null ? `$${Number(v).toFixed(2)}` : 'N/A';\nconst fmtPct   = v => v != null ? `${v >= 0 ? '+' : ''}${Number(v).toFixed(2)}%` : 'N/A';\nconst pctColor = v => v == null ? '#718096' : v >= 0 ? '#276749' : '#c53030';\nconst signal   = v => v == null ? '\u26aa' : v >= 2 ? '\ud83d\udfe2' : v <= -2 ? '\ud83d\udd34' : '\ud83d\udfe1';\n\n// Movers table rows\nconst moverRows = results.map(r => `\n  <tr style='border-top:1px solid #edf2f7;background:transparent;'>\n    <td style='padding:11px 12px;font-weight:700;color:#1a202c;'>${r.ticker}</td>\n    <td style='padding:11px 12px;text-align:right;color:#2d3748;'>${fmtPrice(r.price)}</td>\n    <td style='padding:11px 12px;text-align:right;color:${pctColor(r.changePct)};font-weight:700;'>${fmtPct(r.changePct)}</td>\n    <td style='padding:11px 12px;text-align:center;font-size:18px;'>${signal(r.changePct)}</td>\n  </tr>`).join('');\n\n// News sections\nconst newsSections = results.map(r => {\n  if (!r.news || r.news.length === 0) return '';\n  const articles = r.news.map(a => {\n    const tagsHtml = a.tags.length\n      ? `<div style='margin-top:5px;'>${a.tags.map(t => `<span style='display:inline-block;background:#edf2f7;color:#4a5568;font-size:10px;padding:2px 7px;border-radius:10px;margin-right:4px;'>${t}</span>`).join('')}</div>`\n      : '';\n    const sentHtml = a.sentimentLabel\n      ? `<span style='font-size:11px;color:${a.sentimentColor};font-weight:600;'>${a.sentimentEmoji} ${a.sentimentLabel}</span>`\n      : '';\n    return `\n    <div style='padding:12px 0;border-bottom:1px solid #f0f2f5;'>\n      <div style='display:flex;justify-content:space-between;align-items:flex-start;gap:8px;'>\n        <a href='${a.url}' style='font-size:13px;font-weight:600;color:#2b6cb0;text-decoration:none;line-height:1.4;'>${a.title}</a>\n        ${sentHtml}\n      </div>\n      ${a.summary ? `<p style='margin:5px 0 0;font-size:12px;color:#718096;line-height:1.5;'>${a.summary}\u2026</p>` : ''}\n      ${tagsHtml}\n      <p style='margin:4px 0 0;font-size:11px;color:#a0aec0;'>${a.date ? new Date(a.date).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : ''}</p>\n    </div>`;\n  }).join('');\n\n  return `\n  <div style='background:#fff;border-radius:10px;padding:22px 26px;margin-bottom:16px;box-shadow:0 1px 5px rgba(0,0,0,.07);'>\n    <h3 style='margin:0 0 12px;font-size:15px;font-weight:700;color:#2d3748;'>${r.ticker}</h3>\n    ${articles}\n  </div>`;\n}).join('');\n\nconst failedSection = failedTickers.length\n  ? `<p style='color:#a0aec0;font-size:12px;text-align:center;'>\u26a0\ufe0f No data for: ${failedTickers.join(', ')}</p>`\n  : '';\n\nconst html = `<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'></head>\n<body style='font-family:Arial,sans-serif;max-width:700px;margin:0 auto;padding:20px;background:#f0f2f5;'>\n<div style='background:linear-gradient(135deg,#1a1a2e,#16213e);color:white;padding:24px 28px;border-radius:10px;margin-bottom:24px;'>\n  <h1 style='margin:0;font-size:22px;font-weight:700;'>\ud83d\udcca Daily Market Digest</h1>\n  <p style='margin:8px 0 0;color:#90aec6;font-size:13px;'>${today}</p>\n</div>\n<div style='background:#fff;border-radius:10px;padding:22px 26px;margin-bottom:24px;box-shadow:0 1px 5px rgba(0,0,0,.07);'>\n  <h2 style='margin:0 0 18px;font-size:16px;font-weight:700;color:#2d3748;border-bottom:2px solid #e8ecf0;padding-bottom:12px;'>\ud83d\udcc8 Top Movers</h2>\n  <table style='width:100%;border-collapse:collapse;font-size:14px;'>\n    <thead><tr style='background:#f7f9fc;'>\n      <th style='padding:9px 12px;text-align:left;color:#718096;font-size:11px;text-transform:uppercase;'>Ticker</th>\n      <th style='padding:9px 12px;text-align:right;color:#718096;font-size:11px;text-transform:uppercase;'>Last Close</th>\n      <th style='padding:9px 12px;text-align:right;color:#718096;font-size:11px;text-transform:uppercase;'>Change %</th>\n      <th style='padding:9px 12px;text-align:center;color:#718096;font-size:11px;text-transform:uppercase;'>Signal</th>\n    </tr></thead>\n    <tbody>${moverRows}</tbody>\n  </table>\n</div>\n<div style='background:#fff;border-radius:10px;padding:22px 26px;margin-bottom:24px;box-shadow:0 1px 5px rgba(0,0,0,.07);'>\n  <h2 style='margin:0 0 18px;font-size:16px;font-weight:700;color:#2d3748;border-bottom:2px solid #e8ecf0;padding-bottom:12px;'>\ud83d\udcf0 News \u2014 Last 7 Days</h2>\n  ${newsSections || '<p style=\"color:#a0aec0;font-size:13px;\">No news found for the selected period.</p>'}\n</div>\n${failedSection}\n<p style='text-align:center;color:#a0aec0;font-size:11px;margin-top:8px;'>Generated by n8n \u00b7 Data by EODHD</p>\n</body></html>`;\n\nreturn [{\n  json: {\n    html,\n    subject: `\ud83d\udcca Daily Market Digest \u2014 ${new Date().toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}`\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "node-0006",
      "name": "Send Email via Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1392,
        256
      ],
      "parameters": {
        "sendTo": "={{ $('\u2699\ufe0f Config').first().json.recipient_email }}",
        "message": "={{ $json.html }}",
        "options": {},
        "subject": "={{ $json.subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b369108f-37f6-4d5d-9f70-3d06f3f2e620",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        272,
        352
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "node-cfg-0010",
      "name": "\u2699\ufe0f Config",
      "type": "n8n-nodes-base.set",
      "position": [
        272,
        160
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-a1",
              "name": "api_token",
              "type": "string",
              "value": "YOUR_EODHD_API_KEY"
            },
            {
              "id": "cfg-a2",
              "name": "recipient_email",
              "type": "string",
              "value": "your@email.com"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "sticky-0001",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1008,
        -48
      ],
      "parameters": {
        "width": 684,
        "height": 514,
        "content": "## Send daily stock price movers and financial news digest via Gmail\n\nAutomatically monitor your stock watchlist every morning and receive a formatted email with price changes and curated financial news with sentiment analysis.\n\n**How it works:**\n1. Schedule Trigger fires at 7 AM daily\n2. Reads tickers from Google Sheets (one ticker per row, column `ticker`)\n3. Fetches EOD prices + last 7 days of news from EODHD API per ticker\n4. Calculates daily % change and classifies news sentiment (Positive / Neutral / Negative)\n5. Sends an HTML digest via Gmail with a movers table and news per ticker\n\n**Setup \u2014 4 steps:**\n1. Open **\u2699\ufe0f Config** and fill in your `api_token` (EODHD) and `recipient_email`\n2. Connect **Google Sheets** credential \u2192 set your spreadsheet and sheet name (column must be named `ticker`)\n3. Connect **Gmail** credential\n4. Set your timezone in **Settings \u2192 Workflow Settings**\n\n**Requirements:** EODHD account (free tier works) \u00b7 Google OAuth2 configured in n8n"
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-0002",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -240,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 214,
        "height": 330,
        "content": "## Step 1 \u2014 Trigger & config\n\nSet your **EODHD API key** and **recipient email** in \u2699\ufe0f Config before running.\n\nRuns daily at **7 AM UTC**. Change timezone in Workflow Settings.\nYou can also run manually with the \"Execute workflow\" button."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-0003",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        -176
      ],
      "parameters": {
        "color": 7,
        "height": 362,
        "content": "## Step 2 \u2014 Fetch prices & news (EODHD API)\n\nYour Google Sheet must have a column named **`ticker`** (lowercase). One US ticker per row (MSFT, META, AMZN\u2026).\n\n- **EOD**: fetches last 14 days of closing prices \u2014 calculates daily % change\n- **News**: fetches last 7 days of articles with sentiment polarity score per ticker"
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-0004",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1040,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 298,
        "content": "## Step 3 \u2014 Build & send email\n\nGroups prices and news by ticker, calculates % change vs previous close, and classifies sentiment:\n- \ud83d\ude0a **Positive** (polarity > 0.6)\n- \ud83d\ude10 **Neutral**\n- \ud83d\ude1f **Negative** (polarity < 0.4)\n\nSends the HTML digest to the email set in \u2699\ufe0f Config."
      },
      "typeVersion": 1
    },
    {
      "id": "f7898c1c-4b94-4d85-ad28-2172c03be622",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1584,
        48
      ],
      "parameters": {
        "height": 288,
        "content": "## Output\n![txt](https://ik.imagekit.io/agbb7sr41/image.png)"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "c46ee938-f6c0-491f-97e2-6ebdfcc2d912",
  "connections": {
    "Google Sheets": {
      "main": [
        [
          {
            "node": "HTTP Request - EOD",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2699\ufe0f Config": {
      "main": [
        [
          {
            "node": "Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Email HTML": {
      "main": [
        [
          {
            "node": "Send Email via Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "\u2699\ufe0f Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request - EOD": {
      "main": [
        [
          {
            "node": "HTTP Request - News",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request - News": {
      "main": [
        [
          {
            "node": "Build Email HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "\u2699\ufe0f Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}