{
  "id": "nNjotMFYRKbGYBkO",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets V10",
  "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"
    }
  ],
  "nodes": [
    {
      "id": "a3d2e9f0-323a-48cc-9d8f-7b03dc294628",
      "name": "loop_over_tickers",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -576,
        1296
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "a42ffbce-a597-4c14-a529-8a9843b8cf06",
      "name": "Read_tickers_from_Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -800,
        1296
      ],
      "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": "eb1cd019-3eda-419b-9777-ab8b40a2588c",
      "name": "write_sentiment_to_sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        7744,
        1392
      ],
      "parameters": {
        "columns": {
          "value": {
            "date": "={{ $json.date }}",
            "ma_50": "={{ $json.ma_50 || '' }}",
            "model": "={{ $json.model }}",
            "stock": "={{ $json.stock }}",
            "ma_200": "={{ $json.ma_200 || '' }}",
            "rsi_14": "={{ $json.rsi_14 || '' }}",
            "f_score": "={{ $json.f_score || '' }}",
            "gap_pct": "={{ $json.gap_pct }}",
            "pt_base": "={{ $json.pt_base }}",
            "pt_bear": "={{ $json.pt_bear }}",
            "pt_bull": "={{ $json.pt_bull }}",
            "ma_signal": "={{ $json.ma_signal || '' }}",
            "rationale": "={{ $json.rationale }}",
            "confidence": "={{ $json.confidence }}",
            "conviction": "={{ $json.conviction }}",
            "rsi_signal": "={{ $json.rsi_signal || '' }}",
            "trend_tier": "={{ $json.trend_tier || '' }}",
            "current_price": "={{ $json.current_price || '' }}",
            "verdict_gemini": "={{ $json.verdict_gemini || '' }}",
            "thesis_reversal": "={{ $json.thesis_reversal || false }}",
            "verdict_chatgpt": "={{ $json.verdict_chatgpt || '' }}",
            "previous_verdict": "={{ $json.previous_verdict || '' }}",
            "resolution_method": "={{ $json.resolution_method }}",
            "next_earnings_date": "={{ $json.next_earnings_date || '' }}"
          },
          "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": "current_price",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "current_price",
              "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": "f_score",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "f_score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "confidence",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "confidence",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "gap_pct",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "gap_pct",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "resolution_method",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "resolution_method",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rationale",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rationale",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "conviction",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "conviction",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "model",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "model",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "verdict_chatgpt",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "verdict_chatgpt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "verdict_gemini",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "verdict_gemini",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ma_50",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ma_50",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ma_signal",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ma_signal",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rsi_14",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "rsi_14",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rsi_signal",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "rsi_signal",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "trend_tier",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "trend_tier",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ma_200",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ma_200",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "next_earnings_date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "next_earnings_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "previous_verdict",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "previous_verdict",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "thesis_reversal",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "thesis_reversal",
              "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": "ac80fd3d-5d01-41b8-8897-7a32cbbeaf24",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1024,
        1296
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "daysInterval": 3,
              "triggerAtHour": 16
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8d041788-4107-48be-9c1f-1107b4e549e4",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -656,
        192
      ],
      "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": "35eff45b-2fe7-477e-9bb5-592a12db4d5d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1056,
        704
      ],
      "parameters": {
        "color": 4,
        "width": 1152,
        "height": 992,
        "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": "4ebc103a-c5b8-4825-b2de-2629d007c762",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        128,
        -48
      ],
      "parameters": {
        "color": 5,
        "width": 2760,
        "height": 1420,
        "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": "5c6bf418-f4d5-49c8-911a-6c93d3e0f636",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2928,
        576
      ],
      "parameters": {
        "color": 6,
        "width": 2004,
        "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": "ff55f562-a968-41c4-be97-f8493a9b8612",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4944,
        576
      ],
      "parameters": {
        "color": 2,
        "width": 544,
        "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": "a0e69d33-1308-40ed-92be-ba9100f08133",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7600,
        576
      ],
      "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": "5de75ec0-9b5d-499a-9599-1ea79e3821be",
      "name": "Code in JavaScript",
      "type": "n8n-nodes-base.code",
      "position": [
        4064,
        1344
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 TIEBREAKER ENRICHMENT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst needs_tiebreaker = $input.first().json?.needs_tiebreaker || false;\nconst gap_pct          = $input.first().json?.gap_pct || 0;\nconst verdict_chatgpt  = $input.first().json?.verdict_chatgpt || null;\nconst verdict_gemini   = $input.first().json?.verdict_gemini  || null;\n\nfunction 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 $input.all()) {\n  const j = it.json || {};\n\n  const candidates = [];\n\n  if (j.chatgpt_result && typeof j.chatgpt_result === \"object\") {\n    candidates.push({ ...j.chatgpt_result, model: j.chatgpt_result.model || \"CHATGPT\" });\n  }\n  if (j.gemini_result && typeof j.gemini_result === \"object\") {\n    candidates.push({ ...j.gemini_result, model: j.gemini_result.model || \"GEMINI\" });\n  }\n\n  // Fallback: 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\n// \u2500\u2500 Conviction label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction getConviction(gap) {\n  if (gap < 10) return \"HIGH\";\n  if (gap < 20) return \"MEDIUM\";\n  return \"LOW\";\n}\n\n// \u2500\u2500 Build output rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst out = [];\n\nfor (const [stock, arr] of byStock.entries()) {\n\n  // 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  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        row_key:    `${stock}_${date}_${r.model}`,\n        skip_row:   false,\n        // \u2500\u2500 Hybrid enrichment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        resolution_method: needs_tiebreaker ? \"TIEBREAKER\" : \"CONSENSUS\",\n        conviction:        getConviction(gap_pct),\n        gap_pct,\n        verdict_chatgpt,\n        verdict_gemini\n      }\n    });\n  }\n}\n\n// \u2500\u2500 Fallback if nothing valid came through \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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      resolution_method: \"ERROR\",\n      conviction:        \"NONE\",\n      gap_pct:           0,\n      verdict_chatgpt:   null,\n      verdict_gemini:    null\n    }\n  }];\n}\n\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "9ab1cf9e-5ea9-4d72-b59a-0cd37c20962f",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        3440,
        1120
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "00b14e09-f606-48a3-a026-ca8953c96fbe",
      "name": "XML",
      "type": "n8n-nodes-base.xml",
      "position": [
        1360,
        1632
      ],
      "parameters": {
        "options": {},
        "dataPropertyName": "sa_xml"
      },
      "typeVersion": 1
    },
    {
      "id": "bd8b7af5-003c-40cc-b895-77d78ccf476f",
      "name": "Merge1",
      "type": "n8n-nodes-base.merge",
      "position": [
        2720,
        1152
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "f4fcca1b-bc7a-4d58-8bfc-1da7e3648e5e",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        5008,
        1328
      ],
      "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": "9760091c-547a-4501-bd4f-1d43a1a91250",
      "name": "If1",
      "type": "n8n-nodes-base.if",
      "position": [
        912,
        1776
      ],
      "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": "7ae74b4b-33c2-4cf6-999e-c3c49f96f5a6",
      "name": "Edit Fields2",
      "type": "n8n-nodes-base.set",
      "position": [
        1552,
        1632
      ],
      "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": "f7a5b448-14cc-4204-8541-24ded9f9a1b9",
      "name": "Merge2",
      "type": "n8n-nodes-base.merge",
      "position": [
        1552,
        432
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "70216662-4f36-432f-a5c9-2e479495227f",
      "name": "Is Cache Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        384,
        768
      ],
      "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": "a1929b6f-2930-4d9f-a040-310c94e9a123",
      "name": "Cache Lookup",
      "type": "n8n-nodes-base.googleSheets",
      "maxTries": 3,
      "position": [
        -320,
        1184
      ],
      "parameters": {
        "options": {
          "returnFirstMatch": true
        },
        "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": "705966f9-7e33-45d1-9cf0-35ddad297aa2",
      "name": "alphavantage - Balance Sheet",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        416
      ],
      "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": "8682d4b9-a044-4565-a0c5-4c114c8303a9",
      "name": "alphavantage - Profile",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        576
      ],
      "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": "6f43cf2e-7258-4f4e-b907-101d0a07eda4",
      "name": "alphavantage - Income Statement",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        736
      ],
      "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": "819fcbdc-7ba3-498e-903b-3aac8257aed6",
      "name": "Merge3",
      "type": "n8n-nodes-base.merge",
      "position": [
        1392,
        704
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "3abc3ce3-087b-439c-8bed-b0645971d84e",
      "name": "Get row(s) in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1792,
        1216
      ],
      "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": "f35dc3c3-73ea-4e14-bd74-48f21425bf3a",
      "name": "If2",
      "type": "n8n-nodes-base.if",
      "position": [
        2080,
        448
      ],
      "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": "069ebe17-84c4-485e-b767-ee898685af07",
      "name": "Update row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2288,
        432
      ],
      "parameters": {
        "columns": {
          "value": {
            "stock": "={{ $json.stock }}",
            "sector": "={{ $('Clean Read Financial').item.json.sector }}",
            "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_cash_flag": "={{ $('Clean Read Financial').item.json.net_cash_flag }}",
            "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 }}",
            "net_debt_latest": "={{ $('Clean Read Financial').item.json.net_debt_latest }}",
            "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": "sector",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sector",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_debt_latest",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "net_debt_latest",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_cash_flag",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "net_cash_flag",
              "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": "308167a0-30f0-4216-894c-1e6abfeb4a22",
      "name": "Insert Row",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2304,
        896
      ],
      "parameters": {
        "columns": {
          "value": {
            "stock": "={{ $json.stock }}",
            "sector": "={{ $json.sector }}",
            "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_cash_flag": "={{ $json.net_cash_flag }}",
            "net_income_ttm": "={{ $json.net_income_ttm }}",
            "total_debt_ttm": "={{ $json.total_debt_ttm }}",
            "f_score_data_ok": "={{ $json.f_score_data_ok }}",
            "net_debt_latest": "={{ $json.net_debt_latest }}",
            "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
            },
            {
              "id": "sector",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sector",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_debt_latest",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "net_debt_latest",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_cash_flag",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "net_cash_flag",
              "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": "31efd328-56ed-42fb-995f-0af16f09f28e",
      "name": "alphavantage - CashFlow",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        912
      ],
      "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": "95fe5434-3334-41f3-8f71-0115b01774ac",
      "name": "Merge4",
      "type": "n8n-nodes-base.merge",
      "position": [
        1728,
        800
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "724d38b0-4b1a-4df7-900f-905217fa24b9",
      "name": "Check the cache",
      "type": "n8n-nodes-base.code",
      "position": [
        208,
        768
      ],
      "parameters": {
        "jsCode": "const now = Date.now();\n\nconst row = items?.[0]?.json ?? null;\n\n// Read the manual refresh flag from Google Sheet or input\nconst rawNeedFreshFinancial =\n  row?.need_fresh_financial ??\n  $json.need_fresh_financial ??\n  false;\n\n// Convert different formats (true/false, 1/0, yes/no) safely to boolean\nfunction toBoolean(value) {\n  if (typeof value === 'boolean') return value;\n  if (typeof value === 'number') return value === 1;\n  if (typeof value === 'string') {\n    const v = value.trim().toLowerCase();\n    return ['true','1','yes','y'].includes(v);\n  }\n  return false;\n}\n\nconst needFreshFinancial = toBoolean(rawNeedFreshFinancial);\n\n// Check if cache exists\nconst cacheHit = !!(row && row.stock);\n\nlet shouldFetch = false;\nlet reason = '';\n\nif (!cacheHit) {\n  shouldFetch = true;\n  reason = 'cache_miss';\n} \nelse if (needFreshFinancial) {\n  shouldFetch = true;\n  reason = 'manual_financial_refresh';\n} \nelse {\n  shouldFetch = false;\n  reason = 'use_cached_financials';\n}\n\nreturn [{\n  json: {\n    stock: row?.stock || $json.stock,\n    cacheHit,\n    needFreshFinancial,\n    shouldFetch,\n    reason,\n    cacheRow: row || null,\n    nowISO: new Date(now).toISOString(),\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7bc7610f-155f-4a3f-8087-ac00d61116c8",
      "name": "Seekingalpha Articles",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        496,
        1760
      ],
      "parameters": {
        "url": "=https://seekingalpha.com/api/sa/combined/{{ $json.stock }}.xml",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text",
              "outputPropertyName": "sa_xml"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "d2a2d325-97a4-40c4-8f73-a310cab253cd",
      "name": "Clean balance sheet",
      "type": "n8n-nodes-base.code",
      "position": [
        1184,
        416
      ],
      "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": "e30294e8-4a3c-4a63-a468-54b623fea606",
      "name": "Clean Profile",
      "type": "n8n-nodes-base.code",
      "position": [
        1184,
        576
      ],
      "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": "52731b5a-c076-497a-9e36-2a2f5aed8595",
      "name": "Clean Income statement",
      "type": "n8n-nodes-base.code",
      "position": [
        1184,
        736
      ],
      "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": "0d1e825c-fa1f-49fc-b119-e809b8dec76e",
      "name": "Clean Ccashflow",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        896
      ],
      "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": "14e3bdd5-d3e8-43cb-afee-044b6ae9a43b",
      "name": "Clean Read Financial",
      "type": "n8n-nodes-base.code",
      "position": [
        1888,
        800
      ],
      "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//sector\nconst sector= overview.sector;\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// Issue 3 fix: Revenue growth YoY = TTM vs prior TTM (sum q0-q3 vs sum q4-q7)\nlet revenue_growth_yoy = null;\nconst rev_8q = quarterlyReports.slice(0, 8).map(q => num(q.totalRevenue));\nconst current_ttm_rev = rev_8q.slice(0, 4).every(v => v !== null) ? rev_8q.slice(0, 4).reduce((a, b) => a + b, 0) : null;\nconst prior_ttm_rev = rev_8q.slice(4, 8).every(v => v !== null) ? rev_8q.slice(4, 8).reduce((a, b) => a + b, 0) : null;\nif (current_ttm_rev !== null && prior_ttm_rev !== null && prior_ttm_rev !== 0) {\n  revenue_growth_yoy = Number((((current_ttm_rev - prior_ttm_rev) / prior_ttm_rev) * 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}\n// Bug 6 fix: only compute TTM if all 4 quarters have valid data\nconst net_income_ttm = net_income_last_4q.length === 4 && net_income_last_4q.every(v => v !== null)\n  ? net_income_last_4q.reduce((a, b) => a + b, 0)\n  : null;\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);\n\n// \u2500\u2500\u2500 IDEA 1: Graham Number \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst eps_val  = num(overview.EPS);\nconst bvps_val = num(overview.BookValue);\nlet graham_number = null;\nif (eps_val !== null && bvps_val !== null && eps_val > 0 && bvps_val > 0) {\n  graham_number = Number(Math.sqrt(22.5 * eps_val * bvps_val).toFixed(2));\n}\n\n// \u2500\u2500\u2500 IDEA 4: DCF Anchor (for growth/transition stocks) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst disc_rate_map = {\n  'Technology': 0.10, 'Financials': 0.09, 'Utilities': 0.08,\n  'Consumer Staples': 0.09, 'Consumer Discretionary': 0.11,\n  'Health Care': 0.10, 'Industrials': 0.10, 'Materials': 0.10,\n  'Energy': 0.11, 'Communication Services': 0.10, 'Real Estate': 0.09\n};\nconst disc_rate = disc_rate_map[sector] || 0.10;\nconst terminal_growth = 0.03;\nlet dcf_anchor = null;\nif (fcf_ttm !== null && fcf_ttm > 0 && disc_rate > terminal_growth) {\n  const fcf_growth = (current_ttm_rev !== null && prior_ttm_rev !== null && prior_ttm_rev > 0)\n    ? Math.min((current_ttm_rev - prior_ttm_rev) / prior_ttm_rev, 0.30)\n    : 0.08; // default 8% if growth unknown\n  const fcf5 = fcf_ttm * Math.pow(1 + fcf_growth, 5);\n  dcf_anchor = Math.round(fcf5 / (disc_rate - terminal_growth));\n}\n\n// \u2500\u2500\u2500 IDEA 5: Sector-relative P/E \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sectorPE = {\n  'Technology': 25, 'Financials': 12, 'Utilities': 15,\n  'Consumer Staples': 19, 'Consumer Discretionary': 22,\n  'Health Care': 20, 'Industrials': 18, 'Materials': 16,\n  'Energy': 14, 'Communication Services': 21, 'Real Estate': 17\n};\nconst sector_median_pe = sectorPE[sector] || 18;\n\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    sector:sector,\n    graham_number,\n    dcf_anchor,\n    sector_median_pe,\n    cache: {\n      status: \"refreshed\",\n      source: \"api\",\n      last_updated: $json.last_updated || $json.nowISO || null\n    }\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ab0c83f7-372f-486f-9d55-6dc9efb4aeeb",
      "name": "Clean Old Financial",
      "type": "n8n-nodes-base.code",
      "position": [
        2256,
        1216
      ],
      "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      sector: r.sector || null,\n      net_debt_latest: (r.net_debt_latest !== undefined && r.net_debt_latest !== '') ? Number(r.net_debt_latest) : null,\n      net_cash_flag: (r.net_cash_flag !== undefined && r.net_cash_flag !== '') ? (r.net_cash_flag === 'true' || r.net_cash_flag === true) : null,\n      graham_number: r.graham_number !== undefined && r.graham_number !== '' ? Number(r.graham_number) : null,\n      dcf_anchor: r.dcf_anchor !== undefined && r.dcf_anchor !== '' ? Number(r.dcf_anchor) : null,\n      sector_median_pe: r.sector_median_pe !== undefined && r.sector_median_pe !== '' ? Number(r.sector_median_pe) : 18,\n    cache: {\n      status: \"fresh\",\n      source: \"cache\",\n      last_updated: r.last_updated || null\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9fe24b32-26ac-43f8-9a90-4acd0668fb2a",
      "name": "Merge New Financial",
      "type": "n8n-nodes-base.code",
      "position": [
        2576,
        864
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "f9ba099d-aba3-4ff4-81e0-00da596157bc",
      "name": "alphavantage - Current Price",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        1056
      ],
      "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": "cca34beb-fd09-4e4d-b741-d59c739bf215",
      "name": "Clean Current Price",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        1040
      ],
      "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": "f828aa53-5314-4e78-bf0b-064e81d27d5b",
      "name": "Merge5",
      "type": "n8n-nodes-base.merge",
      "position": [
        1520,
        928
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "4a42def3-aad5-4e9b-b317-6aa5665c2360",
      "name": "Clean the news",
      "type": "n8n-nodes-base.code",
      "position": [
        688,
        1760
      ],
      "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": "140b736e-ee4f-4f28-a720-6081177547b6",
      "name": "return the news",
      "type": "n8n-nodes-base.code",
      "position": [
        1344,
        1856
      ],
      "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": "15b3be64-066e-4118-b9a6-a138109287f2",
      "name": "Final version of news",
      "type": "n8n-nodes-base.code",
      "position": [
        1792,
        1744
      ],
      "parameters": {
        "jsCode": "// ---- CONFIG ----\nconst windowHours = 96;                 // recent window (Issue 4: 4-day safety 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": "114089bf-0ee2-4b90-81d4-4f3312dde47b",
      "name": "Clean Up ChatGPT",
      "type": "n8n-nodes-base.code",
      "position": [
        3248,
        944
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let parsed = $json;\nconst t = $json?.output?.[0]?.content?.[0]?.text;\nif (t) {\n  if (typeof t === \"string\") {\n    try { parsed = JSON.parse(t); } catch(e) { parsed = $json; }\n  } else if (typeof t === \"object\") {\n    parsed = t;\n  }\n}\n\nconst stock = parsed?.stock || $json?.stock || \"UNKNOWN\";\nconst source = $('Merge1').first().json;\n// Carry all financial fields forward from the original input\nconst financials = {\n  current_price:        source?.current_price        ?? null,\n  eps_current:          source?.eps_current          ?? null,\n  bvps_current:         source?.bvps_current         ?? null,\n  revenue_last_4q:      source?.revenue_last_4q      ?? null,\n  net_income_last_4q:   source?.net_income_last_4q   ?? null,\n  gross_margin_last_4q: source?.gross_margin_last_4q ?? null,\n  total_debt_last_4q:   source?.total_debt_last_4q   ?? null,\n  net_debt_latest:      source?.net_debt_latest      ?? null,\n  net_cash_flag:        source?.net_cash_flag        ?? null,\n  operating_margin_ttm: source?.operating_margin_ttm ?? null,\n  fcf_ttm:              source?.fcf_ttm              ?? null,\n  revenue_growth_yoy:   source?.revenue_growth_yoy   ?? null,\n  f_score_data_ok:      source?.f_score_data_ok      ?? null,\n  sector:               source?.sector               ?? null,\n  seekingAlphaNewsText: source?.seekingAlphaNewsText ?? null,\n};\n\nreturn {\n  json: {\n    chatgpt_result: parsed,\n    stock,\n    ...financials\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "44ad2133-7030-4be8-8be5-abedd0bcaa53",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        128,
        1392
      ],
      "parameters": {
        "color": 5,
        "width": 2760,
        "height": 716,
        "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": "b12ed2e1-7cf3-44a1-8581-8f3df09db54e",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "position": [
        7936,
        1792
      ],
      "parameters": {
        "amount": 15
      },
      "typeVersion": 1.1
    },
    {
      "id": "515a0107-9bec-4b4f-a268-6235ccef31bb",
      "name": "Wait1",
      "type": "n8n-nodes-base.wait",
      "position": [
        784,
        416
      ],
      "parameters": {
        "amount": 15
      },
      "typeVersion": 1.1
    },
    {
      "id": "46a312ad-baa3-4690-90f3-87306944c36d",
      "name": "Wait2",
      "type": "n8n-nodes-base.wait",
      "position": [
        784,
        576
      ],
      "parameters": {
        "amount": 20
      },
      "typeVersion": 1.1
    },
    {
      "id": "0093afdc-5c76-4168-8619-66361a4f88f5",
      "name": "Wait3",
      "type": "n8n-nodes-base.wait",
      "position": [
        768,
        736
      ],
      "parameters": {
        "amount": 30
      },
      "typeVersion": 1.1
    },
    {
      "id": "e20a243d-c6c9-4ff4-a109-0e7c19432251",
      "name": "Wait4",
      "type": "n8n-nodes-base.wait",
      "position": [
        768,
        880
      ],
      "parameters": {
        "amount": 25
      },
      "typeVersion": 1.1
    },
    {
      "id": "627df88e-50b9-47b5-aa95-7066673faa2a",
      "name": "Wait5",
      "type": "n8n-nodes-base.wait",
      "position": [
        768,
        1056
      ],
      "parameters": {
        "amount": 20
      },
      "typeVersion": 1.1
    },
    {
      "id": "ccad2786-3a86-4016-b867-27b9f900d45f",
      "name": "Clean Current Price1",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        1232
      ],
      "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": "2facabce-7fb9-4134-b876-8b2b3c37b901",
      "name": "alphavantage - Current Price Second",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        1232
      ],
      "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": "c7338001-85df-4e14-be0d-68224de337e1",
      "name": "Update row in sheet1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1488,
        1216
      ],
      "parameters": {
        "columns": {
          "value": {
            "stock": "={{ $json.stock }}",
            "row_number": 0,
            "last_updated": "={{ new Date().toISOString().split(\"T\")[0] }}",
            "current_price": "={{ $json.current_price }}"
          },
          "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,
              "removed": true,
              "required": false,
              "displayName": "eps_current",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "bvps_current",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "bvps_current",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "shares_outstanding",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "shares_outstanding",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "revenue_ttm",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "revenue_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_debt_ttm",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "total_debt_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "gross_margin_ttm",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "gross_margin_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "operating_margin_ttm",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "operating_margin_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "fcf_ttm",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "fcf_ttm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "revenue_last_4q",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "revenue_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_income_last_4q",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "net_income_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "gross_margin_last_4q",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "gross_margin_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_debt_last_4q",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "total_debt_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "cash_last_4q",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "cash_last_4q",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "net_income_ttm",
              "type": "string",
              "display": true,
              "removed": true,
              "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,
              "removed": true,
              "required": false,
              "displayName": "revenue_growth_yoy",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "f_score",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "f_score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "f_score_data_ok",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "f_score_data_ok",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sector",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "sector",
              "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": "2ff82136-78e2-41d6-af28-0491362c752d",
      "name": "Send a text message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -240,
        1008
      ],
      "parameters": {
        "text": "={{ $json.summary_message }}",
        "chatId": "123456789",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a0145d78-0606-4872-8900-f6b092a3cafb",
      "name": "Merge6",
      "type": "n8n-nodes-base.merge",
      "position": [
        4448,
        976
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "82d16d36-aa00-4f72-9b86-067b921e47e5",
      "name": "Clean up Gemini",
      "type": "n8n-nodes-base.code",
      "position": [
        3248,
        1296
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let parsed = $json;\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) { parsed = $json; }\n} else if ($json.content?.parts?.[0]?.text) {\n  try {\n    parsed = JSON.parse($json.content.parts[0].text);\n  } catch(e) { parsed = $json; }\n}\nif (parsed && parsed.gemini_result) parsed = parsed.gemini_result;\n\nconst stock = parsed?.stock || $json?.stock || \"UNKNOWN\";\nconst source = $('Merge1').first().json;\n// Carry all financial fields forward from the original input\nconst financials = {\n  current_price:        source?.current_price        ?? null,\n  eps_current:          source?.eps_current          ?? null,\n  bvps_current:         source?.bvps_current         ?? null,\n  revenue_last_4q:      source?.revenue_last_4q      ?? null,\n  net_income_last_4q:   source?.net_income_last_4q   ?? null,\n  gross_margin_last_4q: source?.gross_margin_last_4q ?? null,\n  total_debt_last_4q:   source?.total_debt_last_4q   ?? null,\n  net_debt_latest:      source?.net_debt_latest      ?? null,\n  net_cash_flag:        source?.net_cash_flag        ?? null,\n  operating_margin_ttm: source?.operating_margin_ttm ?? null,\n  fcf_ttm:              source?.fcf_ttm              ?? null,\n  revenue_growth_yoy:   source?.revenue_growth_yoy   ?? null,\n  f_score_data_ok:      source?.f_score_data_ok      ?? null,\n  sector:               source?.sector               ?? null,\n  seekingAlphaNewsText: source?.seekingAlphaNewsText ?? null,\n};\n\nreturn {\n  json: {\n    gemini_result: parsed,\n    stock,\n    ...financials\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "168a3bf9-d458-46ac-9659-3c686e784f9e",
      "name": "Code in JavaScript3",
      "type": "n8n-nodes-base.code",
      "position": [
        4608,
        976
      ],
      "parameters": {
        "jsCode": "const items_in = $input.all();\nconst today = new Date().toISOString().slice(0, 10);\n\nlet bull = null;\nlet bear = null;\n\nfor (const it of items_in) {\n  const j = it.json || {};\n  if (j.chatgpt_result) {\n    const r = j.chatgpt_result;\n    if (r.model === \"CHATGPT_BULL\") bull = {\n      ...r,\n      gap_pct:         j.gap_pct,\n      verdict_chatgpt: j.verdict_chatgpt,\n      verdict_gemini:  j.verdict_gemini,\n      current_price:   j.current_price   // \u2190 FIX: carry current_price\n    };\n  }\n  if (j.gemini_result) {\n    const r = j.gemini_result;\n    if (r.model === \"GEMINI_BEAR\") bear = {\n      ...r,\n      gap_pct:         j.gap_pct,\n      verdict_chatgpt: j.verdict_chatgpt,\n      verdict_gemini:  j.verdict_gemini,\n      current_price:   j.current_price   // \u2190 FIX: carry current_price\n    };\n  }\n}\n\n// \u2500\u2500 Conviction label (matches consensus path) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction getConviction(gap) {\n  if (gap < 10) return \"HIGH\";\n  if (gap < 20) return \"MEDIUM\";\n  return \"LOW\";\n}\n\n// \u2500\u2500 Error fallback \u2014 now includes skip_row + enrichment fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (!bull || !bear) {\n  return [{\n    json: {\n      stock:             \"\",\n      date:              today,\n      pt_bear:           0,\n      pt_base:           0,\n      pt_bull:           0,\n      f_score:           null,\n      confidence:        0,\n      rationale:         \"Tiebreaker failed \u2014 missing bull or bear result\",\n      model:             \"ERROR\",\n      current_price:     0,\n      row_key:           `ERROR_${today}`,\n      skip_row:          true,             // \u2190 FIX: was missing\n      resolution_method: \"TIEBREAKER\",\n      conviction:        \"NONE\",\n      gap_pct:           0,\n      verdict_chatgpt:   null,\n      verdict_gemini:    null\n    }\n  }];\n}\n\nconst stock           = bull.stock  || bear.stock  || \"UNKNOWN\";\nconst true_base       = parseFloat(((Number(bull.pt_base) + Number(bear.pt_base)) / 2).toFixed(2));\nconst true_bull       = parseFloat(Number(bull.pt_bull).toFixed(2));\nconst true_bear       = parseFloat(Number(bear.pt_bear).toFixed(2));\nconst avg_conf        = Math.round((Number(bull.confidence) + Number(bear.confidence)) / 2);\nconst f_score         = bull.f_score ?? bear.f_score ?? null;\nconst gap_pct         = Number(bull.gap_pct         ?? bear.gap_pct         ?? 0);\nconst verdict_chatgpt = bull.verdict_chatgpt ?? bear.verdict_chatgpt ?? null;\nconst verdict_gemini  = bull.verdict_gemini  ?? bear.verdict_gemini  ?? null;\nconst current_price   = Number(bull.current_price || bear.current_price || 0); // \u2190 FIX\n\n// \u2500\u2500 Final verdict (now works because current_price is real) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst upside = current_price > 0 ? (true_base - current_price) / current_price : 0;\nlet final_verdict;\nif (upside >= 0.20)       final_verdict = \"BUY\";\nelse if (upside <= -0.15) final_verdict = \"SELL\";\nelse                      final_verdict = \"HOLD\";\n\nconst rationale = `${final_verdict} (tiebreaker) \u2014 `\n  + `Bull: ${bull.rationale} | Bear: ${bear.rationale}`;\n\nreturn [{\n  json: {\n    stock,\n    date:              today,\n    pt_bear:           true_bear,\n    pt_base:           true_base,\n    pt_bull:           true_bull,\n    f_score,\n    confidence:        Math.max(20, Math.min(90, avg_conf)),\n    rationale,\n    model:             \"TIEBREAKER\",\n    current_price,                               // \u2190 FIX: added\n    row_key:           `${stock}_${today}_TIEBREAKER`,\n    skip_row:          false,\n    resolution_method: \"TIEBREAKER\",\n    conviction:        getConviction(gap_pct),   // \u2190 FIX: added\n    gap_pct,                                     // \u2190 FIX: added\n    verdict_chatgpt,                             // \u2190 FIX: added\n    verdict_gemini                               // \u2190 FIX: added\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8c8bd5fb-b1df-4268-851d-2ad14ed48d9b",
      "name": "Merge7",
      "type": "n8n-nodes-base.merge",
      "position": [
        4768,
        1200
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "945ec9a1-e480-4bd7-8d5a-c06882413f99",
      "name": "Tide Breaker - Bull",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        3968,
        896
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "GPT-4O"
        },
        "options": {
          "textFormat": {
            "textOptions": {
              "type": "json_object"
            }
          }
        },
        "responses": {
          "values": [
            {
              "content": "=YOUR ROLE: You are a GROWTH-FOCUSED bull analyst.\nThis is a TIEBREAKER \u2014 two models disagreed on this stock.\nMake the strongest BUY case the data can justify.\n- Select the higher multiple when between tiers\n- Prefer 15% discount when fundamentals are arguable  \n- Classify ambiguous news as POSITIVE\n- Your pt_base = maximum justifiable OPPORTUNITY value\n- Never fabricate data. Stay within input bounds only.\n----\nYou 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\"sector\":  \"{{ $json.sector }}\",\n\"graham_number\": {{ $json.graham_number }},\n\"dcf_anchor\": {{ $json.dcf_anchor }},\n\"sector_median_pe\": {{ $json.sector_median_pe }}\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 96h 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\nMATURE OVERRIDE RULE:\nIf initial classification = growth_transition due to implied_pe > 60 only\n(i.e., eps_current IS > 0):\n  AND revenue_growth_yoy < 15%\n  AND operating_margin_ttm > 10%\n  \u2192 Override to mature_profitable\n  \u2192 Add note: \"Phase overridden to mature_profitable: \n    low growth + positive margins suggest temporary P/E spike, \n    not structural transition.\"\n\nC) Graham Number (secondary only)\n\nsqrt(22.5 \u00d7 eps_current \u00d7 bvps_current)\n\nUse ONLY if ALL of these are true:\neps_current > 0\nbvps_current > 0\nphase = mature_profitable\nbvps_current >= 5.00\nAND (current_price / bvps_current) <= 15\nAND sector \u2260 \"Financials\" \n  \nIf bvps_current < 5.00 OR (current_price / bvps_current) > 15:\n  Graham = null \u2192 skip Graham entirely \u2192 pt_base = pt_eps only\n  Add to rationale: \"Graham skipped: BVPS distorted by buybacks.\"\n\nDo NOT use Graham for growth_transition stocks.\n\nD) BASE TARGET CONSTRUCTION (pt_base)\n\nYOUR ROLE IS BULL \u2014 select the HIGHEST justifiable value at every step.\n\nPrimary valuation logic depends on phase.\n\nIF phase = mature_profitable:\nIf EPS positive:\npt_base = eps_current \u00d7 multiple.\n\nSelect multiple from these RANGES \u2014 as bull, pick the TOP of each range:\n  revenue_growth_yoy > 20%  \u2192 use between 25\u00d7 and 30\u00d7 (bull: pick 30\u00d7)\n  revenue_growth_yoy 5\u201320%  \u2192 use between 18\u00d7 and 25\u00d7 (bull: pick 25\u00d7)\n  revenue_growth_yoy < 5%   \u2192 use between 12\u00d7 and 18\u00d7 (bull: pick 18\u00d7)\n\nSECTOR MULTIPLE FLOORS (apply AFTER selecting multiple):\nIf sector = \"Consumer Staples\" AND selected multiple < 18: raise to 18\u00d7\nIf sector = \"Technology\" AND selected multiple < 18: raise to 20\u00d7\nIf sector = \"Utilities\" AND selected multiple > 14: cap at 14\u00d7\nIf sector = \"Financials\": skip EPS multiple, use pt_base = bvps_current \u00d7 1.5\nIf sector = null or missing: no adjustment.\n\nMARGIN OVERRIDE (apply after sector floors):\nIf operating_margin_ttm > 25%\n  AND selected multiple < 20\n  AND sector \u2260 \"Utilities\":\n  override multiple to 20\u00d7\n  Add note: \"Bull margin floor applied.\"\n\nIf Graham usable:\npt_base = average(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor \u2014 as bull, pick the HIGHER multiplier:\n\nLet g = revenue_growth_yoy\n\nIf g is null  \u2192 pt_base = current_price \u00d7 1.1\nIf g > 60     \u2192 pt_base = current_price \u00d7 2.5\nIf g > 30     \u2192 pt_base = current_price \u00d7 2.0\nIf g > 15     \u2192 pt_base = current_price \u00d7 1.6\nOtherwise     \u2192 pt_base = current_price \u00d7 1.2\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\nNEWS SENTIMENT ADJUSTMENT (apply after selecting base discount/premium):\n\nAnalyze seekingAlphaNewsText and news context. Classify overall sentiment as:\n  POSITIVE: earnings beat, guidance raised, new contracts, analyst upgrades,\n            strong demand signals, competitive wins\n  NEGATIVE: earnings miss, guidance cut, regulatory action, legal risk,\n            competitive threat, leadership change, demand weakness\n  NEUTRAL:  mixed or no material news\n\nThen adjust:\n\nIf sentiment = POSITIVE:\n  reduce discount by 5% (floor at 10%)\n  increase premium by 5%\n  Add to flags: \"News sentiment: POSITIVE \u2014 bands adjusted favorably.\"\n\nIf sentiment = NEGATIVE:\n  increase discount by 5% (cap at 40%)\n  reduce premium by 5% (floor at 10%)\n  Add to flags: \"News sentiment: NEGATIVE \u2014 bands adjusted conservatively.\"\n\nIf sentiment = NEUTRAL or news missing:\n  no adjustment\n  Add to flags: \"News sentiment: NEUTRAL \u2014 no band adjustment.\"\n\nIf seekingAlphaNewsText is null or empty:\n  no adjustment\n  Add to flags: \"No news data \u2014 sentiment adjustment skipped.\"\n\nCompute:\npt_bear = pt_base \u00d7 (1 \u2212 discount)\npt_bull = pt_base \u00d7 (1 + premium)\n\nRound both to 2 decimals.\nPOST-COMPUTATION CHECK:\nIf pt_bull < current_price:\n  Set overvaluation_flag = true\n  Append to rationale: \"WARNING: All price targets below current market \n  price. Model indicates significant overvaluation vs. formula inputs. \n  Manual review recommended before acting on SELL signal.\"\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-10 if eps_current < 0 (loss-making company, path to profitability unproven).\n+10 if news sentiment = POSITIVE (strong thesis confirmation from Seeking Alpha)\n-15 if news sentiment = NEGATIVE (material risk identified in news/articles)\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.chatgpt_result.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": "c8d1a050-dc12-4ef3-89c9-cd5ba907ae44",
      "name": "Tide Breaker - Bear",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        3968,
        1088
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-pro",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=OUR ROLE: You are a RISK-FOCUSED bear analyst.\nThis is a TIEBREAKER \u2014 two models disagreed on this stock.\nMake the strongest RISK case the data can justify.\n- Select the lower multiple when between tiers\n- Prefer 25% or 35% discount when there is any uncertainty\n- Classify ambiguous news as NEGATIVE\n- Your pt_base = minimum justifiable RISK-ADJUSTED value\n- Never fabricate data. Stay within input bounds only.\n----\nYou 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\"sector\":  \"{{ $json.sector }}\",\n\"graham_number\": {{ $json.graham_number }},\n\"dcf_anchor\": {{ $json.dcf_anchor }},\n\"sector_median_pe\": {{ $json.sector_median_pe }}\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 96h 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\nMATURE OVERRIDE RULE:\nIf initial classification = growth_transition due to implied_pe > 60 only\n(i.e., eps_current IS > 0):\n  AND revenue_growth_yoy < 15%\n  AND operating_margin_ttm > 10%\n  \u2192 Override to mature_profitable\n  \u2192 Add note: \"Phase overridden to mature_profitable: \n    low growth + positive margins suggest temporary P/E spike, \n    not structural transition.\"\n\nC) Graham Number (secondary only)\n\nsqrt(22.5 \u00d7 eps_current \u00d7 bvps_current)\n\nUse ONLY if ALL of these are true:\neps_current > 0\nbvps_current > 0\nphase = mature_profitable\nbvps_current >= 5.00\nAND (current_price / bvps_current) <= 15\nAND sector \u2260 \"Financials\" \n  \nIf bvps_current < 5.00 OR (current_price / bvps_current) > 15:\n  Graham = null \u2192 skip Graham entirely \u2192 pt_base = pt_eps only\n  Add to rationale: \"Graham skipped: BVPS distorted by buybacks.\"\n\nDo NOT use Graham for growth_transition stocks.\n\nD) BASE TARGET CONSTRUCTION (pt_base)\n\nYOUR ROLE IS BEAR \u2014 select the LOWEST justifiable value at every step.\n\nPrimary valuation logic depends on phase.\n\nIF phase = mature_profitable:\nIf EPS positive:\npt_base = eps_current \u00d7 multiple.\n\nSelect multiple from these RANGES \u2014 as bear, pick the BOTTOM of each range:\n  revenue_growth_yoy > 20%  \u2192 use between 18\u00d7 and 25\u00d7 (bear: pick 18\u00d7)\n  revenue_growth_yoy 5\u201320%  \u2192 use between 12\u00d7 and 18\u00d7 (bear: pick 12\u00d7)\n  revenue_growth_yoy < 5%   \u2192 use between 8\u00d7  and 12\u00d7 (bear: pick 8\u00d7)\n\nSECTOR MULTIPLE FLOORS (apply AFTER selecting multiple):\nIf sector = \"Consumer Staples\" AND selected multiple < 14: raise to 14\u00d7\nIf sector = \"Technology\" AND selected multiple < 14: raise to 14\u00d7\nIf sector = \"Utilities\" AND selected multiple > 12: cap at 12\u00d7\nIf sector = \"Financials\": skip EPS multiple, use pt_base = bvps_current \u00d7 0.9\nIf sector = null or missing: no adjustment.\n\nMARGIN OVERRIDE (apply after sector floors):\nIf operating_margin_ttm > 25%\n  AND selected multiple < 14\n  AND sector \u2260 \"Utilities\":\n  override multiple to 14\u00d7\n  Add note: \"Bear margin floor applied.\"\n\nIf Graham usable:\npt_base = average(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor \u2014 as bear, pick the LOWER multiplier:\n\nLet g = revenue_growth_yoy\n\nIf g is null  \u2192 pt_base = current_price \u00d7 0.9\nIf g > 60     \u2192 pt_base = current_price \u00d7 1.5\nIf g > 30     \u2192 pt_base = current_price \u00d7 1.2\nIf g > 15     \u2192 pt_base = current_price \u00d7 1.0\nOtherwise     \u2192 pt_base = current_price \u00d7 0.8\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\nNEWS SENTIMENT ADJUSTMENT (apply after selecting base discount/premium):\n\nAnalyze seekingAlphaNewsText and news context. Classify overall sentiment as:\n  POSITIVE: earnings beat, guidance raised, new contracts, analyst upgrades,\n            strong demand signals, competitive wins\n  NEGATIVE: earnings miss, guidance cut, regulatory action, legal risk,\n            competitive threat, leadership change, demand weakness\n  NEUTRAL:  mixed or no material news\n\nThen adjust:\n\nIf sentiment = POSITIVE:\n  reduce discount by 5% (floor at 10%)\n  increase premium by 5%\n  Add to flags: \"News sentiment: POSITIVE \u2014 bands adjusted favorably.\"\n\nIf sentiment = NEGATIVE:\n  increase discount by 5% (cap at 40%)\n  reduce premium by 5% (floor at 10%)\n  Add to flags: \"News sentiment: NEGATIVE \u2014 bands adjusted conservatively.\"\n\nIf sentiment = NEUTRAL or news missing:\n  no adjustment\n  Add to flags: \"News sentiment: NEUTRAL \u2014 no band adjustment.\"\n\nIf seekingAlphaNewsText is null or empty:\n  no adjustment\n  Add to flags: \"No news data \u2014 sentiment adjustment skipped.\"\n\nCompute:\npt_bear = pt_base \u00d7 (1 \u2212 discount)\npt_bull = pt_base \u00d7 (1 + premium)\n\nRound both to 2 decimals.\nPOST-COMPUTATION CHECK:\nIf pt_bull < current_price:\n  Set overvaluation_flag = true\n  Append to rationale: \"WARNING: All price targets below current market \n  price. Model indicates significant overvaluation vs. formula inputs. \n  Manual review recommended before acting on SELL signal.\"\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-10 if eps_current < 0 (loss-making company, path to profitability unproven).\n+10 if news sentiment = POSITIVE (strong thesis confirmation from Seeking Alpha)\n-15 if news sentiment = NEGATIVE (material risk identified in news/articles)\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.chatgpt_result.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": "d0cd397d-0cc1-4e11-9ece-367d4379bdc7",
      "name": "Needs Tiebreaker?",
      "type": "n8n-nodes-base.if",
      "position": [
        3760,
        1120
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "ed7c777e-46db-44d0-b09d-468b58fb421f",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.needs_tiebreaker }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "694834f4-2c5b-4886-8ee5-f5ac0a668f89",
      "name": "Clean Up Results from First Round",
      "type": "n8n-nodes-base.code",
      "position": [
        3600,
        1120
      ],
      "parameters": {
        "jsCode": "const items_in = $input.all();\nlet chatgpt = null;\nlet gemini  = null;\n\nfor (const it of items_in) {\n  const j = it.json || {};\n  if (j.chatgpt_result) chatgpt = j.chatgpt_result;\n  if (j.gemini_result)  gemini  = j.gemini_result;\n}\n\nif (!chatgpt || !gemini) {\n  for (const it of items_in) {\n    const j = it.json || {};\n    if (!chatgpt && j.model === \"CHATGPT\") chatgpt = j;\n    if (!gemini  && j.model === \"GEMINI\")  gemini  = j;\n  }\n}\n\n// \u2500\u2500 Pull financials from original source \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst source = $('Merge').first().json;\n\nconst stock         = chatgpt?.stock || gemini?.stock || source?.stock || \"UNKNOWN\";\nconst current_price = Number(source?.current_price || 0);\n\nfunction getVerdict(result) {\n  const r = (result?.rationale || \"\").trim().toUpperCase();\n  if (r.startsWith(\"BUY\"))  return \"BUY\";\n  if (r.startsWith(\"SELL\")) return \"SELL\";\n  return \"HOLD\";\n}\n\nconst v1 = getVerdict(chatgpt);\nconst v2 = getVerdict(gemini);\n\nconst pt1 = Number(chatgpt?.pt_base || 0);\nconst pt2 = Number(gemini?.pt_base  || 0);\n\nconst gap_pct = current_price > 0\n  ? parseFloat((Math.abs(pt1 - pt2) / current_price * 100).toFixed(2))\n  : 0;\n\nconst verdicts_agree   = (v1 === v2);\nconst high_uncertainty = gap_pct > 25;\nconst needs_tiebreaker = !verdicts_agree || high_uncertainty;\n\nreturn [{\n  json: {\n    ...source,             // \u2190 all original financial fields at top level\n    stock,\n    current_price,\n    chatgpt_result:  chatgpt,\n    gemini_result:   gemini,\n    verdict_chatgpt: v1,\n    verdict_gemini:  v2,\n    gap_pct,\n    verdicts_agree,\n    high_uncertainty,\n    needs_tiebreaker\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "fcc757ab-ba75-4c36-b16d-32e7375b8b3a",
      "name": "Clean up Chatgpt 2",
      "type": "n8n-nodes-base.code",
      "position": [
        4240,
        896
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let parsed = $json;\nconst t = $json?.output?.[0]?.content?.[0]?.text;\nif (t) {\n  if (typeof t === \"string\") {\n    try { parsed = JSON.parse(t); } catch(e) { parsed = $json; }\n  } else if (typeof t === \"object\") {\n    parsed = t;\n  }\n}\n\nconst stock = parsed?.stock || $json?.stock || \"UNKNOWN\";\n\nconst meta = {\n  current_price:    $json?.current_price    ?? null,\n  needs_tiebreaker: $json?.needs_tiebreaker ?? true,\n  verdict_chatgpt:  $json?.verdict_chatgpt  ?? $('Clean Up Results from First Round').first().json?.verdict_chatgpt ?? null,\n  verdict_gemini:   $json?.verdict_gemini   ?? $('Clean Up Results from First Round').first().json?.verdict_gemini  ?? null,\n  gap_pct:          $json?.gap_pct          ?? $('Clean Up Results from First Round').first().json?.gap_pct         ?? 0,\n};\n\nreturn {\n  json: {\n    chatgpt_result: { ...parsed, model: \"CHATGPT_BULL\" },\n    stock,\n    ...meta\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b2e8fb36-c569-4444-8a54-7a2032b469d3",
      "name": "Clean up Gemini 2",
      "type": "n8n-nodes-base.code",
      "position": [
        4240,
        1088
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let parsed = $json;\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) { parsed = $json; }\n} else if ($json.content?.parts?.[0]?.text) {\n  try {\n    parsed = JSON.parse($json.content.parts[0].text);\n  } catch(e) { parsed = $json; }\n}\nif (parsed && parsed.gemini_result) parsed = parsed.gemini_result;\n\nconst stock = parsed?.stock || $json?.stock || \"UNKNOWN\";\n\nconst meta = {\n  current_price:    $json?.current_price    ?? null,\n  needs_tiebreaker: $json?.needs_tiebreaker ?? true,\n  verdict_chatgpt:  $json?.verdict_chatgpt  ?? $('Clean Up Results from First Round').first().json?.verdict_chatgpt ?? null,\n  verdict_gemini:   $json?.verdict_gemini   ?? $('Clean Up Results from First Round').first().json?.verdict_gemini  ?? null,\n  gap_pct:          $json?.gap_pct          ?? $('Clean Up Results from First Round').first().json?.gap_pct         ?? 0,\n};\n\nreturn {\n  json: {\n    gemini_result: { ...parsed, model: \"GEMINI_BEAR\" },\n    stock,\n    ...meta\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "f0402aa7-d6ed-44b8-9bde-0f8013211f01",
      "name": "First round Gemini",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        2960,
        1296
      ],
      "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\"sector\":  \"{{ $json.sector }}\",\n\"graham_number\": {{ $json.graham_number }},\n\"dcf_anchor\": {{ $json.dcf_anchor }},\n\"sector_median_pe\": {{ $json.sector_median_pe }}\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 96h 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\nMATURE OVERRIDE RULE:\nIf initial classification = growth_transition due to implied_pe > 60 only\n(i.e., eps_current IS > 0):\n  AND revenue_growth_yoy < 15%\n  AND operating_margin_ttm > 10%\n  \u2192 Override to mature_profitable\n  \u2192 Add note: \"Phase overridden to mature_profitable: \n    low growth + positive margins suggest temporary P/E spike, \n    not structural transition.\"\n\nC) Graham Number (secondary only)\n\nsqrt(22.5 \u00d7 eps_current \u00d7 bvps_current)\n\nUse ONLY if ALL of these are true:\neps_current > 0\nbvps_current > 0\nphase = mature_profitable\nbvps_current >= 5.00\nAND (current_price / bvps_current) <= 15\nAND sector \u2260 \"Financials\" \n  \nIf bvps_current < 5.00 OR (current_price / bvps_current) > 15:\n  Graham = null \u2192 skip Graham entirely \u2192 pt_base = pt_eps only\n  Add to rationale: \"Graham skipped: BVPS distorted by buybacks.\"\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:\nIf EPS positive:\npt_base = eps_current \u00d7 multiple:\n\n25\u00d7 if revenue_growth_yoy > 20%\n18\u00d7 if revenue_growth_yoy 5\u201320%\n12\u00d7 otherwise\n\nSECTOR MULTIPLE FLOORS (apply AFTER selecting the base multiple above):\n\nIf sector = \"Consumer Staples\" AND selected multiple < 18: raise to 18\u00d7\nIf sector = \"Technology\" AND selected multiple < 18: raise to 20\u00d7\nIf sector = \"Utilities\" AND selected multiple > 14: cap at 14\u00d7\nIf sector = \"Financials\": skip EPS multiple entirely,\n  use pt_base = bvps_current \u00d7 1.2 as anchor\nIf sector = null or missing: no adjustment.\n\nMARGIN OVERRIDE (apply after selecting base multiple):\nIf operating_margin_ttm > 25% AND selected multiple < 18:\n  override multiple to 18\u00d7\n  AND sector \u2260 \"Utilities\":\n  Add note: \"Multiple floor applied: high-margin business.\"\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\nNEWS SENTIMENT ADJUSTMENT (apply after selecting base discount/premium):\n\nAnalyze seekingAlphaNewsText and news context. Classify overall sentiment as:\n  POSITIVE: earnings beat, guidance raised, new contracts, analyst upgrades,\n            strong demand signals, competitive wins\n  NEGATIVE: earnings miss, guidance cut, regulatory action, legal risk,\n            competitive threat, leadership change, demand weakness\n  NEUTRAL:  mixed or no material news\n\nThen adjust:\n\nIf sentiment = POSITIVE:\n  reduce discount by 5% (floor at 10%)\n  increase premium by 5%\n  Add to flags: \"News sentiment: POSITIVE \u2014 bands adjusted favorably.\"\n\nIf sentiment = NEGATIVE:\n  increase discount by 5% (cap at 40%)\n  reduce premium by 5% (floor at 10%)\n  Add to flags: \"News sentiment: NEGATIVE \u2014 bands adjusted conservatively.\"\n\nIf sentiment = NEUTRAL or news missing:\n  no adjustment\n  Add to flags: \"News sentiment: NEUTRAL \u2014 no band adjustment.\"\n\nIf seekingAlphaNewsText is null or empty:\n  no adjustment\n  Add to flags: \"No news data \u2014 sentiment adjustment skipped.\"\n\nCompute:\npt_bear = pt_base \u00d7 (1 \u2212 discount)\npt_bull = pt_base \u00d7 (1 + premium)\n\nRound both to 2 decimals.\nPOST-COMPUTATION CHECK:\nIf pt_bull < current_price:\n  Set overvaluation_flag = true\n  Append to rationale: \"WARNING: All price targets below current market \n  price. Model indicates significant overvaluation vs. formula inputs. \n  Manual review recommended before acting on SELL signal.\"\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-10 if eps_current < 0 (loss-making company, path to profitability unproven).\n+10 if news sentiment = POSITIVE (strong thesis confirmation from Seeking Alpha)\n-15 if news sentiment = NEGATIVE (material risk identified in news/articles)\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 structured summary with the following parts separated by | :\n  1. VERDICT: BUY/HOLD/SELL and one-line reason\n  2. MOAT: moat type and strength (strong/moderate/weak)\n  3. VALUATION: pt_base vs current_price, upside/downside %\n  4. PRIMARY RISK: single biggest threat to the thesis\n  5. PRICING POWER: high/moderate/low with one supporting data point\n  6. NEWS SIGNAL: POSITIVE/NEGATIVE/NEUTRAL and what drove it\"\n}"
            }
          ]
        },
        "jsonOutput": true,
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "69dc0fe8-d09f-446f-8841-894260e3728d",
      "name": "First round ChatGPT",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2976,
        944
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "GPT-4O"
        },
        "options": {
          "textFormat": {
            "textOptions": {
              "type": "json_object"
            }
          }
        },
        "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\"sector\":  \"{{ $json.sector }}\",\n\"graham_number\": {{ $json.graham_number }},\n\"dcf_anchor\": {{ $json.dcf_anchor }},\n\"sector_median_pe\": {{ $json.sector_median_pe }}\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 96h 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\nMATURE OVERRIDE RULE:\nIf initial classification = growth_transition due to implied_pe > 60 only\n(i.e., eps_current IS > 0):\n  AND revenue_growth_yoy < 15%\n  AND operating_margin_ttm > 10%\n  \u2192 Override to mature_profitable\n  \u2192 Add note: \"Phase overridden to mature_profitable: \n    low growth + positive margins suggest temporary P/E spike, \n    not structural transition.\"\n\nC) Graham Number (secondary only)\n\nsqrt(22.5 \u00d7 eps_current \u00d7 bvps_current)\n\nUse ONLY if ALL of these are true:\neps_current > 0\nbvps_current > 0\nphase = mature_profitable\nbvps_current >= 5.00\nAND (current_price / bvps_current) <= 15\nAND sector \u2260 \"Financials\" \n  \nIf bvps_current < 5.00 OR (current_price / bvps_current) > 15:\n  Graham = null \u2192 skip Graham entirely \u2192 pt_base = pt_eps only\n  Add to rationale: \"Graham skipped: BVPS distorted by buybacks.\"\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:\nIf EPS positive:\npt_base = eps_current \u00d7 multiple:\n\n25\u00d7 if revenue_growth_yoy > 20%\n18\u00d7 if revenue_growth_yoy 5\u201320%\n12\u00d7 otherwise\n\nSECTOR MULTIPLE FLOORS (apply AFTER selecting the base multiple above):\n\nIf sector = \"Consumer Staples\" AND selected multiple < 18: raise to 18\u00d7\nIf sector = \"Technology\" AND selected multiple < 18: raise to 20\u00d7\nIf sector = \"Utilities\" AND selected multiple > 14: cap at 14\u00d7\nIf sector = \"Financials\": skip EPS multiple entirely,\n  use pt_base = bvps_current \u00d7 1.2 as anchor\nIf sector = null or missing: no adjustment.\n\nMARGIN OVERRIDE (apply after selecting base multiple):\nIf operating_margin_ttm > 25% AND selected multiple < 18:\n  override multiple to 18\u00d7\n  AND sector \u2260 \"Utilities\":\n  Add note: \"Multiple floor applied: high-margin business.\"\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\nNEWS SENTIMENT ADJUSTMENT (apply after selecting base discount/premium):\n\nAnalyze seekingAlphaNewsText and news context. Classify overall sentiment as:\n  POSITIVE: earnings beat, guidance raised, new contracts, analyst upgrades,\n            strong demand signals, competitive wins\n  NEGATIVE: earnings miss, guidance cut, regulatory action, legal risk,\n            competitive threat, leadership change, demand weakness\n  NEUTRAL:  mixed or no material news\n\nThen adjust:\n\nIf sentiment = POSITIVE:\n  reduce discount by 5% (floor at 10%)\n  increase premium by 5%\n  Add to flags: \"News sentiment: POSITIVE \u2014 bands adjusted favorably.\"\n\nIf sentiment = NEGATIVE:\n  increase discount by 5% (cap at 40%)\n  reduce premium by 5% (floor at 10%)\n  Add to flags: \"News sentiment: NEGATIVE \u2014 bands adjusted conservatively.\"\n\nIf sentiment = NEUTRAL or news missing:\n  no adjustment\n  Add to flags: \"News sentiment: NEUTRAL \u2014 no band adjustment.\"\n\nIf seekingAlphaNewsText is null or empty:\n  no adjustment\n  Add to flags: \"No news data \u2014 sentiment adjustment skipped.\"\n\nCompute:\npt_bear = pt_base \u00d7 (1 \u2212 discount)\npt_bull = pt_base \u00d7 (1 + premium)\n\nRound both to 2 decimals.\nPOST-COMPUTATION CHECK:\nIf pt_bull < current_price:\n  Set overvaluation_flag = true\n  Append to rationale: \"WARNING: All price targets below current market \n  price. Model indicates significant overvaluation vs. formula inputs. \n  Manual review recommended before acting on SELL signal.\"\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-10 if eps_current < 0 (loss-making company, path to profitability unproven).\n+10 if news sentiment = POSITIVE (strong thesis confirmation from Seeking Alpha)\n-15 if news sentiment = NEGATIVE (material risk identified in news/articles)\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 structured summary with the following parts separated by | :\n  1. VERDICT: BUY/HOLD/SELL and one-line reason\n  2. MOAT: moat type and strength (strong/moderate/weak)\n  3. VALUATION: pt_base vs current_price, upside/downside %\n  4. PRIMARY RISK: single biggest threat to the thesis\n  5. PRICING POWER: high/moderate/low with one supporting data point\n  6. NEWS SIGNAL: POSITIVE/NEGATIVE/NEUTRAL and what drove it\"\n}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b703c339-afaf-4677-8da3-4aaf7aae27a3",
      "name": "Alert Filter",
      "type": "n8n-nodes-base.code",
      "position": [
        5552,
        1040
      ],
      "parameters": {
        "jsCode": "const j = $input.first().json;\n\nfunction getVerdict(rationale) {\n  const r = (rationale || '').trim().toUpperCase();\n  if (r.startsWith('BUY'))  return 'BUY';\n  if (r.startsWith('SELL')) return 'SELL';\n  return 'HOLD';\n}\n\nconst verdict      = getVerdict(j.rationale);\nconst confidence   = Number(j.confidence    || 0);\nconst conviction   = j.conviction           || 'LOW';\nconst gap_pct      = Number(j.gap_pct       || 0);\nconst pt_base      = Number(j.pt_base       || 0);\nconst pt_bull      = Number(j.pt_bull       || 0);\nconst pt_bear      = Number(j.pt_bear       || 0);\nconst cur_price    = Number(j.current_price || 0);\nconst f_score      = j.f_score !== null && j.f_score !== undefined ? `F:${j.f_score}/9` : '';\n\nconst upside_pct   = cur_price > 0\n  ? ((pt_base - cur_price) / cur_price * 100).toFixed(1)\n  : null;\n\n// Alert when: clear BUY or SELL signal + minimum confidence threshold\nconst should_alert = (verdict === 'BUY' || verdict === 'SELL') && confidence >= 50;\n\nconst icon = verdict === 'BUY' ? '\\uD83D\\uDFE2' : '\\uD83D\\uDD34';\n\nconst upsideStr = upside_pct !== null\n  ? ` (${Number(upside_pct) >= 0 ? '+' : ''}${upside_pct}%)`\n  : '';\n\nconst alert_message = should_alert\n  ? `${icon} *${verdict}* \\u2014 ${j.stock} [${j.model}]\\n`\n  + `\\uD83D\\uDCC5 ${j.date}\\n`\n  + `\\uD83D\\uDCB0 Price: $${cur_price} \\u2192 Target: $${pt_base}${upsideStr}\\n`\n  + `\\uD83C\\uDFAF Bear: $${pt_bear} | Bull: $${pt_bull}\\n`\n  + `\\uD83D\\uDCCA Confidence: ${confidence}% | Conviction: ${conviction}${f_score ? ` | ${f_score}` : ''}\\n`\n  + (gap_pct ? `\\uD83D\\uDCD0 Model Gap: ${gap_pct}%\\n` : '')\n  + `\\uD83D\\uDCDD ${j.rationale}`\n  : null;\n\nreturn [{\n  json: {\n    ...j,\n    should_alert,\n    alert_verdict:  verdict,\n    alert_message\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "79143262-612a-4140-b63d-28b09544d146",
      "name": "If Alert?",
      "type": "n8n-nodes-base.if",
      "position": [
        5744,
        1040
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "af002-cond-should-alert",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{$json.should_alert}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ca61fc9d-6d97-4ba2-9d23-d7359fc09d4a",
      "name": "Telegram Alert",
      "type": "n8n-nodes-base.telegram",
      "position": [
        6976,
        800
      ],
      "parameters": {
        "text": "={{ $json.alert_message }}",
        "chatId": "123456789",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "fdcddd4e-b9d8-4ad5-ae72-a1245ee4763b",
      "name": "Get 50MA",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        5904,
        928
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=SMA&symbol={{ $json.stock }}&interval=daily&time_period=50&series_type=close&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "edd624c4-7a8b-4197-b9b9-0d99856e23b2",
      "name": "MA Check",
      "type": "n8n-nodes-base.code",
      "position": [
        6224,
        928
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const smaResponse = $input.first().json;\n\n// Recover alert fields\nconst alertData = $('Alert Filter').first().json;\n\n// \u2500\u2500\u2500 50MA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sma50Data = smaResponse['Technical Analysis: SMA'];\nlet ma_50 = null;\nif (sma50Data) {\n  const latestDate50 = Object.keys(sma50Data)[0];\n  ma_50 = parseFloat(sma50Data[latestDate50]['SMA']);\n}\n\n// \u2500\u2500\u2500 200MA (Idea 6) \u2014 fetched separately then passed here \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Get 200MA node result is available from previous Get 200MA step\nlet ma_200 = null;\ntry {\n  const sma200Node = $('Get 200MA').first().json;\n  const sma200Data = sma200Node['Technical Analysis: SMA'];\n  if (sma200Data) {\n    const latestDate200 = Object.keys(sma200Data)[0];\n    ma_200 = parseFloat(sma200Data[latestDate200]['SMA']);\n  }\n} catch(e) { /* 200MA unavailable */ }\n\nconst current_price = Number(alertData.current_price || 0);\n\n// 50MA check\nconst ma_50_passed = current_price > 0 && ma_50 !== null && ma_50 > 0 && current_price >= ma_50;\n\n// 200MA trend tier (Idea 6)\nlet trend_tier = 'UNKNOWN';\nif (ma_200 !== null && ma_50 !== null && current_price > 0) {\n  if (current_price > ma_200 && current_price > ma_50) {\n    trend_tier = 'STRONG_UPTREND';   // Price above both MAs \u2014 strongest entry\n  } else if (current_price > ma_200 && current_price <= ma_50) {\n    trend_tier = 'PULLBACK';         // Above 200MA but below 50MA \u2014 watchlist\n  } else {\n    trend_tier = 'DOWNTREND';        // Below 200MA \u2014 structural risk\n  }\n} else if (ma_50 !== null && current_price > 0) {\n  trend_tier = ma_50_passed ? 'ABOVE_50MA' : 'BELOW_50MA';\n}\n\nif (ma_50 === null) {\n  return [{ json: {\n    ...alertData,\n    ma_50: null, ma_200: null,\n    ma_check_passed: true,   // fail open\n    ma_signal: 'MA_DATA_UNAVAILABLE',\n    trend_tier: 'UNKNOWN'\n  }}];\n}\n\nreturn [{ json: {\n  ...alertData,\n  ma_50:           ma_50 !== null ? Number(ma_50.toFixed(2)) : null,\n  ma_200:          ma_200 !== null ? Number(ma_200.toFixed(2)) : null,\n  ma_check_passed: ma_50_passed,\n  ma_signal:       ma_50_passed ? 'ABOVE_50MA' : 'BELOW_50MA',\n  trend_tier\n}}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "70b0daec-3b54-4a90-86c1-93e7835c5446",
      "name": "Above 50MA?",
      "type": "n8n-nodes-base.if",
      "position": [
        6800,
        928
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "af007-cond-above-ma",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{$json.ma_check_passed}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "48e52eb5-96cb-4ad9-9aa0-4f463a36c22c",
      "name": "Watchlist Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        6992,
        1072
      ],
      "parameters": {
        "text": "=\ud83d\udc40 *WATCHLIST* \u2014 {{ $json.stock }} [{{ $json.model }}]\n\ud83d\udcc5 {{ $json.date }}\n\u26a0\ufe0f Below 50-Day MA (${{ $json.ma_50 }}) \u2014 Wait for breakout\n\ud83d\udcb0 Price: ${{ $json.current_price }} \u2192 Target: ${{ $json.pt_base }}\n\ud83d\udcca Confidence: {{ $json.confidence }}%\n={{ $json.rsi_14 ? `RSI(14): ${$json.rsi_14} [${$json.rsi_signal}]` : '' }}\n={{ $json.earnings_warning ? '\u26a0\ufe0f ' + $json.earnings_warning : '' }}\n={{ $json.trend_tier ? `Trend: ${$json.trend_tier}` : '' }}",
        "chatId": "123456789",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8fe22d1d-f1be-4abc-acfd-23599a2c6e81",
      "name": "Build Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        -416,
        1008
      ],
      "parameters": {
        "jsCode": "// Receives ALL accumulated done-branch items (one per processed stock)\nconst items = $input.all();\n\nif (!items || items.length === 0) {\n  return [{ json: { summary_message: 'Report is done \u2014 no stocks analyzed.' } }];\n}\n\nconst lines = items.map(item => {\n  const j = item.json;\n  const stock   = j.stock         || '?';\n  const verdict = j.alert_verdict || 'HOLD';\n  const conf    = j.confidence    ? ` (${j.confidence}%)` : '';\n   const price   = j.current_price ? ` $${Number(j.current_price).toFixed(2)}` : '';\n  const target  = j.pt_base       ? ` \u2192 $${Number(j.pt_base).toFixed(2)}`     : '';\n  const ma      = j.ma_signal && j.ma_signal !== 'MA_DATA_UNAVAILABLE'\n                    ? ` [${j.ma_signal}]` : '';\n  const icon = verdict === 'BUY'  ? '\ud83d\udfe2'\n             : verdict === 'SELL' ? '\ud83d\udd34'\n             :                      '\ud83d\udfe1';\n   return `${icon} ${stock}: ${verdict}${conf}${ma}${price}${target}`;\n});\n\nconst today = new Date().toISOString().split('T')[0];\nconst summary_message =\n  `\ud83d\udcca *Institutional Report \u2014 ${today}*\\n\\n`\n  + lines.join('\\n')\n  + `\\n\\n${items.length} stock${items.length !== 1 ? 's' : ''} analyzed.`;\n\nreturn [{ json: { summary_message } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "e7080103-92bf-422a-909f-44b01f4a9aa3",
      "name": "Get Previous Verdict",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        5152,
        1040
      ],
      "parameters": {
        "options": {
          "returnFirstMatch": false
        },
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.stock }}",
              "lookupColumn": "stock"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "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.5,
      "alwaysOutputData": true
    },
    {
      "id": "e8f834df-8cfd-4da8-b43f-b67f0d4a4bbb",
      "name": "Thesis Reversal Enricher",
      "type": "n8n-nodes-base.code",
      "position": [
        5360,
        1040
      ],
      "parameters": {
        "jsCode": "// IDEA 2: Thesis Reversal Detection\n// $json here is the analysis item (from If TRUE branch through Get Previous Verdict)\n// We need to recover the analysis data and the prev verdict rows\n\n// The item flowing through is the analysis item (passed as the first input into the lookup)\n// After a Google Sheets lookup, the item might be the lookup result.\n// We need to get the original item from before the lookup.\n\nconst analysisData = $('If').first().json;\nconst stock = analysisData.stock || '';\n\n// Get all previous entries for this stock (from Get Previous Verdict lookup)\nconst prevRows = $input.all().filter(it => it.json.stock === stock && it.json.date);\n\n// Sort by date descending and get the most recent\nprevRows.sort((a, b) => (b.json.date || '').localeCompare(a.json.date || ''));\n\nconst prevEntry = prevRows.length > 0 ? prevRows[0].json : null;\n\nfunction extractVerdict(rationale) {\n  const r = (rationale || '').trim().toUpperCase();\n  if (r.startsWith('BUY'))  return 'BUY';\n  if (r.startsWith('SELL')) return 'SELL';\n  return 'HOLD';\n}\n\nconst prev_verdict = prevEntry ? extractVerdict(prevEntry.rationale) : null;\nconst prev_date    = prevEntry ? prevEntry.date : null;\nconst cur_rationale = analysisData.rationale || (analysisData.chatgpt_result || {}).rationale || '';\nconst cur_verdict   = extractVerdict(cur_rationale);\n\nconst thesis_reversal = prev_verdict !== null && prev_verdict !== cur_verdict;\n\nreturn [{\n  json: {\n    ...analysisData,\n    previous_verdict: prev_verdict,\n    previous_date:    prev_date,\n    thesis_reversal,\n    thesis_reversal_msg: thesis_reversal\n      ? `THESIS REVERSAL: ${stock}\\n${prev_verdict} -> ${cur_verdict} (prev: ${prev_date})\\nF:${analysisData.f_score ?? '?'}/9 | Confidence: ${analysisData.confidence ?? '?'}%\\n${cur_rationale.slice(0, 200)}`\n      : null\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "cd0970b0-7f4f-4b70-af29-ee1ccd7e1b43",
      "name": "Is Thesis Reversal?",
      "type": "n8n-nodes-base.if",
      "position": [
        7168,
        752
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "reversal-check",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.thesis_reversal }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e2fc109b-7a1e-48a2-80ae-ac72e4b93a5c",
      "name": "Thesis Reversal Alert",
      "type": "n8n-nodes-base.telegram",
      "position": [
        7408,
        736
      ],
      "parameters": {
        "text": "={{ $json.thesis_reversal_msg || '' }}",
        "chatId": "={{ $('Send a text message').params.chatId || '' }}",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "7cc62935-8322-41cc-ae59-2acdea5aa3f8",
      "name": "Get 200MA",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        6064,
        928
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=SMA&symbol={{ $json.stock }}&interval=daily&time_period=200&series_type=close&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "ca885a5a-1cff-48de-8b9f-077d0acb9812",
      "name": "Get RSI",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        6352,
        928
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=RSI&symbol={{ $json.stock }}&interval=daily&time_period=14&series_type=close&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "407c2f10-f8fb-471c-9041-c92f752ac386",
      "name": "Get Earnings",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        6496,
        928
      ],
      "parameters": {
        "url": "=https://www.alphavantage.co/query?function=EARNINGS&symbol={{ $json.stock }}&apikey=NV2BZ902M2RIVKPC",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "71610cc0-1f5c-4d9d-b190-d24d6081aa46",
      "name": "Entry Signal Enricher",
      "type": "n8n-nodes-base.code",
      "position": [
        6640,
        928
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// IDEA 3: RSI + Earnings Date enrichment\nconst maData   = $('MA Check').first().json;\nconst rsiResp  = $('Get RSI').first().json;\nconst earningsResp = $('Get Earnings').first().json;\n\n// \u2500\u2500\u2500 RSI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet rsi_14 = null;\nlet rsi_signal = 'UNKNOWN';\ntry {\n  const rsiData = rsiResp['Technical Analysis: RSI'];\n  if (rsiData) {\n    const latestRSI = Object.keys(rsiData)[0];\n    rsi_14 = parseFloat(rsiData[latestRSI]['RSI']);\n    if (rsi_14 <= 35)      rsi_signal = 'OVERSOLD';      // dip opportunity\n    else if (rsi_14 <= 55) rsi_signal = 'NEUTRAL';\n    else if (rsi_14 <= 70) rsi_signal = 'ELEVATED';\n    else                   rsi_signal = 'OVERBOUGHT';     // caution on entry\n  }\n} catch(e) { /* RSI unavailable */ }\n\n// \u2500\u2500\u2500 Earnings Date \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet next_earnings_date = null;\nlet days_to_earnings = null;\nlet earnings_warning = null;\ntry {\n  const annualEarnings = earningsResp.annualEarnings || [];\n  const quarterlyEarnings = earningsResp.quarterlyEarnings || [];\n  // Find the next scheduled earnings (reportedDate in future)\n  const now = Date.now();\n  const upcoming = quarterlyEarnings\n    .filter(e => e.reportedDate)\n    .map(e => ({ date: e.reportedDate, ms: new Date(e.reportedDate).getTime() }))\n    .filter(e => Number.isFinite(e.ms) && e.ms > now)\n    .sort((a, b) => a.ms - b.ms);\n  if (upcoming.length > 0) {\n    next_earnings_date = upcoming[0].date;\n    days_to_earnings = Math.ceil((upcoming[0].ms - now) / (1000 * 60 * 60 * 24));\n    if (days_to_earnings <= 14) {\n      earnings_warning = `EARNINGS IN ${days_to_earnings} DAYS (${next_earnings_date}) \u2014 binary risk`;\n    }\n  }\n} catch(e) { /* earnings unavailable */ }\n\nreturn [{\n  json: {\n    ...maData,\n    rsi_14:             rsi_14 !== null ? Number(rsi_14.toFixed(1)) : null,\n    rsi_signal,\n    next_earnings_date,\n    days_to_earnings,\n    earnings_warning\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "dece9466-9493-46bb-bc8e-fdb4378eb1bc",
      "name": "Sticky Note \u2014 Accuracy Tracker",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -512,
        1712
      ],
      "parameters": {
        "width": 600,
        "height": 400,
        "content": "## IDEA 7: Prediction Accuracy Tracker\n\n**Create a separate scheduled workflow (monthly) that:**\n\n1. Read the Sentiments sheet for all predictions older than 30 days\n2. For each stock+date+model row:\n   - Fetch `alphavantage Current Price` for the stock\n   - Compute: `return_pct = (actual_price_30d - current_price) / current_price * 100`\n   - Determine `was_correct`: BUY verdict + return > 5% = correct\n3. Append to a new **Accuracy_Tracking** Google Sheet with columns:\n   `stock | predicted_on | verdict | pt_base | current_price_at_prediction | actual_price_30d | return_pct | was_correct | model | resolution_method`\n4. Send weekly Telegram summary: *Model accuracy last 90 days: ChatGPT 68%, Gemini 71%, Tiebreaker 74%*\n\n**This turns the system into a learning engine.**\nSheet ID for Sentiments: `1fbptcVE0mBjaIZJHkJzFBTdoVJVLgQOWmJXpCx40YyE`"
      },
      "typeVersion": 1
    },
    {
      "id": "b8252532-a54e-439b-9e09-cce81d30a2c2",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5520,
        576
      ],
      "parameters": {
        "color": 2,
        "width": 2064,
        "height": 1564,
        "content": "# 4.Strong Buy Alerts\n"
      },
      "typeVersion": 1
    },
    {
      "id": "ab3ec24d-13db-427c-ad28-9c2d33f62d35",
      "name": "Wait6",
      "type": "n8n-nodes-base.wait",
      "position": [
        768,
        1232
      ],
      "parameters": {
        "amount": 20
      },
      "typeVersion": 1.1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "6d19e3fa-4904-4a78-a519-b3f88c6a331f",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Get Previous Verdict",
            "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
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "loop_over_tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Clean Up Results from First Round",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait1": {
      "main": [
        [
          {
            "node": "alphavantage - Balance Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait2": {
      "main": [
        [
          {
            "node": "alphavantage - Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait3": {
      "main": [
        [
          {
            "node": "alphavantage - Income Statement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait4": {
      "main": [
        [
          {
            "node": "alphavantage - CashFlow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait5": {
      "main": [
        [
          {
            "node": "alphavantage - Current Price",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait6": {
      "main": [
        [
          {
            "node": "alphavantage - Current Price Second",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge1": {
      "main": [
        [
          {
            "node": "First round ChatGPT",
            "type": "main",
            "index": 0
          },
          {
            "node": "First round Gemini",
            "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
          }
        ]
      ]
    },
    "Merge6": {
      "main": [
        [
          {
            "node": "Code in JavaScript3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge7": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get RSI": {
      "main": [
        [
          {
            "node": "Get Earnings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get 50MA": {
      "main": [
        [
          {
            "node": "Get 200MA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MA Check": {
      "main": [
        [
          {
            "node": "Get RSI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get 200MA": {
      "main": [
        [
          {
            "node": "MA Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Alert?": {
      "main": [
        [
          {
            "node": "Get 50MA",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "write_sentiment_to_sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert Row": {
      "main": [
        [
          {
            "node": "Merge New Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Above 50MA?": {
      "main": [
        [
          {
            "node": "Telegram Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Watchlist Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Filter": {
      "main": [
        [
          {
            "node": "If Alert?",
            "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
          }
        ]
      ]
    },
    "Get Earnings": {
      "main": [
        [
          {
            "node": "Entry Signal Enricher",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Summary": {
      "main": [
        [
          {
            "node": "Send a text message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Profile": {
      "main": [
        [
          {
            "node": "Merge3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean the news": {
      "main": [
        [
          {
            "node": "If1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Alert": {
      "main": [
        [
          {
            "node": "Is Thesis Reversal?",
            "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": "Wait1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Wait2",
            "type": "main",
            "index": 0
          },
          {
            "node": "Wait3",
            "type": "main",
            "index": 0
          },
          {
            "node": "Wait4",
            "type": "main",
            "index": 0
          },
          {
            "node": "Wait5",
            "type": "main",
            "index": 0
          },
          {
            "node": "Wait6",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "return the news": {
      "main": [
        [
          {
            "node": "Final version of news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Up ChatGPT": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Read_tickers_from_Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean up Gemini 2": {
      "main": [
        [
          {
            "node": "Merge6",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Needs Tiebreaker?": {
      "main": [
        [
          {
            "node": "Tide Breaker - Bull",
            "type": "main",
            "index": 0
          },
          {
            "node": "Tide Breaker - Bear",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "loop_over_tickers": {
      "main": [
        [
          {
            "node": "Build Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Cache Lookup",
            "type": "main",
            "index": 0
          },
          {
            "node": "Seekingalpha Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean up Chatgpt 2": {
      "main": [
        [
          {
            "node": "Merge6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Merge7",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "First round Gemini": {
      "main": [
        [
          {
            "node": "Clean up Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Watchlist Telegram": {
      "main": [
        [
          {
            "node": "write_sentiment_to_sheets",
            "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
          }
        ]
      ]
    },
    "Code in JavaScript3": {
      "main": [
        [
          {
            "node": "Merge7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "First round ChatGPT": {
      "main": [
        [
          {
            "node": "Clean Up ChatGPT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get row(s) in sheet": {
      "main": [
        [
          {
            "node": "Clean Old Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Thesis Reversal?": {
      "main": [
        [
          {
            "node": "Thesis Reversal Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "write_sentiment_to_sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge New Financial": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tide Breaker - Bear": {
      "main": [
        [
          {
            "node": "Clean up Gemini 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tide Breaker - Bull": {
      "main": [
        [
          {
            "node": "Clean up Chatgpt 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update row in sheet": {
      "main": [
        [
          {
            "node": "Merge New Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Current Price1": {
      "main": [
        [
          {
            "node": "Update row in sheet1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Read Financial": {
      "main": [
        [
          {
            "node": "If2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Previous Verdict": {
      "main": [
        [
          {
            "node": "Thesis Reversal Enricher",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update row in sheet1": {
      "main": [
        [
          {
            "node": "Get row(s) in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Entry Signal Enricher": {
      "main": [
        [
          {
            "node": "Above 50MA?",
            "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
          }
        ]
      ]
    },
    "Thesis Reversal Alert": {
      "main": [
        [
          {
            "node": "write_sentiment_to_sheets",
            "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
          }
        ]
      ]
    },
    "Thesis Reversal Enricher": {
      "main": [
        [
          {
            "node": "Alert Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "write_sentiment_to_sheets": {
      "main": [
        [
          {
            "node": "Wait",
            "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
          }
        ]
      ]
    },
    "Clean Up Results from First Round": {
      "main": [
        [
          {
            "node": "Needs Tiebreaker?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "alphavantage - Current Price Second": {
      "main": [
        [
          {
            "node": "Clean Current Price1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}