{
  "id": "qnglBpTv8WJXkbybqH3CX",
  "meta": {
    "templateId": "5369",
    "templateCredsSetupCompleted": true
  },
  "name": "AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets",
  "tags": [
    {
      "id": "MrLFqPuujEL6vkPD",
      "name": "Institutional-Grade Stoc",
      "createdAt": "2026-02-24T19:56:14.685Z",
      "updatedAt": "2026-02-24T19:56:14.685Z"
    },
    {
      "id": "T6peYIB3ixkHZuR5",
      "name": "Structured Targets. Quan",
      "createdAt": "2026-02-24T19:56:06.627Z",
      "updatedAt": "2026-02-24T19:56:06.627Z"
    },
    {
      "id": "oJGbBh4g2N3cpwMd",
      "name": "From Financial Data to D",
      "createdAt": "2026-02-24T19:56:21.859Z",
      "updatedAt": "2026-02-24T19:56:21.859Z"
    },
    {
      "id": "ontpnv94DgmZLTrw",
      "name": "Disciplined Valuation. A",
      "createdAt": "2026-02-24T19:55:47.902Z",
      "updatedAt": "2026-02-24T19:55:47.902Z"
    },
    {
      "id": "xEkWy3lMAYX6lO99",
      "name": "AI-Powered Equity Analys",
      "createdAt": "2026-02-24T19:55:58.790Z",
      "updatedAt": "2026-02-24T19:55:58.790Z"
    }
  ],
  "nodes": [
    {
      "id": "392f7ccd-aba3-4911-859a-556535f45eea",
      "name": "loop_over_tickers",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        320,
        1168
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "6e5b9817-b6ec-4c0a-98fd-b41340ab6230",
      "name": "Read_tickers_from_Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        96,
        1168
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fHwDfRMnMZAIhE6LIo3yofVX2hwcAnBkfE7ibyBT-z4/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1fHwDfRMnMZAIhE6LIo3yofVX2hwcAnBkfE7ibyBT-z4",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fHwDfRMnMZAIhE6LIo3yofVX2hwcAnBkfE7ibyBT-z4/edit?usp=drivesdk",
          "cachedResultName": "List of stocks"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "0dc93919-fb1a-46df-b316-1044f11cd791",
      "name": "write_sentiment_to_sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4752,
        1440
      ],
      "parameters": {
        "columns": {
          "value": {
            "date": "={{ $json.date }}",
            "model": "={{ $json.model }}",
            "stock": "={{ $json.stock }}",
            "pt_base": "={{ $json.pt_base }}",
            "pt_bear": "={{ $json.pt_bear }}",
            "pt_bull": "={{ $json.pt_bull }}",
            "rationale": "={{ $json.rationale }}",
            "confidence": "={{ $json.confidence }}"
          },
          "schema": [
            {
              "id": "stock",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "stock",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "pt_bear",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "pt_bear",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "pt_base",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "pt_base",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "pt_bull",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "pt_bull",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "confidence",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "confidence",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rationale",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rationale",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "model",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "model",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fbptcVE0mBjaIZJHkJzFBTdoVJVLgQOWmJXpCx40YyE/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1fbptcVE0mBjaIZJHkJzFBTdoVJVLgQOWmJXpCx40YyE",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fbptcVE0mBjaIZJHkJzFBTdoVJVLgQOWmJXpCx40YyE/edit?usp=drivesdk",
          "cachedResultName": "Sentiments of my stocks"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "3cbf7b55-50ae-4d1c-8f9e-56941441ef00",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -128,
        1168
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "daysInterval": 3,
              "triggerAtHour": 16
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ce8d9b54-2026-4faa-9ee1-c261170c8426",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        64
      ],
      "parameters": {
        "width": 480,
        "height": 440,
        "content": "# Workflow Overview \n**This workflow automates the process of analyzing the sentiment of stock market news.**\n\n- retrieves a list of stock tickers from a Google Sheet \n- fetchs recent news articles for each ticker\n- uses a 2 large language model to perform sentiment analysis on the articles\n- records the sentiment scores and rationale back into a Google Sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "efaee1f9-e57a-4640-a1d6-eb5bc5f341af",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        576
      ],
      "parameters": {
        "color": 4,
        "width": 880,
        "height": 832,
        "content": "# 1. Daily Trigger and Stock Ticker Retrieval\n- **Schedule Trigger:** This workflow is set to run automatically every three day at 4:00 PM (Asia/Jerusalem time). This ensures that the script runs just before the markets open and you get a daily update on the sentiment of the stocks you are tracking.\n\n- **Read_tickers_from_Sheet:** This node connects to a Google Sheet named \"Stock Sentiment\" and reads the list of stock tickers from the \"stocks\" sheet. This is the source of the stocks that the workflow will analyze. I have make every three days to avoid extra fees from Alphavintage since, and you can modify it if you need.\n\n- **loop_over_tickers:** This node takes the list of tickers from the Google Sheet and processes them one by one. This allows the workflow to perform the same set of actions for each stock ticker individually."
      },
      "typeVersion": 1
    },
    {
      "id": "eaca666d-2830-4e98-bfe4-44c15d452939",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        768,
        -160
      ],
      "parameters": {
        "color": 5,
        "width": 2200,
        "height": 1436,
        "content": "# 2. Financial and News Data Retrieval & Validation\n2.1 Financial Data Fetching (Alpha Vantage APIs)\nFetch Financial Data (Alpha Vantage):\nFor each ticker, the workflow sends multiple HTTP requests to Alpha Vantage to retrieve structured financial data, including:\nCompany overview (EPS, BVPS, margins, shares, revenue).\nIncome statement (quarterly revenue and net income).\nBalance sheet (debt and cash position).\nCash flow statement (operating cash flow and capital expenditures for FCF calculation).\nCurrent stock price.\nData Normalization and Processing:\nThe responses from the APIs are processed using code nodes to:\nExtract required financial fields.\nCalculate derived metrics such as revenue growth, gross margins, total debt, cash position, and free cash flow.\nStandardize numeric values and prepare structured financial data for analysis.\nFinancial Cache Validation:\nThe workflow checks Google Sheets to determine whether financial data for the ticker already exists and whether it is still fresh (within a defined time window).\nIf the data is missing or outdated \u2192 financial APIs are called.\nIf the data is still valid \u2192 cached data is reused to avoid unnecessary API calls.\nUpdate or Insert Financial Records:\nBased on the cache result:\nNew records are inserted for first-time tickers.\nExisting records are updated when refreshed data is retrieved."
      },
      "typeVersion": 1
    },
    {
      "id": "92a8b9e8-cdca-4584-938f-b478b8136867",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2992,
        464
      ],
      "parameters": {
        "color": 6,
        "width": 836,
        "height": 1536,
        "content": "# 3. Sentiment Analysis with AI\n\n- **AI Agent & Google Gemini Chat Model/ChatGPT:** This is the core of the sentiment analysis. The \"AI Agent\" node is configured with a detailed prompt that instructs the \"Google Gemini Chat Model & ChatGPT\" to act as a stock sentiment analyzer. The prompt specifies the input format (stock symbol, price targets,confidance, rational), and the desired JSON output format. The combined text of the news articles and the current stock ticker are passed to the model.l analyze all the articles at once."
      },
      "typeVersion": 1
    },
    {
      "id": "aa4c0dfb-0e02-4f74-b908-27b6b81bbef1",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3856,
        464
      ],
      "parameters": {
        "color": 2,
        "width": 560,
        "height": 1532,
        "content": "# 4. Output Formatting and Error Handling\n\n- **format_output_as_json:** The output from the AI models is a raw string that includes a JSON object. This code node extracts the clean JSON from the string and prepares it for the next steps.\n\n- **if_format_succesful:** This conditional node checks if the previous step of formatting the AI's output into a clean JSON was successful. If there was an error, it sends the workflow back to the \"AI Agent\" to try again."
      },
      "typeVersion": 1
    },
    {
      "id": "cd441252-7867-40e4-9fb9-aa1e6fd53fb9",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4464,
        464
      ],
      "parameters": {
        "color": 3,
        "width": 520,
        "height": 1528,
        "content": "# 5. Storing the Results\n\n\n- **write_sentiment_to_sheets:** Once a valid sentiment analysis result is obtained and formatted, this node appends the data to \"Sheet1\" of the \"Stock Sentiment\" Google Sheet. It records the current date, the stock ticker, the sentiment score, and the rationale provided by the AI. After this step, the workflow loops back to process the next ticker from the initial list.\n\n\n\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "7eaf394e-4043-41ce-a094-b08d28413509",
      "name": "Code in JavaScript",
      "type": "n8n-nodes-base.code",
      "position": [
        4016,
        1200
      ],
      "parameters": {
        "jsCode": "function pickValid(x) {\n  if (!x) return null;\n  const base = Number(x.pt_base);\n  const bear = Number(x.pt_bear);\n  const bull = Number(x.pt_bull);\n  const conf = Number(x.confidence);\n  if (!x.stock) return null;\n  if (!isFinite(base) || base <= 0) return null;\n  if (!isFinite(bear) || bear <= 0) return null;\n  if (!isFinite(bull) || bull <= 0) return null;\n  if (!isFinite(conf) || conf <= 0) return null;\n  return x;\n}\n\nconst byStock = new Map();\n\nfor (const it of items) {\n  const j = it.json || {};\n\n  // Candidate extraction (supports either chatgpt_result, gemini_result, or already flattened)\n  const candidates = [];\n\n  if (j.chatgpt_result && typeof j.chatgpt_result === \"object\") {\n    candidates.push({ ...j.chatgpt_result, model: \"CHATGPT\" });\n  }\n\n  if (j.gemini_result && typeof j.gemini_result === \"object\") {\n    candidates.push({ ...j.gemini_result, model: \"GEMINI\" });\n  }\n\n  // fallback if something arrives already flattened\n  if (candidates.length === 0 && j.stock && (j.pt_base || j.pt_bear || j.pt_bull)) {\n    candidates.push({ ...j, model: j.model || \"UNKNOWN\" });\n  }\n\n  for (const cand of candidates) {\n    const stock = cand.stock || j.stock || \"\";\n    const normalized = {\n      stock,\n      date: cand.date || null,\n      pt_bear: Number(cand.pt_bear ?? NaN),\n      pt_base: Number(cand.pt_base ?? NaN),\n      pt_bull: Number(cand.pt_bull ?? NaN),\n      f_score: (cand.f_score ?? null),\n      confidence: Number(cand.confidence ?? NaN),\n      rationale: cand.rationale || \"\",\n      model: cand.model || \"UNKNOWN\"\n    };\n\n    const valid = pickValid(normalized);\n    if (!valid) continue;\n\n    if (!byStock.has(stock)) byStock.set(stock, []);\n    byStock.get(stock).push(valid);\n  }\n}\n\nconst today = new Date().toISOString().slice(0, 10);\n\nconst out = [];\nfor (const [stock, arr] of byStock.entries()) {\n  // Optional: dedupe per model (keep highest confidence per model)\n  const bestByModel = new Map();\n  for (const r of arr) {\n    const key = r.model;\n    const prev = bestByModel.get(key);\n    if (!prev || (r.confidence ?? 0) > (prev.confidence ?? 0)) {\n      bestByModel.set(key, r);\n    }\n  }\n\n  // Emit one row per model\n  for (const r of bestByModel.values()) {\n    const date = r.date || today;\n\n    out.push({\n      json: {\n        stock,\n        date,\n        pt_bear: Number(r.pt_bear.toFixed(2)),\n        pt_base: Number(r.pt_base.toFixed(2)),\n        pt_bull: Number(r.pt_bull.toFixed(2)),\n        f_score: r.f_score,\n        confidence: Math.max(20, Math.min(90, Number(r.confidence.toFixed(0)))),\n        rationale: r.rationale || \"No rationale provided\",\n        model: r.model,\n        // Use this as your Google Sheets upsert key to avoid overwriting\n        row_key: `${stock}_${date}_${r.model}`,\n        skip_row: false\n      }\n    });\n  }\n}\n\nif (out.length === 0) {\n  return [{\n    json: {\n      stock: \"\",\n      date: today,\n      pt_bear: 0,\n      pt_base: 0,\n      pt_bull: 0,\n      confidence: 0,\n      rationale: \"No valid analyst data\",\n      model: \"ERROR\",\n      row_key: `ERROR_${today}`,\n      skip_row: true\n    }\n  }];\n}\n\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "4b094b92-271b-422c-9d89-b603dd199390",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        3648,
        1104
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "ea73393c-1449-43ae-8014-0b59f4b60aa1",
      "name": "Message a model",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        3120,
        880
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini",
          "cachedResultName": "GPT-5-MINI"
        },
        "options": {
          "textFormat": {
            "textOptions": {
              "type": "json_object",
              "verbosity": "low"
            }
          }
        },
        "responses": {
          "values": [
            {
              "content": "=You are a Senior Equity Analyst with 50+ years of institutional market experience.\nYou receive structured financial data, news context, and a Quality F-Score and must produce disciplined valuation targets and a risk-adjusted verdict.\nYou must think like an institutional portfolio manager: conservative, risk-first, evidence-based.\n\nINPUT FIELDS AVAILABLE:\n\nSTRUCTURED DATA INPUT (REAL VALUES \u2014 USE THESE ONLY)\n{\n\"stock\": \"{{ $json.stock }}\",\n\"current_price\": {{ $json.current_price }},\n\"eps_current\": {{ $json.eps_current }},\n\"bvps_current\": {{ $json.bvps_current }},\n\"revenue_last_4q\": {{ $json.revenue_last_4q }},\n\"net_income_last_4q\": {{ $json.net_income_last_4q }},\n\"gross_margin_last_4q\": {{ $json.gross_margin_last_4q }},\n\"total_debt_last_4q\": {{ $json.total_debt_last_4q }},\n\"net_debt_latest\": {{ $json.net_debt_latest }},\n\"net_cash_flag\": {{ $json.net_cash_flag }},\n\"operating_margin_ttm\": {{ $json.operating_margin_ttm }},\n\"fcf_ttm\": {{ $json.fcf_ttm }},\n\"revenue_growth_yoy\": {{ $json.revenue_growth_yoy }},\n\"f_score\": {{ $json.f_score }},\n\"f_score_data_ok\": {{ $json.f_score_data_ok }}\n}\n\nRules:\n\nUse ONLY these values.\n\nIf any field is null \u2192 treat as missing.\n\nNever assume missing data.\n\nNever assume share splits, ADR ratios, or alternative listings.\n\nIMPORTANT \u2014 NEWS SOURCE PRIORITY:\n\nSeeking Alpha articles (highest credibility signal)\n\nOther news_items\n\nMarket sentiment\n\nPrefer Seeking Alpha for thesis framing, but verify with financial data.\nIf conflict exists, flag uncertainty.\n\nSEEKING ALPHA ARTICLES (Recent 80h window):\n{{ $json.seekingAlphaNewsText }}\n\nANALYSIS TASKS\n\nFundamental Trends\n\nEvaluate revenue_last_4q and net_income_last_4q.\n\nEvaluate debt using total_debt_last_4q and net_debt_latest.\n\nEvaluate margin stability using gross_margin_last_4q and operating_margin_ttm.\n\nIf fcf_ttm is null \u2192 state \"FCF unavailable due to missing data.\"\n\nBusiness & Moat\nClassify moat: Brand, Network, Cost, Switching Costs, or IP.\n\nPricing Power\nJudge using margin stability and operating_margin_ttm.\n\nVALUATION FRAMEWORK (CRITICAL SECTION)\n\nA) IMPLIED P/E SANITY CHECK (MANDATORY)\n\nIf eps_current > 0 AND current_price > 0:\n\nimplied_pe = current_price / eps_current\nElse:\n\nimplied_pe = null\n\nEPS VALIDITY RULE:\nIf implied_pe is not null AND implied_pe > 60:\n\nTreat EPS as NOT economically meaningful (growth/transition phase).\n\nEPS multiple valuation is FORBIDDEN.\n\nCompany must be classified as growth_transition.\n\nB) PHASE CLASSIFICATION (MANDATORY \u2014 must pick one)\n\n\"mature_profitable\": eps_current > 0 AND (implied_pe is null OR implied_pe \u2264 60)\n\n\"growth_transition\": eps_current \u2264 0 OR implied_pe > 60\n\nC) Graham Number (secondary only)\n\nsqrt(22.5 \u00d7 eps_current \u00d7 bvps_current)\n\nUse ONLY if:\n\neps_current > 0\n\nbvps_current > 0\n\nphase = mature_profitable\n\nDo NOT use Graham for growth_transition stocks.\n\nD) BASE TARGET CONSTRUCTION (pt_base)\n\nPrimary valuation logic depends on phase.\n\nIF phase = mature_profitable:\n\nIf EPS positive:\npt_base = eps_current \u00d7 multiple:\n\n25\u00d7 if revenue_growth_yoy > 20%\n\n18\u00d7 if revenue_growth_yoy 5\u201320%\n\n12\u00d7 otherwise\n\nIf Graham usable:\npt_base = average(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor only (EPS multiples forbidden):\n\nLet g = revenue_growth_yoy\n\nIf g is null \u2192 pt_base = current_price\n\nIf g > 60 \u2192 pt_base = current_price \u00d7 2.0\n\nIf g > 30 \u2192 pt_base = current_price \u00d7 1.6\n\nIf g > 15 \u2192 pt_base = current_price \u00d7 1.3\n\nOtherwise \u2192 pt_base = current_price\n\nE) FINAL SAFETY RULES\n\npt_base must never be zero.\n\nIf pt_base cannot be determined \u2192 pt_base = current_price.\n\npt_base must not exceed current_price \u00d7 5 unless revenue_growth_yoy > 60.\n\nRound pt_base to 2 decimals.\n\nNEWS & THESIS IMPACT ANALYSIS (REQUIRED)\n\nAnalyze Seeking Alpha + news_items.\nExtract:\n\nregulatory risks\n\nlegal risks\n\nearnings/guidance changes\n\ncompetitive pressure\n\nthesis change signals\n\nDetermine if thesis strengthened or weakened.\n\nANTI-THESIS (RISK ADJUSTMENT)\n\nIdentify TWO biggest risks using priority:\n\nSeeking Alpha risks\n\nFinancial deterioration\n\nGeneral news\n\nF-Score Handling:\nIf f_score_data_ok is false \u2192 reduce confidence by 10.\nIf f_score \u2264 3 \u2192 increase risk discount by +10%.\nIf f_score \u2265 7 \u2192 increase confidence by +5 (max 90).\n\nBEAR / BULL BANDS\n\nDiscount selection:\n\n15% \u2192 strong fundamentals\n\n25% \u2192 moderate risk\n\n35% \u2192 weak fundamentals or uncertainty\n\nPremium selection:\n\n15% \u2192 slow growth\n\n25% \u2192 moderate growth\n\n40% \u2192 high growth (>20% revenue growth)\n\nCompute:\npt_bear = pt_base \u00d7 (1 \u2212 discount)\npt_bull = pt_base \u00d7 (1 + premium)\n\nRound both to 2 decimals.\n\nACTION VERDICT LOGIC\n\nBUY:\n\npt_base \u2265 20% upside\n\nand (f_score \u2265 5 OR fundamentals strong)\n\nSELL:\n\npt_base \u2264 -15% downside\n\nor (f_score_data_ok true AND f_score \u2264 2)\n\nOtherwise HOLD.\n\nCONFIDENCE SCORING\n\nStart at 60.\n\n+10 if revenue_growth_yoy > 20%.\n+10 if strong balance sheet (net_cash_flag true or low debt).\n+5 if f_score \u2265 7.\n\n-15 if declining margins or losses.\n-15 if high leverage.\n-20 if major regulatory/legal risk.\n-10 if F-score unavailable.\n\nClamp between 20 and 90.\n\nOUTPUT FORMAT \u2014 STRICT JSON ONLY\n\nReturn ONLY raw JSON object. No markdown. No commentary.\n\n{\n\"stock\": \"{{ $json.stock }}\",\n\"date\": \"{{ $now.format('yyyy-MM-dd') }}\",\n\"pt_bear\": (number),\n\"pt_base\": (number),\n\"pt_bull\": (number),\n\"f_score\": {{ $json.f_score }},\n\"confidence\": (20-90),\n\"rationale\": \"A one-sentence summary starting with BUY/HOLD/SELL, followed by the moat, primary risk, and pricing power level.\"\n}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b6d3f2f7-a27a-42cf-8942-5e0cd21386a8",
      "name": "XML",
      "type": "n8n-nodes-base.xml",
      "position": [
        1744,
        1680
      ],
      "parameters": {
        "options": {},
        "dataPropertyName": "sa_xml"
      },
      "typeVersion": 1
    },
    {
      "id": "3a1f9abb-a8bd-45c6-ae5d-f0f3e86a22cf",
      "name": "Merge1",
      "type": "n8n-nodes-base.merge",
      "position": [
        2816,
        1088
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "f1c73d00-f4de-4f2f-a229-6998da7baf74",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        4224,
        1312
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "8659bdf4-3891-4cf4-9187-ea800a3810f8",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{$json.skip_row}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "be94ef8b-ea23-4023-835c-8705558e589a",
      "name": "If1",
      "type": "n8n-nodes-base.if",
      "position": [
        1312,
        1680
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "bf9993a5-39a5-4344-a1a6-5ba38e67e47c",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "sa_xml",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "1a5039a2-9424-44d5-90b6-0b6c496a3a8c",
      "name": "Edit Fields2",
      "type": "n8n-nodes-base.set",
      "position": [
        1936,
        1680
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "44ff26a3-5901-4e3b-9b15-9844a2d207b5",
              "name": "stock",
              "type": "string",
              "value": "={{ $('Clean the news').item.json.stock }}"
            },
            {
              "id": "fca99ba0-11fd-40a3-acf7-ea25579a653e",
              "name": "rss",
              "type": "object",
              "value": "={{ $json.rss }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "85c8e4cc-a8fb-44b4-9da5-24e1c4e074cf",
      "name": "Merge2",
      "type": "n8n-nodes-base.merge",
      "position": [
        1808,
        320
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "0a98c267-b2d2-4a23-ad2c-29ee26a1f873",
      "name": "Is Cache Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        944,
        640
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "8579a7cf-3060-4026-b94a-5e5f370c63ae",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{$json.shouldFetch}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "35fcac15-4883-4a6c-a134-6ffab038c940",
      "name": "Cache Lookup",
      "type": "n8n-nodes-base.googleSheets",
      "maxTries": 3,
      "position": [
        576,
        1056
      ],
      "parameters": {
        "options": {
          "returnFirstMatch": false
        },
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.stock }}",
              "lookupColumn": "stock"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit?usp=drivesdk",
          "cachedResultName": "Financial_Data_Cache"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": false,
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "b5e7e0a9-3dd5-4376-ba58-685db5dc98b6",
      "name": "alphavantage - Balance Sheet",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1232,
        304
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=BALANCE_SHEET&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "b189892c-35da-478a-ad6f-23b7f950d1ed",
      "name": "alphavantage - Profile",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1232,
        512
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=OVERVIEW&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "40deac93-692c-4df4-bc28-40af485ddd14",
      "name": "alphavantage - Income Statement",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1232,
        688
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=INCOME_STATEMENT&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "0203cdb9-24dd-403e-8928-34b482fb68ac",
      "name": "Merge3",
      "type": "n8n-nodes-base.merge",
      "position": [
        1648,
        592
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "3d012dc2-9ab6-4ae4-9130-6154a185a011",
      "name": "Get row(s) in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1200,
        1088
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $('loop_over_tickers').item.json.stock }}",
              "lookupColumn": "stock"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit?usp=drivesdk",
          "cachedResultName": "Financial_Data_Cache"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "5c318795-5695-41ed-a8f7-89dc844cb9e2",
      "name": "If2",
      "type": "n8n-nodes-base.if",
      "position": [
        2336,
        336
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5ac312fa-a607-405d-9ab0-6993e2f45b19",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{$json.cacheHit}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ca7f63bd-a588-4ea5-97dd-b52dd7b33190",
      "name": "Update row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2400,
        624
      ],
      "parameters": {
        "columns": {
          "value": {
            "stock": "={{ $json.stock }}",
            "f_score": "={{ $('Clean Read Financial').item.json.f_score }}",
            "fcf_ttm": "={{ $('Clean Read Financial').item.json.fcf_ttm }}",
            "row_number": 0,
            "eps_current": "={{ $('Clean Read Financial').item.json.eps_current }}",
            "revenue_ttm": "={{ $('Clean Read Financial').item.json.revenue_ttm }}",
            "bvps_current": "={{ $('Clean Read Financial').item.json.bvps_current }}",
            "cash_last_4q": "={{ $('Clean Read Financial').item.json.cash_last_4q }}",
            "last_updated": "={{ new Date().toISOString().split(\"T\")[0] }}",
            "current_price": "={{ $('Clean Read Financial').item.json.current_price }}",
            "net_income_ttm": "={{ $('Clean Read Financial').item.json.net_income_ttm }}",
            "total_debt_ttm": "={{ $('Clean Read Financial').item.json.total_debt_ttm }}",
            "f_score_data_ok": "={{ $('Clean Read Financial').item.json.f_score_data_ok }}",
            "revenue_last_4q": "={{ $('Clean Read Financial').item.json.revenue_last_4q }}",
            "gross_margin_ttm": "={{ $('Clean Read Financial').item.json.gross_margin_ttm }}",
            "net_income_last_4q": "={{ $('Clean Read Financial').item.json.net_income_last_4q }}",
            "revenue_growth_yoy": "={{ $('Clean Read Financial').item.json.revenue_growth_yoy }}",
            "shares_outstanding": "={{ $('Clean Read Financial').item.json.shares_outstanding }}",
            "total_debt_last_4q": "={{ $('Clean Read Financial').item.json.total_debt_last_4q }}",
            "gross_margin_last_4q": "={{ $('Clean Read Financial').item.json.gross_margin_last_4q }}",
            "operating_margin_ttm": "={{ $('Clean Read Financial').item.json.operating_margin_ttm }}"
          },
          "schema": [
            {
              "id": "stock",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "stock",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "cache_key",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "cache_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "data_source",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "data_source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_updated",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "last_updated",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "eps_current",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "eps_current",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "bvps_current",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "bvps_current",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "shares_outstanding",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "shares_outstanding",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "revenue_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "revenue_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_debt_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total_debt_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "gross_margin_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "gross_margin_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "operating_margin_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "operating_margin_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "fcf_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "fcf_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "revenue_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "revenue_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_income_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "net_income_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "gross_margin_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "gross_margin_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_debt_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total_debt_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "cash_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "cash_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_income_ttm",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "net_income_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "current_price",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "current_price",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "revenue_growth_yoy",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "revenue_growth_yoy",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "f_score",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "f_score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "f_score_data_ok",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "f_score_data_ok",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "stock"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit?usp=drivesdk",
          "cachedResultName": "Financial_Data_Cache"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "318f0afb-8875-4234-8477-075e21a2d1a5",
      "name": "Insert Row",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2400,
        848
      ],
      "parameters": {
        "columns": {
          "value": {
            "stock": "={{ $json.stock }}",
            "f_score": "={{ $json.f_score }}",
            "fcf_ttm": "={{ $json.fcf_ttm }}",
            "eps_current": "={{ $json.eps_current }}",
            "revenue_ttm": "={{ $json.revenue_ttm }}",
            "bvps_current": "={{ $json.bvps_current }}",
            "cash_last_4q": "={{ $json.cash_last_4q }}",
            "last_updated": "={{ new Date().toISOString().split(\"T\")[0] }}",
            "current_price": "={{ $json.current_price }}",
            "net_income_ttm": "={{ $json.net_income_ttm }}",
            "total_debt_ttm": "={{ $json.total_debt_ttm }}",
            "f_score_data_ok": "={{ $json.f_score_data_ok }}",
            "revenue_last_4q": "={{ $json.revenue_last_4q }}",
            "gross_margin_ttm": "={{ $json.gross_margin_ttm }}",
            "net_income_last_4q": "={{ $json.net_income_last_4q }}",
            "revenue_growth_yoy": "={{ $json.revenue_growth_yoy }}",
            "shares_outstanding": "={{ $json.shares_outstanding }}",
            "total_debt_last_4q": "={{ $json.total_debt_last_4q }}",
            "gross_margin_last_4q": "={{ $json.gross_margin_last_4q }}",
            "operating_margin_ttm": "={{ $json.operating_margin_ttm }}"
          },
          "schema": [
            {
              "id": "stock",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "stock",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "cache_key",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "cache_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "data_source",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "data_source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_updated",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "last_updated",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "eps_current",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "eps_current",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "bvps_current",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "bvps_current",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "shares_outstanding",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "shares_outstanding",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "revenue_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "revenue_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_debt_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total_debt_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "gross_margin_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "gross_margin_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "operating_margin_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "operating_margin_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "fcf_ttm",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "fcf_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "revenue_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "revenue_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_income_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "net_income_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "gross_margin_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "gross_margin_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_debt_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total_debt_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "cash_last_4q",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "cash_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_income_ttm",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "net_income_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "current_price",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "current_price",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "revenue_growth_yoy",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "revenue_growth_yoy",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "f_score",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "f_score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "f_score_data_ok",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "f_score_data_ok",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "stock"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IQeSDT8cqPKmpJgDYIl14fPM_lON_cW1W73KJNn65z8/edit?usp=drivesdk",
          "cachedResultName": "Financial_Data_Cache"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "023ff7ab-93e2-4339-a245-69696f26ea1d",
      "name": "alphavantage - CashFlow",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1216,
        864
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=CASH_FLOW&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "c1ffc9d2-401b-4773-aa1c-846b5c245124",
      "name": "Merge4",
      "type": "n8n-nodes-base.merge",
      "position": [
        1984,
        688
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "6da55c24-c5b0-4376-8d5c-be1f464092e9",
      "name": "Check the cache",
      "type": "n8n-nodes-base.code",
      "position": [
        800,
        1024
      ],
      "parameters": {
        "jsCode": "const FRESH_DAYS = 7;\nconst now = Date.now();\n\nconst row = items?.[0]?.json ?? null;\n\nfunction parseDateMs(v) {\n  if (!v) return null;\n  const t = Date.parse(v);\n  return Number.isFinite(t) ? t : null;\n}\n\nconst cacheHit = !!(row && row.stock);\n\nlet shouldFetch = false;\nlet reason = '';\nlet ageDays = null;\n\nif (!cacheHit) {\n  shouldFetch = true;\n  reason = 'cache_miss';\n} else {\n  const updatedMs = parseDateMs(row.last_updated);\n\n  if (!updatedMs) {\n    shouldFetch = true;\n    reason = 'missing_last_updated';\n  } else {\n    ageDays = (now - updatedMs) / 86400000;\n    shouldFetch = ageDays >= FRESH_DAYS;\n    reason = shouldFetch ? `stale_${ageDays.toFixed(2)}d` : `fresh_${ageDays.toFixed(2)}d`;\n  }\n}\n\nreturn [{\n  json: {\n    stock: row?.stock || $json.stock,\n    cacheHit,                 // \u2705 this is the one you\u2019ll use for update vs append\n    shouldFetch,              // \u2705 this is only for API call decision\n    reason,\n    ageDays,\n    cacheRow: row || null,\n    nowISO: new Date(now).toISOString(),\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0a4cc04c-137e-46b4-84d0-fbf78719cd98",
      "name": "Seekingalpha Articles",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        896,
        1664
      ],
      "parameters": {
        "url": "=https://seekingalpha.com/api/sa/combined/{{ $json.stock }}.xml",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text",
              "outputPropertyName": "sa_xml"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "4f7ab138-6aa4-4d09-8811-ebe715aff036",
      "name": "Clean balance sheet",
      "type": "n8n-nodes-base.code",
      "position": [
        1440,
        304
      ],
      "parameters": {
        "jsCode": "return {\n  json: {\n    stock: $json.stock || $json.Symbol || $json.symbol || $input.item.json.stock || \"UNKNOWN\",\n    balance_data: $json\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "9f07de28-b72c-48ca-b2ed-acc8a8166697",
      "name": "Clean Profile",
      "type": "n8n-nodes-base.code",
      "position": [
        1440,
        496
      ],
      "parameters": {
        "jsCode": "return {\n  json: {\n    stock: $json.stock || $json.Symbol || $json.symbol || $input.item.json.stock || \"UNKNOWN\",\n    overview_data: $json\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "a372771f-529b-44aa-b1cc-7e42940d4834",
      "name": "Clean Income statement",
      "type": "n8n-nodes-base.code",
      "position": [
        1440,
        688
      ],
      "parameters": {
        "jsCode": "const inJson = $json || {};\nconst stock =\n  inJson.stock ||\n  inJson.Symbol ||\n  inJson.symbol ||\n  (items && items[0] && items[0].json && items[0].json.stock) ||\n  \"UNKNOWN\";\n\nreturn [\n  {\n    json: {\n      stock: stock,\n      income_data: inJson\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "81eef3bf-3089-4494-982a-f5aaf756948d",
      "name": "Clean Ccashflow",
      "type": "n8n-nodes-base.code",
      "position": [
        1440,
        848
      ],
      "parameters": {
        "jsCode": "const inJson = $json || {};\nconst stock =\n  inJson.stock ||\n  inJson.Symbol ||\n  inJson.symbol ||\n  (items && items[0] && items[0].json && items[0].json.stock) ||\n  \"UNKNOWN\";\n\nreturn [\n  {\n    json: {\n      stock: stock,\n      cashflow_data: inJson\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "5fe3436c-a83a-45cc-a535-85e3628c52d5",
      "name": "Clean Read Financial",
      "type": "n8n-nodes-base.code",
      "position": [
        2144,
        688
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Helper function to safely get numbers\nfunction num(value) {\n  if (value === null || value === undefined || value === 'None' || value === '') return null;\n  const n = Number(String(value).replace(/[,$%]/g, ''));\n  return isFinite(n) ? n : null;\n}\n\n// Get data from the API responses\nconst overview = $json.overview_data || {};\nconst income = $json.income_data || {};\nconst balance = $json.balance_data || {};\nconst cashflow = $json.cashflow_data || {}; // optional (only if you add CASH_FLOW endpoint)\nconst current_price = num($json.current_price); // null if missing\n\n\n// Extract reports\nconst quarterlyReports = income.quarterlyReports || [];\nconst balanceQuarterly = balance.quarterlyReports || [];\n\n// Revenue last 4 quarters\nconst revenue_last_4q = quarterlyReports.slice(0, 4).map(q => num(q.totalRevenue));\n\n// Net income last 4 quarters\nconst net_income_last_4q = quarterlyReports.slice(0, 4).map(q => num(q.netIncome));\n\n// Gross margin last 4 quarters (calculate from revenue and cost)\nconst gross_margin_last_4q = quarterlyReports.slice(0, 4).map(q => {\n  const rev = num(q.totalRevenue);\n  const cogs = num(q.costOfRevenue);\n  if (rev !== null && cogs !== null && rev > 0) {\n    return Number((((rev - cogs) / rev) * 100).toFixed(2));\n  }\n  return null;\n});\n\n// Total debt last 4 quarters\nconst total_debt_last_4q = balanceQuarterly.slice(0, 4).map(q => {\n  const shortTerm = num(q.shortTermDebt) || 0;\n  const longTerm = num(q.longTermDebt) || 0;\n  const total = shortTerm + longTerm;\n  return total > 0 ? total : null;\n});\n\n// Cash last 4 quarters\nconst cash_last_4q = balanceQuarterly\n  .slice(0, 4)\n  .map(q => num(q.cashAndCashEquivalentsAtCarryingValue));\n\n\nconst debt_latest =\n  total_debt_last_4q && total_debt_last_4q.length\n    ? total_debt_last_4q[0]\n    : null;\n\nconst cash_latest =\n  cash_last_4q && cash_last_4q.length\n    ? cash_last_4q[0]\n    : null;\n\nlet net_debt_latest = null;\nlet net_cash_flag = null;\n\nif (debt_latest !== null && cash_latest !== null) {\n  net_debt_latest = debt_latest - cash_latest;\n  net_cash_flag = net_debt_latest < 0;\n}\n// Revenue growth YoY (quarter 0 vs quarter 3)\nlet revenue_growth_yoy = null;\nif (revenue_last_4q.length >= 4 && revenue_last_4q[0] !== null && revenue_last_4q[3] !== null && revenue_last_4q[3] !== 0) {\n  revenue_growth_yoy = Number((((revenue_last_4q[0] - revenue_last_4q[3]) / revenue_last_4q[3]) * 100).toFixed(2));\n}\n\n// ---- FIX 1: total_debt_ttm should come from BALANCE sheet (latest quarter) ----\nconst latestBQ = balanceQuarterly && balanceQuarterly.length ? balanceQuarterly[0] : null;\n\nlet total_debt_ttm = null;\nif (latestBQ) {\n  const shortTerm = num(latestBQ.shortTermDebt) || 0;\n  const longTerm = num(latestBQ.longTermDebt) || 0;\n  const total = shortTerm + longTerm;\n  total_debt_ttm = total > 0 ? total : null;\n}\n\n// ---- FIX 2: operating margin can be decimal (0.12) OR percent (12.0) ----\nlet operating_margin_ttm = num(overview.OperatingMarginTTM);\nif (operating_margin_ttm !== null) {\n  // if it's a fraction, convert to percent\n  if (Math.abs(operating_margin_ttm) <= 1.5) {\n    operating_margin_ttm = operating_margin_ttm * 100;\n  }\n  operating_margin_ttm = Number(operating_margin_ttm.toFixed(2));\n}\n\n// ---- FIX 3: fcf_ttm (real FCF) only if you have CASH_FLOW endpoint ----\n// Alpha Vantage doesn't reliably give FCF in OVERVIEW.\n// Compute: FCF = OperatingCashflow + CapitalExpenditures (capex is usually negative)\nlet fcf_ttm = null;\n\nconst cfAnnual = cashflow.annualReports || [];\nconst cfQuarterly = cashflow.quarterlyReports || [];\n\nlet ocf = null;\nlet capex = null;\n\nif (cfAnnual.length) {\n  ocf = num(cfAnnual[0].operatingCashflow);\n  capex = num(cfAnnual[0].capitalExpenditures);\n}\n\nif ((ocf === null || capex === null) && cfQuarterly.length >= 4) {\n  const last4 = cfQuarterly.slice(0,4);\n  const ocfList = last4.map(r=>num(r.operatingCashflow));\n  const capexList = last4.map(r=>num(r.capitalExpenditures));\n\n  if (ocfList.every(v=>v!==null) && capexList.every(v=>v!==null)) {\n    ocf = ocfList.reduce((a,b)=>a+b,0);\n    capex = capexList.reduce((a,b)=>a+b,0);\n  }\n}\n\nif (ocf!==null && capex!==null) fcf_ttm = ocf + capex;\n\n// Gross margin TTM (from overview fields)\nlet gross_margin_ttm = null;\nconst grossProfitTTM = num(overview.GrossProfitTTM);\nconst revenueTTM = num(overview.RevenueTTM);\nif (grossProfitTTM !== null && revenueTTM !== null && revenueTTM !== 0) {\n  gross_margin_ttm = Number(((grossProfitTTM / revenueTTM) * 100).toFixed(2));\n}\nconst net_income_ttm = net_income_last_4q.reduce((a,b)=>a+(b||0),0);\n// --------------------\n// Piotroski F-Score (0-9) using annualReports[0] vs annualReports[1]\n// If a component can't be computed, it scores 0 (conservative).\n// --------------------\nfunction safeDiv(a, b) {\n  if (a === null || b === null || b === 0) return null;\n  return a / b;\n}\n\nconst incA = income.annualReports || [];\nconst balA = balance.annualReports || [];\nconst cfA  = cashflow.annualReports || [];\n\nconst y0_inc = incA.length >= 1 ? incA[0] : null;\nconst y1_inc = incA.length >= 2 ? incA[1] : null;\n\nconst y0_bal = balA.length >= 1 ? balA[0] : null;\nconst y1_bal = balA.length >= 2 ? balA[1] : null;\n\nconst y0_cf  = cfA.length >= 1 ? cfA[0] : null;\nconst y1_cf  = cfA.length >= 2 ? cfA[1] : null;\n\nlet f_score = 0;\nconst f_detail = {}; // optional debug\n\n// Helper getters\nconst NI0 = y0_inc ? num(y0_inc.netIncome) : null;\nconst NI1 = y1_inc ? num(y1_inc.netIncome) : null;\n\nconst TA0 = y0_bal ? num(y0_bal.totalAssets) : null;\nconst TA1 = y1_bal ? num(y1_bal.totalAssets) : null;\n\nconst CFO0 = y0_cf ? num(y0_cf.operatingCashflow) : null;\nconst CFO1 = y1_cf ? num(y1_cf.operatingCashflow) : null;\n\nconst LTD0 = y0_bal ? num(y0_bal.longTermDebt) : null;\nconst LTD1 = y1_bal ? num(y1_bal.longTermDebt) : null;\n\nconst CA0 = y0_bal ? num(y0_bal.totalCurrentAssets) : null;\nconst CA1 = y1_bal ? num(y1_bal.totalCurrentAssets) : null;\n\nconst CL0 = y0_bal ? num(y0_bal.totalCurrentLiabilities) : null;\nconst CL1 = y1_bal ? num(y1_bal.totalCurrentLiabilities) : null;\n\nconst REV0 = y0_inc ? num(y0_inc.totalRevenue) : null;\nconst REV1 = y1_inc ? num(y1_inc.totalRevenue) : null;\n\nconst GP0 = y0_inc ? num(y0_inc.grossProfit) : null;\nconst GP1 = y1_inc ? num(y1_inc.grossProfit) : null;\n\n// Shares outstanding (dilution check)\n// Prefer annual balance sheet shares if available; otherwise we can't score it reliably.\nconst SH0 = y0_bal ? num(y0_bal.commonStockSharesOutstanding) : null;\nconst SH1 = y1_bal ? num(y1_bal.commonStockSharesOutstanding) : null;\n\n// ---- PROFITABILITY (4) ----\n// F1: Positive net income\nif (NI0 !== null && NI0 > 0) { f_score++; f_detail.F1_NI_pos = 1; } else f_detail.F1_NI_pos = 0;\n\n// F2: Positive CFO\nif (CFO0 !== null && CFO0 > 0) { f_score++; f_detail.F2_CFO_pos = 1; } else f_detail.F2_CFO_pos = 0;\n\n// F3: ROA improving (NI/TA)\nconst ROA0 = safeDiv(NI0, TA0);\nconst ROA1 = safeDiv(NI1, TA1);\nif (ROA0 !== null && ROA1 !== null && ROA0 > ROA1) { f_score++; f_detail.F3_ROA_up = 1; } else f_detail.F3_ROA_up = 0;\n\n// F4: Accruals: CFO > NI\nif (CFO0 !== null && NI0 !== null && CFO0 > NI0) { f_score++; f_detail.F4_CFO_gt_NI = 1; } else f_detail.F4_CFO_gt_NI = 0;\n\n// ---- LEVERAGE / LIQUIDITY (3) ----\n// F5: Lower long-term debt\nif (LTD0 !== null && LTD1 !== null && LTD0 < LTD1) { f_score++; f_detail.F5_LTD_down = 1; } else f_detail.F5_LTD_down = 0;\n\n// F6: Higher current ratio (CA/CL)\nconst CR0 = safeDiv(CA0, CL0);\nconst CR1 = safeDiv(CA1, CL1);\nif (CR0 !== null && CR1 !== null && CR0 > CR1) { f_score++; f_detail.F6_CR_up = 1; } else f_detail.F6_CR_up = 0;\n\n// F7: No dilution (shares not increased)\nif (SH0 !== null && SH1 !== null && SH0 <= SH1) { f_score++; f_detail.F7_no_dilution = 1; } else f_detail.F7_no_dilution = 0;\n\n// ---- OPERATING EFFICIENCY (2) ----\n// F8: Gross margin improved (GP/REV)\nconst GM0 = (GP0 !== null && REV0) ? safeDiv(GP0, REV0) : null;\nconst GM1 = (GP1 !== null && REV1) ? safeDiv(GP1, REV1) : null;\nif (GM0 !== null && GM1 !== null && GM0 > GM1) { f_score++; f_detail.F8_GM_up = 1; } else f_detail.F8_GM_up = 0;\n\n// F9: Asset turnover improved (REV/TA)\nconst AT0 = safeDiv(REV0, TA0);\nconst AT1 = safeDiv(REV1, TA1);\nif (AT0 !== null && AT1 !== null && AT0 > AT1) { f_score++; f_detail.F9_AT_up = 1; } else f_detail.F9_AT_up = 0;\n\n// If we lack the minimum annual data, f_score is still computed conservatively (likely low).\n// You can also flag completeness:\nconst f_score_data_ok = !!(y0_inc && y1_inc && y0_bal && y1_bal && y0_cf && y1_cf);\nreturn {\n  json: {\n    stock: $json.stock || overview.Symbol || overview.symbol || \"UNKNOWN\",\n    eps_current: num(overview.EPS),\n    bvps_current: num(overview.BookValue),\n    shares_outstanding: num(overview.SharesOutstanding),\n    revenue_ttm: revenueTTM,\n\n    // fixed\n    total_debt_ttm: total_debt_ttm,\n\n    gross_margin_ttm: gross_margin_ttm,\n    operating_margin_ttm: operating_margin_ttm,\n\n    // fixed: real FCF only if CASH_FLOW is available; otherwise stays null\n    fcf_ttm: fcf_ttm,\n    net_income_ttm,\n    revenue_last_4q: revenue_last_4q.every(v => v !== null) ? revenue_last_4q : null,\n    net_income_last_4q: net_income_last_4q.every(v => v !== null) ? net_income_last_4q : null,\n    gross_margin_last_4q: gross_margin_last_4q.some(v => v !== null) ? gross_margin_last_4q : null,\n    total_debt_last_4q: total_debt_last_4q.some(v => v !== null) ? total_debt_last_4q : null,\n    cash_last_4q: cash_last_4q.some(v => v !== null) ? cash_last_4q : null,\n    revenue_growth_yoy: revenue_growth_yoy,\n    current_price,\nnet_debt_latest,\ncash_latest,\ndebt_latest,\nnet_cash_flag,\n    f_score,\nf_score_data_ok,\n    cache: {\n      status: \"refreshed\",\n      source: \"api\",\n      last_updated: $json.last_updated || $json.nowISO || null\n    }\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8e5e3d3d-47de-4a0e-9edd-c0d0e829368c",
      "name": "Clean Old Financial",
      "type": "n8n-nodes-base.code",
      "position": [
        2432,
        1088
      ],
      "parameters": {
        "jsCode": "const r = $json.cacheRow || $json;\n\nreturn [{\n  json: {\n    stock: r.stock,\n      eps_current: Number(r.eps_current) || null,\n      bvps_current: Number(r.bvps_current) || null,\n      shares_outstanding: Number(r.shares_outstanding) || null,\n      revenue_ttm: Number(r.revenue_ttm) || null,\n      total_debt_ttm: Number(r.total_debt_ttm) || null,\n      gross_margin_ttm: Number(r.gross_margin_ttm) || null,\n      operating_margin_ttm: Number(r.operating_margin_ttm) || null,\n      fcf_ttm: r.fcf_ttm !== \"\" ? Number(r.fcf_ttm) : null,\n      revenue_last_4q: r.revenue_last_4q || null,\n      net_income_last_4q: r.net_income_last_4q || null,\n      gross_margin_last_4q: r.gross_margin_last_4q || null,\n      total_debt_last_4q: r.total_debt_last_4q || null,\n      cash_last_4q: r.cash_last_4q || null,\n      revenue_growth_yoy: Number(r.revenue_growth_yoy) || null,\n      current_price: Number(r.current_price) || null,\n      net_income_ttm: Number(r.net_income_ttm) || null,\n      f_score: Number(r.f_score) || null,\n      f_score_data_ok: Boolean(r.f_score_data_ok)|| null,\n    cache: {\n      status: \"fresh\",\n      source: \"cache\",\n      last_updated: r.last_updated || null\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7e003755-bba0-4bbe-b501-c0bbefe3f8ac",
      "name": "Merge New Financial",
      "type": "n8n-nodes-base.code",
      "position": [
        2608,
        720
      ],
      "parameters": {
        "jsCode": "return items;"
      },
      "typeVersion": 2
    },
    {
      "id": "d5704ba4-120e-42fd-9a0e-7be69ba1490b",
      "name": "alphavantage - Current Price",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1456,
        992
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "5fd29a42-a969-47ff-9f72-14a40cdebb49",
      "name": "Clean Current Price",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        976
      ],
      "parameters": {
        "jsCode": "const inJson = $json || {};\nconst q = inJson[\"Global Quote\"] || {};   // \u2190 this was missing\n\nconst stock =\n  q[\"01. symbol\"] ||\n  inJson.stock ||\n  inJson.Symbol ||\n  inJson.symbol ||\n  \"UNKNOWN\";\n\nreturn [\n  {\n    json: {\n      stock: stock,\n      current_price: Number(q[\"05. price\"]) || null\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "5fa9b6cb-9b3e-424e-b726-928f1fd24b9f",
      "name": "Merge5",
      "type": "n8n-nodes-base.merge",
      "position": [
        1776,
        816
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "f5f85439-56d1-4404-800c-f205bbd26d67",
      "name": "Clean the news",
      "type": "n8n-nodes-base.code",
      "position": [
        1088,
        1664
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Carry stock forward\nconst stock =\n  $json.stock ??\n  $json.ticker ??\n  ($node[\"loop_over_tickers\"]?.json?.stock ?? \"\");\n\n// Find XML - CHECK sa_xml FIRST (your custom output field)\nlet saXml =\n  (typeof $json.sa_xml === \"string\" && $json.sa_xml.trim()) ? $json.sa_xml.trim() :\n  (typeof $json.body === \"string\" && $json.body.trim()) ? $json.body.trim() :\n  (typeof $json.data === \"string\" && $json.data.trim()) ? $json.data.trim() :\n  (typeof $json.responseBody === \"string\" && $json.responseBody.trim()) ? $json.responseBody.trim() :\n  (typeof $json.text === \"string\" && $json.text.trim()) ? $json.text.trim() :\n  null;\n\n// If your HTTP node returns binary, try decode first binary field\nif (!saXml && $binary) {\n  const key = Object.keys($binary)[0];\n  const b64 = key && $binary[key] && $binary[key].data ? $binary[key].data : null;\n  if (b64) {\n    saXml = Buffer.from(b64, \"base64\").toString(\"utf8\").trim();\n    if (!saXml) saXml = null;\n  }\n}\n\n// ALWAYS return json as an OBJECT\nreturn {\n  json: {\n    stock,\n    sa_xml: saXml,\n    sa_ok: !!saXml\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "a157d224-6c96-4e5f-8202-520c7a0b66b9",
      "name": "return the news",
      "type": "n8n-nodes-base.code",
      "position": [
        1520,
        1840
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return {\n  json: {\n    stock: $json.stock ?? \"\",\n    seekingAlphaWindowHours: 80,\n    seekingAlphaCount: 0,\n    seekingAlphaNews: [],\n    seekingAlphaNewsText: \"\",\n    seekingAlphaError: $json.sa_error ?? \"No XML to parse\"\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f1bae301-1e6e-49e5-8aba-a85d702e879c",
      "name": "Final version of news",
      "type": "n8n-nodes-base.code",
      "position": [
        2448,
        1680
      ],
      "parameters": {
        "jsCode": "// ---- CONFIG ----\nconst windowHours = 80;                 // recent window\nconst fallbackCount = 3;               // if no recent news, return latest N anyway\nconst now = Date.now();\nconst cutoff = now - windowHours * 3600 * 1000;\n\n// ---- GET STOCK FROM INPUT ----\nconst stock =\n  $input.first()?.json?.stock ??\n  $json.stock ??\n  \"\";\n\n// ---- INPUT PATH ----\nconst items =\n  $json?.rss?.channel?.item ??\n  $json?.channel?.item ??\n  $json?.item ??\n  [];\n\nconst arr = Array.isArray(items) ? items : [items];\n\n// ---- HELPERS ----\nfunction cleanHtml(s) {\n  if (!s) return \"\";\n  return String(s)\n    .replace(/<script[\\s\\S]*?<\\/script>/gi, \"\")\n    .replace(/<style[\\s\\S]*?<\\/style>/gi, \"\")\n    .replace(/<\\/?[^>]+>/g, \" \")\n    .replace(/\\s+/g, \" \")\n    .trim();\n}\n\nfunction toMillis(dateStr) {\n  const t = Date.parse(dateStr);\n  return Number.isFinite(t) ? t : NaN;\n}\n\nfunction getLink(i) {\n  const l = i.link;\n  if (!l) return \"\";\n  if (typeof l === \"string\") return l;\n  if (Array.isArray(l)) return getLink({ link: l[0] });\n  return l[\"#text\"] || l.href || l?.$?.href || \"\";\n}\n\n// ---- PARSE ALL ITEMS (no time filter yet) ----\nconst parsedAll = arr\n  .map(i => {\n    const publishedAt = i.pubDate || i.published || i.date || \"\";\n    const t = toMillis(publishedAt);\n    return {\n      source: \"SeekingAlpha_RSS\",\n      title: cleanHtml(i.title),\n      link: getLink(i),\n      publishedAt,\n      publishedAtMs: t,\n      snippet: cleanHtml(i.description || i.summary || i[\"content:encoded\"])\n    };\n  })\n  .filter(n => n.title && n.link && Number.isFinite(n.publishedAtMs))\n  .sort((a, b) => b.publishedAtMs - a.publishedAtMs);\n\n// ---- DEDUPE (keep newest first) ----\nconst seen = new Set();\nconst dedupedAll = [];\nfor (const n of parsedAll) {\n  if (seen.has(n.link)) continue;\n  seen.add(n.link);\n  dedupedAll.push(n);\n}\n\n// ---- RECENT WINDOW ----\nconst recent = dedupedAll.filter(n => n.publishedAtMs >= cutoff);\n\n// ---- FINAL SELECTION ----\n// If no recent items, fallback to latest N overall\nconst selected = (recent.length > 0) ? recent : dedupedAll.slice(0, fallbackCount);\nconst hasRecent = recent.length > 0;\n\n// ---- CREATE COMPACT TEXT ----\nconst seekingAlphaNewsText = selected.map(n => {\n  const shortSnippet = n.snippet.length > 240 ? n.snippet.slice(0, 240) + \"\u2026\" : n.snippet;\n  return `- ${n.publishedAt} | ${n.title}\\n  ${n.link}\\n  Snippet: ${shortSnippet}`;\n}).join(\"\\n\\n\");\n\nreturn [\n  {\n    json: {\n      stock,\n      seekingAlphaWindowHours: windowHours,\n\n      // signals for the AI prompt later\n      seekingAlphaHasRecent: hasRecent,\n      seekingAlphaRecentCount: recent.length,\n\n      // what you actually pass forward\n      seekingAlphaCount: selected.length,\n      seekingAlphaNews: selected.map(({ publishedAtMs, ...rest }) => rest),\n      seekingAlphaNewsText,\n\n      // optional: last coverage date (useful for prompt)\n      seekingAlphaLatestDate: dedupedAll[0]?.publishedAt ?? null\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "be4d33b2-8736-4328-947b-c29a4866ae97",
      "name": "Clean Up ChatGPT",
      "type": "n8n-nodes-base.code",
      "position": [
        3488,
        736
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let parsed = $json;\n\n// Extract from ChatGPT nested structure\nconst t = $json?.output?.[0]?.content?.[0]?.text;\n\nif (t) {\n  if (typeof t === \"string\") {\n    // sometimes text is a JSON string\n    try {\n      parsed = JSON.parse(t);\n    } catch (e) {\n      // keep original wrapper if parse fails\n      parsed = $json;\n    }\n  } else if (typeof t === \"object\") {\n    // sometimes text is already a JSON object (your case)\n    parsed = t;\n  }\n}\n\nconst stock =\n  parsed?.stock ||\n  $json?.stock ||\n  $json?.ticker ||\n  $json?.symbol ||\n  \"UNKNOWN\";\n\nreturn {\n  json: {\n    chatgpt_result: parsed,\n    stock\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "5c6aaad9-868f-4e70-933f-320a43f74023",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        768,
        1296
      ],
      "parameters": {
        "color": 5,
        "width": 2200,
        "height": 700,
        "content": "# 2. SeekingAlpha News Data Retrieval & Validation\nGet Articles from Seeking Alpha:\nFor each ticker, the workflow sends an HTTP request to retrieve recent Seeking Alpha articles within a defined time window. These articles provide analyst-grade insights and market context.\nTicker Validation Check:\nA conditional node verifies whether valid news results were returned and whether the ticker is recognized.\nIf no articles are found \u2192 the ticker may be invalid or have no recent coverage.\nHandle Invalid Tickers:\nIf the ticker is invalid or returns no data, the workflow logs the ticker in the Google Sheet with an \"Invalid Ticker\" status. This enables tracking of symbols that cannot be processed.\nNews Aggregation:\nFor valid tickers, multiple articles are merged into a single structured text block.\nThis prepares consolidated news context for the AI model to analyze in one step"
      },
      "typeVersion": 1
    },
    {
      "id": "17b04b39-4d23-49fa-882d-21a621fc58bb",
      "name": "Send a text message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        576,
        880
      ],
      "parameters": {
        "text": "Report is done",
        "chatId": "123456789",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "12c9b380-3075-46d0-bf82-11cc67c49a39",
      "name": "Message a model1",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        3088,
        1184
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-pro",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=You are a Senior Equity Analyst with 50+ years of institutional market experience.\nYou receive structured financial data, news context, and a Quality F-Score and must produce disciplined valuation targets and a risk-adjusted verdict.\nYou must think like an institutional portfolio manager: conservative, risk-first, evidence-based.\n\nINPUT FIELDS AVAILABLE:\n\nSTRUCTURED DATA INPUT (REAL VALUES \u2014 USE THESE ONLY)\n{\n\"stock\": \"{{ $json.stock }}\",\n\"current_price\": {{ $json.current_price }},\n\"eps_current\": {{ $json.eps_current }},\n\"bvps_current\": {{ $json.bvps_current }},\n\"revenue_last_4q\": {{ $json.revenue_last_4q }},\n\"net_income_last_4q\": {{ $json.net_income_last_4q }},\n\"gross_margin_last_4q\": {{ $json.gross_margin_last_4q }},\n\"total_debt_last_4q\": {{ $json.total_debt_last_4q }},\n\"net_debt_latest\": {{ $json.net_debt_latest }},\n\"net_cash_flag\": {{ $json.net_cash_flag }},\n\"operating_margin_ttm\": {{ $json.operating_margin_ttm }},\n\"fcf_ttm\": {{ $json.fcf_ttm }},\n\"revenue_growth_yoy\": {{ $json.revenue_growth_yoy }},\n\"f_score\": {{ $json.f_score }},\n\"f_score_data_ok\": {{ $json.f_score_data_ok }}\n}\n\nRules:\n\nUse ONLY these values.\n\nIf any field is null \u2192 treat as missing.\n\nNever assume missing data.\n\nNever assume share splits, ADR ratios, or alternative listings.\n\nIMPORTANT \u2014 NEWS SOURCE PRIORITY:\n\nSeeking Alpha articles (highest credibility signal)\n\nOther news_items\n\nMarket sentiment\n\nPrefer Seeking Alpha for thesis framing, but verify with financial data.\nIf conflict exists, flag uncertainty.\n\nSEEKING ALPHA ARTICLES (Recent 80h window):\n{{ $json.seekingAlphaNewsText }}\n\nANALYSIS TASKS\n\nFundamental Trends\n\nEvaluate revenue_last_4q and net_income_last_4q.\n\nEvaluate debt using total_debt_last_4q and net_debt_latest.\n\nEvaluate margin stability using gross_margin_last_4q and operating_margin_ttm.\n\nIf fcf_ttm is null \u2192 state \"FCF unavailable due to missing data.\"\n\nBusiness & Moat\nClassify moat: Brand, Network, Cost, Switching Costs, or IP.\n\nPricing Power\nJudge using margin stability and operating_margin_ttm.\n\nVALUATION FRAMEWORK (CRITICAL SECTION)\n\nA) IMPLIED P/E SANITY CHECK (MANDATORY)\n\nIf eps_current > 0 AND current_price > 0:\n\nimplied_pe = current_price / eps_current\nElse:\n\nimplied_pe = null\n\nEPS VALIDITY RULE:\nIf implied_pe is not null AND implied_pe > 60:\n\nTreat EPS as NOT economically meaningful (growth/transition phase).\n\nEPS multiple valuation is FORBIDDEN.\n\nCompany must be classified as growth_transition.\n\nB) PHASE CLASSIFICATION (MANDATORY \u2014 must pick one)\n\n\"mature_profitable\": eps_current > 0 AND (implied_pe is null OR implied_pe \u2264 60)\n\n\"growth_transition\": eps_current \u2264 0 OR implied_pe > 60\n\nC) Graham Number (secondary only)\n\nsqrt(22.5 \u00d7 eps_current \u00d7 bvps_current)\n\nUse ONLY if:\n\neps_current > 0\n\nbvps_current > 0\n\nphase = mature_profitable\n\nDo NOT use Graham for growth_transition stocks.\n\nD) BASE TARGET CONSTRUCTION (pt_base)\n\nPrimary valuation logic depends on phase.\n\nIF phase = mature_profitable:\n\nIf EPS positive:\npt_base = eps_current \u00d7 multiple:\n\n25\u00d7 if revenue_growth_yoy > 20%\n\n18\u00d7 if revenue_growth_yoy 5\u201320%\n\n12\u00d7 otherwise\n\nIf Graham usable:\npt_base = average(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor only (EPS multiples forbidden):\n\nLet g = revenue_growth_yoy\n\nIf g is null \u2192 pt_base = current_price\n\nIf g > 60 \u2192 pt_base = current_price \u00d7 2.0\n\nIf g > 30 \u2192 pt_base = current_price \u00d7 1.6\n\nIf g > 15 \u2192 pt_base = current_price \u00d7 1.3\n\nOtherwise \u2192 pt_base = current_price\n\nE) FINAL SAFETY RULES\n\npt_base must never be zero.\n\nIf pt_base cannot be determined \u2192 pt_base = current_price.\n\npt_base must not exceed current_price \u00d7 5 unless revenue_growth_yoy > 60.\n\nRound pt_base to 2 decimals.\n\nNEWS & THESIS IMPACT ANALYSIS (REQUIRED)\n\nAnalyze Seeking Alpha + news_items.\nExtract:\n\nregulatory risks\n\nlegal risks\n\nearnings/guidance changes\n\ncompetitive pressure\n\nthesis change signals\n\nDetermine if thesis strengthened or weakened.\n\nANTI-THESIS (RISK ADJUSTMENT)\n\nIdentify TWO biggest risks using priority:\n\nSeeking Alpha risks\n\nFinancial deterioration\n\nGeneral news\n\nF-Score Handling:\nIf f_score_data_ok is false \u2192 reduce confidence by 10.\nIf f_score \u2264 3 \u2192 increase risk discount by +10%.\nIf f_score \u2265 7 \u2192 increase confidence by +5 (max 90).\n\nBEAR / BULL BANDS\n\nDiscount selection:\n\n15% \u2192 strong fundamentals\n\n25% \u2192 moderate risk\n\n35% \u2192 weak fundamentals or uncertainty\n\nPremium selection:\n\n15% \u2192 slow growth\n\n25% \u2192 moderate growth\n\n40% \u2192 high growth (>20% revenue growth)\n\nCompute:\npt_bear = pt_base \u00d7 (1 \u2212 discount)\npt_bull = pt_base \u00d7 (1 + premium)\n\nRound both to 2 decimals.\n\nACTION VERDICT LOGIC\n\nBUY:\n\npt_base \u2265 20% upside\n\nand (f_score \u2265 5 OR fundamentals strong)\n\nSELL:\n\npt_base \u2264 -15% downside\n\nor (f_score_data_ok true AND f_score \u2264 2)\n\nOtherwise HOLD.\n\nCONFIDENCE SCORING\n\nStart at 60.\n\n+10 if revenue_growth_yoy > 20%.\n+10 if strong balance sheet (net_cash_flag true or low debt).\n+5 if f_score \u2265 7.\n\n-15 if declining margins or losses.\n-15 if high leverage.\n-20 if major regulatory/legal risk.\n-10 if F-score unavailable.\n\nClamp between 20 and 90.\n\nOUTPUT FORMAT \u2014 STRICT JSON ONLY\n\nReturn ONLY raw JSON object. No markdown. No commentary.\n\n{\n\"stock\": \"{{ $json.stock }}\",\n\"date\": \"{{ $now.format('yyyy-MM-dd') }}\",\n\"pt_bear\": (number),\n\"pt_base\": (number),\n\"pt_bull\": (number),\n\"f_score\": {{ $json.f_score }},\n\"confidence\": (20-90),\n\"rationale\": \"A one-sentence summary starting with BUY/HOLD/SELL, followed by the moat, primary risk, and pricing power level.\"\n}"
            }
          ]
        },
        "jsonOutput": true,
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "5b8967b8-a8fa-44ce-90f9-32eb3c011b46",
      "name": "Clean up Gemini",
      "type": "n8n-nodes-base.code",
      "position": [
        3440,
        1184
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let parsed = $json;\n\nif ($json.output && typeof $json.output === \"string\") {\n  try {\n    const cleaned = $json.output.replace(/```json\\n?|```/g, \"\").trim();\n    parsed = JSON.parse(cleaned);\n  } catch (e) {\n    parsed = $json;\n  }\n} else if ($json.content?.parts?.[0]?.text) {\n  try {\n    parsed = JSON.parse($json.content.parts[0].text);\n  } catch (e) {\n    parsed = $json;\n  }\n}\n\n// If Gemini already returns object at top-level\nif (parsed && parsed.gemini_result) parsed = parsed.gemini_result;\n\nconst stock =\n  parsed?.stock ||\n  $json?.stock ||\n  $json?.ticker ||\n  $json?.symbol ||\n  \"UNKNOWN\";\n\nreturn {\n  json: {\n    gemini_result: parsed,\n    stock\n  }\n};"
      },
      "typeVersion": 2
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "3cd4215f-43f4-496b-a520-df70440d8697",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "write_sentiment_to_sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "loop_over_tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If1": {
      "main": [
        [
          {
            "node": "XML",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "return the news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If2": {
      "main": [
        [
          {
            "node": "Update row in sheet",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Insert Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "XML": {
      "main": [
        [
          {
            "node": "Edit Fields2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge1": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          },
          {
            "node": "Message a model1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge2": {
      "main": [
        [
          {
            "node": "Merge4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge3": {
      "main": [
        [
          {
            "node": "Merge2",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge4": {
      "main": [
        [
          {
            "node": "Clean Read Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge5": {
      "main": [
        [
          {
            "node": "Merge4",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Insert Row": {
      "main": [
        [
          {
            "node": "Merge New Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cache Lookup": {
      "main": [
        [
          {
            "node": "Check the cache",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields2": {
      "main": [
        [
          {
            "node": "Final version of news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Profile": {
      "main": [
        [
          {
            "node": "Merge3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean the news": {
      "main": [
        [
          {
            "node": "If1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the cache": {
      "main": [
        [
          {
            "node": "Is Cache Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Ccashflow": {
      "main": [
        [
          {
            "node": "Merge5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean up Gemini": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Is Cache Valid?": {
      "main": [
        [
          {
            "node": "alphavantage - Balance Sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "alphavantage - Profile",
            "type": "main",
            "index": 0
          },
          {
            "node": "alphavantage - Income Statement",
            "type": "main",
            "index": 0
          },
          {
            "node": "alphavantage - CashFlow",
            "type": "main",
            "index": 0
          },
          {
            "node": "alphavantage - Current Price",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get row(s) in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Clean Up ChatGPT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "return the news": {
      "main": [
        [
          {
            "node": "XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Up ChatGPT": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model1": {
      "main": [
        [
          {
            "node": "Clean up Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Read_tickers_from_Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "loop_over_tickers": {
      "main": [
        [
          {
            "node": "Send a text message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Cache Lookup",
            "type": "main",
            "index": 0
          },
          {
            "node": "Seekingalpha Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Current Price": {
      "main": [
        [
          {
            "node": "Merge5",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Clean Old Financial": {
      "main": [
        [
          {
            "node": "Merge New Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean balance sheet": {
      "main": [
        [
          {
            "node": "Merge2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get row(s) in sheet": {
      "main": [
        [
          {
            "node": "Clean Old Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge New Financial": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update row in sheet": {
      "main": [
        [
          {
            "node": "Merge New Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Read Financial": {
      "main": [
        [
          {
            "node": "If2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final version of news": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Seekingalpha Articles": {
      "main": [
        [
          {
            "node": "Clean the news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Income statement": {
      "main": [
        [
          {
            "node": "Merge3",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "alphavantage - Profile": {
      "main": [
        [
          {
            "node": "Clean Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read_tickers_from_Sheet": {
      "main": [
        [
          {
            "node": "loop_over_tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "alphavantage - CashFlow": {
      "main": [
        [
          {
            "node": "Clean Ccashflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "write_sentiment_to_sheets": {
      "main": [
        [
          {
            "node": "loop_over_tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "alphavantage - Balance Sheet": {
      "main": [
        [
          {
            "node": "Clean balance sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "alphavantage - Current Price": {
      "main": [
        [
          {
            "node": "Clean Current Price",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "alphavantage - Income Statement": {
      "main": [
        [
          {
            "node": "Clean Income statement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}