From 92d453897eded517e396a002a4a7d7bedbda2d45 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 2 Jan 2024 18:36:44 +0800 Subject: [PATCH] feat: support for fetching futures & options realtime quote --- README.md | 114 ++++++++- src/enums/scraper.enum.ts | 1 + src/interfaces/ticker.interface.ts | 1 + src/scrapers/index.ts | 1 + src/scrapers/mis-taifex-scraper.ts | 63 +++++ src/scrapers/scraper-factory.ts | 6 + src/twstock.ts | 13 + test/fixtures/fetched-stocks-info.json | 2 +- test/fixtures/futures-quote-afterhours.json | 1 + test/fixtures/futures-quote.json | 1 + test/fixtures/options-quote-afterhours.json | 1 + test/fixtures/options-quote.json | 1 + test/scrapers/mis-taifex-scraper.spec.ts | 249 ++++++++++++++++++++ test/twstock.spec.ts | 25 ++ 14 files changed, 473 insertions(+), 6 deletions(-) create mode 100644 src/scrapers/mis-taifex-scraper.ts create mode 100644 test/fixtures/futures-quote-afterhours.json create mode 100644 test/fixtures/futures-quote.json create mode 100644 test/fixtures/options-quote-afterhours.json create mode 100644 test/fixtures/options-quote.json create mode 100644 test/scrapers/mis-taifex-scraper.spec.ts diff --git a/README.md b/README.md index e4af45c..b1328ef 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ * [.market.breadth(options)](#marketbreadthoptions) * [.market.instTrades(options)](#marketinsttradesoptions) * [.market.marginTrades(options)](#marketmargintradesoptions) + * [.futopt.list(options)](#futoptlist) + * [.futopt.quote(options)](#futoptquoteoptions) * [.futopt.txfInstTrades(options)](#futopttxfinsttradesoptions) * [.futopt.txoInstTrades(options)](#futopttxoinsttradesoptions) * [.futopt.txoPutCallRatio(options)](#futopttxoputcallratiooptions) @@ -121,10 +123,10 @@ twstock.stocks.list({ market: 'TSE' }) * `lastPrice`: {number} 最後成交價格 * `lastSize`: {number} 最後成交數量 * `totalVoluem`: {number} 總成交量 - * `bidPrice`: {number[]} 最佳委託買進價格 - * `askPrice`: {number[]} 最佳委託賣出價格 - * `bidSize`: {number[]} 最佳委託買進數量 - * `askSize`: {number[]} 最佳委託賣出數量 + * `bidPrice`: {number[]} 最佳委買價格 + * `askPrice`: {number[]} 最佳委賣價格 + * `bidSize`: {number[]} 最佳委買數量 + * `askSize`: {number[]} 最佳委賣數量 * `lastUpdated`: {number} 最後更新時間 ```js @@ -835,6 +837,109 @@ twstock.market.marginTrades({ date: '2023-01-30', market: 'TSE' }) // } ``` +### `.futopt.list()` + +取得期貨與選擇權契約列表。 + +* Returns: {Promise} 成功時以 {Object[]} 履行,該陣列包含以下物件屬性: + * `symbol`: {string} 契約代號 + * `name`: {string} 契約名稱 + * `exchange`: {string} 交易所 + * `market`: {string} 市場別 + * `industry`: {string} 產業別 + * `listedDate`: {string} 上市日期 + +```js +twstock.futopt.list() + .then(data => console.log(data)); +// Prints: +// [ +// { +// symbol: 'BRFC4', +// name: '布蘭特原油期貨2024/03', +// exchange: 'TAIFEX', +// market: 'FUTOPT', +// type: '原油期貨', +// industry: '00', +// listedDate: '2023-11-01' +// }, +// ... more items +// ] +``` + +### `.futopt.quote(options)` + +取得期貨與選擇權契約即時行情。 + +* `options`: {Object} + * `symbol`: {string} 契約代號 + * `afterhours` (optional): {boolean} 盤後交易 +* Returns: {Promise} 成功時以 {Object} 履行,包含以下物件屬性: + * `symbol`: {string} 契約代號 + * `name`: {string} 契約名稱 + * `referencePrice`: {number} 參考價 + * `limitUpPrice`: {number} 漲停價 + * `limitDownPrice`: {number} 跌停價 + * `openPrice`: {number} 開盤價 + * `highPrice`: {number} 最高價 + * `lowPrice`: {number} 最低價 + * `lastPrice`: {number} 成交價 + * `lastSize`: {number} 單量 + * `testPrice`: {number} 試撮價 + * `testSize`: {number} 試撮量 + * `testTime`: {string} 試撮時間 + * `totalVoluem`: {number} 成交量 + * `openInterest`: {number} 未平倉量 + * `bidOrders`: {number} 委買筆數 + * `askOrders`: {number} 委賣筆數 + * `bidVolume`: {number} 委買口數 + * `askVolume`: {number} 委賣口數 + * `bidPrice`: {number[]} 最佳委買價格 + * `askPrice`: {number[]} 最佳委賣價格 + * `bidSize`: {number[]} 最佳委買數量 + * `askSize`: {number[]} 最佳委賣數量 + * `extBidPrice`: {number} 最佳衍生一檔買價 + * `extAskPrice`: {number} 最佳衍生一檔賣價 + * `extBidSize`: {number} 最佳衍生一檔買量 + * `extAskSize`: {number} 最佳衍生一檔賣量 + * `lastUpdated`: {string} 最後更新時間 + +```js +twstock.futopt.quote({ symbol: 'TXFA4' }) + .then(data => console.log(data)); +// Prints: +// { +// symbol: 'TXFA4', +// name: '臺股期貨2024/01', +// referencePrice: 17870, +// limitUpPrice: 19657, +// limitDownPrice: 16083, +// openPrice: 17838, +// highPrice: 17920, +// lowPrice: 17751, +// lastPrice: 17798, +// lastSize: 3, +// testPrice: 17831, +// testSize: 256, +// testTime: '08:44:55.000+08:00', +// totalVoluem: 116498, +// openInterest: 103404, +// bidOrders: 67979, +// askOrders: 66456, +// bidVolume: 124038, +// askVolume: 124080, +// bidPrice: [ 17798, 17797, 17796, 17795, 17794 ], +// askPrice: [ 17800, 17801, 17802, 17803, 17804 ], +// bidSize: [ 31, 26, 38, 24, 18 ], +// askSize: [ 6, 16, 30, 18, 22 ], +// extBidPrice: 17795, +// extAskPrice: 0, +// extBidSize: 2, +// extAskSize: 0, +// lastUpdated: '2024-01-02T13:44:59.000+08:00' +// } +``` + ### `.futopt.txfInstTrades(options)` 取得臺股期貨在特定日期的三大法人交易口數、契約金額與未平倉餘額。 @@ -881,7 +986,6 @@ twstock.market.marginTrades({ date: '2023-01-30', market: 'TSE' }) * `dealersNetOiVolume`: {number} 自營商-多空未平倉口數淨額 * `dealersNetOiValue`: {number} 自營商-多空未平倉契約金額淨額(千元) - ```js twstock.futopt.txfInstTrades({ date: '2023-01-30' }) .then(data => console.log(data)); diff --git a/src/enums/scraper.enum.ts b/src/enums/scraper.enum.ts index d01fdca..e9762f7 100644 --- a/src/enums/scraper.enum.ts +++ b/src/enums/scraper.enum.ts @@ -4,6 +4,7 @@ export enum Scraper { Taifex = 'taifex', Tdcc = 'tdcc', MisTwse = 'mis-twse', + MisTaifex = 'mis-taifex', Mops = 'mops', Isin = 'isin', } diff --git a/src/interfaces/ticker.interface.ts b/src/interfaces/ticker.interface.ts index 5b2ecdb..d1d9da1 100644 --- a/src/interfaces/ticker.interface.ts +++ b/src/interfaces/ticker.interface.ts @@ -2,6 +2,7 @@ import { Exchange, Industry, Market } from '../enums'; export interface Ticker { symbol: string; + name: string; exchange: Exchange; market: Market; type?: string; diff --git a/src/scrapers/index.ts b/src/scrapers/index.ts index cdb7876..4fcef22 100644 --- a/src/scrapers/index.ts +++ b/src/scrapers/index.ts @@ -4,6 +4,7 @@ export * from './tpex-scraper'; export * from './taifex-scraper'; export * from './tdcc-scraper'; export * from './mis-twse-scraper'; +export * from './mis-taifex-scraper'; export * from './mops-scraper'; export * from './isin-scraper'; export * from './scraper-factory'; diff --git a/src/scrapers/mis-taifex-scraper.ts b/src/scrapers/mis-taifex-scraper.ts new file mode 100644 index 0000000..ee6e970 --- /dev/null +++ b/src/scrapers/mis-taifex-scraper.ts @@ -0,0 +1,63 @@ +import * as _ from 'lodash'; +import * as numeral from 'numeral'; +import { DateTime } from 'luxon'; +import { Scraper } from './scraper'; +import { Ticker } from '../interfaces'; + +export class MisTaifexScraper extends Scraper { + async fetchFutOptQuote(options: { ticker: Ticker, afterhours?: boolean }) { + const { ticker, afterhours } = options; + const body = JSON.stringify({ SymbolID: [this.extractSymbolIdFromTicker(ticker, { afterhours })] }); + const url = `https://mis.taifex.com.tw/futures/api/getQuoteDetail`; + + const response = await this.httpService.post(url, body, { + headers: { 'Content-Type': 'application/json' }, + }); + const json = (response.data.RtCode === '0') && response.data; + if (!json) return null; + + const data = json.RtData.QuoteList.map((row: any) => ({ + symbol: ticker.symbol, + name: ticker.name, + referencePrice: row.CRefPrice && numeral(row.CRefPrice).value(), + limitUpPrice: row.CCeilPrice && numeral(row.CCeilPrice).value(), + limitDownPrice: row.CFloorPrice && numeral(row.CFloorPrice).value(), + openPrice: row.COpenPrice && numeral(row.COpenPrice).value(), + highPrice: row.CHighPrice && numeral(row.CHighPrice).value(), + lowPrice: row.CLowPrice && numeral(row.CLowPrice).value(), + lastPrice: row.CLastPrice && numeral(row.CLastPrice).value(), + lastSize: row.CSingleVolume && numeral(row.CSingleVolume).value(), + testPrice: row.CTestPrice && numeral(row.CTestPrice).value(), + testSize: row.CTestVolume && numeral(row.CTestVolume).value(), + testTime: row.CTestTime && DateTime.fromFormat(row.CTestTime, 'hhmmss').toISOTime(), + totalVoluem: row.CTotalVolume && numeral(row.CTotalVolume).value(), + openInterest: row.OpenInterest && numeral(row.OpenInterest).value(), + bidOrders: row.CBidCount && numeral(row.CBidCount).value(), + askOrders: row.CAskCount && numeral(row.CAskCount).value(), + bidVolume: row.CBidUnit && numeral(row.CBidUnit).value(), + askVolume: row.CAskUnit && numeral(row.CAskUnit).value(), + bidPrice: [row.CBidPrice1, row.CBidPrice2, row.CBidPrice3, row.CBidPrice4, row.CBidPrice5].map(price => numeral(price).value()), + askPrice: [row.CAskPrice1, row.CAskPrice2, row.CAskPrice3, row.CAskPrice4, row.CAskPrice5].map(price => numeral(price).value()), + bidSize: [row.CBidSize1, row.CBidSize2, row.CBidSize3, row.CBidSize4, row.CBidSize5].map(size => numeral(size).value()), + askSize: [row.CAskSize1, row.CAskSize2, row.CAskSize3, row.CAskSize4, row.CAskSize5].map(size => numeral(size).value()), + extBidPrice: row.CExtBidPrice && numeral(row.CExtBidPrice).value(), + extAskPrice: row.CExtAskPrice && numeral(row.CExtAskPrice).value(), + extBidSize: row.CExtBidSize && numeral(row.CExtBidSize).value(), + extAskSize: row.CExtAskSize && numeral(row.CExtAskSize).value(), + lastUpdated: DateTime.fromFormat(`${row.CDate} ${row.CTime}`, 'yyyyMMdd hhmmss').toISO(), + })) as Record[]; + return data.find(row => row.symbol.includes(ticker.symbol)); + } + + private extractSymbolIdFromTicker(ticker: Ticker, options: { afterhours?: boolean }) { + const { symbol, type } = ticker; + const { afterhours } = options; + + if (type && type.includes('期貨')) { + return afterhours ? `${symbol}-M` : `${symbol}-F`; + } + if (type && type.includes('選擇權')) { + return afterhours ? `${symbol}-N` : `${symbol}-O`; + } + } +} diff --git a/src/scrapers/scraper-factory.ts b/src/scrapers/scraper-factory.ts index dcea0aa..46d5a3b 100644 --- a/src/scrapers/scraper-factory.ts +++ b/src/scrapers/scraper-factory.ts @@ -3,6 +3,7 @@ import { TpexScraper } from './tpex-scraper'; import { TaifexScraper } from './taifex-scraper'; import { TdccScraper } from './tdcc-scraper'; import { MisTwseScraper } from './mis-twse-scraper'; +import { MisTaifexScraper } from './mis-taifex-scraper'; import { MopsScraper } from './mops-scraper'; import { IsinScraper } from './isin-scraper'; import { Scraper } from './scraper'; @@ -24,6 +25,7 @@ export class ScraperFactory { [ScraperType.Taifex]: TaifexScraper, [ScraperType.Tdcc]: TdccScraper, [ScraperType.MisTwse]: MisTwseScraper, + [ScraperType.MisTaifex]: MisTaifexScraper, [ScraperType.Mops]: MopsScraper, [ScraperType.Isin]: IsinScraper, }; @@ -56,6 +58,10 @@ export class ScraperFactory { return this.get(ScraperType.MisTwse) as MisTwseScraper; } + getMisTaifexScraper() { + return this.get(ScraperType.MisTaifex) as MisTaifexScraper; + } + getMopsScraper() { return this.get(ScraperType.Mops) as MopsScraper; } diff --git a/src/twstock.ts b/src/twstock.ts index c3baae6..18194af 100644 --- a/src/twstock.ts +++ b/src/twstock.ts @@ -50,6 +50,7 @@ export class TwStock { get futopt() { return { list: this.getFutOptList.bind(this), + quote: this.getFutOptQuote.bind(this), txfInstTrades: this.getFutOptTxfInstTrades.bind(this), txoInstTrades: this.getFutOptTxoInstTrades.bind(this), txoPutCallRatio: this.getFutOptTxoPutCallRatio.bind(this), @@ -355,6 +356,18 @@ export class TwStock { return data; } + private async getFutOptQuote(options: { symbol: string, afterhours?: boolean }) { + const { symbol, afterhours } = options; + + if (!this._futopt.has(symbol)) { + const futopt = await this.loadFutOpt({ symbol }); + if (!futopt.length) throw new Error('symbol not found'); + } + + const ticker = this._futopt.get(symbol) as Ticker; + return this._scraper.getMisTaifexScraper().fetchFutOptQuote({ ticker, afterhours }); + } + private async getFutOptTxfInstTrades(options: { date: string }) { const { date } = options; return this._scraper.getTaifexScraper().fetchTxfInstTrades({ date }); diff --git a/test/fixtures/fetched-stocks-info.json b/test/fixtures/fetched-stocks-info.json index f0d61ac..395f724 100644 --- a/test/fixtures/fetched-stocks-info.json +++ b/test/fixtures/fetched-stocks-info.json @@ -1 +1 @@ -[{"symbol":"6488","name":"環球晶","exchange":"TPEx","market":"OTC","industry":"24","listedDate":"2015-09-25"},{"symbol":"2330","name":"台積電","exchange":"TWSE","market":"TSE","industry":"24","listedDate":"1994-09-05"}] +[{"symbol":"6488","name":"環球晶","exchange":"TPEx","market":"OTC","industry":"24","listedDate":"2015-09-25"},{"symbol":"2330","name":"台積電","exchange":"TWSE","market":"TSE","industry":"24","listedDate":"1994-09-05"},{"symbol":"TXFA4","name":"臺股期貨2024/01","exchange":"TAIFEX","market":"FUTOPT","type":"臺股期貨","listedDate":"2023-10-19"}] diff --git a/test/fixtures/futures-quote-afterhours.json b/test/fixtures/futures-quote-afterhours.json new file mode 100644 index 0000000..564f777 --- /dev/null +++ b/test/fixtures/futures-quote-afterhours.json @@ -0,0 +1 @@ +{"RtCode":"0","RtMsg":"","RtData":{"QuoteList":[{"Status":"","SymbolID":"TXFA4-M","SpotID":"","DispCName":"臺指期014","DispEName":"TX014","COpenPrice":"17793.00","CHighPrice":"17817.00","CLowPrice":"17791.00","CLastPrice":"17804.00","CSingleVolume":"1","CTestPrice":"17792.00","CTestVolume":"40","CTestTime":"145955","CTotalVolume":"4184","OpenInterest":"","CRefPrice":"17800.00","CCeilPrice":"19580.00","CFloorPrice":"16020.00","CBidCount":"3431","CAskCount":"3526","CBidUnit":"7171","CAskUnit":"7191","CDate":"20240102","CTime":"172322","CBidPrice1":"17804.00","CBidPrice2":"17803.00","CBidPrice3":"17802.00","CBidPrice4":"17801.00","CBidPrice5":"17800.00","CAskPrice1":"17805.00","CAskPrice2":"17806.00","CAskPrice3":"17807.00","CAskPrice4":"17808.00","CAskPrice5":"17809.00","CBidSize1":"4","CBidSize2":"27","CBidSize3":"40","CBidSize4":"32","CBidSize5":"61","CAskSize1":"14","CAskSize2":"35","CAskSize3":"44","CAskSize4":"54","CAskSize5":"25","CExtBidPrice":"17801.00","CExtAskPrice":"17805.00","CExtBidSize":"2","CExtAskSize":"1","CTermHighPrice":"","CTermLowPrice":"","CBestBidPrice":"17804.00","CBestAskPrice":"17805.00","CBestBidSize":"4","CBestAskSize":"15","HugeTotalVolume":""}]}} \ No newline at end of file diff --git a/test/fixtures/futures-quote.json b/test/fixtures/futures-quote.json new file mode 100644 index 0000000..12ad1f3 --- /dev/null +++ b/test/fixtures/futures-quote.json @@ -0,0 +1 @@ +{"RtCode":"0","RtMsg":"","RtData":{"QuoteList":[{"Status":"TC","SymbolID":"TXFA4-F","SpotID":"","DispCName":"臺指期014","DispEName":"TX014","COpenPrice":"17838.00","CHighPrice":"17920.00","CLowPrice":"17751.00","CLastPrice":"17798.00","CSingleVolume":"3","CTestPrice":"17831.00","CTestVolume":"256","CTestTime":"084455","CTotalVolume":"116498","OpenInterest":"103404","CRefPrice":"17870.00","CCeilPrice":"19657.00","CFloorPrice":"16083.00","CBidCount":"67979","CAskCount":"66456","CBidUnit":"124038","CAskUnit":"124080","CDate":"20240102","CTime":"134459","CBidPrice1":"17798.00","CBidPrice2":"17797.00","CBidPrice3":"17796.00","CBidPrice4":"17795.00","CBidPrice5":"17794.00","CAskPrice1":"17800.00","CAskPrice2":"17801.00","CAskPrice3":"17802.00","CAskPrice4":"17803.00","CAskPrice5":"17804.00","CBidSize1":"31","CBidSize2":"26","CBidSize3":"38","CBidSize4":"24","CBidSize5":"18","CAskSize1":"6","CAskSize2":"16","CAskSize3":"30","CAskSize4":"18","CAskSize5":"22","CExtBidPrice":"17795.00","CExtAskPrice":"0.00","CExtBidSize":"2","CExtAskSize":"0","CTermHighPrice":"17920.00","CTermLowPrice":"17751.00","CBestBidPrice":"17798.00","CBestAskPrice":"17800.00","CBestBidSize":"31","CBestAskSize":"6","HugeTotalVolume":""}]}} \ No newline at end of file diff --git a/test/fixtures/options-quote-afterhours.json b/test/fixtures/options-quote-afterhours.json new file mode 100644 index 0000000..ded5b76 --- /dev/null +++ b/test/fixtures/options-quote-afterhours.json @@ -0,0 +1 @@ +{"RtCode":"0","RtMsg":"","RtData":{"QuoteList":[{"Status":"","SymbolID":"TX118000A4-N","SpotID":"","DispCName":"臺指選W1014;18000買權","DispEName":"TX1W1014;18000C","COpenPrice":"2.800","CHighPrice":"3.400","CLowPrice":"2.000","CLastPrice":"2.500","CSingleVolume":"1","CTestPrice":"0.000","CTestVolume":"0","CTestTime":"","CTotalVolume":"2794","OpenInterest":"","CRefPrice":"2.600","CCeilPrice":"1780.000","CFloorPrice":"0.100","CBidCount":"284","CAskCount":"391","CBidUnit":"3014","CAskUnit":"4082","CDate":"20240102","CTime":"173912","CBidPrice1":"2.400","CBidPrice2":"2.300","CBidPrice3":"2.200","CBidPrice4":"2.100","CBidPrice5":"2.000","CAskPrice1":"2.600","CAskPrice2":"2.700","CAskPrice3":"2.800","CAskPrice4":"2.900","CAskPrice5":"3.000","CBidSize1":"4","CBidSize2":"97","CBidSize3":"24","CBidSize4":"5","CBidSize5":"7","CAskSize1":"99","CAskSize2":"35","CAskSize3":"24","CAskSize4":"7","CAskSize5":"14","CExtBidPrice":"","CExtAskPrice":"","CExtBidSize":"","CExtAskSize":"","CTermHighPrice":"","CTermLowPrice":"","CBestBidPrice":"2.400","CBestAskPrice":"2.600","CBestBidSize":"4","CBestAskSize":"99","HugeTotalVolume":""}]}} \ No newline at end of file diff --git a/test/fixtures/options-quote.json b/test/fixtures/options-quote.json new file mode 100644 index 0000000..dcb489f --- /dev/null +++ b/test/fixtures/options-quote.json @@ -0,0 +1 @@ +{"RtCode":"0","RtMsg":"","RtData":{"QuoteList":[{"Status":"TC","SymbolID":"TX118000A4-O","SpotID":"","DispCName":"臺指選W1014;18000買權","DispEName":"TX1W1014;18000C","COpenPrice":"13.500","CHighPrice":"29.000","CLowPrice":"1.500","CLastPrice":"2.600","CSingleVolume":"25","CTestPrice":"13.500","CTestVolume":"18","CTestTime":"084455","CTotalVolume":"63256","OpenInterest":"20036","CRefPrice":"30.500","CCeilPrice":"1820.000","CFloorPrice":"0.100","CBidCount":"8406","CAskCount":"7963","CBidUnit":"61019","CAskUnit":"61561","CDate":"20240102","CTime":"134454","CBidPrice1":"2.500","CBidPrice2":"2.300","CBidPrice3":"2.200","CBidPrice4":"2.100","CBidPrice5":"2.000","CAskPrice1":"2.800","CAskPrice2":"2.900","CAskPrice3":"3.000","CAskPrice4":"3.100","CAskPrice5":"3.200","CBidSize1":"17","CBidSize2":"1","CBidSize3":"2","CBidSize4":"17","CBidSize5":"12","CAskSize1":"3","CAskSize2":"4","CAskSize3":"7","CAskSize4":"8","CAskSize5":"3","CExtBidPrice":"","CExtAskPrice":"","CExtBidSize":"","CExtAskSize":"","CTermHighPrice":"29.000","CTermLowPrice":"1.500","CBestBidPrice":"2.500","CBestAskPrice":"2.800","CBestBidSize":"17","CBestAskSize":"3","HugeTotalVolume":""}]}} \ No newline at end of file diff --git a/test/scrapers/mis-taifex-scraper.spec.ts b/test/scrapers/mis-taifex-scraper.spec.ts new file mode 100644 index 0000000..316f3df --- /dev/null +++ b/test/scrapers/mis-taifex-scraper.spec.ts @@ -0,0 +1,249 @@ +import mockAxios from 'jest-mock-axios'; +import { MisTaifexScraper } from '../../src/scrapers/mis-taifex-scraper'; +import { Ticker } from '../../src/interfaces'; + +describe('MisTaifexScraper', () => { + let scraper: MisTaifexScraper; + + beforeEach(() => { + scraper = new MisTaifexScraper(); + }); + + afterEach(() => { + mockAxios.reset(); + }); + + describe('.fetchFutOptQuote()', () => { + it('should fetch futures realtime quote', async () => { + mockAxios.post.mockResolvedValueOnce({ data: require('../fixtures/futures-quote.json') }); + + const data = await scraper.fetchFutOptQuote({ + ticker: { + symbol: 'TXFA4', + name: '臺股期貨2024/01', + exchange: 'TAIFEX', + market: 'FUTOPT', + type: '臺股期貨', + industry: '00', + listedDate: '2023-10-19', + } as Ticker, + }); + expect(mockAxios.post).toHaveBeenCalledWith( + 'https://mis.taifex.com.tw/futures/api/getQuoteDetail', + JSON.stringify({ SymbolID: ['TXFA4-F'] }), + { headers: { 'Content-Type': 'application/json' } }, + ); + expect(data).toBeDefined(); + expect(data).toEqual({ + symbol: 'TXFA4', + name: '臺股期貨2024/01', + referencePrice: 17870, + limitUpPrice: 19657, + limitDownPrice: 16083, + openPrice: 17838, + highPrice: 17920, + lowPrice: 17751, + lastPrice: 17798, + lastSize: 3, + testPrice: 17831, + testSize: 256, + testTime: '08:44:55.000+08:00', + totalVoluem: 116498, + openInterest: 103404, + bidOrders: 67979, + askOrders: 66456, + bidVolume: 124038, + askVolume: 124080, + bidPrice: [ 17798, 17797, 17796, 17795, 17794 ], + askPrice: [ 17800, 17801, 17802, 17803, 17804 ], + bidSize: [ 31, 26, 38, 24, 18 ], + askSize: [ 6, 16, 30, 18, 22 ], + extBidPrice: 17795, + extAskPrice: 0, + extBidSize: 2, + extAskSize: 0, + lastUpdated: '2024-01-02T13:44:59.000+08:00', + }); + }); + + it('should fetch futures realtime quote for afterhours trading', async () => { + mockAxios.post.mockResolvedValueOnce({ data: require('../fixtures/futures-quote-afterhours.json') }); + + const data = await scraper.fetchFutOptQuote({ + ticker: { + symbol: 'TXFA4', + name: '臺股期貨2024/01', + exchange: 'TAIFEX', + market: 'FUTOPT', + type: '臺股期貨', + industry: '00', + listedDate: '2023-10-19', + } as Ticker, + afterhours: true, + }); + expect(mockAxios.post).toHaveBeenCalledWith( + 'https://mis.taifex.com.tw/futures/api/getQuoteDetail', + JSON.stringify({ SymbolID: ['TXFA4-M'] }), + { headers: { 'Content-Type': 'application/json' } }, + ); + expect(data).toBeDefined(); + expect(data).toEqual({ + symbol: 'TXFA4', + name: '臺股期貨2024/01', + referencePrice: 17800, + limitUpPrice: 19580, + limitDownPrice: 16020, + openPrice: 17793, + highPrice: 17817, + lowPrice: 17791, + lastPrice: 17804, + lastSize: 1, + testPrice: 17792, + testSize: 40, + testTime: '14:59:55.000+08:00', + totalVoluem: 4184, + openInterest: '', + bidOrders: 3431, + askOrders: 3526, + bidVolume: 7171, + askVolume: 7191, + bidPrice: [ 17804, 17803, 17802, 17801, 17800 ], + askPrice: [ 17805, 17806, 17807, 17808, 17809 ], + bidSize: [ 4, 27, 40, 32, 61 ], + askSize: [ 14, 35, 44, 54, 25 ], + extBidPrice: 17801, + extAskPrice: 17805, + extBidSize: 2, + extAskSize: 1, + lastUpdated: '2024-01-02T17:23:22.000+08:00', + }); + }); + + it('should fetch options realtime quote', async () => { + mockAxios.post.mockResolvedValueOnce({ data: require('../fixtures/options-quote.json') }); + + const data = await scraper.fetchFutOptQuote({ + ticker: { + symbol: 'TX118000A4', + name: '臺指選擇權,2024/01,履約價18000,買權', + exchange: 'TAIFEX', + market: 'FUTOPT', + type: '臺指選擇權買權', + industry: '00', + listedDate: '2023-12-20' + } as Ticker, + }); + expect(mockAxios.post).toHaveBeenCalledWith( + 'https://mis.taifex.com.tw/futures/api/getQuoteDetail', + JSON.stringify({ SymbolID: ['TX118000A4-O'] }), + { headers: { 'Content-Type': 'application/json' } }, + ); + expect(data).toBeDefined(); + expect(data).toEqual({ + symbol: 'TX118000A4', + name: '臺指選擇權,2024/01,履約價18000,買權', + referencePrice: 30.5, + limitUpPrice: 1820, + limitDownPrice: 0.1, + openPrice: 13.5, + highPrice: 29, + lowPrice: 1.5, + lastPrice: 2.6, + lastSize: 25, + testPrice: 13.5, + testSize: 18, + testTime: '08:44:55.000+08:00', + totalVoluem: 63256, + openInterest: 20036, + bidOrders: 8406, + askOrders: 7963, + bidVolume: 61019, + askVolume: 61561, + bidPrice: [ 2.5, 2.3, 2.2, 2.1, 2 ], + askPrice: [ 2.8, 2.9, 3, 3.1, 3.2 ], + bidSize: [ 17, 1, 2, 17, 12 ], + askSize: [ 3, 4, 7, 8, 3 ], + extBidPrice: '', + extAskPrice: '', + extBidSize: '', + extAskSize: '', + lastUpdated: '2024-01-02T13:44:54.000+08:00', + }); + }); + + it('should fetch options realtime quote for afterhours trading', async () => { + mockAxios.post.mockResolvedValueOnce({ data: require('../fixtures/options-quote-afterhours.json') }); + + const data = await scraper.fetchFutOptQuote({ + ticker: { + symbol: 'TX118000A4', + name: '臺指選擇權,2024/01,履約價18000,買權', + exchange: 'TAIFEX', + market: 'FUTOPT', + type: '臺指選擇權買權', + industry: '00', + listedDate: '2023-12-20' + } as Ticker, + afterhours: true, + }); + expect(mockAxios.post).toHaveBeenCalledWith( + 'https://mis.taifex.com.tw/futures/api/getQuoteDetail', + JSON.stringify({ SymbolID: ['TX118000A4-N'] }), + { headers: { 'Content-Type': 'application/json' } }, + ); + expect(data).toBeDefined(); + expect(data).toEqual({ + symbol: 'TX118000A4', + name: '臺指選擇權,2024/01,履約價18000,買權', + referencePrice: 2.6, + limitUpPrice: 1780, + limitDownPrice: 0.1, + openPrice: 2.8, + highPrice: 3.4, + lowPrice: 2, + lastPrice: 2.5, + lastSize: 1, + testPrice: 0, + testSize: 0, + testTime: '', + totalVoluem: 2794, + openInterest: '', + bidOrders: 284, + askOrders: 391, + bidVolume: 3014, + askVolume: 4082, + bidPrice: [ 2.4, 2.3, 2.2, 2.1, 2 ], + askPrice: [ 2.6, 2.7, 2.8, 2.9, 3 ], + bidSize: [ 4, 97, 24, 5, 7 ], + askSize: [ 99, 35, 24, 7, 14 ], + extBidPrice: '', + extAskPrice: '', + extBidSize: '', + extAskSize: '', + lastUpdated: '2024-01-02T17:39:12.000+08:00', + }); + }); + + it('should return null when no data is available', async () => { + mockAxios.post.mockResolvedValueOnce({ data: {} }); + + const data = await scraper.fetchFutOptQuote({ + ticker: { + symbol: 'TXFA4', + name: '臺股期貨2024/01', + exchange: 'TAIFEX', + market: 'FUTOPT', + type: '臺股期貨', + industry: '00', + listedDate: '2023-10-19', + } as Ticker, + }); + expect(mockAxios.post).toHaveBeenCalledWith( + 'https://mis.taifex.com.tw/futures/api/getQuoteDetail', + JSON.stringify({ SymbolID: ['TXFA4-F'] }), + { headers: { 'Content-Type': 'application/json' } }, + ); + expect(data).toBe(null); + }); + }); +}); diff --git a/test/twstock.spec.ts b/test/twstock.spec.ts index b03ab14..dd3f3fd 100644 --- a/test/twstock.spec.ts +++ b/test/twstock.spec.ts @@ -4,6 +4,7 @@ import { TpexScraper } from '../src/scrapers/tpex-scraper'; import { TaifexScraper } from '../src/scrapers/taifex-scraper'; import { TdccScraper } from '../src/scrapers/tdcc-scraper'; import { MisTwseScraper } from '../src/scrapers/mis-twse-scraper'; +import { MisTaifexScraper } from '../src/scrapers/mis-taifex-scraper'; import { MopsScraper } from '../src/scrapers/mops-scraper'; import { IsinScraper } from '../src/scrapers/isin-scraper'; @@ -14,6 +15,7 @@ jest.mock('../src/scrapers/isin-scraper', () => { fetchStocksInfo: jest.fn(({ symbol }) => { if (symbol.split(',').includes('2330')) return require('./fixtures/fetched-stocks-info.json'); if (symbol.split(',').includes('6488')) return require('./fixtures/fetched-stocks-info.json'); + if (symbol.split(',').includes('TXFA4')) return require('./fixtures/fetched-stocks-info.json'); return []; }), fetchListedStocks: jest.fn(({ market }) => { @@ -21,6 +23,9 @@ jest.mock('../src/scrapers/isin-scraper', () => { if (market === 'OTC') return require('./fixtures/fetched-otc-stocks-list.json'); return []; }), + fetchListedFutOpt: jest.fn(() => { + return require('./fixtures/fetched-taifex-futopt-list.json'); + }), } } } @@ -36,6 +41,7 @@ jest.mock('../src/scrapers/mis-twse-scraper', () => { MisTwseScraper.prototype.fetchIndicesQuote = jest.fn(); return { MisTwseScraper }; }); +jest.mock('../src/scrapers/mis-taifex-scraper'); jest.mock('../src/scrapers/twse-scraper'); jest.mock('../src/scrapers/tpex-scraper'); jest.mock('../src/scrapers/taifex-scraper'); @@ -429,6 +435,25 @@ describe('TwStock', () => { }); describe('.futopt', () => { + describe('.list()', () => { + it('should load stocks and return the list', async () => { + const futopt = await twstock.futopt.list(); + expect(futopt).toBeDefined(); + expect(futopt?.length).toBeGreaterThan(0); + }); + }); + + describe('.quote()', () => { + it('should fetch futopt realtime quote', async () => { + await twstock.futopt.quote({ symbol: 'TXFA4' }); + expect(MisTaifexScraper.prototype.fetchFutOptQuote).toHaveBeenCalled(); + }); + + it('should throw an error if the symbol is not found', async () => { + await expect(() => twstock.futopt.quote({ symbol: 'foobar' })).rejects.toThrow('symbol not found'); + }); + }); + describe('.txfInstTrades()', () => { it('should fetch TXF institutional investors\' trades', async () => { await twstock.futopt.txfInstTrades({ date: '2023-01-30' });