前言
在瞬息萬變的金融市場中,抓住投資機會並應對風險至關重要,你可能已經擁有自己的交易策略,但要時刻關注市場動態並非易事,因此需要一套可靠行情監控工具,幫助你隨時掌握市場的脈動。
打造一個量身定制的行情監控系統,不僅能節省大量盯盤的時間,還能根據你的投資風格和目標,設定警示和提醒,更精準地捕捉符合策略的市場事件。這個系統能追蹤行情變化,即時地向你發送通知,讓你專注於更深入的投資研究,進而制定相應的交易策略。
許多投資者選擇運用技術指標來決定進出場時機,在坊間流行一種以 KD 指標 來操作 元大台灣50(0050) 的交易策略。KD(KDJ)是一種擺盪指標,它是由隨機指標(Stochastic Oscillator)演變而來,由三個線構成:K 線(快速隨機線)、D 線(慢速隨機線)和 J 線。KD 指標的主要功能在於判斷市場的超買和超賣狀態,並提供進出場的參考。例如:
- 當 K 值大於 80 時,表示市場處於超買狀態,投資者可以考慮賣出。
- 當 K 值小於 20 時,表示市場處於超賣狀態,投資者可以考慮買進。
本篇文章將以 NestJS 運用 Fugle API 及 LINE Notify 來打造股票進出場訊號通知系統,並以「元大台灣50」為例,使用 KD 指標作為進出場的參考,並在每個交易日的 13:25
時發送通知。這個時間點可供投資者運用收盤前 5 分鐘進行試撮期間,以便在考慮是否進行交易。
本篇文章僅作為範例說明,而非投資建議。
目錄
事前準備
在開始實作前,請先準備好你的開發環境,正如俗話說:「工欲善其事,必先利其器。」
安裝開發環境時,請留意不同作業系統和版本之間的差異,為確保安裝工具能順利運作,請先確認你的本機執行環境,詳細閱讀官方文件和教學後,再進行安裝及相關設定。
安裝 Node.js
Node.js 是基於 Chrome V8 JavaScript 引擎的開放原始碼、跨平台、可用於伺服器端應用程式的執行環境,它提供事件驅動、非阻塞的I/O 模型,讓你能有效率地建立可擴展的網路應用程式。
首先,你需要安裝 Node.js 環境,請前往 Node.js 官方網站 下載適合你作業系統的安裝檔,通常建議選擇 LTS(Long Term Support)版本,這是官方提供長期支援的穩定版本。當然,如果你想體驗 Node.js 最新功能,也可以選擇下載最新版。
安裝 Nest CLI
Nest(NestJS)是基於 Node.js 和 TypeScript 開發的框架,能幫助你打造高效、可靠且易於擴展的應用程式,它提供了多種實用功能,支援常用的伺服端技術。透過模組化的結構,你能更方便地管理和組織程式碼。
Nest CLI 是由 NestJS 提供的命令列工具,能讓你輕鬆地建立、執行和管理 Nest 應用程式的各種操作。只要你已安裝好 Node.js,打開終端機並執行以下指令,即可安裝 Nest CLI:
$ npm install -g @nestjs/cli
安裝完成後,你可以輸入以下指令,查看 Nest CLI 提供的指令及其使用方式:
$ nest -h
取得 Fugle API 金鑰
在使用 Fugle API 之前,你必須註冊成為富果會員。請至富果網站完成會員註冊並且登入後 ,然後進行以下步驟。
STEP 1:前往富果帳戶開發者網站首頁(developer.fugle.tw),點選「文件」→「行情」(圖 5)。
STEP 2:跳轉頁面後,在右上方點選「金鑰申請」(圖 6)。
STEP 3:「金鑰申請及管理」頁面下,即可新增行情 API 金鑰(圖 7)。
取得 API 金鑰之後,即可開始使用富果行情 API。不同的 API 方案下,有不同的存取限制,請參考官方網站的 說明。
取得 LINE Notify 存取權杖
請確認你已經註冊並認證了你的 LINE 帳號。如果沒有,請先在你的行動裝置下載 LINE App 來完成註冊和認證。
STEP 1:前往 LINE Notify 首頁(notify-bot.line.me),登入你的 LINE 帳號後,點選「個人頁面」(圖 8)。
STEP 2:跳轉頁面後,選擇「發行權杖」(圖 9)。
LINE Notify 授權是基於 OAuth 2.0 的授權碼(Authorization Code)模式。這種授權機制能讓你的應用程式能夠安全地取得其他使用者的同意。如果你只需要將訊息透過 LINE Notify 推播給自己,則直接選擇「發行權杖」即可。
STEP 3:接著會跳出一個表單視窗。請填寫權杖名稱,然後接收通知的聊天室請選擇「透過1對1聊天接收Line Notify的通知」,然後點選「發行」(圖 10)。
STEP 4:LINE Notify 將產生你的個人存取權杖(Access Token)。因為這段代碼只會出現一次,請務必記住這組權杖代碼(圖 11)。
STEP 5:完成後,在「連動的服務」清單裡,就會出現我們剛剛所設定的服務(圖 12)。
設定應用程式
建立 Nest 應用程式
首先,請打開終端機,使用 Nest CLI 建立一個名為 trading-signal-notifier
的 Nest 應用程式:
$ nest new trading-signal-notifier
應用程式建立後,我們需要調整 Nest CLI 預設產生的內容。請將應用程式 AppModule
修改如下:
import { Module } from '@nestjs/common';
@Module({})
export class AppModule {}
我們不會用上預設建立的 AppController
與 AppService
,你可以移除相關檔案。
安裝依賴模組
請在終端機輸入以下指令安裝相關套件:
$ npm install --save @fugle/marketdata @fugle/marketdata-nest @nestjs/config @nestjs/schedule kdj luxon nest-line-notify numeral
$ npm install --save-dev @types/luxon @types/numeral
以下是各個套件的簡要說明:
@fugle/marketdata
: 富果行情 API 客戶端函式庫。@fugle/marketdata-nest
: 提供在 NestJS 應用程式中整合@fugle/marketdata
的模組。@nestjs/config
: NestJS 的配置模組,可用於管理應用程式中的配置參數。@nestjs/schedule
: NestJS 中的任務調度模組,允許你在應用程式中定義和管理定期執行的任務,例如排程任務、定時執行等。nest-line-notify
: 在 NestJS 應用程式中整合 Line Notify 服務的套件,可用於發送 Line 通知。kdj
: 用於計算 KDJ 指標。你也可以使用其他技術指標套件,例如technicalindicators
或tulind
。luxon
: 用於處理和解析日期和時間的工具。numeral
: 用於格式化數值型態資料。@types/luxon
: 這是luxon
的 TypeScript 類型定義檔。@types/numeral
: 這是numeral
的 TypeScript 類型定義檔。
安裝完成後,請在 AppModule
中匯入相關模組:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { FugleMarketDataModule } from '@fugle/marketdata-nest';
import { LineNotifyModule } from 'nest-line-notify';
@Module({
imports: [
ConfigModule.forRoot(),
ScheduleModule.forRoot(),
FugleMarketDataModule.forRoot({
apiKey: process.env.FUGLE_MARKETDATA_API_KEY,
}),
LineNotifyModule.forRoot({
accessToken: process.env.LINE_NOTIFY_ACCESS_TOKEN,
}),
],
})
export class AppModule {}
設定環境變數
請在專案目錄下建立 .env
檔案,新增以下內容:
FUGLE_MARKETDATA_API_KEY=
LINE_NOTIFY_ACCESS_TOKEN=
這裡解釋上述變數的意義與用途:
FUGLE_MARKETDATA_API_KEY
:你的富果行情 API 金鑰。LINE_NOTIFY_ACCESS_TOKEN
:你的 LINE Notify 存取權杖。
應用程式將透過環境變數來讀取富果行情 API 金鑰以及 LINE Notify 存取權杖。
實作通知服務
完成應用程式的設定後,我們要實作應用程式的核心功能。首先,建立一個模組來實作 LINE Notifier 通知功能。請使用 Nest CLI 執行以下指令來建立 NotifierModule
:
$ nest g module notifier
執行上述指令後,Nest CLI 會在專案的 src
目錄下新增一個名為 notifier
的資料夾,並在其中建立 notifier.module.ts
檔案。
接下來,請在 NotifierModule
下新增 NotifierService
,這是用於實現 LINE Notifier 通知的核心服務,你可以使用以下 Nest CLI 指令來完成這個步驟:
$ nest g service notifier --no-spec
執行後,請開啟建立的檔案,並完成以下的實作。
匯入模組與依賴注入
請在 NotifierService
建構式中注入 @fugle/marketdata
提供的 RestClient
和 nest-line-notify
提供的 LineNotify
。這樣我們就可以使用這兩個服務來獲取股票行情資料並且發送 LINE Notify 訊息。
import * as numeral from 'numeral';
import * as kdj from 'kdj';
import { DateTime } from 'luxon';
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { RestClient } from '@fugle/marketdata';
import { InjectRestClient } from '@fugle/marketdata-nest';
import { InjectLineNotify, LineNotify } from 'nest-line-notify';
@Injectable()
export class NotifierService {
private readonly symbol = '0050'; // 股票代號
private candles: Record<string, any>;
constructor(
@InjectRestClient() private readonly client: RestClient,
@InjectLineNotify() private readonly lineNotify: LineNotify,
) { }
}
初始化應用程式與 K 線數據
然後,我們在應用啟動時調用 onApplicationBootstrap()
方法,該方法會執行 initCandles()
方法,用於初始化歷史 K 線數據。
當 initCandles()
方法被呼叫時,使用富果行情 API 獲取指定股票代號的最近三個月的歷史 K 線數據,然後將其保存在 this.candles
屬性中。
此外,initCandles()
使用 @Cron()
裝飾器設定為定時任務,這個方法將會在每日上午 08:00
時自動執行。
...
@Injectable()
export class NotifierService {
...
async onApplicationBootstrap() {
await this.initCandles();
}
@Cron('0 0 8 * * *')
async initCandles() {
const symbol = this.symbol;
const to = DateTime.local().toISODate();
const from = DateTime.local().minus({ month: 3 }).toISODate();
const candles = await this.client.stock.historical.candles({
symbol, from, to,
});
this.candles = candles.data.reverse().reduce((candles, candle) => ({
...candles,
date: [...candles.date, candle.date],
open: [...candles.open, candle.open],
high: [...candles.high, candle.high],
low: [...candles.low, candle.low],
close: [...candles.close, candle.close],
volume: [...candles.volume, candle.volume],
}), { date: [], open: [], high: [], low: [], close: [], volume: [] });
Logger.log('candles data initialized', NotifierService.name);
}
}
定時取得即時報價
下一步實作 fetchQuote()
方法使用富果行情 API 獲取即時報價,並檢查報價日期是否與當前日期相符。如果符合,表示當天為交易日,則更新歷史 K 線數據,計算 KDJ 指標,並根據條件發送 LINE Notify 通知。
我們在 fetchQuote()
聲明 @Cron()
裝飾器設定為定時任務,這個方法將會在每日下午 13:25
時自動執行。
...
@Injectable()
export class NotifierService {
...
@Cron('00 25 13 * * *')
async fetchQuote() {
const symbol = this.symbol;
const quote = await this.client.stock.intraday.quote({ symbol });
if (quote.date !== DateTime.local().toISODate()) return; // 確認當天是否為交易日
const index = this.candles.date.indexOf(quote.date);
if (index === -1) {
this.candles.date.push(quote.date);
this.candles.open.push(quote.openPrice);
this.candles.high.push(quote.highPrice);
this.candles.low.push(quote.lowPrice);
this.candles.close.push(quote.closePrice);
this.candles.volume.push(quote.total?.tradeVolume * 1000);
} else {
this.candles.date[index] = quote.date;
this.candles.open[index] = quote.openPrice;
this.candles.high[index] = quote.highPrice;
this.candles.low[index] = quote.lowPrice;
this.candles.close[index] = quote.closePrice;
this.candles.volume[index] = quote.total.tradeVolume * 1000;
}
const { close, low, high } = this.candles;
const indicator = kdj(close, low, high);
const k = indicator.K.slice(-1)[0];
const d = indicator.D.slice(-1)[0];
const j = indicator.J.slice(-1)[0];
await this.sendNotification({
symbol: quote.symbol,
name: quote.name,
price: numeral(quote.closePrice).format('0.00'),
volume: numeral(quote.total.tradeVolume).format('0'),
change: numeral(quote.change).format('+0.00'),
changePercent: numeral(quote.changePercent).format('+0.00'),
time: DateTime.fromMillis(Math.floor(quote.lastUpdated / 1000)).toFormat('yyyy/MM/dd HH:mm:ss'),
k: numeral(k).format('0.00'),
d: numeral(d).format('0.00'),
j: numeral(j).format('0.00'),
});
}
}
發送 LINE Notify 通知
最後,實作 sendNotification()
方法用於建構 LINE Notify 訊息內容並發送通知。
...
@Injectable()
export class NotifierService {
...
async sendNotification(payload: Record<string, any>) {
const { symbol, name, price, change, changePercent, time, k, d, j } = payload;
const message = [''].concat([
`${name} (${symbol})`,
`---`,
`成交: ${price}`,
`漲跌: ${change} (${changePercent})`,
`K: ${k} D: ${d} J: ${j}`,
`---`,
`時間: ${time}`,
]).join('\n');
await this.lineNotify.send({ message })
.then(() => Logger.log(message, NotifierService.name))
.catch(err => Logger.error(err.message, err.stack, NotifierService.name));
}
}