突然间想使用Go从通达信读取A股历史行情信息,其实也蛮简单的。从通达信获取数据难点在于分析数据结构,而读取则各类语言分分钟搞定。

1. 准备工作

下载安装通达信,通达信官网 下载历史行情数据 下载操作路径:系统->盘后数据下载

通达信下载盘后数据

下载后数据按股票市场分别存放:

1上海交易所:{通达信安装目录}\vipdoc\sh\lday\*.day
2深圳交易所:{通达信安装目录}\vipdoc\sz\lday\*.day

2. 通达信历史日线数据文件格式

每只股票一个day文件,如:sh000001.day。文件中每一天数据总共32字节。其中每32字节数据格式如下:

数据含义 数据类型 数据长度 举例 单位
日期 Integer 4 20170703
开盘价 Integer 4 2476 当前值/100,元
最高价 Integer 4 2520 当前值 /100,元
最低价 Integer 4 2436 当前值 / 100,元
收盘价 Integer 4 2457 当前值 / 100,元
成交金额 single 4 1317335898 元
成交量 Integer 4 45293799 股
保留 Integer 4

注意,因为价格均是两位小数,故文件中的价格放大100倍,以便按数字存储。

3. Go读取日线数据文件

文件每32字节存储一天数据,在Go中只需读取指定长度的字节,再转换为int即可。

第一步:读取文件 以万科股票为例,打开该day文件,获得 reader。

1f, err := os.Open(`D:\new_tdx\vipdoc\sz\lday\sz000002.day`)
2if err != nil {
3	log.Fatal(err)
4}
5defer f.Close()

第二步:获取32字节数据

从文件流中填充32个字节的数据到oneDay中,如果无错误则可以按照数据格式读取单独一天的日行情数据。

注意取价格时需再除以100,已显示正确的金额。

 1oneDay := make([]byte, 32)
 2_, err = f.Read(oneDay)
 3if err != nil {
 4	log.Fatal(err)
 5}
 6fmt.Println("日期:\t", binary.LittleEndian.Uint32(oneDay[0:4]))
 7fmt.Println("开盘价(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[4:8]))/100)
 8fmt.Println("最高价(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[8:12]))/100)
 9fmt.Println("最低价(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[12:16]))/100)
10fmt.Println("收盘价(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[16:20]))/100)
11fmt.Println("成交金额(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[20:24]))/100)
12fmt.Println("成交量:\t", binary.LittleEndian.Uint32(oneDay[24:28]))
13fmt.Println("Other:\t", binary.LittleEndian.Uint32(oneDay[28:32]))
14//output:
15/*
16日期:    20170703
17开盘价(元):      24.76
18最高价(元):      25.2
19最低价(元):      24.36
20收盘价(元):      24.57
21成交金额(元):    1.317335898e+07
22成交量:  45293799
23Other:   65536
24*/

第三步:遍历获取所有日线数据

 1oneDay := make([]byte, 32)
 2for {
 3	l, err := f.Read(oneDay)
 4	if err == io.EOF {
 5		break
 6	} else if err != nil {
 7		log.Fatal(err)
 8	} else if l != 32 {
 9		log.Fatal("数据不完整,终止")
10	}
11    //Date
12	fmt.Printf("%d,", binary.LittleEndian.Uint32(oneDay[0:4]))
13	//other
14}

历史数据可在其他网站上查看,下图来自搜狐数据

通达信下载盘后数据

完整代码:

 1package main
 2import (
 3	"bytes"
 4	"encoding/binary"
 5	"fmt"
 6	"io"
 7	"log"
 8	"os" 
 9)
10func main() {
11	f, err := os.Open(`D:\new_tdx\vipdoc\sz\lday\sz000002.day`)
12	if err != nil {
13		log.Fatal(err)
14	}
15	defer f.Close()
16	oneDay := make([]byte, 32)
17	fmt.Println("日期\t\t开盘价(元)\t最高价(元)\t最低价(元)\t收盘价(元)\t成交金额(元)\t成交量\tOther")
18	for {
19		l, err := f.Read(oneDay)
20		if err == io.EOF {
21			break
22		} else if err != nil {
23			log.Fatal(err)
24		} else if l != 32 {
25			log.Fatal("数据不完整,终止")
26		}
27		fmt.Printf("%d\t%f\t%f\t%f\t%f\t%f\t%d\t%d\t\n",
28			binary.LittleEndian.Uint32(oneDay[0:4]),
29			(float64)(binary.LittleEndian.Uint32(oneDay[4:8]))/100,
30			(float64)(binary.LittleEndian.Uint32(oneDay[8:12]))/100,
31			(float64)(binary.LittleEndian.Uint32(oneDay[12:16]))/100,
32			(float64)(binary.LittleEndian.Uint32(oneDay[16:20]))/100,
33			(float64)(binary.LittleEndian.Uint32(oneDay[20:24]))/100,
34			binary.LittleEndian.Uint32(oneDay[24:28]),
35			binary.LittleEndian.Uint32(oneDay[28:32]),
36		)
37	}
38}

4. 进价

有没有更好的办法来进行数据转换?如下格式处理,过于麻烦,幸好该格式数据不多。但容易弄错,或者调整麻烦。

1fmt.Println("日期:\t", binary.LittleEndian.Uint32(oneDay[0:4]))
2fmt.Println("开盘价(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[4:8]))/100)
3fmt.Println("最高价(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[8:12]))/100)
4fmt.Println("最低价(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[12:16]))/100)
5fmt.Println("收盘价(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[16:20]))/100)
6fmt.Println("成交金额(元):\t", (float64)(binary.LittleEndian.Uint32(oneDay[20:24]))/100)
7fmt.Println("成交量:\t", binary.LittleEndian.Uint32(oneDay[24:28]))
8fmt.Println("Other:\t", binary.LittleEndian.Uint32(oneDay[28:32])) `

可以利用Go的encoding/binary包从reader中读取二元数据到指针对象中:

 1// Read reads structured binary data from r into data.
 2// Data must be a pointer to a fixed-size value or a slice
 3// of fixed-size values.
 4// Bytes read from r are decoded using the specified byte order
 5// and written to successive fields of the data.
 6// When decoding boolean values, a zero byte is decoded as false, and
 7// any other non-zero byte is decoded as true.
 8// When reading into structs, the field data for fields with
 9// blank (_) field names is skipped; i.e., blank field names
10// may be used for padding.
11// When reading into a struct, all non-blank fields must be exported.
12//
13// The error is EOF only if no bytes were read.
14// If an EOF happens after reading some but not all the bytes,
15// Read returns ErrUnexpectedEOF.
16func binary.Read(r io.Reader, order ByteOrder, data interface{}) error{}

这样便可以方便的将byte读取到对象中,下面我们定义data结构:

 1type DayData struct {
 2	Date                   int32
 3	Open, High, Low, Close int32
 4	Amount, Qty            int32
 5	Other                  int32
 6}
 7var d DataData
 8buf := bytes.NewBuffer(oneDay)
 9binary.Read(buf, binary.LittleEndian, &d)
10fmt.Printf("%v\n", d)

注意,在binary读取buf到对象时,是依次遍历对象的内存结构赋值的,因为文件中各数据均是4位长度。故在定义字段类型时,选择int32,占4个字节。字段定义顺序便是内存结构顺序,同文件数据定义顺序保持一致。

但因为数据中均存放的是int32,而收盘价等需要进行转换,故对外时提供另一个结构体,已方便正常访问各数据。 在解析时,进行一次解析即可。

 1//DayQuotaion 日线行情
 2type DayQuotaion struct {
 3	Marker    string  //股票市场
 4	StockCode string  //股票代码
 5	Date      int     //日期
 6	Open      float32 //开盘价
 7	High      float32 //最高价
 8	Low       float32 //最低价
 9	Close     float32 //收盘价
10	Amount    float32 //总成交金额
11	Qty       float32 //  总成交量
12} 

5. 完整代码

 1package main
 2import (
 3	"bytes"
 4	"encoding/binary"
 5	"errors"
 6	"fmt"
 7	"io"
 8	"log"
 9	"os"
10	"path/filepath"
11	"strings"
12)
13const historyDailyQuotationPath = `D:\new_tdx\vipdoc`
14type dayData struct {
15	Date                   int32
16	Open, High, Low, Close int32
17	Amount, Qty            int32
18	Other                  int32
19}
20// To 转换为日线行情数据
21func (d *dayData) To() *DayQuotation {
22	return &DayQuotation{
23		Date:   d.Date,
24		Open:   float32(d.Open) / 100,
25		High:   float32(d.High) / 100,
26		Low:    float32(d.Low) / 100,
27		Close:  float32(d.Close) / 100,
28		Amount: float32(d.Amount),
29		Qty:    d.Qty,
30	}
31}
32// DayQuotation 日线行情
33type DayQuotation struct {
34	Date   int32   //日期
35	Open   float32 //开盘价
36	High   float32 //最高价
37	Low    float32 //最低价
38	Close  float32 //收盘价
39	Amount float32 //总成交金额
40	Qty    int32   //  总成交量
41}
42//GetStockQuoation 获取股票历史行情
43func GetStockQuoation(marker, stockCode string) ([]*DayQuotation, error) {
44	marker = strings.ToLower(strings.TrimSpace(marker))
45	stockCode = strings.ToLower(strings.TrimSpace(stockCode))
46	if marker == "" || stockCode == "" {
47		return nil, errors.New("marker和stockCode不能为空")
48	}
49	//文件路径,e.g. D:\new_tdx\vipdoc\sz\lday\sz000002.day
50	name := filepath.Join(historyDailyQuotationPath, marker, "lday", fmt.Sprintf("%s%s.day", marker, stockCode))
51	f, err := os.Open(name)
52	if err != nil {
53		return nil, err
54	}
55	defer f.Close()
56	quos := []*DayQuotation{}
57	oneDay := make([]byte, 32)
58	data := &dayData{}
59	for {
60		l, err := f.Read(oneDay)
61		if err == io.EOF {
62			break
63		} else if err != nil {
64			return quos, err
65		} else if l != 32 {
66			return quos, errors.New("数据不完整")
67		}
68		buf := bytes.NewBuffer(oneDay)
69		err = binary.Read(buf, binary.LittleEndian, data)
70		if err != nil {
71			return quos, err
72		}
73		quos = append(quos, data.To())
74	}
75	return quos, nil
76}
77func main() {
78	quos, err := GetStockQuoation("sz", "000002")
79	if err != nil {
80		log.Fatal(err)
81	}
82	for _, v := range quos {
83		fmt.Printf("%v\n", v)
84	}
85}