總是錯過理想的股票買賣點嗎?盯盤耗時又費力,現在你可以透過 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"
}
}