iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0

總是錯過理想的股票買賣點嗎?盯盤耗時又費力,現在你可以透過 n8n 這款強大的自動化工具,結合 Google Sheets 與 Discord,輕鬆打造一個全天候運作的股市到價提醒器。只要設定好你的目標價位,系統就會在價格觸及時自動發送通知給你,讓你不再錯失任何交易良機哦

核心功能

  • 自動抓取即時股價:串接 API,取得最新的股票價格

  • 自訂監控清單:使用 Google Sheets 輕鬆管理你想追蹤的股票及觸發條件

  • 即時通訊軟體通知:當股價達到設定的目標時,立即透過 Discord 發送訊息

  • 智慧排程:只在台股交易時段內執行,避免不必要的資源浪費

前置作業

  • 準備一個 Google Sheets 試算表

  • 在第一列 (Row 1) 建立以下欄位標頭:

    • StockSymbol (股票代號)

    • StockName (股票名稱)

    • Condition (觸發條件,請填寫 >=<=)

    • TargetPrice (目標價)

    • LastNotified (上次通知時間,此欄位由 n8n 自動填寫,請留白)

  • 像是這樣

    StockSymbol StockName Condition TargetPrice LastNotified

    image 0.png

workflow

步驟 1:設定排程觸發

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

    image 1.png

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

    image 2.png

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

    */5 9-13 * * 1-5
    

    image 3.png

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

    image 4.png

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

    image 5.png

步驟 2:讀取 Google Sheets 資料

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

    image 6.png

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

    • 節點名稱命名為「List」,之後會用到

    image 7.png

步驟 3:抓取即時股價

  • 下個節點選擇「HTTP Request」

    image 8.png

  • 方法「GET」,網址如下

    http://mis.twse.com.tw/stock/api/getStockInfo.jsp?ex_ch=tse_{{ $json.StockSymbol }}.tw&_=CURRENT_TIME
    

    image 9.png

步驟 4:整理與設定變數

API 回傳的資料格式比較複雜,我們需要一個節點來整理成方便使用的格式,並加入一些判斷邏輯

  • 下個節點選擇「Edit Fields (Set)」來整理資料

    image 10.png

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

    image 11.png

  • 變數設定

    • 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);
          }
        }
        

    image 12.png

步驟 5:條件判斷

  • 下個節點選擇「If」

    image 13.png

  • 接著在 Value1 的欄位填上底下程式碼,運算子選擇「Boolean」的「is true」

    {
      {
        ($json.isNotifiedEmpty || $json.isOlderThan24Hours) &&
          (($json.condition === ">=" &&
            $json.currentPrice >= $json.targetPrice) ||
            ($json.condition === "<=" &&
              $json.currentPrice <= $json.targetPrice));
      }
    }
    

    image 14.png

步驟 6:發送 Discord 通知

  • 接著在下個節點選擇 Discord

    image 15.png

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

    image 16.png

  • Message 可以填寫如下

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

    image 17.png

步驟 7:更新通知時間

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

    image 18.png

  • 接著選擇對應的試算表,並在對應的屬性填寫以下內容

    • Column to match on:StockSymbol

    • StockSymbol (using to match):{{ $('List').item.json.StockSymbol }}

    • LastNotified:{{ $now.toISO() }}

    image 19.png

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

    image 20.png

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

    image 21.png

  • 最後也附上完整的 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"
      }
    }
    

上一篇
[Day18]_商品降價通知
系列文
告別重複瑣事: n8n workflow 自動化工作實踐19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言