{
  "id": "SpM1Pa120REd0LTN",
  "name": "AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets V12",
  "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": "c367abe2-4feb-4f01-839a-ba2427d86872",
      "name": "loop_over_tickers",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1488,
        1792
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "d49cb0cd-5482-42ac-a78d-c25592460947",
      "name": "write_sentiment_to_sheets",
      "type": "n8n-nodes-base.googleSheets",
      "maxTries": 2,
      "position": [
        7584,
        1344
      ],
      "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 ?? 0 }}",
            "pt_base": "={{ $json.pt_base || '' }}",
            "pt_bear": "={{ $json.pt_bear || '' }}",
            "pt_bull": "={{ $json.pt_bull || '' }}",
            "verdict": "={{ $json.verdict || $json.alert_verdict || '' }}",
            "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?.[0] || '' }}",
            "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": "verdict",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "verdict",
              "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": "1UwNknqr9d7W0Egq8ahlo9kb40B1BIGZEGdhUhCeM0Ck",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1UwNknqr9d7W0Egq8ahlo9kb40B1BIGZEGdhUhCeM0Ck/edit?usp=drivesdk",
          "cachedResultName": "Sentiments of my stocks for screener"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.6
    },
    {
      "id": "bba9fb51-a0e6-4023-aa87-6738ea40902c",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2832,
        1792
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "d8e52ba2-be9a-4cc5-a858-ddfea568e406",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2896,
        816
      ],
      "parameters": {
        "color": 7,
        "width": 2080,
        "height": 1488,
        "content": "# 1. \ud83d\udce1 PHASE 1 \u2014 SCREENING & FILTERING\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nRuns daily on schedule. Calls the FMP Company Screener \nto fetch up to 100 US-listed stocks filtered by:\n  \u2022 Market Cap > $5B\n  \u2022 Volume > 500K\n  \u2022 Price > $10\n  \u2022 Beta < 2.0\n  \u2022 Actively trading, US only, NYSE/NASDAQ, no ETFs/funds\n\nScore_and_Prefilter then scores each stock on volume \nhealth, market cap sweet spot ($5B\u2013$100B), and beta \ngradient \u2014 with a sector diversity cap of max 4 stocks \nper sector \u2014 and outputs the top 20 candidates.\n\nThe 20 selected stocks are then passed one by one into \nthe analysis loop via \"Loop Over Items\".\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2699\ufe0f  SETUP REQUIRED\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2022 FMP API Key \u2192 add to all HTTP Request nodes as query \n  param: apikey = {{ your_key }}\n\u2022 FMP plan must support /stable/ endpoints and allow \n  \u2265 300 calls/day (Starter plan or above)\n\u2022 Schedule Trigger \u2192 set your preferred daily run time\n  (default: weekdays 7:00 AM)"
      },
      "typeVersion": 1
    },
    {
      "id": "6de678ec-e030-490a-a8f1-8c222614d742",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -784,
        -480
      ],
      "parameters": {
        "color": 7,
        "width": 2744,
        "height": 2252,
        "content": "# 2. \ud83d\udcca PHASE 2A \u2014 FINANCIAL DATA COLLECTION\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nFor each stock, the workflow fetches 13 FMP endpoints:\n  \u2022 Income Statement (annual + TTM quarters)\n  \u2022 Balance Sheet (annual + quarterly)\n  \u2022 Cash Flow Statement (annual + quarterly)\n  \u2022 Key Metrics & Ratios\n  \u2022 Company Profile (price, sector, shares outstanding)\n  \u2022 Sector P/E median\n\n\"Clean Read Financial\" computes and outputs:\n  \u2022 EPS, BVPS, Revenue TTM, Gross/Operating Margins\n  \u2022 FCF TTM, Net Income, Net Debt, Cash\n  \u2022 Revenue Growth YoY\n  \u2022 Piotroski F-Score (9-point scale)\n  \u2022 Graham Number (intrinsic value estimate)\n  \u2022 DCF Anchor (EV-based fair value per share)\n  \u2022 Sector Median P/E for relative valuation\n\nAll values are null-safe. Stocks with missing critical \ndata still pass through with reduced confidence scores.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2699\ufe0f  SETUP REQUIRED\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2022 Same FMP API Key as Phase 1\n\u2022 No additional credentials needed for this phase\n\u2022 Verify all HTTP Request nodes point to /stable/ \n  endpoints (NOT the deprecated /api/v3/)"
      },
      "typeVersion": 1
    },
    {
      "id": "41563363-4ff3-4293-bad1-335940a352b7",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1984,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 2308,
        "height": 1552,
        "content": "# 3. \ud83e\udd16 PHASE 3 \u2014 AI DUAL-MODEL ANALYSIS & TIEBREAKER\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\nROUND 1 \u2014 Parallel Analysis\n\nGPT-4o (ChatGPT): Institutional bull perspective\nGemini 2.5 Pro: Institutional bear perspective\n\nBoth models analyze full financial and news data, producing:\npt_bear, pt_base, pt_bull, confidence (0\u2013100), verdict (BUY/HOLD/SELL), and rationale.\n\nConsensus Check\n\nDo verdicts match?\nIs the price target gap < 25% of the current price?\nIf both are HOLD with confidence \u2265 65%, the consensus is accepted and the tiebreaker is skipped.\n\nTIEBREAKER \u2014 When Needed\n\nTriggered if verdicts differ or the gap exceeds 25%.\nChatGPT re-runs as a growth-focused bull.\nGemini re-runs as a risk-focused bear.\nFinal pt_base: Average of bull and bear targets.\nFinal verdict: Determined by upside percentage and the F-Score gate.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2699\ufe0f SETUP REQUIRED\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nOpenAI API Key: Add to ChatGPT HTTP nodes\nAuthorization: Bearer {{ your_key }} (GPT-4o access required)\nGoogle Gemini API Key: Add to Gemini HTTP nodes\nkey={{ your_key }} (via Google AI Studio or Vertex AI)\nEstimated Cost: ~$0.43/day for 25 stocks\n(~$10\u2013$13/month across both models)"
      },
      "typeVersion": 1
    },
    {
      "id": "816455d8-a4d5-4f38-8182-a1773ba2e07a",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7504,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 984,
        "height": 1560,
        "content": "# 5. \ud83d\udcbe PHASE 5 \u2014 STORE RESULTS & TELEGRAM SUMMARY\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\nAll analyzed stocks are stored in Google Sheets with key fields:\n\nstock, date, current_price, sector\npt_bear, pt_base, pt_bull\nverdict, confidence, f_score\nverdict_chatgpt, verdict_gemini\nprevious_verdict, previous_date (for backtesting)\nrationale summary\n\nAt the end of each daily run, a Telegram message summarizes:\n\nTotal stocks scanned\nBUY / HOLD / SELL counts\nTop BUY opportunities with upside and confidence\nAny STRONG BUY alerts from Phase 4\n\nA separate Signal Outcome Checker workflow reviews the sheet weekly to backtest past signals and sends a performance summary every Monday.\n\n\u2699\ufe0f SETUP REQUIRED\nGoogle Sheets\nCreate a sheet named Stock_Signals with these headers:\nstock | date | current_price | sector | pt_bear | pt_base | pt_bull | verdict | confidence | f_score | verdict_chatgpt | verdict_gemini | rationale | previous_verdict | previous_date\nIn n8n \u2192 Credentials, add Google Sheets OAuth2 and grant access.\nPaste the Sheet ID (from the URL between /d/ and /edit) into all Google Sheets nodes.\nTelegram\nCreate a bot via @BotFather using /newbot and copy the API Token.\nStart a chat with the bot (or add it to a group).\nRetrieve the Chat ID by visiting:\nhttps://api.telegram.org/bot<TOKEN>/getUpdates\nIn n8n \u2192 Credentials, add Telegram API and set the Chat ID in the Telegram node.\n\n\n\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "00eed74a-c58c-452c-aef2-55cd38837814",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        2672,
        1200
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "94ef6a9c-5d45-408e-ac48-e9dcd26a7278",
      "name": "XML",
      "type": "n8n-nodes-base.xml",
      "position": [
        576,
        1952
      ],
      "parameters": {
        "options": {},
        "dataPropertyName": "sa_xml"
      },
      "typeVersion": 1
    },
    {
      "id": "8872eb3d-a788-4d47-bcd0-972d5181f31c",
      "name": "Merge1",
      "type": "n8n-nodes-base.merge",
      "position": [
        1792,
        1120
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "c23cd5dc-fe35-40dc-b80b-6ce9a981dd9b",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        4592,
        1296
      ],
      "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": "658cd539-7db6-48e9-9fb0-4829b290d974",
      "name": "If1",
      "type": "n8n-nodes-base.if",
      "position": [
        352,
        2048
      ],
      "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": "27171373-95f9-4c28-9641-f0ea03188d4e",
      "name": "Edit Fields2",
      "type": "n8n-nodes-base.set",
      "position": [
        800,
        1952
      ],
      "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": "d54ef15e-741d-4ec1-a782-3a37fbb4aa58",
      "name": "Merge2",
      "type": "n8n-nodes-base.merge",
      "position": [
        480,
        528
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "c9fec1fe-afed-43b9-b504-ab5d1eed10b1",
      "name": "Merge3",
      "type": "n8n-nodes-base.merge",
      "position": [
        320,
        400
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "b7fcd4c5-aba9-4197-993e-deba52f4d71c",
      "name": "Merge4",
      "type": "n8n-nodes-base.merge",
      "position": [
        656,
        656
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "728d1fa8-3da4-4081-9cf9-bb2b14568d96",
      "name": "Seekingalpha Articles",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -96,
        2048
      ],
      "parameters": {
        "url": "=https://seekingalpha.com/api/sa/combined/{{ $json.stock }}.xml",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text",
              "outputPropertyName": "sa_xml"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "d68dfe96-4193-4b9f-b904-1ac6d51797cc",
      "name": "Clean Profile",
      "type": "n8n-nodes-base.code",
      "position": [
        112,
        336
      ],
      "parameters": {
        "jsCode": "const profile = Array.isArray($json) ? $json[0] : $json;\n\nreturn {\n  json: {\n    stock: profile.symbol || $input.item.json.stock || \"UNKNOWN\",\n    profile_data: profile\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "cdf428ac-b822-4457-bae9-8bb1d14616d1",
      "name": "Clean Income statement",
      "type": "n8n-nodes-base.code",
      "position": [
        112,
        480
      ],
      "parameters": {
        "jsCode": "const data = Array.isArray($json.data) ? $json.data : (Array.isArray($json) ? $json : [$json]);\nconst stock = (data[0] && data[0].symbol) || \"UNKNOWN\";\n\nreturn [{\n  json: {\n    stock: stock,\n    income_quarterly: data\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a9ad0d28-d7bd-41c1-aba4-12d670c4ae9c",
      "name": "Clean Ccashflow",
      "type": "n8n-nodes-base.code",
      "position": [
        112,
        864
      ],
      "parameters": {
        "jsCode": "const data = Array.isArray($json.data) ? $json.data : (Array.isArray($json) ? $json : [$json]);\nconst stock = (data[0] && data[0].symbol) || \"UNKNOWN\";\n\nreturn [{\n  json: {\n    stock: stock,\n    cashflow_quarterly: data\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "60674b73-2d6b-430e-9601-11d751368ef4",
      "name": "Clean Read Financial",
      "type": "n8n-nodes-base.code",
      "position": [
        1664,
        720
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// \u2500\u2500 Guard: skip the loop's done-signal item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction num(v) {\n  if (v === null || v === undefined || v === '' || v === 'None') return null;\n  const n = Number(String(v).replace(/[,$%]/g, ''));\n  return isFinite(n) ? n : null;\n}\nfunction safeDiv(a, b) {\n  return (a !== null && b !== null && b !== 0) ? a / b : null;\n}\n\nconst stock    = $json.stock;\nconst profile  = $json.profile_data  || {};\nconst kmTTM    = Array.isArray($json.key_metrics_ttm)\n                   ? ($json.key_metrics_ttm[0] || {})\n                   : ($json.key_metrics_ttm || {});\n\n// Use quarterly arrays; fall back gracefully when period/limit not yet added\nconst incQ  = Array.isArray($json.income_quarterly)   ? $json.income_quarterly   : [];\nconst incA  = Array.isArray($json.income_annual)       ? $json.income_annual      : [];\nconst balQ  = Array.isArray($json.balance_quarterly)   ? $json.balance_quarterly  : [];\nconst balA  = Array.isArray($json.balance_annual)      ? $json.balance_annual     : [];\nconst cfQ   = Array.isArray($json.cashflow_quarterly)  ? $json.cashflow_quarterly : [];\nconst cfA   = Array.isArray($json.cashflow_annual)     ? $json.cashflow_annual    : [];\n\nconst current_price = num($json.current_price) || num(profile.price);\nconst sector        = profile.sector || '';\n\n// For balance: prefer quarterly (debt/cash tracking), fall back to annual\nconst balSource = balQ.length ? balQ : balA;\n\n// \u2500\u2500 Quarterly arrays (last 4Q) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 revenue_last_4q = incQ.slice(0, 4).map(q => num(q.revenue));\n\nconst net_income_last_4q = incQ.slice(0, 4).map(q => num(q.netIncome));\n\nconst gross_margin_last_4q = incQ.slice(0, 4).map(q => {\n  const rev = num(q.revenue), gp = num(q.grossProfit);\n  return (rev && gp !== null && rev > 0) ? Number(((gp / rev) * 100).toFixed(2)) : null;\n});\n\nconst total_debt_last_4q = balSource.slice(0, 4).map(q => {\n  const t = num(q.totalDebt);\n  if (t !== null && t > 0) return t;\n  const s = num(q.shortTermDebt) || 0, l = num(q.longTermDebt) || 0;\n  return (s + l) > 0 ? (s + l) : null;\n});\n\nconst cash_last_4q = balSource.slice(0, 4).map(q => num(q.cashAndCashEquivalents));\n\n// \u2500\u2500 Latest balance \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 latestBal  = balSource[0] || {};\nconst debt_latest = num(latestBal.totalDebt) ||\n  ((num(latestBal.shortTermDebt) || 0) + (num(latestBal.longTermDebt) || 0)) || null;\nconst cash_latest = num(latestBal.cashAndCashEquivalents);\n\nlet net_debt_latest = null, net_cash_flag = null;\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\n// \u2500\u2500 TTM: Revenue \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Prefer key_metrics_ttm; fall back to summing quarters\nlet revenue_ttm = null;\nif (num(kmTTM.revenuePerShareTTM) !== null && num(kmTTM.weightedAverageShsOutDilTTM) !== null) {\n  revenue_ttm = num(kmTTM.revenuePerShareTTM) * num(kmTTM.weightedAverageShsOutDilTTM);\n} else {\n  const r4 = revenue_last_4q.filter(v => v !== null);\n  if (r4.length > 0) revenue_ttm = r4.reduce((a, b) => a + b, 0);\n}\n\n// \u2500\u2500 Revenue growth YoY (needs 8 quarters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 revenue_growth_yoy = null;\nconst rev8      = incQ.slice(0, 8).map(q => num(q.revenue));\nconst curr_ttm  = rev8.slice(0, 4).every(v => v !== null) ? rev8.slice(0, 4).reduce((a, b) => a + b, 0) : null;\nconst prev_ttm  = rev8.slice(4, 8).every(v => v !== null) ? rev8.slice(4, 8).reduce((a, b) => a + b, 0) : null;\nif (curr_ttm !== null && prev_ttm !== null && prev_ttm !== 0) {\n  revenue_growth_yoy = Number((((curr_ttm - prev_ttm) / prev_ttm) * 100).toFixed(2));\n}\n\n// \u2500\u2500 Net income TTM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 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// \u2500\u2500 Gross margin TTM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 gross_margin_ttm = null;\nif (num(kmTTM.grossProfitMarginTTM) !== null) {\n  // FMP returns this as decimal (0.36 = 36%)\n  gross_margin_ttm = Number((num(kmTTM.grossProfitMarginTTM) * 100).toFixed(2));\n} else if (curr_ttm !== null && curr_ttm > 0) {\n  const gp4 = incQ.slice(0, 4).map(q => num(q.grossProfit));\n  if (gp4.every(v => v !== null)) {\n    gross_margin_ttm = Number(((gp4.reduce((a, b) => a + b, 0) / curr_ttm) * 100).toFixed(2));\n  }\n}\n\n// \u2500\u2500 Operating margin TTM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 operating_margin_ttm = null;\nif (num(kmTTM.operatingIncomeRatioTTM) !== null) {\n  operating_margin_ttm = Number((num(kmTTM.operatingIncomeRatioTTM) * 100).toFixed(2));\n} else if (curr_ttm !== null && curr_ttm > 0) {\n  const oi4 = incQ.slice(0, 4).map(q => num(q.operatingIncome));\n  if (oi4.every(v => v !== null)) {\n    operating_margin_ttm = Number(((oi4.reduce((a, b) => a + b, 0) / curr_ttm) * 100).toFixed(2));\n  }\n}\n\n// \u2500\u2500 FCF TTM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 fcf_ttm = null;\nif (cfQ.length >= 4) {\n  // Sum last 4 quarters: FMP provides freeCashFlow directly\n  const fcf4 = cfQ.slice(0, 4).map(r => num(r.freeCashFlow));\n  if (fcf4.every(v => v !== null)) {\n    fcf_ttm = fcf4.reduce((a, b) => a + b, 0);\n  } else {\n    // Compute from OCF + capex (capex is negative in FMP)\n    const ocf4  = cfQ.slice(0, 4).map(r => num(r.operatingCashFlow));\n    const cap4  = cfQ.slice(0, 4).map(r => num(r.capitalExpenditure));\n    if (ocf4.every(v => v !== null) && cap4.every(v => v !== null)) {\n      fcf_ttm = ocf4.reduce((a, b) => a + b, 0) + cap4.reduce((a, b) => a + b, 0);\n    }\n  }\n} else if (cfQ.length > 0) {\n  // Only 1 quarter available \u2014 use what we have as best estimate\n  const r = cfQ[0];\n  fcf_ttm = num(r.freeCashFlow) ??\n            ((num(r.operatingCashFlow) !== null && num(r.capitalExpenditure) !== null)\n              ? num(r.operatingCashFlow) + num(r.capitalExpenditure)\n              : null);\n} else if (cfA.length) {\n  const r = cfA[0];\n  fcf_ttm = num(r.freeCashFlow) ??\n            ((num(r.operatingCashFlow) !== null && num(r.capitalExpenditure) !== null)\n              ? num(r.operatingCashFlow) + num(r.capitalExpenditure)\n              : null);\n}\n\n// \u2500\u2500 Total debt TTM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 total_debt_ttm = debt_latest;\n\n// \u2500\u2500 EPS, BVPS, Shares \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Prefer key_metrics_ttm; fall back to latest quarterly income\nconst eps_current = num(kmTTM.netIncomePerShareTTM)\n  || (incQ.length ? num(incQ[0].epsDiluted) : null);\n\nconst bvps_current = num(kmTTM.bookValuePerShareTTM)\n  || (() => {\n    const eq = num(latestBal.totalStockholdersEquity);\n    const sh = incQ.length ? num(incQ[0].weightedAverageShsOutDil) : null;\n    return (eq !== null && sh !== null && sh > 0) ? Number((eq / sh).toFixed(2)) : null;\n  })();\n\nconst shares_outstanding = num(kmTTM.weightedAverageShsOutDilTTM)\n  || (incQ.length ? num(incQ[0].weightedAverageShsOutDil) : null);\n\n// \u2500\u2500 Piotroski F-Score (needs 2 annual years for meaningful score) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Uses annual income + annual balance + annual cashflow\nconst y0_inc = incA[0] || null, y1_inc = incA[1] || null;\nconst y0_bal = balA[0] || null, y1_bal = balA[1] || null;\nconst y0_cf  = cfA[0]  || null, y1_cf  = cfA[1]  || null;\n\nconst NI0  = y0_inc ? num(y0_inc.netIncome)                : null;\nconst NI1  = y1_inc ? num(y1_inc.netIncome)                : null;\nconst TA0  = y0_bal ? num(y0_bal.totalAssets)              : null;\nconst TA1  = y1_bal ? num(y1_bal.totalAssets)              : null;\nconst CFO0 = y0_cf  ? num(y0_cf.operatingCashFlow)         : null;\nconst CFO1 = y1_cf  ? num(y1_cf.operatingCashFlow)         : null;\nconst LTD0 = y0_bal ? num(y0_bal.longTermDebt)             : null;\nconst LTD1 = y1_bal ? num(y1_bal.longTermDebt)             : null;\nconst CA0  = y0_bal ? num(y0_bal.totalCurrentAssets)       : null;\nconst CA1  = y1_bal ? num(y1_bal.totalCurrentAssets)       : null;\nconst CL0  = y0_bal ? num(y0_bal.totalCurrentLiabilities)  : null;\nconst CL1  = y1_bal ? num(y1_bal.totalCurrentLiabilities)  : null;\nconst REV0 = y0_inc ? num(y0_inc.revenue)                  : null;\nconst REV1 = y1_inc ? num(y1_inc.revenue)                  : null;\nconst GP0  = y0_inc ? num(y0_inc.grossProfit)              : null;\nconst GP1  = y1_inc ? num(y1_inc.grossProfit)              : null;\nconst SH0  = y0_inc ? num(y0_inc.weightedAverageShsOutDil) : null;\nconst SH1  = y1_inc ? num(y1_inc.weightedAverageShsOutDil) : null;\n\n// Add this before the F-score block:\nconst hasY1 = !!(y1_inc && y1_bal && y1_cf);\n\n// Then wrap year-over-year comparisons:\nlet f_score = 0;\nif (NI0 !== null && NI0 > 0)                                                         f_score++; // F1\nif (CFO0 !== null && CFO0 > 0)                                                        f_score++; // F2\nif (hasY1 && safeDiv(NI0,TA0) !== null && safeDiv(NI0,TA0) > safeDiv(NI1,TA1))      f_score++; // F3\nif (CFO0 !== null && NI0 !== null && CFO0 > NI0)                                      f_score++; // F4\nif (hasY1 && LTD0 !== null && LTD1 !== null && LTD0 < LTD1)                          f_score++; // F5\nif (hasY1 && safeDiv(CA0,CL0) !== null && safeDiv(CA0,CL0) > safeDiv(CA1,CL1))      f_score++; // F6\nif (hasY1 && SH0 !== null && SH1 !== null && SH0 <= SH1)                             f_score++; // F7\nif (hasY1 && safeDiv(GP0,REV0) !== null && safeDiv(GP0,REV0) > safeDiv(GP1,REV1))   f_score++; // F8\nif (hasY1 && safeDiv(REV0,TA0) !== null && safeDiv(REV0,TA0) > safeDiv(REV1,TA1))   f_score++; // F9\n\n\nconst f_score_data_ok = !!(y0_inc && y1_inc && y0_bal && y1_bal && y0_cf && y1_cf);\n\n// \u2500\u2500 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet graham_number = null;\nif (eps_current > 0 && bvps_current > 0) {\n  graham_number = Number(Math.sqrt(22.5 * eps_current * bvps_current).toFixed(2));\n}\n\n// \u2500\u2500 DCF Anchor \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 disc_rate_map = {\n  'Technology': 0.10, 'Financial Services': 0.09, 'Utilities': 0.08,\n  'Consumer Defensive': 0.09, 'Consumer Cyclical': 0.11,\n  'Healthcare': 0.10, 'Industrials': 0.10, 'Basic 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) {\n  const fcf_growth = (curr_ttm !== null && prev_ttm !== null && prev_ttm > 0)\n    ? Math.min((curr_ttm - prev_ttm) / prev_ttm, 0.30)\n    : 0.08;\n  const fcf5 = fcf_ttm * Math.pow(1 + fcf_growth, 5);\nconst dcf_ev = fcf5 / (disc_rate - terminal_growth);\ndcf_anchor = shares_outstanding > 0\n  ? Number((dcf_ev / shares_outstanding).toFixed(2))\n  : null;}\n\n// \u2500\u2500 Sector 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\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, 'Financial Services': 12, 'Utilities': 15,\n  'Consumer Defensive': 19, 'Consumer Cyclical': 22,\n  'Healthcare': 20, 'Industrials': 18, 'Basic Materials': 16,\n  'Energy': 14, 'Communication Services': 21, 'Real Estate': 17\n};\nconst sector_median_pe = sectorPE[sector] || 18;\n\n// \u2500\u2500 Output \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nreturn {\n  json: {\n    stock,\n    eps_current,\n    bvps_current,\n    shares_outstanding,\n    revenue_ttm,\n    total_debt_ttm,\n    gross_margin_ttm,\n    operating_margin_ttm,\n    fcf_ttm,\n    net_income_ttm,\n    revenue_last_4q:      revenue_last_4q.some(v => v !== null)      ? revenue_last_4q      : [],\n    net_income_last_4q:   net_income_last_4q.some(v => v !== null)   ? net_income_last_4q   : [],\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,\n    current_price,\n    net_debt_latest,\n    cash_latest,\n    debt_latest,\n    net_cash_flag,\n    f_score,\n    f_score_data_ok,\n    sector,\n    graham_number,\n    dcf_anchor,\n    sector_median_pe,\n    cache: {\n      status: 'fresh',\n      source: 'fmp',\n      last_updated: new Date().toISOString()\n    }\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "1cdf9419-1e47-4cf6-a0c0-fae5d413418b",
      "name": "Clean Current Price",
      "type": "n8n-nodes-base.code",
      "position": [
        96,
        1168
      ],
      "parameters": {
        "jsCode": "const inJson = $json || {};\nconst q = Array.isArray(inJson) ? inJson[0] : inJson;\nconst stock = q.symbol || $input.item.json.stock || \"UNKNOWN\";\n\nreturn [{\n  json: {\n    stock: stock,\n    current_price: q.price ? Number(q.price) : null\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d468b9b4-ecd3-4f9e-bf6e-0ead530e8357",
      "name": "Merge5",
      "type": "n8n-nodes-base.merge",
      "position": [
        304,
        960
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "5651b347-1f06-4321-9ca3-38c1df12ccc7",
      "name": "Clean the news",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        2048
      ],
      "parameters": {
        "jsCode": "let saXml = null;\nlet stock = \"\";\n\ntry {\n  stock =\n    $json.stock ??\n    $json.ticker ??\n    ($node[\"loop_over_tickers\"]?.json?.stock ?? \"\");\n\n  if (!$json.errorMessage && !$json.error) {\n\n    saXml =                    // \u2190 no \"let\" here, assigns to outer variable\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 (!saXml && $binary) {\n      const key = Object.keys($binary)[0];\n      const b64 = key && $binary[key]?.data ? $binary[key].data : null;\n      if (b64) {\n        const decoded = Buffer.from(b64, \"base64\").toString(\"utf8\").trim();\n        if (decoded) saXml = decoded;\n      }\n    }\n  }\n} catch(e) {\n  saXml = null;\n}\n\nreturn [{\n  json: {\n    stock,\n    sa_xml: saXml,\n    sa_ok:  !!saXml\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "dabe6865-3a54-4f8f-93bf-e13a25925cb2",
      "name": "return the news",
      "type": "n8n-nodes-base.code",
      "position": [
        800,
        2144
      ],
      "parameters": {
        "jsCode": "return [{\n  json: {\n    stock:                   $json.stock ?? \"\",\n    seekingAlphaWindowHours: 96,\n    seekingAlphaCount:       0,\n    seekingAlphaHasRecent:   false,\n    seekingAlphaRecentCount: 0,\n    seekingAlphaNews:        [],\n    seekingAlphaNewsText:    null,\n    seekingAlphaLatestDate:  null,\n    seekingAlphaError:       $json.sa_error ?? \"No XML to parse\"\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "148c44a2-cae7-41b9-916d-95b1231078bb",
      "name": "Final version of news",
      "type": "n8n-nodes-base.code",
      "position": [
        1024,
        2048
      ],
      "parameters": {
        "jsCode": "// ---- CONFIG ----\nconst windowHours  = 96;\nconst fallbackCount = 3;\nconst now    = Date.now();\nconst cutoff = now - windowHours * 3600 * 1000;\n\n// ---- GET INPUT ----\nconst input = $input.first().json;\n\nconst stock = input?.stock ?? input?.ticker ?? \"\";\n\n// ---- INPUT PATH ----\nconst items =\n  input?.rss?.channel?.item ??\n  input?.channel?.item ??\n  input?.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 ----\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 ----\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 ----\nconst selected  = recent.length > 0 ? recent : dedupedAll.slice(0, fallbackCount);\nconst hasRecent = recent.length > 0;\n\n// ---- CREATE COMPACT TEXT ----\nconst seekingAlphaNewsText = selected.length > 0\n  ? selected.map(n => {\n      const shortSnippet = n.snippet.length > 240\n        ? n.snippet.slice(0, 240) + \"\u2026\"\n        : n.snippet;\n      return `- ${n.publishedAt} | ${n.title}\\n  ${n.link}\\n  Snippet: ${shortSnippet}`;\n    }).join(\"\\n\\n\")\n  : null;\n\nreturn [{\n  json: {\n    stock,\n    seekingAlphaWindowHours: windowHours,\n    seekingAlphaHasRecent:   hasRecent,\n    seekingAlphaRecentCount: recent.length,\n    seekingAlphaCount:       selected.length,\n    seekingAlphaNews:        selected.map(({ publishedAtMs, ...rest }) => rest),\n    seekingAlphaNewsText,\n    seekingAlphaLatestDate:  dedupedAll[0]?.publishedAt ?? null\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "35e3f1cb-39ed-4ee8-ba67-c07b544c8a31",
      "name": "Clean Up ChatGPT",
      "type": "n8n-nodes-base.code",
      "position": [
        2448,
        1072
      ],
      "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": "fb32f4fd-27f3-4191-b6b9-aaf31dc1f5ab",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -768,
        1792
      ],
      "parameters": {
        "color": 7,
        "width": 2728,
        "height": 732,
        "content": "# 2. \ud83d\udcf0 PHASE 2B \u2014 NEWS & SENTIMENT SCRAPING\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nFor each stock, the workflow fetches the latest news \nheadlines and sentiment signals from Seeking Alpha via \nthe FMP /stable/news/stock endpoint.\n\nThe top recent articles are extracted and passed as \na news context block into the AI prompt, allowing the \nmodel to factor in:\n  \u2022 Earnings surprises / guidance changes\n  \u2022 Analyst upgrades or downgrades\n  \u2022 Macro or sector headwinds\n  \u2022 Legal, regulatory, or ESG risks\n\nNews sentiment feeds directly into the AI confidence \nscoring:\n  +10 if POSITIVE sentiment detected\n  \u221215 if NEGATIVE sentiment detected\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2699\ufe0f  SETUP REQUIRED\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2022 No Seeking Alpha account needed \u2014 data is sourced \n  through the FMP news aggregation layer"
      },
      "typeVersion": 1
    },
    {
      "id": "3b0140cf-887b-48c4-a8d2-e6fcfd49cb7b",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "position": [
        7968,
        1536
      ],
      "parameters": {
        "amount": 30
      },
      "typeVersion": 1.1
    },
    {
      "id": "f929fe77-17c6-4401-a1bc-1af59423e909",
      "name": "Send a text message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -1152,
        1152
      ],
      "parameters": {
        "text": "={{ $json.summary_message }}",
        "chatId": "123456789",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "19faf08a-4096-4564-a8b4-c4d5411016db",
      "name": "Merge6",
      "type": "n8n-nodes-base.merge",
      "position": [
        3920,
        1168
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "ea4b2dc7-568f-45e0-a6bc-89f37b28c5e4",
      "name": "Clean up Gemini",
      "type": "n8n-nodes-base.code",
      "position": [
        2448,
        1536
      ],
      "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": "9fdf4d46-3b28-4530-80ca-b213e8f67772",
      "name": "Merge7",
      "type": "n8n-nodes-base.merge",
      "position": [
        4368,
        1296
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepEverything",
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "f2bc794c-1313-4b41-bee2-a9c87ae6fcf2",
      "name": "Tide Breaker - Bull",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "maxTries": 2,
      "position": [
        3344,
        976
      ],
      "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 or mixed news as NEUTRAL unless there is a clear, evidence-backed positive catalyst\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\"revenue_ttm\":{{ $json.revenue_ttm }},\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)\nOther seekingAlphaNewsText\nMarket sentiment\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.\nEvaluate debt using total_debt_last_4q and net_debt_latest.\nEvaluate margin stability using gross_margin_last_4q and operating_margin_ttm.\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:\nimplied_pe = current_price / eps_current\nElse:\nimplied_pe = null\n\nEPS VALIDITY RULE:\nIf implied_pe is not null AND implied_pe > 60:\nEPS is considered less reliable as a primary valuation anchor.\n\u2192 Prefer growth-based valuation\n\u2192 EPS multiple can still be used, but:\n - must be capped at 25\u00d7\n - should not be the sole driver of pt_base\n\nB) PHASE CLASSIFICATION (MANDATORY \u2014 must pick one)\n\n\"mature_profitable\":   eps_current > 0 AND (implied_pe \u2264 60 OR (implied_pe > 60 AND operating_margin_ttm \u2265 15))\n\"growth_transition\": eps_current \u2264 0  OR (implied_pe > 60 AND operating_margin_ttm < 15)\n  \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\nUse the graham_number field provided directly.\nIf graham_number is null \u2192 Graham skipped.\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) <= 25\nAND sector \u2260 \"Financials\" \n  \nIf bvps_current < 5.00 OR (current_price / bvps_current) > 25:\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.\nPrimary valuation logic depends on phase.\nIF phase = mature_profitable:\nIf EPS positive:\npt_base = eps_current \u00d7 selected_multiple.\n\nSelect multiple from these RANGES \u2014 as bull, pick the TOP of each range:\nselected_multiple=\n  revenue_growth_yoy > 20%  \u2192 use between 30\u00d7 and 35\u00d7 (bull: pick 35\u00d7)\n  revenue_growth_yoy 5\u201320%  \u2192 use between 25\u00d7 and 30\u00d7 (bull: pick 30\u00d7)\n  revenue_growth_yoy < 5%   \u2192 use between 20\u00d7 and 25\u00d7 (bull: pick 25\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 < 25: raise to 30\u00d7\nIf sector = \"Utilities\" AND selected_multiple > 25: cap at 25\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 30\u00d7\n  Add note: \"Bull margin floor applied.\"\n\nIf Graham usable:\npt_base = max(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor \u2014 as bull, pick the HIGHER multiplier:\n\nLet g = revenue_growth_yoy\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.1\n\nD2) NEWS-TO-BASE ADJUSTMENT (MODEST, MANDATORY)\n\nAfter pt_base is calculated, apply a small thesis/news adjustment to pt_base itself.\n\nClassify news impact from seekingAlphaNewsText as one of:\n\nSTRONG_POSITIVE:\n  clear earnings beat + guidance raise,\n  major contract / major customer,\n  regulatory approval,\n  clear demand acceleration,\n  strong competitive win\n\nMILD_POSITIVE:\n  earnings beat without major guidance change,\n  constructive analyst reaction,\n  modestly favorable product/commercial update\n\nNEUTRAL:\n  mixed, limited, or no material news\n\nMILD_NEGATIVE:\n  small miss,\n  cautious guidance,\n  moderate competitive pressure,\n  execution concerns without thesis break\n\nSTRONG_NEGATIVE:\n  regulatory/legal action,\n  major guidance cut,\n  serious demand deterioration,\n  leadership shock,\n  explicit thesis break\n\nApply:\nSTRONG_POSITIVE  \u2192 pt_base = pt_base \u00d7 1.05\nMILD_POSITIVE    \u2192 pt_base = pt_base \u00d7 1.025\nNEUTRAL          \u2192 no change\nMILD_NEGATIVE    \u2192 pt_base = pt_base \u00d7 0.975\nSTRONG_NEGATIVE  \u2192 pt_base = pt_base \u00d7 0.95\n\nRules:\n- News adjustment must never exceed \u00b15%.\n- Do not apply this if seekingAlphaNewsText is null or empty.\n- If news is mixed, default to NEUTRAL unless a clear thesis-changing catalyst exists.\n- Round pt_base to 2 decimals after applying adjustment.\n\nE) FINAL SAFETY RULES\n\npt_base must never be zero.\nIf pt_base cannot be determined \u2192 pt_base = current_price.\npt_base must not exceed current_price \u00d7 5 unless revenue_growth_yoy > 60.\nRound pt_base to 2 decimals.\n\nNEWS & THESIS IMPACT ANALYSIS (REQUIRED)\n\nAnalyze seekingAlphaNewsText.\nExtract:\n\nregulatory risks\nlegal risks\nearnings/guidance changes\ncompetitive pressure\nthesis change signals\nDetermine if thesis strengthened or weakened.\n\nANTI-THESIS (RISK ADJUSTMENT)\n\nIdentify TWO biggest risks using priority:\nSeeking Alpha risks\nFinancial deterioration\nGeneral news\n\nF-Score Handling:\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\n25% \u2192 moderate risk\n35% \u2192 weak fundamentals or uncertainty\n\nPremium selection:\n\n15% \u2192 slow growth\n25% \u2192 moderate growth\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.\n\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: pt_base >= current_price \u00d7 1.08 AND (f_score >= 5 OR (operating_margin_ttm > 15 AND revenue_growth_yoy > 10 AND net_cash_flag = true))\nSELL: pt_base \u2264 -15% downside OR (f_score_data_ok true AND f_score \u2264 2)\nOtherwise HOLD.\n\nCONFIDENCE SCORING:\n\nStart at 65.\n\nPOSITIVE ADJUSTMENTS:\n+10 if revenue_growth_yoy > 20%.\n+5  if revenue_growth_yoy between 10\u201320% (moderate growth still valuable)\n+10 if strong balance sheet (net_cash_flag true OR net_debt_latest < revenue_ttm \u00d7 0.5)\n+5 if f_score \u2265 7.\n+3  if f_score between 5\u20136 (decent quality, partial credit)\n+5  if fcf_ttm > 0 (positive free cash flow \u2014 capital efficiency signal)\n+5 if implied_pe > 0 AND sector_median_pe > 0 AND implied_pe < sector_median_pe \u00d7 0.8\n    (stock is trading cheap vs sector peers \u2014 valuation support)\n+10 if news sentiment = POSITIVE\n\nNEGATIVE ADJUSTMENTS:\n-5  if implied_pe > 0 AND sector_median_pe > 0 \n    AND implied_pe > sector_median_pe \u00d7 1.3 (stretched valuation)\n-10 if operating margins declining AND operating_margin_ttm < 10%\n    (only penalise if margin is actually weak, not just slightly lower)\n-10 if high leverage: net_debt_latest > 0 AND \n    net_debt_latest > revenue_ttm \u00d7 1.5\n    (penalise only structurally overleveraged companies)\n-20 if major regulatory/legal risk explicitly identified in news\n-10 if f_score_data_ok is false (data quality issue)\n-10 if eps_current < 0 (loss-making, path to profitability unproven)\n-10 if news sentiment = NEGATIVE\n\nClamp between 20 and 90.\n\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\"verdict\": \"<BUY|HOLD|SELL>\",\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. SECTOR PE: implied X vs sector Y \u2014 cheap/fair/expensive\n  5. PRIMARY RISK: single biggest threat to the thesis\n  6. PRICING POWER: high/moderate/low with one supporting data point\n  7. NEWS SIGNAL: POSITIVE/NEGATIVE/NEUTRAL and what drove it\",\n  \"pt_eps\": <number or null>,\n\"phase\": \"mature_profitable or growth_transition\",\n\"implied_pe\": <number or null>\n}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.1
    },
    {
      "id": "c14200c3-929f-45af-9ae0-57d9de895a2a",
      "name": "Tide Breaker - Bear",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "maxTries": 2,
      "position": [
        3344,
        1168
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-pro",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=YOUR 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 35% discount when fundamentals are arguable\n- Classify ambiguous or mixed news as NEUTRAL unless there is a clear, evidence-backed negative catalyst\n- Your pt_base = minimum justifiable FAIR 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\"revenue_ttm\":{{ $json.revenue_ttm }},\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.\nNever assume missing data.\nNever assume share splits, ADR ratios, or alternative listings.\n\nIMPORTANT \u2014 NEWS SOURCE PRIORITY:\n\nSeeking Alpha articles (highest credibility signal)\nseekingAlphaNewsText\nMarket sentiment\nPrefer Seeking Alpha for thesis framing, but verify with financial data.\nIf conflict exists, flag uncertainty.\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.\nEvaluate debt using total_debt_last_4q and net_debt_latest.\nEvaluate margin stability using gross_margin_last_4q and operating_margin_ttm.\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:\nimplied_pe = current_price / eps_current\nElse:\nimplied_pe = null\n\nEPS VALIDITY RULE:\nIf implied_pe is not null AND implied_pe > 60:\nEPS is considered less reliable as a primary valuation anchor.\n\u2192 Prefer growth-based valuation\n\u2192 EPS multiple can still be used, but:\n - must be capped at 25\u00d7\n - should not be the sole driver of pt_base\n \nB) PHASE CLASSIFICATION (MANDATORY \u2014 must pick one)\n\n\"mature_profitable\":   eps_current > 0 AND (implied_pe \u2264 60 OR (implied_pe > 60 AND operating_margin_ttm \u2265 15))\n\"growth_transition\": eps_current \u2264 0  OR (implied_pe > 60 AND operating_margin_ttm < 15)\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\nUse the graham_number field provided directly.\nIf graham_number is null \u2192 Graham skipped.\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) <= 25\nAND sector \u2260 \"Financial Services\" \n  \nIf bvps_current < 5.00 OR (current_price / bvps_current) > 25:\n  Graham = null \u2192 skip Graham entirely \u2192 pt_base = pt_eps only\n  Add to rationale: \"Graham skipped: BVPS distorted by buybacks.\"\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 selected_multiple.\n\nSelect multiple from these RANGES \u2014 as bear, pick the BOTTOM of each range:\nselected_multiple=\n  revenue_growth_yoy > 20%  \u2192 use between 20\u00d7 and 25\u00d7 (bear: pick 20\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 Defensive\" AND selected multiple < 16: raise to 16\u00d7\nIf sector = \"Technology\" AND selected multiple < 14: raise to 14\u00d7\nIf sector = \"Utilities\" AND selected multiple > 14: cap at 14\u00d7\nIf sector = \"Financial Services\": skip EPS multiple, use pt_base = bvps_current \u00d7 0.9\nIf sector = null or missing: no adjustment.\n\nMARGIN OVERRIDE: Bear does NOT apply upward margin overrides.\n\nIf Graham usable:\npt_base = max(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor \u2014 as bear, pick the LOWER multiplier:\n\nLet g = revenue_growth_yoy\nIf g is null  \u2192 pt_base = current_price \u00d7 0.9\nIf g > 60     \u2192 pt_base = current_price \u00d7 1.6\nIf g > 30     \u2192 pt_base = current_price \u00d7 1.3\nIf g > 15     \u2192 pt_base = current_price \u00d7 1.1\nOtherwise     \u2192 pt_base = current_price \u00d7 0.9\n\nD2) NEWS-TO-BASE ADJUSTMENT (MODEST, MANDATORY)\n\nAfter pt_base is calculated, apply a small thesis/news adjustment to pt_base itself.\n\nClassify news impact from seekingAlphaNewsText as one of:\n\nSTRONG_POSITIVE:\n  clear earnings beat + guidance raise,\n  major contract / major customer,\n  regulatory approval,\n  clear demand acceleration,\n  strong competitive win\n\nMILD_POSITIVE:\n  earnings beat without major guidance change,\n  constructive analyst reaction,\n  modestly favorable product/commercial update\n\nNEUTRAL:\n  mixed, limited, or no material news\n\nMILD_NEGATIVE:\n  small miss,\n  cautious guidance,\n  moderate competitive pressure,\n  execution concerns without thesis break\n\nSTRONG_NEGATIVE:\n  regulatory/legal action,\n  major guidance cut,\n  serious demand deterioration,\n  leadership shock,\n  explicit thesis break\n\nApply:\nSTRONG_POSITIVE  \u2192 pt_base = pt_base \u00d7 1.05\nMILD_POSITIVE    \u2192 pt_base = pt_base \u00d7 1.025\nNEUTRAL          \u2192 no change\nMILD_NEGATIVE    \u2192 pt_base = pt_base \u00d7 0.975\nSTRONG_NEGATIVE  \u2192 pt_base = pt_base \u00d7 0.95\n\nRules:\n- News adjustment must never exceed \u00b15%.\n- Do not apply this if seekingAlphaNewsText is null or empty.\n- If news is mixed, default to NEUTRAL unless a clear thesis-changing catalyst exists.\n- Round pt_base to 2 decimals after applying adjustment.\n\nE) FINAL SAFETY RULES\n\npt_base must never be zero.\nIf pt_base cannot be determined \u2192 pt_base = current_price.\npt_base must not exceed current_price \u00d7 5 unless revenue_growth_yoy > 60.\nRound pt_base to 2 decimals.\n\nNEWS & THESIS IMPACT ANALYSIS (REQUIRED)\n\nAnalyze seekingAlphaNewsText.\nExtract:\nregulatory risks\nlegal risks\nearnings/guidance changes\ncompetitive pressure\nthesis change signals\nDetermine if thesis strengthened or weakened.\n\nANTI-THESIS (RISK ADJUSTMENT)\n\nIdentify TWO biggest risks using priority:\nSeeking Alpha risks\nFinancial deterioration\nGeneral news\n\nF-Score Handling:\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\n25% \u2192 moderate risk\n35% \u2192 weak fundamentals or uncertainty\n\nPremium selection:\n\n15% \u2192 slow growth\n25% \u2192 moderate growth\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: pt_base >= current_price \u00d7 1.08 AND (f_score >= 5 OR (operating_margin_ttm > 15 AND revenue_growth_yoy > 10 AND net_cash_flag = true))\nSELL: pt_base <= current_price \u00d7 0.90 OR (f_score_data_ok true AND f_score \u2264 2)\nOtherwise HOLD.\n\nCONFIDENCE SCORING:\n\nStart at 65.\n\n\nPOSITIVE ADJUSTMENTS:\n+10 if revenue_growth_yoy > 20%.\n+5  if revenue_growth_yoy between 10\u201320% (moderate growth still valuable)\n+10 if strong balance sheet (net_cash_flag true OR net_debt_latest < revenue_ttm \u00d7 0.5)\n+5 if f_score \u2265 7.\n+3  if f_score between 5\u20136 (decent quality, partial credit)\n+5  if fcf_ttm > 0 (positive free cash flow \u2014 capital efficiency signal)\n+5 if implied_pe > 0 AND sector_median_pe > 0 AND implied_pe < sector_median_pe \u00d7 0.8\n    (stock is trading cheap vs sector peers \u2014 valuation support)\n+10 if news sentiment = POSITIVE\n\nNEGATIVE ADJUSTMENTS:\n-5  if implied_pe > 0 AND sector_median_pe > 0 \n    AND implied_pe > sector_median_pe \u00d7 1.3 (stretched valuation)\n-10 if operating margins declining AND operating_margin_ttm < 10%\n    (only penalise if margin is actually weak, not just slightly lower)\n-10 if high leverage: net_debt_latest > 0 AND \n    net_debt_latest > revenue_ttm \u00d7 1.5\n    (penalise only structurally overleveraged companies)\n-20 if major regulatory/legal risk explicitly identified in news\n-10 if f_score_data_ok is false (data quality issue)\n-10 if eps_current < 0 (loss-making, path to profitability unproven)\n-10 if news sentiment = NEGATIVE\n\nClamp between 20 and 90.\n\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.gemini_result.f_score }},\n\"confidence\": (20-90),\n\"verdict\": \"<BUY|HOLD|SELL>\",\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. SECTOR PE: implied X vs sector Y \u2014 cheap/fair/expensive\n  5. PRIMARY RISK: single biggest threat to the thesis\n  6. PRICING POWER: high/moderate/low with one supporting data point\n  7. NEWS SIGNAL: POSITIVE/NEGATIVE/NEUTRAL and what drove it\",\n  \"pt_eps\": <number or null>,\n\"phase\": \"mature_profitable or growth_transition\",\n\"implied_pe\": <number or null>\n}"
            }
          ]
        },
        "jsonOutput": true,
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1.1
    },
    {
      "id": "6fb270a0-71dc-4a73-b94b-93a5610c990c",
      "name": "Needs Tiebreaker?",
      "type": "n8n-nodes-base.if",
      "position": [
        3120,
        1200
      ],
      "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": "cb772b83-fafe-43d0-9aa2-0d709fdfc8df",
      "name": "Clean Up Results from First Round",
      "type": "n8n-nodes-base.code",
      "position": [
        2896,
        1200
      ],
      "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(rationale) {\n  const r = (rationale || '').toUpperCase();\n  const m = r.match(/VERDICT:\\s*(BUY|SELL|HOLD)/);\n  if (m) return m[1];\n  if (r.includes('BUY'))  return 'BUY';\n  if (r.includes('SELL')) return 'SELL';\n  return 'HOLD';\n}\n\nconst v1 = chatgpt?.verdict || getVerdict(chatgpt?.rationale);\nconst v2 = gemini?.verdict  || getVerdict(gemini?.rationale);\n\nconst pt1 = chatgpt?.pt_base ? Number(chatgpt.pt_base) : null;\nconst pt2 = gemini?.pt_base  ? Number(gemini.pt_base)  : null;\n\nconst gap_pct = (pt1 !== null && pt2 !== null && 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;\n\nconst conf1 = Number(chatgpt?.confidence || 0);\nconst conf2 = Number(gemini?.confidence  || 0);\nconst both_hold_high_conf = (v1 === \"HOLD\" && v2 === \"HOLD\" && conf1 >= 65 && conf2 >= 65);\n\nconst needs_tiebreaker = (!verdicts_agree || high_uncertainty) && !both_hold_high_conf;\n\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    conf_chatgpt:    conf1,   // \u2190 add\n    conf_gemini:     conf2,   // \u2190 add\n    gap_pct,\n    verdicts_agree,\n    high_uncertainty,\n    both_hold_high_conf,      // \u2190 add\n    needs_tiebreaker\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "86991133-2c30-404f-95e8-09dd763ddc58",
      "name": "Clean up Chatgpt 2",
      "type": "n8n-nodes-base.code",
      "position": [
        3696,
        976
      ],
      "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    ??  $('Clean Up Results from First Round').first().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": "2ae885b1-140f-41f9-9fee-0ab67dce7e4c",
      "name": "Clean up Gemini 2",
      "type": "n8n-nodes-base.code",
      "position": [
        3696,
        1168
      ],
      "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    ?? $('Clean Up Results from First Round').first().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": "41aacb56-f185-4492-89b1-22f566702178",
      "name": "First round Gemini",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "onError": "continueErrorOutput",
      "maxTries": 2,
      "position": [
        2064,
        1536
      ],
      "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\"revenue_ttm\": {{ $json.revenue_ttm }},\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.\nIf any field is null \u2192 treat as missing.\nNever assume missing data.\nNever assume share splits, ADR ratios, or alternative listings.\n\nIMPORTANT \u2014 NEWS SOURCE PRIORITY:\n\nSeeking Alpha articles (highest credibility signal)\nMarket sentiment\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\nEvaluate revenue_last_4q and net_income_last_4q.\nEvaluate debt using total_debt_last_4q and net_debt_latest.\nEvaluate margin stability using gross_margin_last_4q and operating_margin_ttm.\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:\nimplied_pe = current_price / eps_current\nElse:\nimplied_pe = null\n\nEPS VALIDITY RULE:\nIf implied_pe is not null AND implied_pe > 60:\nEPS is considered less reliable as a primary valuation anchor.\n\u2192 Prefer growth-based valuation\n\u2192 EPS multiple can still be used, but:\n - must be capped at 25\u00d7\n - should not be the sole driver of pt_base\n\t  \nB) PHASE CLASSIFICATION (MANDATORY \u2014 must pick one)\n\nmature_profitable:\n  eps_current > 0 AND (\n    implied_pe \u2264 60\n    OR (implied_pe > 60 AND operating_margin_ttm \u2265 15)\n  )\n\ngrowth_transition:\n  eps_current \u2264 0\n  OR (implied_pe > 60 AND operating_margin_ttm < 15)\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\t\nC) Graham Number (secondary only)\n\nUse the graham_number field provided directly.\nIf graham_number is null \u2192 Graham skipped.\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) <= 25\nAND sector \u2260 \"Financial Services\" \n  \nIf bvps_current < 5.00 OR (current_price / bvps_current) > 25:\n  Graham = null \u2192 skip Graham entirely \u2192 pt_base = pt_eps only\n  Add to rationale: \"Graham skipped: BVPS not a reliable valuation anchor for this business model.\"\n\nDo NOT use Graham for growth_transition stocks.\n\nD) BASE TARGET CONSTRUCTION (pt_base)\n\nPrimary valuation logic depends on phase.\nIF phase = mature_profitable:\nIf EPS positive:\npt_base = eps_current \u00d7 selected_multiple:\nselected_multiple =\n30\u00d7 if revenue_growth_yoy > 20%\n22\u00d7 if revenue_growth_yoy 5\u201320%\n15\u00d7 otherwise\n\nSECTOR MULTIPLE FLOORS (apply AFTER selecting the base multiple above):\nIf sector = \"Consumer Defensive\" 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 = \"Financial Services\": 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 < 20:\n  override multiple to 22\u00d7\n  AND sector \u2260 \"Utilities\":\n  Add note: \"Multiple floor applied: high-margin business.\"\n\nIf Graham usable:\npt_base = max(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor only (EPS multiples de-emphasized):\n\nLet g = revenue_growth_yoy\nIf g is null \u2192 pt_base = current_price\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.5\nOtherwise \u2192 pt_base = current_price \u00d7 1.1\n\nQUALITY ADJUSTMENT (MANDATORY):\nIf operating_margin_ttm < 0:\n  downgrade multiplier by one tier\n  (e.g., 2.0 \u2192 1.6, 1.6 \u2192 1.3, 1.3 \u2192 1.0)\nIf fcf_ttm is negative:\n  apply additional -0.1 multiplier reduction\nFinal:\npt_base = current_price \u00d7 adjusted_mult\n\nD2) NEWS-TO-BASE ADJUSTMENT (MODEST, MANDATORY)\n\nAfter pt_base is calculated, apply a small thesis/news adjustment to pt_base itself.\n\nClassify news impact from seekingAlphaNewsText as one of:\n\nSTRONG_POSITIVE:\n  clear earnings beat + guidance raise,\n  major contract / major customer,\n  regulatory approval,\n  clear demand acceleration,\n  strong competitive win\n\nMILD_POSITIVE:\n  earnings beat without major guidance change,\n  constructive analyst reaction,\n  modestly favorable product/commercial update\n\nNEUTRAL:\n  mixed, limited, or no material news\n\nMILD_NEGATIVE:\n  small miss,\n  cautious guidance,\n  moderate competitive pressure,\n  execution concerns without thesis break\n\nSTRONG_NEGATIVE:\n  regulatory/legal action,\n  major guidance cut,\n  serious demand deterioration,\n  leadership shock,\n  explicit thesis break\n\nApply:\nSTRONG_POSITIVE  \u2192 pt_base = pt_base \u00d7 1.05\nMILD_POSITIVE    \u2192 pt_base = pt_base \u00d7 1.025\nNEUTRAL          \u2192 no change\nMILD_NEGATIVE    \u2192 pt_base = pt_base \u00d7 0.975\nSTRONG_NEGATIVE  \u2192 pt_base = pt_base \u00d7 0.95\n\nRules:\n- News adjustment must never exceed \u00b15%.\n- Do not apply this if seekingAlphaNewsText is null or empty.\n- If news is mixed, default to NEUTRAL unless a clear thesis-changing catalyst exists.\n- Round pt_base to 2 decimals after applying adjustment.\n\nE) FINAL SAFETY RULES\n\npt_base must never be zero.\nIf pt_base cannot be determined \u2192 pt_base = current_price.\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)\nSeeking Alpha articles (seekingAlphaNewsText \u2014 only source available).\n\nExtract:\nregulatory risks\nlegal risks\nearnings/guidance changes\ncompetitive pressure\nthesis change signals\nDetermine if thesis strengthened or weakened.\n\nANTI-THESIS (RISK ADJUSTMENT)\nIdentify TWO biggest risks using priority:\nSeeking Alpha risks\nFinancial deterioration\nGeneral news\n\nF-Score Handling:\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\n25% \u2192 moderate risk\n35% \u2192 weak fundamentals or uncertainty\n\nPremium selection:\n\n15% \u2192 slow growth\n25% \u2192 moderate growth\n40% \u2192 high growth (>20% revenue growth)\n\nNEWS SENTIMENT ADJUSTMENT (apply after selecting base discount/premium):\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.\n\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:\npt_base \u2265 current_price \u00d7 1.08 and (f_score \u2265 5 OR (operating_margin_ttm > 15 AND (net_cash_flag = true OR net_debt_latest < revenue_ttm * 0.5) AND (fcf_ttm > 0 OR revenue_growth_yoy > 15)))\n\nSELL:\npt_base <= current_price * 0.85 OR (f_score_data_ok true AND f_score \u2264 2)\n\nOtherwise HOLD.\n\nCONFIDENCE SCORING:\n\nStart at 65.\n\nPOSITIVE ADJUSTMENTS:\n+10 if revenue_growth_yoy > 20%.\n+5  if revenue_growth_yoy between 10\u201320% (moderate growth still valuable)\n+10 if strong balance sheet (net_cash_flag true OR net_debt_latest < revenue_ttm \u00d7 0.5)\n+5 if f_score \u2265 7.\n+3  if f_score between 5\u20136 (decent quality, partial credit)\n+5  if fcf_ttm > 0 (positive free cash flow \u2014 capital efficiency signal)\n+5 if implied_pe > 0 AND sector_median_pe > 0 AND implied_pe < sector_median_pe \u00d7 0.8\n    (stock is trading cheap vs sector peers \u2014 valuation support)\n+10 if news sentiment = POSITIVE\n\nNEGATIVE ADJUSTMENTS:\n-5  if implied_pe > 0 AND sector_median_pe > 0 \n    AND implied_pe > sector_median_pe \u00d7 1.3 (stretched valuation)\n-10 if operating margins declining AND operating_margin_ttm < 10%\n    (only penalise if margin is actually weak, not just slightly lower)\n-10 if high leverage: net_debt_latest > 0 AND \n    net_debt_latest > revenue_ttm \u00d7 1.5\n    (penalise only structurally overleveraged companies)\n-20 if major regulatory/legal risk explicitly identified in news\n-10 if f_score_data_ok is false (data quality issue)\n-10 if eps_current < 0 (loss-making, path to profitability unproven)\n-10 if news sentiment = NEGATIVE\n\nClamp between 20 and 90.\n\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\"verdict\": \"<BUY|HOLD|SELL>\",\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. SECTOR PE: implied X vs sector Y \u2014 cheap/fair/expensive\n  5. PRIMARY RISK: single biggest threat to the thesis\n  6. PRICING POWER: high/moderate/low with one supporting data point\n  7. NEWS SIGNAL: POSITIVE/NEGATIVE/NEUTRAL and what drove it\"\n}"
            }
          ]
        },
        "jsonOutput": true,
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1.1
    },
    {
      "id": "16607274-113d-4283-90bf-86ea0be53b31",
      "name": "First round ChatGPT",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "onError": "continueErrorOutput",
      "maxTries": 2,
      "position": [
        2080,
        1072
      ],
      "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\"revenue_ttm\": {{ $json.revenue_ttm }},\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.\nIf any field is null \u2192 treat as missing.\nNever assume missing data.\nNever assume share splits, ADR ratios, or alternative listings.\n\nIMPORTANT \u2014 NEWS SOURCE PRIORITY:\n\nSeeking Alpha articles (highest credibility signal)\nMarket sentiment\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\nEvaluate revenue_last_4q and net_income_last_4q.\nEvaluate debt using total_debt_last_4q and net_debt_latest.\nEvaluate margin stability using gross_margin_last_4q and operating_margin_ttm.\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:\nimplied_pe = current_price / eps_current\nElse:\nimplied_pe = null\n\nEPS VALIDITY RULE:\nIf implied_pe is not null AND implied_pe > 60:\nEPS is considered less reliable as a primary valuation anchor.\n\u2192 Prefer growth-based valuation\n\u2192 EPS multiple can still be used, but:\n - must be capped at 25\u00d7\n - should not be the sole driver of pt_base\n\t  \nB) PHASE CLASSIFICATION (MANDATORY \u2014 must pick one)\n\nmature_profitable:\n  eps_current > 0 AND (\n    implied_pe \u2264 60\n    OR (implied_pe > 60 AND operating_margin_ttm \u2265 15)\n  )\n\ngrowth_transition:\n  eps_current \u2264 0\n  OR (implied_pe > 60 AND operating_margin_ttm < 15)\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\t\nC) Graham Number (secondary only)\n\nUse the graham_number field provided directly.\nIf graham_number is null \u2192 Graham skipped.\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) <= 25\nAND sector \u2260 \"Financial Services\" \n  \nIf bvps_current < 5.00 OR (current_price / bvps_current) > 25:\n  Graham = null \u2192 skip Graham entirely \u2192 pt_base = pt_eps only\n  Add to rationale: \"Graham skipped: BVPS not a reliable valuation anchor for this business model.\"\n\nDo NOT use Graham for growth_transition stocks.\n\nD) BASE TARGET CONSTRUCTION (pt_base)\n\nPrimary valuation logic depends on phase.\nIF phase = mature_profitable:\nIf EPS positive:\npt_base = eps_current \u00d7 selected_multiple:\nselected_multiple =\n30\u00d7 if revenue_growth_yoy > 20%\n22\u00d7 if revenue_growth_yoy 5\u201320%\n15\u00d7 otherwise\n\nSECTOR MULTIPLE FLOORS (apply AFTER selecting the base multiple above):\nIf sector = \"Consumer Defensive\" 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 = \"Financial Services\": 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 < 20:\n  override multiple to 22\u00d7\n  AND sector \u2260 \"Utilities\":\n  Add note: \"Multiple floor applied: high-margin business.\"\n\nIf Graham usable:\npt_base = max(pt_base, Graham)\n\nIF phase = growth_transition:\nGrowth-based anchor only (EPS multiples de-emphasized):\n\nLet g = revenue_growth_yoy\nIf g is null \u2192 pt_base = current_price\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.5\nOtherwise \u2192 pt_base = current_price \u00d7 1.1\n\nQUALITY ADJUSTMENT (MANDATORY):\nIf operating_margin_ttm < 0:\n  downgrade multiplier by one tier\n  (e.g., 2.0 \u2192 1.6, 1.6 \u2192 1.3, 1.3 \u2192 1.0)\nIf fcf_ttm is negative:\n  apply additional -0.1 multiplier reduction\nFinal:\npt_base = current_price \u00d7 adjusted_mult\n\nD2) NEWS-TO-BASE ADJUSTMENT (MODEST, MANDATORY)\n\nAfter pt_base is calculated, apply a small thesis/news adjustment to pt_base itself.\n\nClassify news impact from seekingAlphaNewsText as one of:\n\nSTRONG_POSITIVE:\n  clear earnings beat + guidance raise,\n  major contract / major customer,\n  regulatory approval,\n  clear demand acceleration,\n  strong competitive win\n\nMILD_POSITIVE:\n  earnings beat without major guidance change,\n  constructive analyst reaction,\n  modestly favorable product/commercial update\n\nNEUTRAL:\n  mixed, limited, or no material news\n\nMILD_NEGATIVE:\n  small miss,\n  cautious guidance,\n  moderate competitive pressure,\n  execution concerns without thesis break\n\nSTRONG_NEGATIVE:\n  regulatory/legal action,\n  major guidance cut,\n  serious demand deterioration,\n  leadership shock,\n  explicit thesis break\n\nApply:\nSTRONG_POSITIVE  \u2192 pt_base = pt_base \u00d7 1.05\nMILD_POSITIVE    \u2192 pt_base = pt_base \u00d7 1.025\nNEUTRAL          \u2192 no change\nMILD_NEGATIVE    \u2192 pt_base = pt_base \u00d7 0.975\nSTRONG_NEGATIVE  \u2192 pt_base = pt_base \u00d7 0.95\n\nRules:\n- News adjustment must never exceed \u00b15%.\n- Do not apply this if seekingAlphaNewsText is null or empty.\n- If news is mixed, default to NEUTRAL unless a clear thesis-changing catalyst exists.\n- Round pt_base to 2 decimals after applying adjustment.\n\nE) FINAL SAFETY RULES\n\npt_base must never be zero.\nIf pt_base cannot be determined \u2192 pt_base = current_price.\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)\nSeeking Alpha articles (seekingAlphaNewsText \u2014 only source available).\n\nExtract:\nregulatory risks\nlegal risks\nearnings/guidance changes\ncompetitive pressure\nthesis change signals\nDetermine if thesis strengthened or weakened.\n\nANTI-THESIS (RISK ADJUSTMENT)\nIdentify TWO biggest risks using priority:\nSeeking Alpha risks\nFinancial deterioration\nGeneral news\n\nF-Score Handling:\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\n25% \u2192 moderate risk\n35% \u2192 weak fundamentals or uncertainty\n\nPremium selection:\n\n15% \u2192 slow growth\n25% \u2192 moderate growth\n40% \u2192 high growth (>20% revenue growth)\n\nNEWS SENTIMENT ADJUSTMENT (apply after selecting base discount/premium):\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.\n\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:\npt_base \u2265 current_price \u00d7 1.08 and (f_score \u2265 5 OR (operating_margin_ttm > 15 AND (net_cash_flag = true OR net_debt_latest < revenue_ttm * 0.5) AND (fcf_ttm > 0 OR revenue_growth_yoy > 15)))\n\nSELL:\npt_base <= current_price * 0.85 OR (f_score_data_ok true AND f_score \u2264 2)\n\nOtherwise HOLD.\n\nCONFIDENCE SCORING:\n\nStart at 65.\n\nPOSITIVE ADJUSTMENTS:\n+10 if revenue_growth_yoy > 20%.\n+5  if revenue_growth_yoy between 10\u201320% (moderate growth still valuable)\n+10 if strong balance sheet (net_cash_flag true OR net_debt_latest < revenue_ttm \u00d7 0.5)\n+5 if f_score \u2265 7.\n+3  if f_score between 5\u20136 (decent quality, partial credit)\n+5  if fcf_ttm > 0 (positive free cash flow \u2014 capital efficiency signal)\n+5 if implied_pe > 0 AND sector_median_pe > 0 AND implied_pe < sector_median_pe \u00d7 0.8\n    (stock is trading cheap vs sector peers \u2014 valuation support)\n+10 if news sentiment = POSITIVE\n\nNEGATIVE ADJUSTMENTS:\n-5  if implied_pe > 0 AND sector_median_pe > 0 \n    AND implied_pe > sector_median_pe \u00d7 1.3 (stretched valuation)\n-10 if operating margins declining AND operating_margin_ttm < 10%\n    (only penalise if margin is actually weak, not just slightly lower)\n-10 if high leverage: net_debt_latest > 0 AND \n    net_debt_latest > revenue_ttm \u00d7 1.5\n    (penalise only structurally overleveraged companies)\n-20 if major regulatory/legal risk explicitly identified in news\n-10 if f_score_data_ok is false (data quality issue)\n-10 if eps_current < 0 (loss-making, path to profitability unproven)\n-10 if news sentiment = NEGATIVE\n\nClamp between 20 and 90.\n\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\"verdict\": \"<BUY|HOLD|SELL>\",\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. SECTOR PE: implied X vs sector Y \u2014 cheap/fair/expensive\n  5. PRIMARY RISK: single biggest threat to the thesis\n  6. PRICING POWER: high/moderate/low with one supporting data point\n  7. NEWS SIGNAL: POSITIVE/NEGATIVE/NEUTRAL and what drove it\"\n}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.1
    },
    {
      "id": "02c131fe-5ae5-47be-8cc9-af9413c5098c",
      "name": "Alert Filter",
      "type": "n8n-nodes-base.code",
      "position": [
        5200,
        1296
      ],
      "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 = j.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 >= 60;\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": "f59959a9-82d6-4573-854b-80ccb1884269",
      "name": "If Alert?",
      "type": "n8n-nodes-base.if",
      "position": [
        5344,
        1296
      ],
      "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": "a4c6a895-bf37-432c-a582-efd49618c97a",
      "name": "Telegram Alert",
      "type": "n8n-nodes-base.telegram",
      "position": [
        6848,
        912
      ],
      "parameters": {
        "text": "={{ `\ud83d\udcca *SIGNAL CARD \u2014 ${$json.stock}*\\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\n\ud83d\udd35 Verdict: *${$json.alert_verdict || 'N/A'}* | ${$json.conviction || 'N/A'}\\n\ud83d\udcb0 Price: $${$json.current_price || 'N/A'} | Gap: ${$json.gap_pct || 0}%\\n\ud83c\udfaf Targets: Bear $${$json.pt_bear || 'N/A'} | Base $${$json.pt_base || 'N/A'} | Bull $${$json.pt_bull || 'N/A'}\\n\ud83d\udcc8 MA Signal: ${$json.ma_signal || 'N/A'} | Trend: ${$json.trend_tier || 'N/A'}\\n\ud83d\udcc9 RSI(14): ${$json.rsi_14 !== null && $json.rsi_14 !== undefined ? $json.rsi_14 : 'N/A'} \u2014 ${$json.rsi_signal || ''}\\n\ud83e\uddee F-Score: ${$json.f_score !== null && $json.f_score !== undefined ? $json.f_score + '/9' : 'N/A'} | Confidence: ${$json.confidence || 0}%\\n\ud83d\udcc5 Next Earnings: ${$json.next_earnings_date || 'N/A'}\\n\ud83d\udd04 Thesis Reversal: ${$json.thesis_reversal ? 'YES \u26a0\ufe0f' : 'No'}${$json.shares_to_buy ? '\\n\\n\ud83d\udcbc *POSITION SIZING (' + $json.risk_pct_label + ' Rule)*\\n   Buy ' + $json.shares_to_buy + ' shares = $' + $json.position_size_usd + '\\n   Stop: $' + $json.stop_loss_price + ' (' + $json.stop_distance_pct + '% drop) \u2192 Risk: $' + $json.actual_risk_usd + '\\n   Target: $' + $json.pt_base + ' \u2192 Reward: $' + $json.potential_reward_usd + '\\n   R:R = 1:' + ($json.risk_reward_ratio || 'N/A') + ' | Kelly ref: $' + $json.kelly_position_usd : ''}\\n\\n\ud83d\udcdd Rationale:\\n${$json.rationale || 'N/A'}` }}",
        "chatId": "123456789",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "60482230-446f-4392-9454-cbb1e32d82d5",
      "name": "Watchlist Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        6960,
        1200
      ],
      "parameters": {
        "text": "={{ `\u26a0\ufe0f *WATCHLIST (Below 50MA \u2014 not actionable yet)*\n\n\ud83d\udcca *SIGNAL CARD \u2014 ${$json.stock}*\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ud83d\udd35 Verdict: *${$json.alert_verdict || 'N/A'}* | ${$json.conviction || 'N/A'}\n\ud83d\udcb0 Price: $${$json.current_price || 'N/A'} | Model Gap: ${$json.gap_pct || 0}%\n\ud83c\udfaf Targets: Bear $${$json.pt_bear || 'N/A'} | Base $${$json.pt_base || 'N/A'} | Bull $${$json.pt_bull || 'N/A'}\n\ud83d\udcc8 MA Signal: ${$json.ma_signal || 'N/A'} | Trend: ${$json.trend_tier || 'N/A'}\n\ud83d\udcc9 RSI(14): ${$json.rsi_14 !== null && $json.rsi_14 !== undefined ? $json.rsi_14 : 'N/A'} \u2014 ${$json.rsi_signal || ''}\n\ud83e\uddee F-Score: ${$json.f_score !== null && $json.f_score !== undefined ? $json.f_score + '/9' : 'N/A'} | Confidence: ${$json.confidence || 0}%\n\ud83d\udcc5 Next Earnings: ${$json.next_earnings_date || 'N/A'}${$json.earnings_warning ? '\\n\u26a0\ufe0f ' + $json.earnings_warning : ''}\n\n\ud83d\udd04 Thesis Reversal: ${$json.thesis_reversal ? 'YES \u26a0\ufe0f' : 'No'}\n\n\ud83d\udcdd Rationale:\n${$json.rationale || 'N/A'}` }}",
        "chatId": "123456789",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6cfe96c9-3a60-4131-a050-e6b099a98888",
      "name": "Build Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        -1312,
        1184
      ],
      "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.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 *Screener 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": "1b08b48d-ad16-4707-9c05-1be67384de47",
      "name": "Get Previous Verdict",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4816,
        1296
      ],
      "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": "1UwNknqr9d7W0Egq8ahlo9kb40B1BIGZEGdhUhCeM0Ck",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1UwNknqr9d7W0Egq8ahlo9kb40B1BIGZEGdhUhCeM0Ck/edit?usp=drivesdk",
          "cachedResultName": "Sentiments of my stocks for screener"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5,
      "alwaysOutputData": true
    },
    {
      "id": "446ec0dd-57cf-487e-9a20-03e757b4913a",
      "name": "Thesis Reversal Enricher",
      "type": "n8n-nodes-base.code",
      "position": [
        5040,
        1296
      ],
      "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 ? (prevEntry.verdict || extractVerdict(prevEntry.rationale)) : null;\nconst prev_date    = prevEntry ? prevEntry.date : null;\nconst cur_rationale = analysisData.rationale || (analysisData.chatgpt_result || {}).rationale || '';\nconst cur_verdict = analysisData.verdict || extractVerdict(cur_rationale);\n\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": "40abbb54-db68-4b6e-b027-0a76a8910b09",
      "name": "Is Thesis Reversal?",
      "type": "n8n-nodes-base.if",
      "position": [
        7056,
        736
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "reversal-check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.thesis_reversal }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b741bab3-5119-45f9-abdc-91062721189b",
      "name": "Thesis Reversal Alert",
      "type": "n8n-nodes-base.telegram",
      "position": [
        7264,
        560
      ],
      "parameters": {
        "text": "={{ $json.thesis_reversal_msg || '\u26a0\ufe0f Thesis reversal detected for ' + $json.stock + ' \u2014 check sheet for details.' }}",
        "chatId": "={{ $('Send a text message').params.chatId || '' }}",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "147dab4b-82ad-4567-a642-f0968a7164e7",
      "name": "Get Earnings",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 2,
      "position": [
        5920,
        992
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/earnings?symbol={{ $json.stock }}&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "328eb37d-64bd-42ed-8eac-b7bb9c61bbd8",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4304,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 3184,
        "height": 1564,
        "content": "# 4.\ud83d\udea8 PHASE 4 \u2014 STRONG BUY ALERT & RISK ANALYSIS\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nOnly stocks that reach a final verdict of BUY with \nsufficient upside are passed into this phase.\n\nThe workflow performs a final-layer analysis including:\n  \u2022 Technical confirmation (price vs key levels)\n  \u2022 Position sizing suggestion based on confidence score\n  \u2022 Risk/Reward ratio: pt_bear vs pt_bull vs current price\n  \u2022 Stop-loss level derived from pt_bear\n  \u2022 Upside to pt_base and pt_bull in percentage terms\n  \u2022 Composite risk flag: F-Score gate + leverage check \n    + confidence threshold\n\nStocks that pass all gates generate a formatted \nSTRONG BUY alert message with full context for the \ntrader to review before acting.\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2699\ufe0f  SETUP REQUIRED\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2022 No additional APIs required for this phase\n\u2022 Review the IF node thresholds to match your own \n  risk tolerance:\n    - Minimum upside to trigger alert (default: 20%)\n    - Minimum confidence score (default: 65)\n    - F-Score gate (default: \u2265 5, or null allowed)"
      },
      "typeVersion": 1
    },
    {
      "id": "ec7a3533-a366-47cf-996b-84635f055375",
      "name": "FMP_Stock_Screener",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2480,
        1712
      ],
      "parameters": {
        "url": "https://financialmodelingprep.com/stable/company-screener",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "marketCapMoreThan",
              "value": "5+1234567890"
            },
            {
              "name": "volumeMoreThan",
              "value": "5000000"
            },
            {
              "name": "priceMoreThan",
              "value": "10"
            },
            {
              "name": "betaLowerThan",
              "value": "2.0"
            },
            {
              "name": "isActivelyTrading",
              "value": "true"
            },
            {
              "name": "apikey",
              "value": "=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv"
            },
            {
              "name": "country=US"
            },
            {
              "name": "isEtf",
              "value": "false"
            },
            {
              "name": "isFund",
              "value": "false"
            },
            {
              "name": "limit",
              "value": "100"
            },
            {
              "name": "exchange",
              "value": "NYSE,NASDAQ"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "96db604b-3e15-44e3-b79b-3c674561b5ae",
      "name": "Score_and_Prefilter",
      "type": "n8n-nodes-base.code",
      "position": [
        -2160,
        1696
      ],
      "parameters": {
        "jsCode": "const all = $input.all();\nconst stocks = [];\n\nconst ALLOWED_SECTORS = [\n  'Technology',\n  'Healthcare',\n  'Consumer Cyclical',\n  'Financial Services',\n  'Industrials',\n  'Communication Services',\n  'Consumer Defensive',\n  'Energy',\n  'Basic Materials',\n  'Real Estate',\n  'Utilities'\n];\n\nconst MAX_PER_SECTOR = 4;   // sector diversity cap\nconst FINAL_LIMIT    = 20;  // max stocks to pass downstream\n\n// \u2500\u2500 Parse screener results \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfor (const item of all) {\n  const j = item.json;\n  if (!j.symbol || typeof j.symbol !== 'string') continue;\n\n  const sector = j.sector || '';\n  if (sector && !ALLOWED_SECTORS.includes(sector)) continue; // drop disallowed sectors\n\n  stocks.push({\n    stock:      j.symbol,\n    company:    j.companyName  || '',\n    market_cap: Number(j.marketCap          || 0),\n    volume:     Number(j.volume             || 0),\n    price:      Number(j.price              || 0),\n    beta:       Number(j.beta               || 1),\n    sector:     sector,\n    exchange:   j.exchangeShortName         || '',\n    scan_source: 'SCAN_STOCK'\n  });\n}\n\n// \u2500\u2500 Scoring function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Max possible score = 7 points\n//   Volume score  : 0\u20133 pts  (rewards healthy volume without mega-cap bias)\n//   Cap score     : 0\u20132 pts  (sweet-spot $5B\u2013$100B; penalises micro & mega)\n//   Beta score    : 0\u20132 pts  (rewards 0.7\u20131.5; partial credit 0.5\u20130.7 / 1.5\u20132.0)\n\nfunction scoreStock(s) {\n  // Volume: 3 pts at 30M+, linear below\n  const vol_score = Math.min(s.volume / 30_000_000, 1) * 3;\n\n  // Market cap sweet spot: $5B\u2013$100B = 2 pts; above $100B or below $5B = scaled down\n  let cap_score = 0;\n  const cap_b = s.market_cap / 1e9; // in billions\n  if (cap_b >= 5 && cap_b <= 100) {\n    cap_score = 2;\n  } else if (cap_b > 100) {\n    // Gradually reduce above $100B (still liquid but already well-covered)\n    cap_score = Math.max(2 - (cap_b - 100) / 400, 0.5);\n  } else if (cap_b >= 1) {\n    // Below $5B but above $1B: partial credit\n    cap_score = (cap_b / 5) * 1.5;\n  }\n\n  // Beta gradient: 0.7\u20131.5 ideal (2 pts), 0.5\u20130.7 or 1.5\u20132.0 partial (1 pt), else 0\n  let beta_score = 0;\n  if (s.beta >= 0.7 && s.beta <= 1.5) {\n    beta_score = 2;\n  } else if ((s.beta >= 0.5 && s.beta < 0.7) || (s.beta > 1.5 && s.beta <= 2.0)) {\n    beta_score = 1;\n  }\n\n  return vol_score + cap_score + beta_score;\n}\n\n// \u2500\u2500 Sort by score descending \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nstocks.sort((a, b) => scoreStock(b) - scoreStock(a));\n\n// \u2500\u2500 Apply sector diversity cap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 sectorCount = {};\nconst selected    = [];\n\nfor (const s of stocks) {\n  if (selected.length >= FINAL_LIMIT) break;\n\n  const sec = s.sector || 'Unknown';\n  const count = sectorCount[sec] || 0;\n\n  if (count >= MAX_PER_SECTOR) continue; // skip if sector is full\n\n  sectorCount[sec] = count + 1;\n  selected.push(s);\n}\n\n// \u2500\u2500 Return \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nreturn selected.map(item => ({ json: item }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "267327dc-ea5e-4319-acdb-bb3752ec968d",
      "name": "Write_Scan_Opportunities",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -1856,
        1472
      ],
      "parameters": {
        "columns": {
          "value": {
            "beta": "={{ $json.beta }}",
            "date": "={{ new Date().toISOString().slice(0,10) }}",
            "name": "={{ $json.company }}",
            "score": "=",
            "stock": "={{ $json.stock }}",
            "sector": "={{ $json.sector }}",
            "volume": "={{ $json.volume }}",
            "exchange": "={{ $json.exchange }}",
            "market_cap": "={{ $json.market_cap }}",
            "scan_source": "={{ $json.scan_source || '' }}",
            "current_price": "={{ $json.price || '' }}"
          },
          "schema": [
            {
              "id": "date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stock",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "stock",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "scan_source",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "scan_source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "current_price",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "current_price",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "beta",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "beta",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "volume",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "volume",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "market_cap",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "market_cap",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sector",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sector",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "exchange",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "exchange",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "score",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "16VrGz4b29xw5HeAHg3CyS6UK61XYMEIZE0maQCu3PKU",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/16VrGz4b29xw5HeAHg3CyS6UK61XYMEIZE0maQCu3PKU/edit?usp=drivesdk",
          "cachedResultName": "Scan_Opportunities"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "88344a29-eee4-4fae-bb0b-f3d7ae4a9bba",
      "name": "Risk_Manager",
      "type": "n8n-nodes-base.code",
      "position": [
        6256,
        1232
      ],
      "parameters": {
        "jsCode": "const j = $input.first().json;\n\nconst account_size = Number($vars?.ACCOUNT_SIZE || 20000);\nconst risk_pct = Number($vars?.RISK_PER_TRADE_PCT || 2) / 100;\nconst max_position_pct = 0.10;\n\nconst current_price = Number(j.current_price || 0);\nconst pt_bear = Number(j.pt_bear || 0);\nconst pt_base = Number(j.pt_base || 0);\nconst pt_bull = Number(j.pt_bull || 0);\nconst confidence = Number(j.confidence || 50);\n\n// Only size for BUY signals with valid stop\nif (current_price <= 0 || pt_bear <= 0 || pt_bear >= current_price || j.alert_verdict !== 'BUY') {\n  return [{ json: {\n    ...j,\n    position_size_usd: null,\n    shares_to_buy: null,\n    stop_loss_price: pt_bear > 0 ? parseFloat(pt_bear.toFixed(2)) : null,\n    risk_reward_ratio: null,\n    kelly_position_usd: null,\n    risk_note: j.alert_verdict !== 'BUY' ? 'Position sizing: N/A (not a BUY signal)' : 'Position sizing skipped: invalid stop level'\n  }}];\n}\n\nconst stop_distance = current_price - pt_bear;\nconst stop_distance_pct = stop_distance / current_price;\nconst risk_amount = account_size * risk_pct;\n\nlet raw_position_usd = risk_amount / stop_distance_pct;\nconst max_position_usd = account_size * max_position_pct;\nconst capped = raw_position_usd > max_position_usd;\nconst position_usd = Math.min(raw_position_usd, max_position_usd);\n\nconst shares_to_buy = Math.floor(position_usd / current_price);\nif (shares_to_buy === 0) {\n  return [{ json: {\n    ...j,\n    position_size_usd:  null,\n    shares_to_buy:      0,\n    stop_loss_price:    parseFloat(pt_bear.toFixed(2)),\n    risk_reward_ratio:  null,\n    kelly_position_usd: null,\n    risk_note: `Stock price ($${current_price}) exceeds max position size ($${max_position_usd.toFixed(0)}) \u2014 increase account size or skip`\n  }}];\n}\nconst actual_cost = parseFloat((shares_to_buy * current_price).toFixed(2));\nconst actual_risk = parseFloat((shares_to_buy * stop_distance).toFixed(2));\nconst potential_reward = parseFloat((shares_to_buy * Math.max(pt_base - current_price, 0)).toFixed(2));\nconst rr = actual_risk > 0 ? parseFloat((potential_reward / actual_risk).toFixed(2)) : null;\n\n// Kelly (quarter-Kelly for safety)\nconst p = confidence / 100;\nconst b = stop_distance > 0 ? (pt_bull - current_price) / stop_distance : 1;\nconst kelly_raw = p - (1 - p) / Math.max(b, 0.01);\nconst kelly_q = Math.max(0, kelly_raw * 0.25);\nconst kelly_usd = parseFloat((account_size * kelly_q).toFixed(2));\n\nreturn [{ json: {\n  ...j,\n  account_size,\n  risk_pct_label: (risk_pct * 100).toFixed(0) + '%',\n  position_size_usd: actual_cost,\n  shares_to_buy,\n  stop_loss_price: parseFloat(pt_bear.toFixed(2)),\n  stop_distance_pct: parseFloat((stop_distance_pct * 100).toFixed(1)),\n  actual_risk_usd: actual_risk,\n  potential_reward_usd: potential_reward,\n  risk_reward_ratio: rr,\n  kelly_position_usd: kelly_usd,\n  position_capped: capped,\n  risk_note: capped ? `Capped at 10% max ($${max_position_usd.toFixed(0)})` : `${(risk_pct*100).toFixed(0)}% risk rule`\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "965ade6f-ddd9-4648-9ef7-5919557b80f0",
      "name": "Log_Signal_Outcome",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        7104,
        1040
      ],
      "parameters": {
        "columns": {
          "value": {
            "stock": "={{ $('Risk_Manager').first().json.stock || '' }}",
            "pt_base": "={{ $('Risk_Manager').first().json.pt_base || '' }}",
            "pt_bear": "={{ $('Risk_Manager').first().json.pt_bear || '' }}",
            "pt_bull": "={{ $('Risk_Manager').first().json.pt_bull || '' }}",
            "verdict": "={{ $('Risk_Manager').first().json.alert_verdict || '' }}",
            "signal_id": "={{  $('Risk_Manager').first().json.stock + \"_\" +  $('Risk_Manager').first().json.date }}",
            "confidence": "={{ $('Risk_Manager').first().json.confidence || '' }}",
            "entry_price": "={{ $('Risk_Manager').first().json.current_price || '' }}",
            "signal_date": "=={{ new Date().toISOString().slice(0,10) }}",
            "check_date_30": "=={{ (() => { const d = new Date(); d.setDate(d.getDate() + 30); return d.toISOString().slice(0,10); })() }}",
            "check_date_60": "=={{ (() => { const d = new Date(); d.setDate(d.getDate() + 60); return d.toISOString().slice(0,10); })() }}",
            "check_date_90": "=={{ (() => { const d = new Date(); d.setDate(d.getDate() + 90); return d.toISOString().slice(0,10); })() }}"
          },
          "schema": [
            {
              "id": "signal_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "signal_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "signal_date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "signal_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stock",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "stock",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "verdict",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "verdict",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "confidence",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "confidence",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "entry_price",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "entry_price",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "pt_bear",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "pt_bear",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "pt_base",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "pt_base",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "pt_bull",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "pt_bull",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "check_date_30",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "check_date_30",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "check_date_60",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "check_date_60",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "check_date_90",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "check_date_90",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "actual_price_30",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "actual_price_30",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "actual_price_60",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "actual_price_60",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "actual_price_90",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "actual_price_90",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "return_pct_30",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "return_pct_30",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "return_pct_60",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "return_pct_60",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "return_pct_90",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "return_pct_90",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "correct_30",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "correct_30",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "correct_60",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "correct_60",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "correct_90",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "correct_90",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1xdKJ1bAxxiVFKIA6x0a5n9kmpE68izgZxkyzbt3ITUM",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1xdKJ1bAxxiVFKIA6x0a5n9kmpE68izgZxkyzbt3ITUM/edit?usp=drivesdk",
          "cachedResultName": "Signal_Outcomes"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "ba7acd1e-0844-48ba-9b6f-59a04d48f941",
      "name": "FMP - CashFlow",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -320,
        912
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/cash-flow-statement?symbol={{ $('loop_over_tickers').item.json.stock }}&period=quarter&limit=8&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "7bf74627-d524-46d0-ae18-46aba9dd446f",
      "name": "FMP- Current Price",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -336,
        1184
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/quote?symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "69f9d3b1-db6e-4a26-8691-6fb0a4704281",
      "name": "FMP - Income Statement",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -320,
        496
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/income-statement?symbol={{ $('loop_over_tickers').item.json.stock }}&period=quarter&limit=8&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "a6e721d7-1750-4402-b656-c669ee42d0c7",
      "name": "FMP - Profile",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -320,
        352
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/profile?symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "7fb90563-5fb5-42ec-befe-9c8500f98a20",
      "name": "FMP - CashFlow Annual",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -336,
        1056
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/cash-flow-statement?symbol={{ $('loop_over_tickers').item.json.stock }}&period=annual&limit=5&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "4236d0c2-a167-429a-b405-502aea9da2bd",
      "name": "Clean Ccashflow Annual",
      "type": "n8n-nodes-base.code",
      "position": [
        112,
        1024
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\nlet data = [];\n\nfor (const item of allItems) {\n  const j = item.json;\n  if (Array.isArray(j))      data = data.concat(j);\n  else if (j && j.symbol)    data.push(j);\n}\n\ndata.sort((a, b) => (b.date || '').localeCompare(a.date || ''));\nconst stock = (data[0] && data[0].symbol) || \"UNKNOWN\";\n\nreturn [{ json: { stock, cashflow_annual: data } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6d45a668-4924-45e0-a4b5-01a4a78b923c",
      "name": "FMP - Key Metrics TTM",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -336,
        1344
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/key-metrics-ttm?symbol={{ $('loop_over_tickers').item.json.stock }}&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "f4f5242c-91f7-40ee-95a2-2765fa988bf3",
      "name": "FMP - Key Metrics TTM1",
      "type": "n8n-nodes-base.code",
      "position": [
        96,
        1344
      ],
      "parameters": {
        "jsCode": "const q = Array.isArray($json) ? ($json[0] || {}) : ($json || {});\nconst stock = q.symbol || \"UNKNOWN\";\n\nreturn [{\n  json: {\n    stock,\n    key_metrics_ttm: q\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "840b5825-b5c5-4100-86a3-b26efb85e391",
      "name": "FMP - Income Statement annual",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -320,
        688
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/income-statement?symbol={{ $('loop_over_tickers').item.json.stock }}&period=annual&limit=5&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "c975905f-ce58-47c4-ac0e-ea779fb21a2e",
      "name": "Clean Income statement annual",
      "type": "n8n-nodes-base.code",
      "position": [
        112,
        672
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\nlet data = [];\n\nfor (const item of allItems) {\n  const j = item.json;\n  if (Array.isArray(j))      data = data.concat(j);\n  else if (j && j.symbol)    data.push(j);\n}\n\ndata.sort((a, b) => (b.date || '').localeCompare(a.date || ''));\nconst stock = (data[0] && data[0].symbol) || \"UNKNOWN\";\n\nreturn [{ json: { stock, income_annual: data } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8d4bbc1b-306d-4591-bb3f-0498a82586c0",
      "name": "Clean balance quarter sheet",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        -64
      ],
      "parameters": {
        "jsCode": "const data = Array.isArray($json.data) ? $json.data : (Array.isArray($json) ? $json : [$json]);\nconst stock = (data[0] && data[0].symbol) || \"UNKNOWN\";\n\nreturn {\n  json: {\n    stock: stock,\n    balance_quarterly: data\n  }\n};\n\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "93f80bae-6ef9-40b2-ac1e-35464b14644b",
      "name": "FMP - Balance Sheet Quarter",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -288,
        -48
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/balance-sheet-statement?symbol={{ $('loop_over_tickers').item.json.stock }}&period=quarter&limit=8&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "9cd86f30-d7ff-4ecb-9690-28e7429ac7a4",
      "name": "FMP - Balance Sheet Annual",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -304,
        160
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/balance-sheet-statement?symbol={{ $('loop_over_tickers').item.json.stock }}&period=annual&limit=5&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "5761660d-9cdc-4dae-aca0-7e8fadf30cf3",
      "name": "Clean balance sheet Annual",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        160
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\nlet data = [];\n\nfor (const item of allItems) {\n  const j = item.json;\n  if (Array.isArray(j))      data = data.concat(j);\n  else if (j && j.symbol)    data.push(j);\n}\n\ndata.sort((a, b) => (b.date || '').localeCompare(a.date || ''));\nconst stock = (data[0] && data[0].symbol) || \"UNKNOWN\";\n\nreturn [{ json: { stock, balance_annual: data } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "259dd360-0a07-4a0c-98d3-448fe10f441c",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -64,
        -64
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "f23b50c9-d781-47e3-bd41-836429b44cba",
      "name": "Aggregate1",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -96,
        496
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "a8993d44-bdd0-4625-b1ea-7d6023702ccd",
      "name": "Aggregate2",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -112,
        912
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "f0ab8476-a134-4b49-a08e-7dd317ad1226",
      "name": "Merge66",
      "type": "n8n-nodes-base.merge",
      "position": [
        800,
        416
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "719fb2a5-5380-4966-ad34-b8246a421e33",
      "name": "Merge77",
      "type": "n8n-nodes-base.merge",
      "position": [
        992,
        288
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "56dae708-dda1-4180-ac68-c8ce00844d38",
      "name": "Merge88",
      "type": "n8n-nodes-base.merge",
      "position": [
        1296,
        464
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "a1eb4340-4b74-4a73-ba0f-7d95f9023946",
      "name": "Merge99",
      "type": "n8n-nodes-base.merge",
      "position": [
        1456,
        672
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "stock"
      },
      "typeVersion": 3.2
    },
    {
      "id": "7b4c5d56-1a33-4cee-8b39-a8fa21eb3c16",
      "name": "TideBreaker Cleanup",
      "type": "n8n-nodes-base.code",
      "position": [
        4144,
        1168
      ],
      "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;\nconst f_score_ok = f_score === null || f_score >= 5;\n\nlet final_verdict;\nif (upside >= 0.20 && f_score_ok)  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    verdict:           final_verdict,   // \u2190 ADD THIS\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": "90445cf4-fcc1-48d4-b799-0010b8c577f2",
      "name": "No Tidebreaker",
      "type": "n8n-nodes-base.code",
      "position": [
        3488,
        1552
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 CONSENSUS PATH \u2014 No Tiebreaker Needed \u2500\u2500\u2500\u2500\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 current_price    = $input.first().json?.current_price    || 0;\nconst verdict_chatgpt  = $input.first().json?.verdict_chatgpt  || null;\nconst verdict_gemini   = $input.first().json?.verdict_gemini   || null;\n\n// \u2500\u2500 Validate a candidate result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 pickValid(x) {\n  if (!x) return null;\n  if (!x.stock) 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 (!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\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 Collect and normalise candidates \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 byStock = new Map();\nconst today   = new Date().toISOString().slice(0, 10);\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 item\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    if (!stock) continue;\n\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      verdict:    cand.verdict    || null,\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\n// \u2500\u2500 Build one consolidated output row per stock \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  // Best result per model (highest confidence wins)\n  const bestByModel = new Map();\n  for (const r of arr) {\n    const prev = bestByModel.get(r.model);\n    if (!prev || (r.confidence ?? 0) > (prev.confidence ?? 0)) {\n      bestByModel.set(r.model, r);\n    }\n  }\n\n  const models = [...bestByModel.values()];\n\n  // Average numeric fields across both models\n  const avg = (fn) => models.reduce((s, r) => s + fn(r), 0) / models.length;\n\n  // Use highest-confidence model for verdict, rationale, f_score, date\n  const best = [...models].sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))[0];\n\n  const date = best.date || today;\n\n  out.push({\n    json: {\n      stock,\n      date,\n      pt_bear:           Number(avg(r => r.pt_bear).toFixed(2)),\n      pt_base:           Number(avg(r => r.pt_base).toFixed(2)),\n      pt_bull:           Number(avg(r => r.pt_bull).toFixed(2)),\n      f_score:           best.f_score,\n      confidence:        Math.max(20, Math.min(90, Math.round(avg(r => r.confidence)))),\n      verdict:           best.verdict || verdict_chatgpt || verdict_gemini || null,\n      rationale:         best.rationale || \"No rationale provided\",\n      model:             \"CONSENSUS\",\n      row_key:           `${stock}_${date}_CONSENSUS`,\n      skip_row:          false,\n      // \u2500\u2500 Enrichment fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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: \"CONSENSUS\",\n      conviction:        getConviction(gap_pct),\n      gap_pct,\n      current_price,\n      verdict_chatgpt,   // audit trail\n      verdict_gemini     // audit trail\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      f_score:           null,\n      confidence:        0,\n      verdict:           null,\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      current_price:     0,\n      verdict_chatgpt:   null,\n      verdict_gemini:    null\n    }\n  }];\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "04b7984f-9f0d-427c-9375-e38872053084",
      "name": "Technical & Earnings Enricher",
      "type": "n8n-nodes-base.code",
      "position": [
        6080,
        992
      ],
      "parameters": {
        "jsCode": "const priceResp    = $('Consolidate price history').first().json;\nconst alertData    = $('Alert Filter').first().json;\nconst earningsResp = $input.all().map(item => item.json);\n\n\n// \u2500\u2500 Price History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 closes = (priceResp?.historical || [])\n  .map(p => Number(p.close))\n  .filter(v => !isNaN(v));\n\n// \u2500\u2500 SMA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 sma(arr, period) {\n  if (arr.length < period) return null;\n  return arr.slice(0, period).reduce((a, b) => a + b, 0) / period;\n}\n\nconst ma_50  = sma(closes, 50);\nconst ma_200 = sma(closes, 200);\n\n// \u2500\u2500 RSI-14 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 rsi(arr, period = 14) {\n  if (arr.length < period + 1) return null;\n  const c = [...arr].reverse(); // oldest first for calculation\n  let gains = 0, losses = 0;\n  for (let i = c.length - period; i < c.length; i++) {\n    const diff = c[i] - c[i - 1];\n    if (diff >= 0) gains  += diff;\n    else           losses -= diff;\n  }\n  const avgGain = gains  / period;\n  const avgLoss = losses / period;\n  if (avgLoss === 0) return 100;\n  const rs = avgGain / avgLoss;\n  return Number((100 - 100 / (1 + rs)).toFixed(1));\n}\n\nconst rsi_14 = rsi(closes, 14);\n\n// \u2500\u2500 RSI Signal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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_signal = 'UNKNOWN';\nif (rsi_14 !== null) {\n  if      (rsi_14 <= 35) rsi_signal = 'OVERSOLD';\n  else if (rsi_14 <= 55) rsi_signal = 'NEUTRAL';\n  else if (rsi_14 <= 70) rsi_signal = 'ELEVATED';\n  else                   rsi_signal = 'OVERBOUGHT';\n}\n\n// \u2500\u2500 MA Check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 current_price = Number(alertData.current_price || 0);\nconst ma_50_passed  = ma_50 !== null && current_price > 0 && current_price >= ma_50;\n\nlet trend_tier = 'UNKNOWN';\nif (ma_200 !== null && ma_50 !== null && current_price > 0) {\n  if      (current_price > ma_200 && current_price > ma_50)  trend_tier = 'STRONG_UPTREND';\n  else if (current_price > ma_200 && current_price <= ma_50) trend_tier = 'PULLBACK';\n  else                                                        trend_tier = 'DOWNTREND';\n} else if (ma_50 !== null && current_price > 0) {\n  trend_tier = ma_50_passed ? 'ABOVE_50MA' : 'BELOW_50MA';\n}\n\n// \u2500\u2500 Earnings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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  if (Array.isArray(earningsResp) && earningsResp.length > 0) {\n    const now = Date.now();\n    const upcoming = earningsResp\n      .filter(e => e.date)\n      .map(e => ({ date: e.date, ms: new Date(e.date).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) {}\n\n// \u2500\u2500 Output \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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  rsi_14,\n  ma_check_passed:   ma_50 === null ? true : ma_50_passed,\n  ma_signal:         ma_50 === null ? 'MA_DATA_UNAVAILABLE' : (ma_50_passed ? 'ABOVE_50MA' : 'BELOW_50MA'),\n  trend_tier,\n  rsi_signal,\n  next_earnings_date,\n  days_to_earnings,\n  earnings_warning\n}}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "4c4d08a6-aeac-4e3d-b863-e1806de270fa",
      "name": "Get Price History",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 2,
      "position": [
        5552,
        992
      ],
      "parameters": {
        "url": "=https://financialmodelingprep.com/stable/historical-price-eod/full?symbol={{ $json.stock }}&limit=250&apikey=HGCrzhQOvhudkIaCHtjk4mCfRCqsTeLv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "0d149edc-66ab-4e00-98cb-64f0c0df3f1e",
      "name": "Consolidate price history",
      "type": "n8n-nodes-base.code",
      "position": [
        5728,
        992
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\nconst stock = $('Alert Filter').first().json?.stock || \"\";\n\n// Gather all price records into one array\nconst historical = allItems\n  .map(item => item.json)\n  .filter(j => j && j.close !== undefined);\n\nreturn [{\n  json: {\n    stock,\n    historical\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "435cba6d-0e3e-45ef-8f5e-e229691022f9",
      "name": "Above 50MA and BUY?",
      "type": "n8n-nodes-base.if",
      "position": [
        6640,
        1120
      ],
      "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": ""
            },
            {
              "id": "1e16e32f-c140-4b21-9846-6333c5feebfe",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.alert_verdict }}",
              "rightValue": "BUY"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "352f443d-6097-436c-b64b-fe9ddc6dbc40",
      "name": "Above 50MA & SELL?",
      "type": "n8n-nodes-base.if",
      "position": [
        6496,
        992
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "195d89d5-d5d9-4ba7-bff1-6ed507e9f793",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.alert_verdict }}",
              "rightValue": "SELL"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "c361845b-456c-4756-a941-14d94cd5055f",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2880,
        -512
      ],
      "parameters": {
        "width": 1696,
        "height": 1296,
        "content": "# \ud83c\udfe6 AI Institutional Stock Valuation Engine\n### Version 16 \u2014 Risk Scoring & Scenario Targets\n\n---\n\n### How it works\n\n1. **Screener** \u2014 Pulls a watchlist of tickers, filters by sector and minimum score threshold.\n\n2. **Financial Data** \u2014 Fetches income, balance sheet, and cashflow statements (annual + quarterly) from FMP. Computes Piotroski F-Score (9-point) using 2 years of annual data.\n\n3. **AI Dual Consensus** \u2014 GPT-4o and Gemini 2.5 Pro independently produce BUY / SELL / HOLD verdicts with Bear / Base / Bull price targets. If they agree \u2192 Consensus path. If they disagree \u2192 Tiebreaker path (ChatGPT Bull vs Gemini Bear).\n\n4. **News Sentiment** \u2014 Fetches Seeking Alpha RSS feed. Parsed and summarised for each stock before the AI round.\n\n5. **Alert Filter** \u2014 Stocks with confidence \u2265 60% and a BUY or SELL verdict are flagged as alerts.\n\n6. **Technical Enrichment** \u2014 For alert stocks: fetches 250 days of price history, computes 50MA, 200MA, RSI-14, trend tier, and next earnings date.\n\n7. **Risk Manager** \u2014 Sizes BUY positions using a 2% risk rule with Kelly adjustment and 10% account cap.\n\n8. **Signal Routing**\n   - BUY + above 50MA \u2192 Telegram Alert + Log_Signal_Outcome\n   - BUY + below 50MA \u2192 Watchlist Telegram\n   - SELL \u2192 Telegram Alert + Log_Signal_Outcome\n   - No alert \u2192 written directly to sheet\n\n9. **Logging** \u2014 All stocks written to `write_sentiment_to_sheets`. Actionable signals additionally logged to `Signal_Outcomes` sheet for 30 / 60 / 90-day performance tracking.\n\n10. **Thesis Reversal** \u2014 Detects when a stock's verdict flips vs the previous entry and fires a separate Telegram alert.\n\n---\n\n### Setup\n\n1. Set your FMP API key in n8n **Credentials** (HTTP Header Auth).\n2. Set your OpenAI and Google Gemini API keys in n8n **Credentials**.\n3. Set your Telegram Bot token and Chat ID in the Telegram nodes.\n4. Set n8n **Variables**:\n   - `ACCOUNT_SIZE` \u2014 e.g. 20000\n   - `RISK_PER_TRADE_PCT` \u2014 e.g. 2\n5. Connect your Google Sheets (two sheets):\n   - `write_sentiment_to_sheets` \u2192 your valuation log sheet\n   - `Log_Signal_Outcome` \u2192 Signal_Outcomes sheet (for tracking)\n6. Add your ticker watchlist to the first node.\n\n---\n\n### Customization\n\n- **Confidence threshold** \u2014 Change `>= 60` in Alert Filter to raise/lower the bar for alerts.\n- **Sector filter** \u2014 Edit `ALLOWED_SECTORS` in Score_and_Prefilter to target specific industries.\n- **Position sizing** \u2014 Adjust `ACCOUNT_SIZE` and `RISK_PER_TRADE_PCT` variables, or change the 10% max position cap inside Risk_Manager.\n- **MA filter** \u2014 The \"Above 50MA and BUY?\" node gates actionable BUY alerts. Remove it to alert on all BUY signals regardless of trend.\n- **Tiebreaker** \u2014 Triggered when GPT-4o and Gemini disagree. ChatGPT takes the Bull role, Gemini takes the Bear role. Final verdict is the higher-confidence output.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "61c86b17-84a6-41a1-abb4-944557cf3008",
  "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
          }
        ]
      ]
    },
    "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
          }
        ]
      ]
    },
    "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": 0
          }
        ]
      ]
    },
    "Merge4": {
      "main": [
        [
          {
            "node": "Merge66",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge5": {
      "main": [
        [
          {
            "node": "Merge4",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge6": {
      "main": [
        [
          {
            "node": "TideBreaker Cleanup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge7": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge66": {
      "main": [
        [
          {
            "node": "Merge77",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge77": {
      "main": [
        [
          {
            "node": "Merge88",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge88": {
      "main": [
        [
          {
            "node": "Merge99",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge99": {
      "main": [
        [
          {
            "node": "Clean Read Financial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Clean balance quarter sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Alert?": {
      "main": [
        [
          {
            "node": "Get Price History",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "write_sentiment_to_sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate1": {
      "main": [
        [
          {
            "node": "Clean Income statement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate2": {
      "main": [
        [
          {
            "node": "Clean Ccashflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Filter": {
      "main": [
        [
          {
            "node": "If Alert?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields2": {
      "main": [
        [
          {
            "node": "Final version of news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Earnings": {
      "main": [
        [
          {
            "node": "Technical & Earnings Enricher",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Risk_Manager": {
      "main": [
        [
          {
            "node": "Above 50MA & SELL?",
            "type": "main",
            "index": 0
          },
          {
            "node": "write_sentiment_to_sheets",
            "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
          }
        ]
      ]
    },
    "FMP - Profile": {
      "main": [
        [
          {
            "node": "Clean Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean the news": {
      "main": [
        [
          {
            "node": "If1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FMP - CashFlow": {
      "main": [
        [
          {
            "node": "Aggregate2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Tidebreaker": {
      "main": [
        [
          {
            "node": "Merge7",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Telegram Alert": {
      "main": [
        [
          {
            "node": "Is Thesis Reversal?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log_Signal_Outcome",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Ccashflow": {
      "main": [
        [
          {
            "node": "Merge5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean up Gemini": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "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": "FMP_Stock_Screener",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean up Gemini 2": {
      "main": [
        [
          {
            "node": "Merge6",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get Price History": {
      "main": [
        [
          {
            "node": "Consolidate price history",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Tiebreaker?": {
      "main": [
        [
          {
            "node": "Tide Breaker - Bull",
            "type": "main",
            "index": 0
          },
          {
            "node": "Tide Breaker - Bear",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Tidebreaker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "loop_over_tickers": {
      "main": [
        [
          {
            "node": "Build Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Seekingalpha Articles",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP - Profile",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP - Income Statement",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP - CashFlow",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP- Current Price",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP - Balance Sheet Quarter",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP - Income Statement annual",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP - Balance Sheet Annual",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP - CashFlow Annual",
            "type": "main",
            "index": 0
          },
          {
            "node": "FMP - Key Metrics TTM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Above 50MA & SELL?": {
      "main": [
        [
          {
            "node": "Telegram Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Above 50MA and BUY?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean up Chatgpt 2": {
      "main": [
        [
          {
            "node": "Merge6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FMP- Current Price": {
      "main": [
        [
          {
            "node": "Clean Current Price",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FMP_Stock_Screener": {
      "main": [
        [
          {
            "node": "Score_and_Prefilter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "First round Gemini": {
      "main": [
        [
          {
            "node": "Clean up Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Watchlist Telegram": {
      "main": [
        []
      ]
    },
    "Above 50MA and BUY?": {
      "main": [
        [
          {
            "node": "Telegram Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Watchlist Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Current Price": {
      "main": [
        [
          {
            "node": "Merge88",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "First round ChatGPT": {
      "main": [
        [
          {
            "node": "Clean Up ChatGPT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Thesis Reversal?": {
      "main": [
        [
          {
            "node": "Thesis Reversal Alert",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Score_and_Prefilter": {
      "main": [
        [
          {
            "node": "Write_Scan_Opportunities",
            "type": "main",
            "index": 0
          },
          {
            "node": "loop_over_tickers",
            "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
          }
        ]
      ]
    },
    "TideBreaker Cleanup": {
      "main": [
        [
          {
            "node": "Merge7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Read Financial": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Previous Verdict": {
      "main": [
        [
          {
            "node": "Thesis Reversal Enricher",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FMP - CashFlow Annual": {
      "main": [
        [
          {
            "node": "Clean Ccashflow Annual",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FMP - Key Metrics TTM": {
      "main": [
        [
          {
            "node": "FMP - Key Metrics TTM1",
            "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": [
        []
      ]
    },
    "Clean Ccashflow Annual": {
      "main": [
        [
          {
            "node": "Merge5",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Clean Income statement": {
      "main": [
        [
          {
            "node": "Merge3",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "FMP - Income Statement": {
      "main": [
        [
          {
            "node": "Aggregate1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FMP - Key Metrics TTM1": {
      "main": [
        [
          {
            "node": "Merge99",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Thesis Reversal Enricher": {
      "main": [
        [
          {
            "node": "Alert Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Consolidate price history": {
      "main": [
        [
          {
            "node": "Get Earnings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "write_sentiment_to_sheets": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean balance sheet Annual": {
      "main": [
        [
          {
            "node": "Merge77",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FMP - Balance Sheet Annual": {
      "main": [
        [
          {
            "node": "Clean balance sheet Annual",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean balance quarter sheet": {
      "main": [
        [
          {
            "node": "Merge66",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FMP - Balance Sheet Quarter": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Income statement annual": {
      "main": [
        [
          {
            "node": "Merge2",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "FMP - Income Statement annual": {
      "main": [
        [
          {
            "node": "Clean Income statement annual",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Technical & Earnings Enricher": {
      "main": [
        [
          {
            "node": "Risk_Manager",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Up Results from First Round": {
      "main": [
        [
          {
            "node": "Needs Tiebreaker?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}