總是錯過理想的股票買賣點嗎?盯盤耗時又費力,現在你可以透過 n8n 這款強大的自動化工具,結合 Google Sheets 與 Discord,輕鬆打造一個全天候運作的股市到價提醒器。只要設定好你的目標價位,系統就會在價格觸及時自動發送通知給你,讓你不再錯失任何交易良機哦
自動抓取即時股價:串接 API,取得最新的股票價格
自訂監控清單:使用 Google Sheets 輕鬆管理你想追蹤的股票及觸發條件
即時通訊軟體通知:當股價達到設定的目標時,立即透過 Discord 發送訊息
智慧排程:只在台股交易時段內執行,避免不必要的資源浪費
準備一個 Google Sheets 試算表
在第一列 (Row 1) 建立以下欄位標頭:
StockSymbol (股票代號)
StockName (股票名稱)
Condition (觸發條件,請填寫 >= 或 <=)
TargetPrice (目標價)
LastNotified (上次通知時間,此欄位由 n8n 自動填寫,請留白)
像是這樣
| StockSymbol | StockName | Condition | TargetPrice | LastNotified | 
|---|

來到儀表板,新增一個流程「Create Workflow」

初始節點選擇排程「On a schedule」

選擇「Cron」,指令填寫如下,表示在平日的上午 9:00 到下午 1:59 之間,每 5 分鐘自動執行一次
*/5 9-13 * * 1-5

接下來到「設定」裡面調整時區

把時區的「Timezone」設定為台灣時間

下個節點選擇「Google Sheets」的「Get row(s) in sheet」來讀取股票表單

選擇自己的試算表,而設定憑證在前面的文章介紹過,這邊就不重複惹

下個節點選擇「HTTP Request」

方法「GET」,網址如下
http://mis.twse.com.tw/stock/api/getStockInfo.jsp?ex_ch=tse_{{ $json.StockSymbol }}.tw&_=CURRENT_TIME

API 回傳的資料格式比較複雜,我們需要一個節點來整理成方便使用的格式,並加入一些判斷邏輯
下個節點選擇「Edit Fields (Set)」來整理資料

點選正中間的「Add Field」來新增變數

變數設定
currentPrice
{
  {
    parseFloat(
      JSON.parse($node["HTTP Request"].json.data).msgArray[0].z === "-"
        ? JSON.parse($node["HTTP Request"].json.data).msgArray[0].y
        : JSON.parse($node["HTTP Request"].json.data).msgArray[0].z
    );
  }
}
priceTime
{
  {
    $now.toFormat("yyyy-MM-dd HH:mm:ss");
  }
}
isNotifiedEmpty
型別:Boolean
{
  {
    !$("List").item.json.LastNotified;
  }
}
isOlderThan24Hours
型別:Boolean
{
  {
    $("List").item.json.LastNotified
      ? DateTime.fromISO($("List").item.json.LastNotified)
          .diffNow("hours")
          .as("hours") <= -24
      : false;
  }
}
condition
{
  {
    $("List").item.json.Condition;
  }
}
targetPrice
型別:Number
{
  {
    parseFloat($("List").item.json.TargetPrice);
  }
}

下個節點選擇「If」

接著在 Value1 的欄位填上底下程式碼,運算子選擇「Boolean」的「is true」
{
  {
    ($json.isNotifiedEmpty || $json.isOlderThan24Hours) &&
      (($json.condition === ">=" &&
        $json.currentPrice >= $json.targetPrice) ||
        ($json.condition === "<=" &&
          $json.currentPrice <= $json.targetPrice));
  }
}

接著在下個節點選擇 Discord

「Connection Type」選擇「Webhook」,憑證的串接在之前的文章有撰寫過,這邊就不重複惹

Message 可以填寫如下
**📈 股市到價提醒!**
**股票名稱:** {{ $('List').item.json.StockName }} | ({{ $('List').item.json.StockSymbol }})
**目前價格:** **`{{ $json.currentPrice }}`**
**觸發條件:** `{{ $('List').item.json.Condition }} {{ $('List').item.json.TargetPrice }}`
**更新時間:** {{ $json.priceTime }}

下個節點再選擇「Google Sheets」的「Update row in sheet」來更新通知時間,避免下次執行的時候重複發送

接著選擇對應的試算表,並在對應的屬性填寫以下內容
Column to match on:StockSymbol
StockSymbol (using to match):{{ $('List').item.json.StockSymbol }}
LastNotified:{{ $now.toISO() }}

試跑看看可以得到類似這樣的訊息

完成畫布的節點會長這樣,記得到上方切換為「Active」

最後也附上完整的 JSON 內容
{
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/5 9-13 * * 1-5"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, -100],
      "id": "b5aa62d0-9585-47ae-abc1-ab946b455526",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "url": "=http://mis.twse.com.tw/stock/api/getStockInfo.jsp?ex_ch=tse_{{ $json.StockSymbol }}.tw&_=CURRENT_TIME",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [440, -100],
      "id": "f212fbeb-ffa4-4ba8-9b50-c27bfd7bb0c4",
      "name": "HTTP Request"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "fe4d905a-93ba-4dc6-8929-b9684132bf91",
              "name": "currentPrice",
              "value": "={{ parseFloat(JSON.parse($json.data).msgArray[0].z) }}",
              "type": "string"
            },
            {
              "id": "0ddddfae-b0a6-4f9c-b0b8-cb8ac792d1da",
              "name": "priceTime",
              "value": "={{ $now.toFormat('yyyy-MM-dd HH:mm:ss') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [660, -100],
      "id": "e28e8320-d0b4-477a-9d31-ae0b8cb7e368",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "de5e6fa9-5cdc-41c8-854c-adf66ce1dd1e",
              "leftValue": "=(({{ $('List').item.json.Condition }} == \">=\" && {{ $json.currentPrice }} >= {{ $('List').item.json.TargetPrice }}) || ({{ $('List').item.json.Condition }} == \"<=\" && {{ $json.currentPrice }} <= {{ $('List').item.json.TargetPrice }}) ) && ( !{{ $('List').item.json.LastNotified }} || (DateTime.fromISO({{ $('List').item.json.LastNotified }}).diffNow('hours').as('hours') <= -24))",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [880, -100],
      "id": "5fa187e6-74c6-44fa-8295-3d0d3bc80d0d",
      "name": "If"
    },
    {
      "parameters": {
        "authentication": "webhook",
        "content": "=**📈 股市到價提醒!**\n\n**股票名稱:** {{ $('List').item.json.StockName }} | ({{ $('List').item.json.StockSymbol }})\n**目前價格:** **`{{ $json.currentPrice }}`**\n**觸發條件:** `{{ $('List').item.json.Condition }} {{ $('List').item.json.TargetPrice }}`\n**更新時間:** {{ $json.priceTime }}",
        "options": {}
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [1100, -100],
      "id": "5f740168-d05c-4e47-b513-cac2d272876f",
      "name": "Discord",
      "webhookId": "YOUR_WEBHOOK_ID"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_GOOGLE_SHEET_ID",
          "mode": "list",
          "cachedResultName": "Stock",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "stocks",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=0"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.6,
      "position": [220, -100],
      "id": "a77f656d-8056-4006-b550-88251aaef487",
      "name": "List"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "YOUR_GOOGLE_SHEET_ID",
          "mode": "list",
          "cachedResultName": "Stock",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "stocks",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=0"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "StockSymbol": "={{ $('List').item.json.StockSymbol }}",
            "LastNotified": "={{ $now.toISO() }}"
          },
          "matchingColumns": ["StockSymbol"],
          "schema": [
            {
              "id": "StockSymbol",
              "displayName": "StockSymbol",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "StockName",
              "displayName": "StockName",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "Condition",
              "displayName": "Condition",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "TargetPrice",
              "displayName": "TargetPrice",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "LastNotified",
              "displayName": "LastNotified",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "displayName": "row_number",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "readOnly": true,
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.6,
      "position": [1320, -100],
      "id": "03373aa9-c8ae-4f68-88ce-651f54cff2cc",
      "name": "Google Sheets"
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Discord": {
      "main": [
        [
          {
            "node": "Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "REMOVED_FOR_PRIVACY"
  }
}