{
  "id": "72lks0Gu4Zqi4t0i",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Stock Price Anomaly Detection and Related News Automatic Notification Workflow",
  "tags": [],
  "nodes": [
    {
      "id": "a69b05fc-16c6-4f68-bd14-717b647ad900",
      "name": "Daily Check",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -544,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            },
            {}
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "7ab68dfb-380e-4880-a948-89ad068e7533",
      "name": "Get Stock Data",
      "type": "n8n-nodes-base.marketstack",
      "position": [
        -320,
        0
      ],
      "parameters": {
        "filters": {
          "dateTo": "2025-09-30T00:00:00",
          "latest": false,
          "dateFrom": "2025-09-01T00:00:00"
        },
        "symbols": "AMZN"
      },
      "credentials": {
        "marketstackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "e8b1795e-59dd-4c7a-9db0-78d3e3228b25",
      "name": "Calculate Deviation",
      "type": "n8n-nodes-base.code",
      "position": [
        -96,
        0
      ],
      "parameters": {
        "jsCode": "// === \u30d1\u30e9\u30e1\u30fc\u30bf\u8a2d\u5b9a ===\nconst N = 20;   // \u79fb\u52d5\u5e73\u5747\u306e\u671f\u9593\nconst k = 2;    // \u03c3\u306e\u500d\u7387\uff08\u00b12\u03c3\uff09\n\n// === \u30c7\u30fc\u30bf\u53d6\u5f97 ===\nlet data = [];\n\n// Marketstack\u306e\u51fa\u529b\u69cb\u9020\u3092\u30c1\u30a7\u30c3\u30af\nif (items[0].json.data) {\n  // \u901a\u5e38\u30b1\u30fc\u30b9\uff08Marketstack\u30ce\u30fc\u30c9\u304b\u3089\u305d\u306e\u307e\u307e\uff09\n  data = items[0].json.data;\n} else {\n  // \u4e88\u5099\u30d1\u30bf\u30fc\u30f3\uff1aitems\u914d\u5217\u306b\u30c7\u30fc\u30bf\u304c\u76f4\u63a5\u4e26\u3093\u3067\u3044\u308b\u30b1\u30fc\u30b9\n  data = items.map(item => item.json);\n}\n\n// N\u4ef6\u3060\u3051\u62bd\u51fa\ndata = data.slice(0, N);\n\n// === \u7d42\u5024\u306e\u307f\u62bd\u51fa ===\nconst closes = data.map(d => d.close).filter(v => typeof v === 'number');\n\n// \u30c7\u30fc\u30bf\u304c\u306a\u3044\u5834\u5408\u306e\u5bfe\u7b56\nif (closes.length === 0) {\n  return [{ json: { error: \"No valid close data found.\" } }];\n}\n\n// === \u79fb\u52d5\u5e73\u5747\u3068\u6a19\u6e96\u504f\u5dee\u3092\u8a08\u7b97 ===\nconst mean = closes.reduce((a, b) => a + b, 0) / closes.length;\nconst variance = closes.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / closes.length;\nconst sigma = Math.sqrt(variance);\n\n// === \u6700\u65b0\u5024\uff08\u5148\u982d\uff09 ===\nconst latestClose = closes[0] + 100;\n\n// === \u5224\u5b9a ===\nlet status = \"normal\";\nif (latestClose > mean + k * sigma) {\n  status = \"high (above +2\u03c3)\";\n} else if (latestClose < mean - k * sigma) {\n  status = \"low (below -2\u03c3)\";\n}\n\n// === \u51fa\u529b ===\nreturn [\n  {\n    json: {\n      mean: mean.toFixed(2),\n      sigma: sigma.toFixed(2),\n      upper: (mean + k * sigma).toFixed(2),\n      lower: (mean - k * sigma).toFixed(2),\n      latest: latestClose.toFixed(2),\n      status,\n      count: closes.length\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "5f5c9951-f936-495d-985f-7ae2108d2787",
      "name": "Is Anomaly? (status != \"normal\")",
      "type": "n8n-nodes-base.if",
      "position": [
        128,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f79f5888-e19f-4273-81b7-5b6f434fea65",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{$json[\"status\"]}}",
              "rightValue": "=normal"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "830c1c3b-92d7-42c2-a698-ec213787ace9",
      "name": "Get Related News",
      "type": "n8n-nodes-base.hackerNews",
      "position": [
        800,
        96
      ],
      "parameters": {
        "resource": "all",
        "additionalFields": {
          "keyword": "={{ $json.keyword }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "31e2ca1d-9c30-4dbc-9a67-54e12ca974a3",
      "name": "Translate News",
      "type": "n8n-nodes-base.deepL",
      "position": [
        1248,
        32
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "translateTo": "JA",
        "additionalFields": {}
      },
      "credentials": {
        "deepLApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "478fdb62-dde4-40e3-9e0b-2676330f91c0",
      "name": "Send Alert to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        1696,
        96
      ],
      "parameters": {
        "text": "=\ud83c\udf10 *Original (English)*  \n{{ $json.message }}\n\n---\n\n\ud83c\uddef\ud83c\uddf5 *Translated (Japanese)*  \n{{ $json.text }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "CKUCBTG0H",
          "cachedResultName": "general"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "7ea2b37e-e074-400d-9e3b-28ac0bc9c816",
      "name": "Send Normal Report to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        352,
        -96
      ],
      "parameters": {
        "text": "=\u2705 \u7570\u5e38\u306a\u3057 \u3053\u306e\u9298\u67c4\u306e\u7d42\u5024\u306f\u5b89\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 \u73fe\u5728\u5024\uff1a{{ $('Calculate Deviation').item.json.latest }}\uff08\u5e73\u5747 {{ $('Calculate Deviation').item.json.mean }} \u00b1 {{ $('Calculate Deviation').item.json.sigma }})",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "CKUCBTG0H",
          "cachedResultName": "general"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "34c1613d-306b-47fa-b982-db156c54e053",
      "name": "Merge Original + Translated",
      "type": "n8n-nodes-base.merge",
      "position": [
        1472,
        96
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "be7b89c0-99ac-44cb-aec0-03b844bc50f5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -640,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 224,
        "height": 192,
        "content": "## Daily Check (09:00 JST)\n\nStarts the workflow every morning at 09:00 JST. Adjust schedule/timezone as needed."
      },
      "typeVersion": 1
    },
    {
      "id": "2ea66681-b507-4737-a6f4-0d0c2590646f",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        208
      ],
      "parameters": {
        "color": 7,
        "height": 192,
        "content": "## Get Stock Data (Marketstack)\nRetrieves the latest EOD prices for the configured ticker. Edit symbol and date range. Keep the limit \u2265 20 for a stable mean/\u03c3."
      },
      "typeVersion": 1
    },
    {
      "id": "e85baa53-b0ed-4017-98b0-e7a07a88c7aa",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1472,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 256,
        "content": "## Customization Tips\n\n- Monitor multiple tickers: duplicate **Get Stock Data** and merge results.\n- Replace Hacker News with **NewsAPI** / **Google News RSS** for broader coverage.\n- Switch Slack to **Telegram / Discord / Teams / LINE Messaging API**.\n- Add a **Set (Fields)** node to centralize user-configurable variables:\n  - `symbol` (ticker), `days` (N), `sigmaK` (k), `newsQuery`, `slackChannel`\n- Add an **IF** to translate only when language != JA (cost optimization).\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "22cad535-6c1f-44c1-9018-910baa276cba",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 384,
        "content": "## Node Structure Overview\n\nFlow:\n\ud83d\udd58 Daily Check (09:00) \n \u2192 \ud83d\udcca Get Stock Data (Marketstack)\n \u2192 \ud83e\uddee Calculate Deviation (\u00b12\u03c3)\n \u2192 \ud83d\udd00 IF: Is Anomaly? (status != \"normal\")\n\nTrue (Anomaly):\n  \u2192 \ud83d\udcf0 Get Related News (Hacker News)\n  \u2192 \u270d\ufe0f Format News (Title + Summary + URL)\n  \u2192 \ud83c\udf10 Translate News (EN \u2192 JA, DeepL)\n  \u2192 \ud83d\udd17 Merge Original + Translated\n  \u2192 \ud83d\udcac Send Alert to Slack\n\nFalse (Normal):\n  \u2192 \ud83d\udcac Send Normal Report to Slack\n"
      },
      "typeVersion": 1
    },
    {
      "id": "fa231074-0537-4cc6-b33c-e90006605377",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        320,
        352
      ],
      "parameters": {
        "color": 7,
        "height": 176,
        "content": "## Send Normal Report to Slack\n\nSends a concise \u201cno anomaly\u201d message with basic stats.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2c8bbc35-0320-4350-b64f-e58eacd7c974",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        -448
      ],
      "parameters": {
        "color": 7,
        "width": 304,
        "height": 272,
        "content": "## \ud83d\udee0\ufe0f Setup Instructions\n\n1. Connect your **Marketstack**, **DeepL**, and **Slack** credentials.\n2. Update ticker symbol inside \u201cGet Stock Data\u201d.\n3. Customize Slack channel in both Slack nodes.\n4. Modify time or schedule in \u201cDaily Check\u201d as needed.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "7fbb2805-1ffd-4ce6-86a6-04d9ba827191",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1184,
        -288
      ],
      "parameters": {
        "width": 480,
        "height": 816,
        "content": "## Price Anomaly Detection & News Alert (Marketstack + HN + DeepL + Slack)\n\n## Overview\nThis workflow monitors a stock\u2019s closing price via **Marketstack**. It computes a **20-day moving average** and **standard deviation (\u00b12\u03c3)**. If the latest close is outside \u00b12\u03c3, it flags an **anomaly**, fetches **related headlines from Hacker News**, **translates** them to Japanese with **DeepL**, and **posts both original and translated text to Slack**. When no anomaly is detected, it sends a concise \u201cnormal\u201d report.\n\n## How it works\n1) Daily trigger at 09:00 JST  \n2) Marketstack: fetch EOD data  \n3) Code: compute mean/\u03c3 and classify (normal/high/low)  \n4) IF: anomaly? \u2192 yes = news path / no = normal report  \n5) Hacker News: search related items  \n6) DeepL: translate EN \u2192 JA  \n7) Slack: send bilingual notification\n\n## Requirements\n- Marketstack API key\n- DeepL API key\n- Slack OAuth2 (bot token / channel permission)\n\n## Notes\n- Edit the ticker in **Get Stock Data**.\n- Adjust **N** (days) and **k** (sigma multiplier) in **Calculate Deviation**.\n- Keep credentials out of HTTP nodes (use n8n Credentials).\n"
      },
      "typeVersion": 1
    },
    {
      "id": "4a60a88c-3f9d-4919-9f8e-769138eebad2",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -192,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "content": "## Calculate Deviation\n\nComputes 20-day mean and standard deviation; classifies the latest close as normal, high (+2\u03c3), or low (\u22122\u03c3)."
      },
      "typeVersion": 1
    },
    {
      "id": "d3e597a0-0400-400d-970d-f179bf3c574c",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 176,
        "content": "## Is Anomaly? (status != \"normal\")\n\nBranches to the news path only when the latest close exceeds \u00b12\u03c3."
      },
      "typeVersion": 1
    },
    {
      "id": "cb2c48c9-349d-4c3e-9f17-ed4636ca0b82",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        720,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 176,
        "content": "## Get Related News (Hacker News)\n\nSearches articles by the ticker/company keyword. You can switch to NewsAPI/Google RSS."
      },
      "typeVersion": 1
    },
    {
      "id": "8a824d8c-3d66-4838-9c5e-a8708afcfa1d",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        944,
        -208
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 176,
        "content": "## Format News (Title + Summary + URL)\n\nCleans HTML (e.g., <em>) and formats top items for translation/Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "1ef7949a-e396-449a-b21c-4e72e7031c38",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        -208
      ],
      "parameters": {
        "color": 7,
        "width": 208,
        "height": 176,
        "content": "## Translate News (EN \u2192 JA)\n\nDeepL translation to Japanese. Change target_lang if needed."
      },
      "typeVersion": 1
    },
    {
      "id": "4a410b17-9763-4723-a5f2-61fb6ab50fd7",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1504,
        -208
      ],
      "parameters": {
        "color": 7,
        "width": 208,
        "height": 176,
        "content": "## Merge Original + Translated\n\nAppends original English and translated Japanese into one message payload."
      },
      "typeVersion": 1
    },
    {
      "id": "0d5d6b48-6983-4136-80e3-0cf9de926972",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1744,
        -208
      ],
      "parameters": {
        "color": 7,
        "width": 208,
        "height": 176,
        "content": "## Send Alert to Slack (Anomaly)\n\nSends bilingual alert with stats (latest, mean, \u03c3) and related news."
      },
      "typeVersion": 1
    },
    {
      "id": "fe474a5c-9358-4f1b-ad07-b2ae6169c280",
      "name": "Add Symbol Field",
      "type": "n8n-nodes-base.set",
      "position": [
        352,
        96
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1de4889-f506-46c6-8189-4e173dad6c39",
              "name": "symbol",
              "type": "string",
              "value": "={{$item(0).$node[\"Get Stock Data\"].json[\"symbol\"]}}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ad957b08-b108-427e-9843-b48be879cb3b",
      "name": "Compose Slack Message",
      "type": "n8n-nodes-base.code",
      "position": [
        1024,
        96
      ],
      "parameters": {
        "jsCode": "// === Hacker News \u51fa\u529b\u304b\u3089\u30bf\u30a4\u30c8\u30eb\u30fb\u672c\u6587\u30fbURL\u3092\u6574\u5f62 ===\nconst results = items.map(item => {\n  const highlight = item.json._highlightResult || {};\n\n  const title =\n    highlight.title?.value ||\n    item.json.title ||\n    \"No Title\";\n\n  const story =\n    highlight.story_text?.value ||\n    item.json.story_text ||\n    \"No summary available.\";\n\n  const url =\n    item.json.url ||\n    highlight.url?.value ||\n    \"No URL\";\n\n  // === HTML\u30bf\u30b0\uff08<em>\u306a\u3069\uff09\u3092\u9664\u53bb ===\n  const cleanTitle = title.replace(/<[^>]*>/g, \"\");\n  const cleanStory = story.replace(/<[^>]*>/g, \"\");\n\n  return `\ud83d\udcf0 ${cleanTitle}\\n${cleanStory}\\n\ud83d\udd17 ${url}`;\n});\n\n// === \u51fa\u529b ===\nreturn [\n  {\n    json: {\n      message: results.slice(0, 3).join(\"\\n\\n---\\n\\n\"), // \u4e0a\u4f4d3\u4ef6\u3092\u533a\u5207\u3063\u3066\u7d50\u5408\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a5ecf56c-8e7d-42da-9335-a2b1a08b71a1",
      "name": "Build News Keyword",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        96
      ],
      "parameters": {
        "jsCode": "const map = {\n  AMZN: 'Amazon',\n  AAPL: 'Apple',\n  GOOG: 'Google',\n  MSFT: 'Microsoft',\n  TSLA: 'Tesla',\n};\n\n// \u5165\u529b\u306e\u4e2d\u8eab\u3092\u78ba\u8a8d\nconsole.log('INPUT items:', items);\n\nconst first = (items && items[0] && items[0].json) ? items[0].json : {};\nconst symbol = String(first.symbol || first.ticker || '').toUpperCase();\n\nif (!symbol) {\n  // symbol\u304c\u7121\u3044\u6642\u306f\u7406\u7531\u3068\u5165\u529b\u3092\u8fd4\u3057\u3066\u53ef\u8996\u5316\n  return [\n    {\n      json: {\n        error: 'symbol is missing',\n        debugInput: first,\n      },\n    },\n  ];\n}\n\nconst keyword = map[symbol] || symbol;\n\nreturn [\n  {\n    json: { keyword, symbol },\n  },\n];\n"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "e90eb5a4-58e8-4a2d-82c3-30862bc8832f",
  "connections": {
    "Daily Check": {
      "main": [
        [
          {
            "node": "Get Stock Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Stock Data": {
      "main": [
        [
          {
            "node": "Calculate Deviation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Translate News": {
      "main": [
        [
          {
            "node": "Merge Original + Translated",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Symbol Field": {
      "main": [
        [
          {
            "node": "Build News Keyword",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Related News": {
      "main": [
        [
          {
            "node": "Compose Slack Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build News Keyword": {
      "main": [
        [
          {
            "node": "Get Related News",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Deviation": {
      "main": [
        [
          {
            "node": "Is Anomaly? (status != \"normal\")",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Alert to Slack": {
      "main": [
        []
      ]
    },
    "Compose Slack Message": {
      "main": [
        [
          {
            "node": "Translate News",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Original + Translated",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Original + Translated": {
      "main": [
        [
          {
            "node": "Send Alert to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Anomaly? (status != \"normal\")": {
      "main": [
        [
          {
            "node": "Add Symbol Field",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Normal Report to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}