diff --git a/test/integration/quote_test.go b/test/integration/quote_test.go index 2a983f4..2d50c5f 100644 --- a/test/integration/quote_test.go +++ b/test/integration/quote_test.go @@ -22,7 +22,7 @@ func TestQuote_DownloadTpex(t *testing.T) { client := twstock.NewClient() _, err := client.Quote.DownloadTpex("3374", 2022, 12) if err != nil { - t.Fatalf("DownloadTwse returned error: %v", err) + t.Fatalf("DownloadTpex returned error: %v", err) } _, err = client.Quote.DownloadTpex("2330", 2022, 12) if err != twstock.ErrNoData { diff --git a/twstock/marketdata.go b/twstock/marketdata.go index 510c054..b153f32 100644 --- a/twstock/marketdata.go +++ b/twstock/marketdata.go @@ -18,7 +18,7 @@ const ( twseMarketDataPath = "/exchangeReport/FMTQIK" // 上櫃每日市場成交資訊 - tpexMarketDataPath = "/web/stock/aftertrading/daily_trading_index/st41_result.php" + tpexMarketDataPath = "/www/zh-tw/afterTrading/tradingIndex" ) type MarketData struct { @@ -130,8 +130,8 @@ func (s *MarketDataService) DownloadTpex(year int, month time.Month) ([]MarketDa } url, _ := s.client.tpexBaseURL.Parse(tpexMarketDataPath) opts := tpexOptions{ - // 需要將西元年轉為民國年 - Date: fmt.Sprintf("%d/%02d", date.Year-1911, date.Month), + Response: "json", + Date: fmt.Sprintf("%04d/%02d/%02d", date.Year, date.Month, date.Day), } url, _ = addOptions(url, opts) req, _ := s.client.NewRequest("GET", url.String(), nil) @@ -140,18 +140,26 @@ func (s *MarketDataService) DownloadTpex(year int, month time.Month) ([]MarketDa if err != nil { return nil, err } - if resp.DataLength != len(resp.Data) { - return nil, fmt.Errorf("failed parsing market data length returned %d, want %d", resp.DataLength, len(resp.Data)) - } - if resp.DataLength == 0 { + if len(resp.Tables) != 1 || resp.Tables[0].TotalCount == 0 { return nil, ErrNoData } + if resp.Tables[0].TotalCount != len(resp.Tables[0].Data) { + return nil, fmt.Errorf("failed parsing market data length returned %d, want %d", resp.Tables[0].TotalCount, len(resp.Tables[0].Data)) + } result := []MarketData{} - for _, data := range resp.Data { - marketData, err := s.parse(data) + for _, data := range resp.Tables[0].Data { + stringData := make([]string, len(data)) + for i, v := range data { + stringData[i] = string(v) + } + marketData, err := s.parse(stringData) if err != nil { return nil, err } + // 成交股數(仟股) + marketData.TradeVolume *= 1000 + // 金額(仟元) + marketData.TradeValue = marketData.TradeValue.Mul(decimal.NewFromInt(1000)) result = append(result, marketData) } return result, nil diff --git a/twstock/marketdata_test.go b/twstock/marketdata_test.go index 1265b1e..52c8db5 100644 --- a/twstock/marketdata_test.go +++ b/twstock/marketdata_test.go @@ -304,50 +304,70 @@ func TestMarketDataService_DownloadTpex(t *testing.T) { mux.HandleFunc(tpexMarketDataPath, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "reportDate": "111/08", - "iTotalRecords": 5, - "aaData": [ - [ - "111/08/01", - "630,222,871", - "46,240,795,178", - "436,953", - "182.75", - "-0.83" - ], - [ - "111/08/02", - "694,614,949", - "51,249,692,749", - "484,905", - "179.30", - "-3.45" - ], - [ - "111/08/03", - "683,637,182", - "50,799,048,121", - "473,344", - "178.17", - "-1.13" - ], - [ - "111/08/04", - "677,879,834", - "51,578,056,349", - "458,468", - "178.18", - "0.01" - ], - [ - "111/08/05", - "651,963,371", - "57,144,930,041", - "435,858", - "182.37", - "4.19" - ] - ] + "tables": [ + { + "title": "日成交量值指數", + "date": "20220801", + "data": [ + [ + "111/08/01", + "630,223", + "46,240,795", + "436,953", + 182.75, + -0.83 + ], + [ + "111/08/02", + "694,615", + "51,249,693", + "484,905", + 179.30, + -3.45 + ], + [ + "111/08/03", + "683,637", + "50,799,048", + "473,344", + 178.17, + -1.13 + ], + [ + "111/08/04", + "677,880", + "51,578,056", + "458,468", + 178.18, + 0.01 + ], + [ + "111/08/05", + "651,963", + "57,144,930", + "435,858", + 182.37, + 4.19 + ] + ], + "fields": [ + "日期", + "成交股數(仟股)", + "金額(仟元)", + "筆數", + "櫃買指數", + "漲/跌" + ], + "notes": [ + "上表為於等價、零股、盤後定價等交易系統交易之上櫃股票成交資訊。", + "每日下午6:00另行產製於等價、零股、盤後定價、鉅額等交易系統交易之上櫃股票、權證、TDR、ETF、ETN、受益證券等上櫃有價證券之成交資訊,但不含轉(交)換公司債之成交統計報表,如連結" + ], + "totalCount": 5, + "summary": [] + } + ], + "date": "20220801", + "stat": "ok" }`) }) @@ -358,40 +378,40 @@ func TestMarketDataService_DownloadTpex(t *testing.T) { want := []MarketData{ { Date: civil.Date{Year: 2022, Month: time.August, Day: 1}, - TradeVolume: 630222871, - TradeValue: decimal.NewFromInt(46240795178), + TradeVolume: 630223000, + TradeValue: decimal.NewFromInt(46240795000), Transaction: 436953, Index: decimal.NewFromFloat(182.75), Change: decimal.NewFromFloat(-0.83), }, { Date: civil.Date{Year: 2022, Month: time.August, Day: 2}, - TradeVolume: 694614949, - TradeValue: decimal.NewFromInt(51249692749), + TradeVolume: 694615000, + TradeValue: decimal.NewFromInt(51249693000), Transaction: 484905, Index: decimal.NewFromFloat(179.30), Change: decimal.NewFromFloat(-3.45), }, { Date: civil.Date{Year: 2022, Month: time.August, Day: 3}, - TradeVolume: 683637182, - TradeValue: decimal.NewFromInt(50799048121), + TradeVolume: 683637000, + TradeValue: decimal.NewFromInt(50799048000), Transaction: 473344, Index: decimal.NewFromFloat(178.17), Change: decimal.NewFromFloat(-1.13), }, { Date: civil.Date{Year: 2022, Month: time.August, Day: 4}, - TradeVolume: 677879834, - TradeValue: decimal.NewFromInt(51578056349), + TradeVolume: 677880000, + TradeValue: decimal.NewFromInt(51578056000), Transaction: 458468, Index: decimal.NewFromFloat(178.18), Change: decimal.NewFromFloat(0.01), }, { Date: civil.Date{Year: 2022, Month: time.August, Day: 5}, - TradeVolume: 651963371, - TradeValue: decimal.NewFromInt(57144930041), + TradeVolume: 651963000, + TradeValue: decimal.NewFromInt(57144930000), Transaction: 435858, Index: decimal.NewFromFloat(182.37), Change: decimal.NewFromFloat(4.19), @@ -430,9 +450,36 @@ func TestMarketDataService_DownloadTpexErrNoData(t *testing.T) { mux.HandleFunc(tpexMarketDataPath, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "reportDate": "111/08", - "iTotalRecords": 0, - "aaData": [] + "date": "20220801", + "stat": "ok", + "tables": [] + }`) + }) + + _, err := client.MarketData.DownloadTpex(2022, 8) + if err == nil { + t.Error("MarketData.DownloadTpex returned nil; expected error") + } + if !errors.Is(err, ErrNoData) { + t.Errorf("MarketData.DownloadTpex returned %v, want %v", err, ErrNoData) + } +} + +func TestMarketDataService_DownloadTpexErrNoData2(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc(tpexMarketDataPath, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "date": "20220801", + "stat": "ok", + "tables": [ + { + "data": [], + "totalCount": 0 + } + ] }`) }) @@ -452,9 +499,14 @@ func TestMarketDataService_DownloadTpexBadDataLength(t *testing.T) { mux.HandleFunc(tpexMarketDataPath, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "reportDate": "111/08", - "iTotalRecords": 1, - "aaData": [] + "tables": [ + { + "data": [], + "totalCount": 1 + } + ], + "date": "20220801", + "stat": "ok" }`) }) @@ -471,18 +523,38 @@ func TestMarketDataService_DownloadTpexBadData(t *testing.T) { mux.HandleFunc(tpexMarketDataPath, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "reportDate": "111/08", - "iTotalRecords": 1, - "aaData": [ - [ - "111/30/01", - "630,222,871", - "46,240,795,178", - "436,953", - "182.75", - "-0.83" - ] - ] + "tables": [ + { + "title": "日成交量值指數", + "date": "20220801", + "data": [ + [ + "111/08/01", + "BADDATA", + "46,240,795", + "436,953", + 182.75, + -0.83 + ] + ], + "fields": [ + "日期", + "成交股數(仟股)", + "金額(仟元)", + "筆數", + "櫃買指數", + "漲/跌" + ], + "notes": [ + "上表為於等價、零股、盤後定價等交易系統交易之上櫃股票成交資訊。", + "每日下午6:00另行產製於等價、零股、盤後定價、鉅額等交易系統交易之上櫃股票、權證、TDR、ETF、ETN、受益證券等上櫃有價證券之成交資訊,但不含轉(交)換公司債之成交統計報表,如連結" + ], + "totalCount": 1, + "summary": [] + } + ], + "date": "20220801", + "stat": "ok" }`) }) diff --git a/twstock/quote.go b/twstock/quote.go index f66c80c..8cd67ef 100644 --- a/twstock/quote.go +++ b/twstock/quote.go @@ -30,7 +30,7 @@ const ( twseQuotesPath = "/rwd/zh/afterTrading/STOCK_DAY" // 上櫃個股日成交資訊 - tpexQuotesPath = "/web/stock/aftertrading/daily_trading_info/st43_result.php" + tpexQuotesPath = "/www/zh-tw/afterTrading/tradingStock" // 個股即時交易行情 realtimeQuotesPath = "/stock/api/getStockInfo.jsp" @@ -214,16 +214,43 @@ func (s *QuoteService) DownloadTwse(code string, year int, month time.Month) ([] } type tpexOptions struct { - Date string `url:"d"` - Code string `url:"stkno,omitempty"` + Response string `url:"response"` + Date string `url:"date"` + Code string `url:"code,omitempty"` +} + +type StringOrNumber string + +func (s *StringOrNumber) UnmarshalJSON(data []byte) error { + // Try unmarshaling as string first + var str string + if err := json.Unmarshal(data, &str); err == nil { + *s = StringOrNumber(str) + return nil + } + + // If that fails, try as number + var num float64 + if err := json.Unmarshal(data, &num); err == nil { + *s = StringOrNumber(strconv.FormatFloat(num, 'f', -1, 64)) + return nil + } + + return fmt.Errorf("value must be string or number") } type tpexResponse struct { - Code string `json:"stkNo"` - Name string `json:"stkName"` - Date string `json:"reportDate"` - DataLength int `json:"iTotalRecords"` - Data [][]string `json:"aaData"` + Stat string `json:"stat"` + Date string `json:"date"` + Code string `json:"code"` + Tables []struct { + Title string `json:"title"` + Date string `json:"date"` + Data [][]StringOrNumber `json:"data"` + Fields []string `json:"fields"` + Notes []string `json:"notes"` + TotalCount int `json:"totalCount"` + } `json:"tables"` } // 從證券櫃檯買賣中心下載盤後個股日成交資訊 @@ -234,9 +261,9 @@ func (s *QuoteService) DownloadTpex(code string, year int, month time.Month) ([] } url, _ := s.client.tpexBaseURL.Parse(tpexQuotesPath) opts := tpexOptions{ - // 需要將西元年轉為民國年 - Date: fmt.Sprintf("%d/%02d", date.Year-1911, date.Month), - Code: code, + Response: "json", + Date: fmt.Sprintf("%04d/%02d/%02d", date.Year, date.Month, date.Day), + Code: code, } url, _ = addOptions(url, opts) req, _ := s.client.NewRequest("GET", url.String(), nil) @@ -248,18 +275,22 @@ func (s *QuoteService) DownloadTpex(code string, year int, month time.Month) ([] if resp.Code != code { return nil, fmt.Errorf("invalid tpex code returned %s, want %s", resp.Code, code) } - if resp.DataLength != len(resp.Data) { - return nil, fmt.Errorf("failed parsing quote data length returned %d, want %d", resp.DataLength, len(resp.Data)) - } - if resp.DataLength == 0 { + if len(resp.Tables) != 1 || resp.Tables[0].TotalCount == 0 { return nil, ErrNoData } + if resp.Tables[0].TotalCount != len(resp.Tables[0].Data) { + return nil, fmt.Errorf("failed parsing quote data length returned %d, want %d", resp.Tables[0].TotalCount, len(resp.Tables[0].Data)) + } quotes := []Quote{} - for _, data := range resp.Data { + for _, data := range resp.Tables[0].Data { if len(data) != 9 { return nil, fmt.Errorf("failed parsing quote fields") } - quote, err := s.parse(data) + stringData := make([]string, len(data)) + for i, v := range data { + stringData[i] = string(v) + } + quote, err := s.parse(stringData) if err != nil { if errors.Is(err, errSuspendedTrading) { continue diff --git a/twstock/quote_test.go b/twstock/quote_test.go index dff18e6..de97213 100644 --- a/twstock/quote_test.go +++ b/twstock/quote_test.go @@ -362,70 +362,93 @@ func TestQuoteService_DownloadTpex(t *testing.T) { testMethod(t, r, "GET") fmt.Fprint(w, ` { - "stkNo": "3374", - "stkName": "精材 ", + "tables": [ + { + "title": "個股日成交資訊", + "subtitle": "3374 精材 111年08月", + "date": "20220801", + "data": [ + [ + "111/08/01", + "1,328", + "168,265", + "127.50", + "128.00", + "125.50", + "127.00", + "-2.00", + "1,272" + ], + [ + "111/08/02", + "1,593", + "199,305", + "125.00", + "127.00", + "123.00", + "127.00", + "0.00", + "1,078" + ], + [ + "111/08/03", + "1,603", + "201,304", + "124.50", + "127.00", + "124.00", + "126.00", + "-1.00", + "1,124" + ], + [ + "111/08/04", + "3,920", + "500,389", + "126.50", + "130.00", + "124.50", + "129.50", + "3.50", + "2,474" + ], + [ + "111/08/05", + "7,244", + "940,126", + "129.50", + "132.00", + "126.50", + "129.50", + "0.00", + "5,073" + ] + ], + "fields": [ + "日 期", + "成交仟股", + "成交仟元", + "開盤", + "最高", + "最低", + "收盤", + "漲跌", + "筆數" + ], + "notes": [ + "以上資料不含上櫃股票鉅額交易" + ], + "totalCount": 5, + "summary": [] + } + ], + "date": "20220801", + "code": "3374", + "name": "精材", "showListPriceNote": false, "showListPriceLink": false, - "reportDate": "111/08", - "iTotalRecords": 5, - "aaData": [ - [ - "111/08/01", - "1,328", - "168,265", - "127.50", - "128.00", - "125.50", - "127.00", - "-2.00", - "1,272" - ], - [ - "111/08/02", - "1,593", - "199,305", - "125.00", - "127.00", - "123.00", - "127.00", - "0.00", - "1,078" - ], - [ - "111/08/03", - "1,603", - "201,304", - "124.50", - "127.00", - "124.00", - "126.00", - "-1.00", - "1,124" - ], - [ - "111/08/04", - "3,920", - "500,389", - "--", - "130.00", - "124.50", - "129.50", - "3.50", - "2,474" - ], - [ - "111/08/05", - "7,244", - "940,126", - "129.50", - "132.00", - "126.50", - "129.50", - "0.00", - "5,073" - ] - ] - }`) + "stat": "ok" + }`) }) quotes, err := client.Quote.Download("3374", 2022, 8) @@ -457,6 +480,14 @@ func TestQuoteService_DownloadTpex(t *testing.T) { Close: decimal.NewFromFloat(126), Volume: 1603000, }, + { + Date: civil.Date{Year: 2022, Month: time.August, Day: 4}, + Open: decimal.NewFromFloat(126.5), + High: decimal.NewFromFloat(130), + Low: decimal.NewFromFloat(124.5), + Close: decimal.NewFromFloat(129.5), + Volume: 3920000, + }, { Date: civil.Date{Year: 2022, Month: time.August, Day: 5}, Open: decimal.NewFromFloat(129.5), @@ -505,12 +536,63 @@ func TestQuoteService_DownloadTpexErrNoData(t *testing.T) { testMethod(t, r, "GET") fmt.Fprint(w, ` { - "stkNo": "3374", - "stkName": "精材 ", + "tables": [], + "date": "20220801", + "code": "3374", + "name": "精材", "showListPriceNote": false, "showListPriceLink": false, - "reportDate": "111/08", - "iTotalRecords": 0 + "stat": "ok" + }`) + }) + + _, err := client.Quote.Download("3374", 2022, 8) + if err == nil { + t.Errorf("Quote.Download returned nil; expected error") + } + if !errors.Is(err, ErrNoData) { + t.Errorf("Quote.Download returned %v, want %v", err, ErrNoData) + } +} + +func TestQuoteService_DownloadTpexErrNoData2(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc(tpexQuotesPath, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, ` + { + "tables": [ + { + "title": "個股日成交資訊", + "subtitle": "3374 精材 111年08月", + "date": "20220801", + "data": [], + "fields": [ + "日 期", + "成交仟股", + "成交仟元", + "開盤", + "最高", + "最低", + "收盤", + "漲跌", + "筆數" + ], + "notes": [ + "以上資料不含上櫃股票鉅額交易" + ], + "totalCount": 0, + "summary": [] + } + ], + "date": "20220801", + "code": "3374", + "name": "精材", + "showListPriceNote": false, + "showListPriceLink": false, + "stat": "ok" }`) }) @@ -531,14 +613,14 @@ func TestQuoteService_DownloadBadCode(t *testing.T) { testMethod(t, r, "GET") fmt.Fprint(w, ` { - "stkNo": "3375", - "stkName": "精材 ", + "tables": [], + "date": "20220801", + "code": "3375", + "name": "精材", "showListPriceNote": false, "showListPriceLink": false, - "reportDate": "111/08", - "iTotalRecords": 5, - "aaData": [] - }`) + "stat": "ok" + }`) }) _, err := client.Quote.DownloadTpex("3374", 2022, 8) @@ -555,14 +637,37 @@ func TestQuoteService_DownloadBadDataLength(t *testing.T) { testMethod(t, r, "GET") fmt.Fprint(w, ` { - "stkNo": "3374", - "stkName": "精材 ", + "tables": [ + { + "title": "個股日成交資訊", + "subtitle": "3374 精材 111年08月", + "date": "20220801", + "data": [], + "fields": [ + "日 期", + "成交仟股", + "成交仟元", + "開盤", + "最高", + "最低", + "收盤", + "漲跌", + "筆數" + ], + "notes": [ + "以上資料不含上櫃股票鉅額交易" + ], + "totalCount": 23, + "summary": [] + } + ], + "date": "20220801", + "code": "3374", + "name": "精材", "showListPriceNote": false, "showListPriceLink": false, - "reportDate": "111/08", - "iTotalRecords": 5, - "aaData": [] - }`) + "stat": "ok" + }`) }) _, err := client.Quote.DownloadTpex("3374", 2022, 8) @@ -579,27 +684,50 @@ func TestQuoteService_DownloadBadFieldCount(t *testing.T) { testMethod(t, r, "GET") fmt.Fprint(w, ` { - "stkNo": "3374", - "stkName": "精材 ", + "tables": [ + { + "title": "個股日成交資訊", + "subtitle": "3374 精材 111年08月", + "date": "20220801", + "data": [ + [ + "111/08/01", + "1,328", + "168,265", + "127.50", + "128.00", + "125.50", + "127.00", + "-2.00", + "1,272", + "" + ] + ], + "fields": [ + "日 期", + "成交仟股", + "成交仟元", + "開盤", + "最高", + "最低", + "收盤", + "漲跌", + "筆數" + ], + "notes": [ + "以上資料不含上櫃股票鉅額交易" + ], + "totalCount": 1, + "summary": [] + } + ], + "date": "20220801", + "code": "3374", + "name": "精材", "showListPriceNote": false, "showListPriceLink": false, - "reportDate": "111/08", - "iTotalRecords": 1, - "aaData": [ - [ - "111/08/01", - "1,328", - "168,265", - "127.50", - "128.00", - "125.50", - "127.00", - "-2.00", - "1,272", - "" - ] - ] - }`) + "stat": "ok" + }`) }) _, err := client.Quote.DownloadTpex("3374", 2022, 8) @@ -616,25 +744,49 @@ func TestQuoteService_DownloadBadData(t *testing.T) { testMethod(t, r, "GET") fmt.Fprint(w, ` { - "stkNo": "3374", - "stkName": "精材 ", + "tables": [ + { + "title": "個股日成交資訊", + "subtitle": "3374 精材 111年08月", + "date": "20220801", + "data": [ + [ + "111/08/01", + "1,328", + "168,265", + "BADDATA", + "128.00", + "125.50", + "127.00", + "-2.00", + "1,272", + "" + ] + ], + "fields": [ + "日 期", + "成交仟股", + "成交仟元", + "開盤", + "最高", + "最低", + "收盤", + "漲跌", + "筆數" + ], + "notes": [ + "以上資料不含上櫃股票鉅額交易" + ], + "totalCount": 1, + "summary": [] + } + ], + "date": "20220801", + "code": "3374", + "name": "精材", "showListPriceNote": false, "showListPriceLink": false, - "reportDate": "111/08", - "iTotalRecords": 1, - "aaData": [ - [ - "111/08/01", - "1,328", - "168,265", - "BADDATA", - "128.00", - "125.50", - "127.00", - "-2.00", - "1,272" - ] - ] + "stat": "ok" }`) })