{
  "updatedAt": "2026-02-05T23:01:16.094Z",
  "createdAt": "2026-01-08T14:32:39.044Z",
  "id": "K0JZPGQRe-jTTVFEwunI4",
  "name": "AI Stock Analysis Automation (EODHD + Google Sheets + AI)",
  "active": false,
  "isArchived": true,
  "nodes": [
    {
      "name": "When clicking \u2018Execute workflow\u2019",
      "parameters": {},
      "position": [
        1856,
        640
      ],
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "id": "ff04316c-d564-4be5-81db-0d33fb6c67f8"
    },
    {
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "name": "Get row(s) in sheet",
      "parameters": {
        "documentId": {
          "__rl": true,
          "cachedResultName": "Portfolio_Performance",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8/edit?usp=drivesdk",
          "mode": "list",
          "value": "1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8"
        },
        "options": {},
        "sheetName": {
          "__rl": true,
          "cachedResultName": "Holdings_2026",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8/edit#gid=231712198",
          "mode": "list",
          "value": 231712198
        }
      },
      "position": [
        2080,
        640
      ],
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "id": "f95981a5-a7fd-4391-b692-b84d6f0fab3c"
    },
    {
      "name": "Loop Over Items",
      "parameters": {
        "options": {}
      },
      "position": [
        2304,
        640
      ],
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "id": "f76740bf-68ef-4bc3-b3bd-1f9fd97e4bcd"
    },
    {
      "alwaysOutputData": true,
      "name": "HTTP Request1",
      "parameters": {
        "options": {},
        "queryParameters": {
          "parameters": [
            {
              "name": "from",
              "value": "2023-01-01"
            },
            {
              "name": "to",
              "value": "2025-12-30"
            },
            {
              "name": "period",
              "value": "d"
            },
            {
              "name": "fmt",
              "value": "json"
            },
            {
              "name": "api_token",
              "value": " 6962c4de96b756.96575586"
            }
          ]
        },
        "sendQuery": true,
        "url": "=https://eodhd.com/api/eod/{{ $json.General.Code }}.US"
      },
      "position": [
        2752,
        432
      ],
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "id": "844f4e29-080c-4211-8556-884148c5461a"
    },
    {
      "name": "Merge",
      "parameters": {},
      "position": [
        3200,
        512
      ],
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "id": "fc5bbd1e-b2e6-41c6-b9e3-82d200e28320"
    },
    {
      "name": "Code in JavaScript",
      "parameters": {
        "jsCode": "// n8n Code node (WAF-safe / ES5)\n// Output: 1 item with fundamentals + OHLCV metrics + growth_potential_score\n// No modern JS: no =>, no ?. , no ??, no backticks, no spread\n\nfunction isArray(x) {\n  return Object.prototype.toString.call(x) === '[object Array]';\n}\nfunction toNum(x) {\n  var n = Number(x);\n  return (isNaN(n) || !isFinite(n)) ? null : n;\n}\nfunction safeStr(x) {\n  if (x === undefined || x === null) return '';\n  return String(x);\n}\nfunction mean(arr) {\n  if (!arr || !arr.length) return null;\n  var s = 0, c = 0;\n  for (var i = 0; i < arr.length; i++) {\n    if (arr[i] !== null) { s += arr[i]; c++; }\n  }\n  return c ? (s / c) : null;\n}\nfunction std(arr) {\n  if (!arr || arr.length < 2) return null;\n  var m = mean(arr);\n  if (m === null) return null;\n  var s = 0, c = 0;\n  for (var i = 0; i < arr.length; i++) {\n    if (arr[i] !== null) {\n      var d = arr[i] - m;\n      s += d * d;\n      c++;\n    }\n  }\n  if (c < 2) return null;\n  return Math.sqrt(s / (c - 1));\n}\nfunction sma(values, period) {\n  if (!values || values.length < period) return null;\n  var sum = 0;\n  for (var i = values.length - period; i < values.length; i++) sum += values[i];\n  return sum / period;\n}\nfunction rsi(values, period) {\n  if (!values || values.length < period + 1) return null;\n  var gains = 0;\n  var losses = 0;\n  for (var i = values.length - period; i < values.length; i++) {\n    var ch = values[i] - values[i - 1];\n    if (ch >= 0) gains += ch;\n    else losses += (-ch);\n  }\n  var avgGain = gains / period;\n  var avgLoss = losses / period;\n  if (avgLoss === 0) return 100;\n  var rs = avgGain / avgLoss;\n  return 100 - (100 / (1 + rs));\n}\nfunction pct(a, b) {\n  if (a === null || b === null || b === 0) return null;\n  return ((a / b) - 1) * 100;\n}\nfunction clamp(x, a, b) {\n  return Math.max(a, Math.min(b, x));\n}\n\n// ------------------------------------------------------------\n// 1) Find fundamentals object (the one that contains General/Highlights)\n// ------------------------------------------------------------\nfunction findFundamentalsObject(allItems) {\n  for (var i = 0; i < allItems.length; i++) {\n    var j = allItems[i].json;\n    if (j && j.General && j.Highlights) return j;\n  }\n  // sometimes fundamentals nested\n  for (var k = 0; k < allItems.length; k++) {\n    var jj = allItems[k].json;\n    if (jj && jj.fundamentals && jj.fundamentals.General) return jj.fundamentals;\n  }\n  return null;\n}\n\n// ------------------------------------------------------------\n// 2) Collect OHLCV candles\n//    Supports these cases:\n//    A) items[] are candles (each item.json has date/open/high/low/close)\n//    B) one item.json has ohlc array: { ohlc: [...] }\n// ------------------------------------------------------------\nfunction collectCandles(allItems) {\n  // Case B: already aggregated\n  for (var i = 0; i < allItems.length; i++) {\n    var j = allItems[i].json;\n    if (j && j.ohlc && isArray(j.ohlc) && j.ohlc.length) return j.ohlc;\n  }\n\n  // Case A: items are candles\n  var candles = [];\n  for (var k = 0; k < allItems.length; k++) {\n    var c = allItems[k].json;\n    if (c && c.date !== undefined && c.close !== undefined) {\n      candles.push(c);\n    }\n  }\n  if (candles.length) return candles;\n\n  return null;\n}\n\n// ------------------------------------------------------------\n// 3) Extract fundamentals fields from your confirmed structure\n// ------------------------------------------------------------\nfunction extractFundamentals(f) {\n  var gen = f.General || {};\n  var hi = f.Highlights || {};\n  var val = f.Valuation || {};\n  var tech = f.Technicals || {};\n\n  var out = {\n    ticker: gen.Code || '',\n    symbol: gen.PrimaryTicker || '',\n    sector: gen.Sector || null,\n    industry: gen.Industry || null,\n\n    market_cap: toNum(hi.MarketCapitalization),\n    pe: toNum(hi.PERatio),\n    forward_pe: toNum(val.ForwardPE),\n    eps: toNum(hi.EarningsShare),\n    eps_ttm: toNum(hi.DilutedEpsTTM),\n    peg: toNum(hi.PEGRatio),\n\n    revenue_ttm: toNum(hi.RevenueTTM),\n    gross_profit_ttm: toNum(hi.GrossProfitTTM),\n\n    q_rev_growth_yoy: (hi.QuarterlyRevenueGrowthYOY !== undefined) ? toNum(hi.QuarterlyRevenueGrowthYOY) : null,\n    q_eps_growth_yoy: (hi.QuarterlyEarningsGrowthYOY !== undefined) ? toNum(hi.QuarterlyEarningsGrowthYOY) : null,\n\n    beta: toNum(tech.Beta),\n    ma_50d: toNum(tech[\"50DayMA\"]),\n    ma_200d: toNum(tech[\"200DayMA\"]),\n    week52_high: toNum(tech[\"52WeekHigh\"]),\n    week52_low: toNum(tech[\"52WeekLow\"])\n  };\n\n  return out;\n}\n\n// ------------------------------------------------------------\n// 4) Compute OHLCV technical metrics\n// ------------------------------------------------------------\nfunction computeTechnicals(candles) {\n  // sort by date asc\n  candles.sort(function(a, b) {\n    return safeStr(a.date).localeCompare(safeStr(b.date));\n  });\n\n  var closes = [];\n  var lows = [];\n  var highs = [];\n  var vols = [];\n  var dates = [];\n\n  for (var i = 0; i < candles.length; i++) {\n    var c = toNum(candles[i].close);\n    if (c === null) continue;\n    closes.push(c);\n    lows.push(toNum(candles[i].low));\n    highs.push(toNum(candles[i].high));\n    vols.push(toNum(candles[i].volume));\n    dates.push(safeStr(candles[i].date));\n  }\n\n  if (closes.length < 60) {\n    return { error: 'Not enough OHLC data', closes_count: closes.length };\n  }\n\n  var lastClose = closes[closes.length - 1];\n  var lastDate = dates[dates.length - 1];\n\n  // returns\n  function closeAt(daysAgo) {\n    var idx = closes.length - 1 - daysAgo;\n    if (idx < 0) return null;\n    return closes[idx];\n  }\n\n  var ret30 = pct(lastClose, closeAt(30));\n  var ret90 = pct(lastClose, closeAt(90));\n\n  // support/resistance 30\n  var start30 = Math.max(0, closes.length - 30);\n  var support30 = null;\n  var resist30 = null;\n  for (var j = start30; j < closes.length; j++) {\n    var lo = lows[j];\n    var hi = highs[j];\n    if (lo !== null) support30 = (support30 === null) ? lo : Math.min(support30, lo);\n    if (hi !== null) resist30 = (resist30 === null) ? hi : Math.max(resist30, hi);\n  }\n\n  // volatility (annualized %)\n  var rets = [];\n  for (var k = 1; k < closes.length; k++) {\n    rets.push((closes[k] / closes[k - 1]) - 1);\n  }\n  var volDaily = std(rets);\n  var volAnnPct = (volDaily === null) ? null : (volDaily * Math.sqrt(252) * 100);\n\n  // RSI/SMA\n  var rsi14 = rsi(closes, 14);\n  var sma20 = sma(closes, 20);\n  var sma50 = sma(closes, 50);\n  var sma200 = sma(closes, 200);\n\n  return {\n    date: lastDate,\n    last_close: lastClose,\n    return_30d_pct: ret30,\n    return_90d_pct: ret90,\n    volatility_ann_pct: volAnnPct,\n    rsi_14: rsi14,\n    sma_20: sma20,\n    sma_50: sma50,\n    sma_200: sma200,\n    support_30d: support30,\n    resistance_30d: resist30\n  };\n}\n\n// ------------------------------------------------------------\n// 5) Growth potential score (0-100) from fundamentals + technicals\n// ------------------------------------------------------------\nfunction growthScore(fund, tech) {\n  var score = 50;\n\n  // Growth (YOY)\n  if (fund.q_rev_growth_yoy !== null) {\n    // 0.18 -> 18 points max 25\n    score += clamp(fund.q_rev_growth_yoy * 100, 0, 25);\n  }\n  if (fund.q_eps_growth_yoy !== null) {\n    score += clamp(fund.q_eps_growth_yoy * 100 * 0.5, 0, 12);\n  }\n\n  // Valuation penalty\n  if (fund.pe !== null) {\n    if (fund.pe > 45) score -= 12;\n    else if (fund.pe > 35) score -= 7;\n    else if (fund.pe < 12) score += 5;\n  }\n\n  // Trend\n  if (tech.sma_50 !== null && tech.last_close > tech.sma_50) score += 6;\n  if (tech.sma_200 !== null && tech.last_close > tech.sma_200) score += 6;\n  if (tech.sma_20 !== null && tech.last_close > tech.sma_20) score += 3;\n\n  // RSI sanity\n  if (tech.rsi_14 !== null) {\n    if (tech.rsi_14 > 75) score -= 5;\n    else if (tech.rsi_14 < 25) score -= 2;\n  }\n\n  // Volatility penalty\n  if (tech.volatility_ann_pct !== null) {\n    if (tech.volatility_ann_pct > 45) score -= 10;\n    else if (tech.volatility_ann_pct > 30) score -= 5;\n  }\n\n  return Math.round(clamp(score, 0, 100));\n}\n\n// ------------------------------------------------------------\n// MAIN\n// ------------------------------------------------------------\nvar fObj = findFundamentalsObject(items);\nvar candles = collectCandles(items);\n\nif (!fObj) {\n  return [{ json: { error: 'Fundamentals not found in items. Check merge.', hint: 'Make sure the fundamentals item with General/Highlights is included.' } }];\n}\nif (!candles) {\n  return [{ json: { error: 'OHLCV candles not found in items.', hint: 'Make sure OHLC items contain date/close or aggregate as {ohlc:[...]}' } }];\n}\n\nvar fundOut = extractFundamentals(fObj);\nvar techOut = computeTechnicals(candles);\n\nif (techOut.error) {\n  return [{ json: { error: techOut.error, closes_count: techOut.closes_count || 0, ticker: fundOut.ticker, symbol: fundOut.symbol } }];\n}\n\nvar gps = growthScore(fundOut, techOut);\n\n// Final payload\nreturn [{\n  json: {\n    ticker: fundOut.ticker,\n    symbol: fundOut.symbol,\n    date: techOut.date,\n    fundamentals: fundOut,\n    technical: techOut,\n    growth_potential_score: gps\n  }\n}];\n"
      },
      "position": [
        3424,
        512
      ],
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "id": "d62ea2ee-4703-41dc-9f9a-a8791a59bc4e"
    },
    {
      "name": "Code in JavaScript1",
      "parameters": {
        "jsCode": "var arr = [];\nfor (var i = 0; i < items.length; i++) {\n  arr.push(items[i].json);\n}\n\n// Ordena por fecha asc (opcional, pero recomendado)\narr.sort(function(a, b) {\n  return String(a.date).localeCompare(String(b.date));\n});\n\nreturn [{ json: { ohlc: arr, ohlc_len: arr.length } }];\n"
      },
      "position": [
        2976,
        432
      ],
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "id": "6da531e0-0a9a-4447-9c9a-2242b454a0eb"
    },
    {
      "name": "AI Agent",
      "parameters": {
        "options": {
          "systemMessage": "You are a disciplined, quantitative and fundamental equity analyst. Your task is to evaluate a single stock using ONLY the metrics provided in the input (fundamentals, technicals, and growth_potential_score).\n\nRULES:\n- Return ONLY valid JSON. No markdown. No extra text.\n- Do NOT invent data. If a metric is missing, use null and mention the limitation in \"notes\".\n- Use probabilistic, cautious language. Never claim certainty.\n- This is NOT financial advice.\n\nOBJECTIVE:\nProduce a practical, actionable assessment: whether you would enter the trade or not, suggested entry/stop/take-profit levels, and a fundamental quality score (1\u201310) based on ratios, business quality, growth, valuation, and risk.\n\nMETRIC INTERPRETATION GUIDELINES:\n- Technicals:\n  - Support / resistance: use provided levels as the primary reference.\n  - RSI: >70 suggests overbought (higher short-term risk), <30 suggests oversold.\n  - SMA20/50/200: trend strength improves when price > SMA200 and > SMA50.\n  - volatility_ann_pct: high volatility increases risk and lowers confidence.\n- Fundamentals:\n  - High PE penalizes the score unless justified by strong growth.\n  - EPS and YoY revenue/earnings growth support business quality.\n  - Market cap is context only (not a quality metric).\n- growth_potential_score (0\u2013100): use as a composite signal, but do not blindly follow it if it contradicts other indicators.\n\nOUTPUT SCHEMA (MANDATORY):\n{\n  \"ticker\": \"string\",\n  \"decision\": {\n    \"would_enter\": \"YES|NO\",\n    \"signal\": \"BUY|WATCH|SELL\",\n    \"time_horizon\": \"short|medium|long\",\n    \"confidence_0_100\": number\n  },\n  \"trade_plan\": {\n    \"entry\": number,\n    \"support\": number,\n    \"resistance\": number,\n    \"stop_loss\": number,\n    \"take_profit\": number,\n    \"rationale\": \"string\"\n  },\n  \"fundamental_score_1_10\": number,\n  \"fundamental_assessment\": {\n    \"business_quality\": \"weak|average|strong\",\n    \"valuation\": \"cheap|fair|expensive|unknown\",\n    \"growth\": \"low|moderate|high|unknown\",\n    \"profitability\": \"low|moderate|high|unknown\"\n  },\n  \"positives\": [\"string\", \"string\", \"string\"],\n  \"negatives\": [\"string\", \"string\", \"string\"],\n  \"thesis\": [\"string\", \"string\", \"string\"],\n  \"key_metrics_used\": {\n    \"pe\": number,\n    \"forward_pe\": number,\n    \"eps\": number,\n    \"revenue_growth_yoy\": number,\n    \"earnings_growth_yoy\": number,\n    \"rsi_14\": number,\n    \"volatility_ann_pct\": number,\n    \"price_vs_sma200\": \"above|below|unknown\",\n    \"growth_potential_score_0_100\": number\n  },\n  \"notes\": [\"string\"]\n}\n\nTRADE LEVEL LOGIC:\n- support: use technical.support_30d if available; otherwise null.\n- resistance: use technical.resistance_30d if available; otherwise null.\n- entry:\n  - BUY: near support (but not below it).\n  - WATCH: conservative entry (near support or after confirmation).\n  - SELL: entry may be null or near resistance (hypothetical short).\n- stop_loss:\n  - BUY/WATCH: support * 0.97 (\u22483% below support) if support exists;\n    otherwise last_close * 0.93.\n- take_profit:\n  - BUY/WATCH: min(resistance * 0.99, last_close * 1.10) if resistance exists;\n    otherwise last_close * 1.08.\n- If RSI > 75 or volatility_ann_pct > 40, reduce confidence and be conservative with take_profit.\n\nFUNDAMENTAL SCORE (1\u201310):\nEvaluate holistically:\n- Growth (YoY revenue/earnings, EPS quality)\n- Valuation (PE, Forward PE, PEG when available)\n- Business quality and stability\n- Risk (volatility, trend strength)\n- Use growth_potential_score as a tiebreaker\nReturn an integer from 1 to 10.\n\nINPUT:\nYou will receive a JSON object containing fields such as:\n{\n  \"ticker\": \"...\",\n  \"fundamentals\": {...},\n  \"technical\": {...},\n  \"growth_potential_score\": ...\n}\n"
        },
        "promptType": "define",
        "text": "=Analyze the following stock data and return the output strictly following the defined JSON schema.\n\nINPUT DATA:\n{{ JSON.stringify($json) }}\n"
      },
      "position": [
        3648,
        512
      ],
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3,
      "id": "353cbd02-4a5b-4649-9c6f-b1bb5cc72d87"
    },
    {
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "name": "OpenAI Chat Model",
      "parameters": {
        "builtInTools": {},
        "model": {
          "__rl": true,
          "cachedResultName": "gpt-4.1-nano",
          "mode": "list",
          "value": "gpt-4.1-nano"
        },
        "options": {}
      },
      "position": [
        3520,
        960
      ],
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "id": "e16a6da7-9aea-4f06-8854-8ee524be834c"
    },
    {
      "name": "Code in JavaScript2",
      "parameters": {
        "jsCode": "// Parse AI output string -> JSON object\nvar raw = items[0].json.output;\n\n// A veces viene con espacios/line breaks\nif (!raw || typeof raw !== 'string') {\n  return [{ json: { error: 'No output string to parse', raw: raw } }];\n}\n\nvar obj = JSON.parse(raw);\n\n// Devuelve el objeto ya parseado\nreturn [{ json: obj }];\n"
      },
      "position": [
        4000,
        512
      ],
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "id": "b8a30f47-5983-482d-9894-77eaa1a3050b"
    },
    {
      "name": "Code in JavaScript3",
      "parameters": {
        "jsCode": "var j = items[0].json;\n\nfunction joinArr(a) {\n  if (!a || !a.length) return '';\n  // Une bullets en una sola celda\n  return a.join(' | ');\n}\n\nvar out = {\n  ticker: j.ticker || '',\n  would_enter: j.decision ? j.decision.would_enter : '',\n  signal: j.decision ? j.decision.signal : '',\n  time_horizon: j.decision ? j.decision.time_horizon : '',\n  confidence_0_100: j.decision ? j.decision.confidence_0_100 : null,\n\n  entry: j.trade_plan ? j.trade_plan.entry : null,\n  support: j.trade_plan ? j.trade_plan.support : null,\n  resistance: j.trade_plan ? j.trade_plan.resistance : null,\n  stop_loss: j.trade_plan ? j.trade_plan.stop_loss : null,\n  take_profit: j.trade_plan ? j.trade_plan.take_profit : null,\n  rationale: j.trade_plan ? j.trade_plan.rationale : '',\n\n  fundamental_score_1_10: j.fundamental_score_1_10 !== undefined ? j.fundamental_score_1_10 : null,\n  business_quality: j.fundamental_assessment ? j.fundamental_assessment.business_quality : '',\n  valuation: j.fundamental_assessment ? j.fundamental_assessment.valuation : '',\n  growth: j.fundamental_assessment ? j.fundamental_assessment.growth : '',\n  profitability: j.fundamental_assessment ? j.fundamental_assessment.profitability : '',\n\n  positives: joinArr(j.positives),\n  negatives: joinArr(j.negatives),\n  thesis: joinArr(j.thesis),\n  notes: joinArr(j.notes),\n\n  // key metrics\n  pe: j.key_metrics_used ? j.key_metrics_used.pe : null,\n  forward_pe: j.key_metrics_used ? j.key_metrics_used.forward_pe : null,\n  eps: j.key_metrics_used ? j.key_metrics_used.eps : null,\n  revenue_growth_yoy: j.key_metrics_used ? j.key_metrics_used.revenue_growth_yoy : null,\n  earnings_growth_yoy: j.key_metrics_used ? j.key_metrics_used.earnings_growth_yoy : null,\n  rsi_14: j.key_metrics_used ? j.key_metrics_used.rsi_14 : null,\n  volatility_ann_pct: j.key_metrics_used ? j.key_metrics_used.volatility_ann_pct : null,\n  price_vs_sma200: j.key_metrics_used ? j.key_metrics_used.price_vs_sma200 : '',\n  growth_potential_score_0_100: j.key_metrics_used ? j.key_metrics_used.growth_potential_score_0_100 : null\n};\n\nreturn [{ json: out }];\n"
      },
      "position": [
        4224,
        512
      ],
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "id": "d17d4d61-9e55-4929-81dd-f1f2568163c8"
    },
    {
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "name": "Append row in sheet",
      "parameters": {
        "columns": {
          "attemptToConvertTypes": false,
          "convertFieldsToString": false,
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "schema": [
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "ticker",
              "id": "ticker",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "ENTER(YES/NO)",
              "id": "ENTER(YES/NO)",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "ENTRY",
              "id": "ENTRY",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "support",
              "id": "support",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "resistance",
              "id": "resistance",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "stop_loss",
              "id": "stop_loss",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "take_profit",
              "id": "take_profit",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "tecnhical_tesis",
              "id": "tecnhical_tesis",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "fundamental_score",
              "id": "fundamental_score",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "negative",
              "id": "negative",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "positives",
              "id": "positives",
              "required": false,
              "type": "string"
            },
            {
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "display": true,
              "displayName": "fundamental_thesis",
              "id": "fundamental_thesis",
              "required": false,
              "type": "string"
            }
          ],
          "value": {
            "ENTER(YES/NO)": "={{ $json.would_enter }}",
            "ENTRY": "={{ $json.entry }}",
            "fundamental_score": "={{ $json.fundamental_score_1_10 }}",
            "fundamental_thesis": "={{ $json.thesis }}",
            "negative": "={{ $json.negatives }}",
            "positives": "={{ $json.positives }}",
            "resistance": "={{ $json.resistance }}",
            "stop_loss": "={{ $json.stop_loss }}",
            "support": "={{ $json.support }}",
            "take_profit": "={{ $json.take_profit }}",
            "tecnhical_tesis": "={{ $json.rationale }}",
            "ticker": "={{ $json.ticker }}"
          }
        },
        "documentId": {
          "__rl": true,
          "cachedResultName": "Portfolio_Performance",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8/edit?usp=drivesdk",
          "mode": "list",
          "value": "1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8"
        },
        "operation": "append",
        "options": {},
        "sheetName": {
          "__rl": true,
          "cachedResultName": "Holdings_2026",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ksZvZR0cuQHYJ0-W0FUZUNm3rwdudlZxckLIUcgY_r8/edit#gid=231712198",
          "mode": "list",
          "value": 231712198
        }
      },
      "position": [
        4448,
        640
      ],
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "id": "d13eea8e-bce5-4f21-96b5-bbcb4853d366"
    },
    {
      "name": "Sticky Note",
      "parameters": {
        "content": "## What this workflow does\nThis workflow automates **end-to-end stock analysis** using real market data and AI:\n\n- Reads a list of stock tickers from **Google Sheets**\n- Fetches **fundamental data** (valuation, growth, profitability) and **OHLCV price data** from **EODHD APIs**\n- Computes key **technical indicators** (RSI, SMA 20/50/200, volatility, support & resistance)\n- Uses an **AI model** to generate:\n  - Buy / Watch / Sell recommendation\n  - Entry price, stop-loss, and take-profit levels\n  - Investment thesis, pros & cons\n  - Fundamental quality score (1\u201310)\n- Stores the final structured analysis back into **Google Sheets**\n\nThis creates a **repeatable, no-code stock analysis pipeline** ready for decision-making or dashboards.\n\n\n### Key benefits\n1. **Automated & scalable**  \n   Analyze dozens of stocks in minutes without manual work.\n\n2. **Data-driven decisions**  \n   Combines fundamentals, technicals, and AI reasoning in one place.\n\n3. **Actionable outputs**  \n   Clear trade levels, risk notes, and investment scores\u2014ready to use.\n\n---\n\n### Who this workflow is for\n- Retail investors and swing traders  \n- Data-driven investors and analysts  \n- Automation builders using n8n  \n- Anyone wanting AI-assisted stock analysis without writing code\n\n### Data source\nMarket data is powered by **EODHD APIs**  \n\ud83d\udc49 Get a **10% discount** using this link:  \nhttps://eodhd.com/pricing-special-10?via=kmg&ref1=Meneses\n\n## How to configure this workflow\n\n### 1. Google Sheets (Input)\nCreate a sheet with a column called:\n- `ticker` (e.g. MSFT, AAPL, AMZN)\n\nEach row represents one stock to analyze.\n\n\n### 2. EODHD API\n- Create an EODHD account\n- Get your API token\n- Add it to the HTTP Request nodes as:\n  - `api_token=YOUR_API_KEY`\n\nDiscount link (10% off):  \nhttps://eodhd.com/pricing-special-10?via=kmg&ref1=Meneses\n\n\n### 3. AI Model\n- Configure your AI provider (OpenAI / compatible model)\n- The AI receives:\n  - Fundamentals\n  - Technical indicators\n  - Growth potential score\n- It returns structured JSON with recommendations and trade levels\n\n\n### 4. Google Sheets (Output)\nResults are appended to a `Signals` tab with:\n- Signal (BUY / WATCH / SELL)\n- Entry, Stop Loss, Take Profit\n- Fundamental score (1\u201310)\n- Investment thesis and risk notes\n",
        "height": 1872,
        "width": 832
      },
      "position": [
        896,
        -160
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "id": "4f19f3f7-0090-4cb8-96aa-3554d6594a5e"
    },
    {
      "name": "Sticky Note1",
      "parameters": {
        "content": "## INPUT\n\n![txt](https://ik.imagekit.io/agbb7sr41/eodhd_input.png)\n",
        "height": 336
      },
      "position": [
        1792,
        192
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "id": "47cd1c34-a8ef-4ec0-8b85-2b937abfce66"
    },
    {
      "name": "Sticky Note2",
      "parameters": {
        "color": 7,
        "content": "## AI Output\n- The merged data is sent to the **AI system**\n- The AI evaluates fundamentals, technicals, and risk\n- The final output includes:\n  - BUY / WATCH / SELL signal\n  - Entry, Stop Loss, Take Profit\n  - Investment score and rationale\n\nResults are ready to use or store in Google Sheets.",
        "height": 224,
        "width": 624
      },
      "position": [
        3696,
        192
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "id": "aab476af-a23a-45f6-bc51-30aa8db91706"
    },
    {
      "name": "Sticky Note3",
      "parameters": {
        "color": 7,
        "content": "##  Transform & Merge\n\n- Raw market data is cleaned and normalized\n- Technical indicators are calculated\n- Fundamentals and price data are **merged into one dataset**\n\nAt the end of this step, each stock has a single, structured data object.\n",
        "height": 192,
        "width": 576
      },
      "position": [
        2960,
        208
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "id": "82922d6d-73db-4a36-bb4b-b3b311067dc2"
    },
    {
      "name": "Sticky Note4",
      "parameters": {
        "color": 7,
        "content": "## Input (Data sources)\n- Stock tickers come from **Google Sheets**\n- Market data is fetched via **API calls**:\n  - Fundamentals\n  - Price candles (OHLCV)\n\nThis step collects all the raw data needed for the analysis.",
        "height": 192,
        "width": 480
      },
      "position": [
        2160,
        224
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "id": "907f4da5-c643-470e-9b40-1bc7518c8dda"
    },
    {
      "name": "Sticky Note5",
      "parameters": {
        "content": "## OUTPUT\n![txt](https://ik.imagekit.io/agbb7sr41/eodhd_ouput.png)",
        "height": 208,
        "width": 1008
      },
      "position": [
        4416,
        272
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "id": "525b201d-5c7d-473b-bda9-4d3b827f3b1b"
    },
    {
      "name": "Call EODHD Financial API",
      "parameters": {
        "options": {
          "redirect": {
            "redirect": {}
          }
        },
        "queryParameters": {
          "parameters": [
            {
              "name": "api_token",
              "value": " 6962c4de96b756.96575586"
            },
            {
              "name": "fmt",
              "value": "json"
            }
          ]
        },
        "sendQuery": true,
        "url": "=https://eodhd.com/api/eod/{{ $json.Symbol }}.US"
      },
      "position": [
        2528,
        512
      ],
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "id": "9f5046b4-6eef-4f4f-b3e8-e26d8370c082"
    },
    {
      "credentials": {
        "ollamaApi": {
          "name": "<your credential>"
        }
      },
      "name": "Ollama Chat Model",
      "parameters": {
        "model": "glm-4.7:cloud",
        "options": {}
      },
      "position": [
        3648,
        688
      ],
      "type": "@n8n/n8n-nodes-langchain.lmChatOllama",
      "typeVersion": 1,
      "id": "ec6767f9-0f51-42d4-a4b0-60ea80589e52"
    }
  ],
  "connections": {
    "AI Agent": {
      "main": [
        [
          {
            "index": 0,
            "node": "Code in JavaScript2",
            "type": "main"
          }
        ]
      ]
    },
    "Append row in sheet": {
      "main": [
        [
          {
            "index": 0,
            "node": "Loop Over Items",
            "type": "main"
          }
        ]
      ]
    },
    "Call EODHD Financial API": {
      "main": [
        [
          {
            "index": 0,
            "node": "HTTP Request1",
            "type": "main"
          },
          {
            "index": 1,
            "node": "Merge",
            "type": "main"
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "index": 0,
            "node": "AI Agent",
            "type": "main"
          }
        ]
      ]
    },
    "Code in JavaScript1": {
      "main": [
        [
          {
            "index": 0,
            "node": "Merge",
            "type": "main"
          }
        ]
      ]
    },
    "Code in JavaScript2": {
      "main": [
        [
          {
            "index": 0,
            "node": "Code in JavaScript3",
            "type": "main"
          }
        ]
      ]
    },
    "Code in JavaScript3": {
      "main": [
        [
          {
            "index": 0,
            "node": "Append row in sheet",
            "type": "main"
          }
        ]
      ]
    },
    "Get row(s) in sheet": {
      "main": [
        [
          {
            "index": 0,
            "node": "Loop Over Items",
            "type": "main"
          }
        ]
      ]
    },
    "HTTP Request1": {
      "main": [
        [
          {
            "index": 0,
            "node": "Code in JavaScript1",
            "type": "main"
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "index": 0,
            "node": "Call EODHD Financial API",
            "type": "main"
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "index": 0,
            "node": "Code in JavaScript",
            "type": "main"
          }
        ]
      ]
    },
    "Ollama Chat Model": {
      "ai_languageModel": [
        [
          {
            "index": 0,
            "node": "AI Agent",
            "type": "ai_languageModel"
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        []
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "index": 0,
            "node": "Get row(s) in sheet",
            "type": "main"
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": false,
    "callerPolicy": "workflowsFromSameOwner"
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "versionId": "0b77eb34-2c8c-4014-ae61-df12413e0e3e",
  "activeVersionId": null,
  "triggerCount": 0,
  "shared": [
    {
      "updatedAt": "2026-01-08T14:32:39.044Z",
      "createdAt": "2026-01-08T14:32:39.044Z",
      "role": "workflow:owner",
      "workflowId": "K0JZPGQRe-jTTVFEwunI4",
      "projectId": "aRJv9cLftn98cx8V"
    }
  ],
  "activeVersion": null,
  "tags": []
}