├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal ├── compress.go ├── hex.go ├── hex_test.go ├── number.go ├── number_test.go ├── sequence.go ├── sequence_test.go ├── strings.go ├── time.go ├── time_test.go ├── unit.go └── varints.go ├── proto ├── TdxProtocol.md ├── command.go ├── command_test.go ├── ext │ └── ex_cmd1.go └── std │ ├── finance_info.go │ ├── protocol.go │ ├── security_count.go │ ├── security_list.go │ ├── setup_cmd1.go │ ├── setup_cmd2.go │ └── setup_cmd3.go ├── quotes ├── api.go ├── api_functions.go ├── api_test.go ├── base_client.go ├── base_client_test.go ├── base_consts.go ├── base_message.go ├── base_message_test.go ├── base_pool.go ├── base_timer.go ├── base_timer_test.go ├── bestip.go ├── bestip.txt ├── bestip_address.go ├── bestip_cache.go ├── bestip_cache_test.go ├── bestip_client.go ├── bestip_command.go ├── bestip_embed.go ├── bestip_test.go ├── block_info.go ├── block_meta.go ├── cmd_hearbeat.go ├── cmd_hello1.go ├── cmd_hello2.go ├── index_bars.go ├── resources │ ├── guotaijunan.cfg │ ├── huatai.cfg │ ├── tdx.cfg │ └── zhongxin.cfg ├── stock_company_info_category.go ├── stock_company_info_category_test.go ├── stock_company_info_content.go ├── stock_company_info_content_test.go ├── stock_finance_info.go ├── stock_finance_info_test.go ├── stock_minute_time_data.go ├── stock_minute_time_data_history.go ├── stock_minute_time_test.go ├── stock_security_bars.go ├── stock_security_bars_test.go ├── stock_security_count.go ├── stock_security_list.go ├── stock_security_list_test.go ├── stock_security_quotes.go ├── stock_security_quotes_new.go ├── stock_security_quotes_new_test.go ├── stock_security_quotes_test.go ├── stock_security_snapshot.go ├── stock_security_snapshot_test.go ├── stock_transaction_data.go ├── stock_transaction_data_history.go ├── stock_transaction_data_test.go ├── stock_xdxr_info.go └── stock_xdxr_info_test.go ├── securities ├── block.go ├── block_config.go ├── block_data.go ├── block_embed.go ├── block_industry.go ├── block_parse.go ├── block_raw.go ├── block_test.go ├── block_type.go ├── margin_trading.go ├── margin_trading_test.go ├── resources │ ├── README.md │ ├── margin-trading.csv │ ├── tdxhy.cfg │ ├── tdxzs.cfg │ └── tdxzs3.cfg ├── security_list.go └── security_list_test.go ├── tdx-client.go └── tdx-client_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | *.csv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 王布衣 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gotdx 2 | golang实现的一个通达信数据协议库 3 | 4 | ## 1. 概要 5 | - 整合了[gotdx](https://github.com/bensema/gotdx.git)和[TdxPy](https://github.com/rainx/pytdx) 6 | - 增加了连接池的功能 7 | - 自动探测主机网络速度 8 | - 调用简单 9 | 10 | ## 2. 第一次使用, 获取日K线 11 | 第一次运行时, 连接池会探测服务器网络速度会慢一些, 网络测速后会缓存到本地。 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "gitee.com/quant1x/gotdx" 19 | "gitee.com/quant1x/gotdx/proto" 20 | ) 21 | 22 | func main() { 23 | api := gotdx.GetTdxApi() 24 | klines, err := api.GetKLine("sh600600", proto.KLINE_TYPE_RI_K, 0, 1) 25 | fmt.Println(err) 26 | fmt.Println(klines) 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gitee.com/quant1x/gotdx 2 | 3 | go 1.24 4 | 5 | require ( 6 | gitee.com/quant1x/exchange v0.6.3 7 | gitee.com/quant1x/gox v1.22.11 8 | gitee.com/quant1x/num v0.4.6 9 | gitee.com/quant1x/pkg v0.5.1 10 | golang.org/x/text v0.23.0 11 | ) 12 | 13 | require ( 14 | github.com/dlclark/regexp2 v1.11.5 // indirect 15 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 16 | github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect 17 | github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 // indirect 18 | golang.org/x/sys v0.31.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gitee.com/quant1x/exchange v0.6.3 h1:5NrmcAoJdAwToC1btQsIMD/pDnySlSgXu4MwLk598fo= 2 | gitee.com/quant1x/exchange v0.6.3/go.mod h1:4q259ffHccwMPrMZkJNyBst6KmETptcq0v214aMWKcc= 3 | gitee.com/quant1x/gox v1.22.11 h1:2ASA1AwzY1RCTKEC6hJ6v8JwFHFc6zUD7bG86Cxl19s= 4 | gitee.com/quant1x/gox v1.22.11/go.mod h1:b4+VHSGarZOR1iUvv7frNK4mrFaWBmVbqXJjbi8Zf8I= 5 | gitee.com/quant1x/num v0.4.6 h1:SCbbvVVbYSLpZWJS95RkDPm+X+7s0oMGip2e4/kxYNY= 6 | gitee.com/quant1x/num v0.4.6/go.mod h1:IDY6NbLMqrw85/vbLnfDDa9O12RJXpeWN4rzVEc5jHw= 7 | gitee.com/quant1x/pkg v0.5.1 h1:Uj0jruPoElL08NnNEAzSpf6w6vw7j22O9OTnbANuzZw= 8 | gitee.com/quant1x/pkg v0.5.1/go.mod h1:DsUiZYEP3r+PE1SyrU9RFXMhnNTSh8kvTOQn2S54xrw= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 12 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 13 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= 14 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 15 | github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= 16 | github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 17 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 18 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs= 22 | github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 26 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 27 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 28 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 29 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 30 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 31 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 32 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 33 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 34 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /internal/compress.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "gitee.com/quant1x/gox/api" 7 | "io" 8 | ) 9 | 10 | // ZlibCompress 进行zlib压缩 11 | func ZlibCompress(src []byte) ([]byte, error) { 12 | var in bytes.Buffer 13 | w := zlib.NewWriter(&in) 14 | _, err := w.Write(src) 15 | if err != nil { 16 | return nil, err 17 | } 18 | err = w.Close() 19 | if err != nil { 20 | return nil, err 21 | } 22 | return in.Bytes(), nil 23 | } 24 | 25 | // ZlibUnCompress 进行zlib解压缩 26 | func ZlibUnCompress(compressSrc []byte) ([]byte, error) { 27 | b := bytes.NewReader(compressSrc) 28 | var out bytes.Buffer 29 | r, err := zlib.NewReader(b) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer api.CloseQuietly(r) 34 | _, err = io.Copy(&out, r) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return out.Bytes(), nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/hex.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | // HexString2Bytes 16进制字符串转bytes 11 | func HexString2Bytes(hexStr string) []byte { 12 | hexStr = strings.Replace(hexStr, " ", "", -1) 13 | data, err := hex.DecodeString(hexStr) 14 | if err != nil { 15 | // handle error 16 | log.Println(err.Error()) 17 | return nil 18 | } 19 | return data 20 | } 21 | 22 | // Bytes2HexString bytes转16进制字符串 23 | func Bytes2HexString(b []byte) string { 24 | // with "%x" format byte array into hex string 25 | return fmt.Sprintf("% x", b) 26 | } 27 | -------------------------------------------------------------------------------- /internal/hex_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBytes2HexString(t *testing.T) { 8 | str := Bytes2HexString([]byte{0, 1, 1, 5, 4, 16, 255}) 9 | t.Log(str) 10 | //assert.Equal(t, "00 01 01 05 04 10 ff", str) 11 | } 12 | 13 | func TestHexString2Bytes(t *testing.T) { 14 | hexStr := "0c 02 18 93 00 01 03 00 03 00 0d 00 01" 15 | b := HexString2Bytes(hexStr) 16 | t.Log(b) 17 | } 18 | -------------------------------------------------------------------------------- /internal/number.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Float64IsNaN 判断float64是否NaN 8 | func Float64IsNaN(f float64) bool { 9 | return math.IsNaN(f) || math.IsInf(f, 0) 10 | } 11 | 12 | func NumberToFloat64[T uint16 | uint32 | float32](v T) float64 { 13 | return float64(v) 14 | } 15 | 16 | // IntToFloat64 整型转float64 17 | func IntToFloat64[T ~uint32 | ~int | ~int64](integer T) float64 { 18 | ivol := int(integer) 19 | logPoint := ivol >> (8 * 3) 20 | //hheax := ivol >> (8*3) // [4] 21 | hleax := (ivol >> (8 * 2)) & 0xff // [2] 22 | lheax := (ivol >> 8) & 0xff // [1] 23 | lleax := ivol & 0xff // [0] 24 | 25 | //dbl_1 := 1.0 26 | //dbl_2 := 2.0 27 | //dbl_128 := 128. 28 | 29 | dwEcx := logPoint*2 - 0x7f 30 | dwEdx := logPoint*2 - 0x86 31 | dwEsi := logPoint*2 - 0x8e 32 | dwEax := logPoint*2 - 0x96 33 | 34 | tmpEax := 0 35 | if dwEcx < 0 { 36 | tmpEax = -dwEcx 37 | } else { 38 | tmpEax = dwEcx 39 | } 40 | 41 | dblXmm6 := math.Pow(2.0, float64(tmpEax)) 42 | if dwEcx < 0 { 43 | dblXmm6 = 1.0 / dblXmm6 44 | } 45 | 46 | dblXmm4 := 0.0 47 | if hleax > 0x80 { 48 | tmpdblXmm3 := 0.0 49 | //tmpdblXmm1 := 0.0 50 | dwtmpeax := dwEdx + 1 51 | tmpdblXmm3 = math.Pow(2.0, float64(dwtmpeax)) 52 | dblXmm0 := math.Pow(2.0, float64(dwEdx)) * 128.0 53 | dblXmm0 += float64(hleax&0x7f) * tmpdblXmm3 54 | dblXmm4 = dblXmm0 55 | } else { 56 | dblXmm0 := 0.0 57 | if dwEdx >= 0 { 58 | dblXmm0 = math.Pow(2.0, float64(dwEdx)) * float64(hleax) 59 | } else { 60 | dblXmm0 = (1 / math.Pow(2.0, float64(dwEdx))) * float64(hleax) 61 | } 62 | dblXmm4 = dblXmm0 63 | } 64 | dblXmm3 := math.Pow(2.0, float64(dwEsi)) * float64(lheax) 65 | dblXmm1 := math.Pow(2.0, float64(dwEax)) * float64(lleax) 66 | if hleax&0x80 != 0 { 67 | dblXmm3 *= 2.0 68 | dblXmm1 *= 2.0 69 | } 70 | return dblXmm6 + dblXmm4 + dblXmm3 + dblXmm1 71 | } 72 | -------------------------------------------------------------------------------- /internal/number_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | ) 8 | 9 | func TestIntToFloat64(t *testing.T) { 10 | vol := 1226585056 11 | v1 := IntToFloat64(vol) 12 | v2 := getVolume(vol) 13 | fmt.Println(vol) 14 | fmt.Println(v1, v2) 15 | } 16 | 17 | func getVolume(ivol int) (volume float64) { 18 | logpoint := ivol >> (8 * 3) 19 | //hheax := ivol >> (8 * 3) // [3] 20 | hleax := (ivol >> (8 * 2)) & 0xff // [2] 21 | lheax := (ivol >> 8) & 0xff //[1] 22 | lleax := ivol & 0xff //[0] 23 | 24 | //dbl_1 := 1.0 25 | //dbl_2 := 2.0 26 | //dbl_128 := 128.0 27 | 28 | dwEcx := logpoint*2 - 0x7f 29 | dwEdx := logpoint*2 - 0x86 30 | dwEsi := logpoint*2 - 0x8e 31 | dwEax := logpoint*2 - 0x96 32 | tmpEax := dwEcx 33 | if dwEcx < 0 { 34 | tmpEax = -dwEcx 35 | } else { 36 | tmpEax = dwEcx 37 | } 38 | 39 | dbl_xmm6 := 0.0 40 | dbl_xmm6 = math.Pow(2.0, float64(tmpEax)) 41 | if dwEcx < 0 { 42 | dbl_xmm6 = 1.0 / dbl_xmm6 43 | } 44 | 45 | dbl_xmm4 := 0.0 46 | dbl_xmm0 := 0.0 47 | 48 | if hleax > 0x80 { 49 | tmpdbl_xmm3 := 0.0 50 | //tmpdbl_xmm1 := 0.0 51 | dwtmpeax := dwEdx + 1 52 | tmpdbl_xmm3 = math.Pow(2.0, float64(dwtmpeax)) 53 | dbl_xmm0 = math.Pow(2.0, float64(dwEdx)) * 128.0 54 | dbl_xmm0 += float64(hleax&0x7f) * tmpdbl_xmm3 55 | dbl_xmm4 = dbl_xmm0 56 | } else { 57 | if dwEdx >= 0 { 58 | dbl_xmm0 = math.Pow(2.0, float64(dwEdx)) * float64(hleax) 59 | } else { 60 | dbl_xmm0 = (1 / math.Pow(2.0, float64(dwEdx))) * float64(hleax) 61 | } 62 | dbl_xmm4 = dbl_xmm0 63 | } 64 | 65 | dbl_xmm3 := math.Pow(2.0, float64(dwEsi)) * float64(lheax) 66 | dbl_xmm1 := math.Pow(2.0, float64(dwEax)) * float64(lleax) 67 | if (hleax & 0x80) > 0 { 68 | dbl_xmm3 *= 2.0 69 | dbl_xmm1 *= 2.0 70 | } 71 | return dbl_xmm6 + dbl_xmm4 + dbl_xmm3 + dbl_xmm1 72 | } 73 | -------------------------------------------------------------------------------- /internal/sequence.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "sync/atomic" 4 | 5 | // 局部变量 6 | var ( 7 | // 序列号 8 | _seqId atomic.Uint32 9 | ) 10 | 11 | // SequenceId 生成序列号 12 | func SequenceId() uint32 { 13 | return _seqId.Add(1) 14 | } 15 | -------------------------------------------------------------------------------- /internal/sequence_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSeqID(t *testing.T) { 9 | for i := 0; i < 10; i++ { 10 | fmt.Println(SequenceId()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/strings.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "golang.org/x/text/encoding/simplifiedchinese" 6 | "golang.org/x/text/encoding/traditionalchinese" 7 | "golang.org/x/text/transform" 8 | "io" 9 | "strings" 10 | ) 11 | 12 | func Utf8ToGbk(text []byte) string { 13 | pos := bytes.IndexByte(text, 0x00) 14 | if pos >= 0 { 15 | text = text[:pos] 16 | } 17 | r := bytes.NewReader(text) 18 | decoder := transform.NewReader(r, simplifiedchinese.GBK.NewDecoder()) //GB18030 19 | content, _ := io.ReadAll(decoder) 20 | return strings.ReplaceAll(string(content), string([]byte{0x00}), "") 21 | } 22 | 23 | // Utf8ToGbk utf8 转gbk 24 | func v1Utf8ToGbk(text []byte) string { 25 | r := bytes.NewReader(text) 26 | decoder := transform.NewReader(r, simplifiedchinese.GBK.NewDecoder()) //GB18030 27 | content, _ := io.ReadAll(decoder) 28 | return strings.ReplaceAll(string(content), string([]byte{0x00}), "") 29 | } 30 | 31 | // DecodeGBK convert GBK to UTF-8 32 | func DecodeGBK(s []byte) ([]byte, error) { 33 | I := bytes.NewReader(s) 34 | decoder := transform.NewReader(I, simplifiedchinese.GBK.NewDecoder()) 35 | d, err := io.ReadAll(decoder) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return d, nil 40 | } 41 | 42 | // EncodeGBK convert UTF-8 to GBK 43 | func EncodeGBK(s []byte) ([]byte, error) { 44 | I := bytes.NewReader(s) 45 | encoder := transform.NewReader(I, simplifiedchinese.GBK.NewEncoder()) 46 | d, err := io.ReadAll(encoder) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return d, nil 51 | } 52 | 53 | // DecodeBig5 convert BIG5 to UTF-8 54 | func DecodeBig5(s []byte) ([]byte, error) { 55 | I := bytes.NewReader(s) 56 | O := transform.NewReader(I, traditionalchinese.Big5.NewDecoder()) 57 | d, e := io.ReadAll(O) 58 | if e != nil { 59 | return nil, e 60 | } 61 | return d, nil 62 | } 63 | 64 | // EncodeBig5 convert UTF-8 to BIG5 65 | func EncodeBig5(s []byte) ([]byte, error) { 66 | I := bytes.NewReader(s) 67 | O := transform.NewReader(I, traditionalchinese.Big5.NewEncoder()) 68 | d, e := io.ReadAll(O) 69 | if e != nil { 70 | return nil, e 71 | } 72 | return d, nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/time.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "gitee.com/quant1x/gotdx/proto" 8 | "gitee.com/quant1x/gox/api" 9 | "time" 10 | ) 11 | 12 | const ( 13 | __tm_h_width = 1000000 14 | __tm_m_width = 10000 15 | __tm_t_width = 1000 16 | ) 17 | 18 | func _format_time0(time_stamp string) string { 19 | // format time from reversed_bytes0 20 | // by using method from https://github.com/rainx/pytdx/issues/187 21 | length := len(time_stamp) 22 | t1 := api.ParseInt(time_stamp[:length-6]) 23 | tm := fmt.Sprintf("%02d:", t1) 24 | tmp := time_stamp[length-6 : length-4] 25 | n := api.ParseInt(tmp) 26 | if n < 60 { 27 | tm += fmt.Sprintf("%02s:", tmp) 28 | tmp = time_stamp[length-4:] 29 | f := api.ParseFloat(tmp) 30 | tm += fmt.Sprintf("%06.3f", (f*60.0)/10000.00) 31 | } else { 32 | tmp = time_stamp[length-6:] 33 | f := api.ParseFloat(tmp) 34 | tm += fmt.Sprintf("%02d:", int64(f*60.0)/1000000) 35 | n = int64(f) 36 | tm += fmt.Sprintf("%06.3f", float64((n*60)%1000000)*60/1000000.0) 37 | } 38 | return tm 39 | } 40 | 41 | func timeFromStr(time_stamp string) string { 42 | // format time from reversed_bytes0 43 | // by using method from https://github.com/rainx/pytdx/issues/187 44 | length := len(time_stamp) 45 | t1 := api.ParseInt(time_stamp[:length-6]) 46 | tm := fmt.Sprintf("%02d:", t1) 47 | tmp := time_stamp[length-6 : length-4] 48 | n := api.ParseInt(tmp) 49 | if n < 60 { 50 | tm += fmt.Sprintf("%02s:", tmp) 51 | tmp = time_stamp[length-4:] 52 | f := api.ParseFloat(tmp) 53 | tm += fmt.Sprintf("%06.3f", (f*60.0)/10000.00) 54 | } else { 55 | tmp = time_stamp[length-6:] 56 | f := api.ParseFloat(tmp) 57 | tm += fmt.Sprintf("%02d:", int64(f*60.0)/1000000) 58 | n = int64(f) 59 | tm += fmt.Sprintf("%06.3f", float64((n*60)%1000000)*60/1000000.0) 60 | } 61 | return tm 62 | } 63 | 64 | func TimeFromInt(stamp int) string { 65 | //123456789 66 | h := stamp / __tm_h_width 67 | tmp1 := stamp % __tm_h_width 68 | m1 := tmp1 / __tm_m_width 69 | tmp2 := tmp1 % __tm_m_width 70 | m := 0 71 | s := 0 72 | t := 0 73 | st := float64(0.00) 74 | if m1 < 60 { 75 | m = m1 76 | tmp3 := tmp2 * 60 77 | s = tmp3 / __tm_m_width 78 | t = tmp3 % __tm_m_width 79 | t /= 10 80 | st = float64(tmp3) / __tm_m_width 81 | } else { 82 | h += 1 83 | tmp3 := tmp1 84 | m = tmp3 / __tm_h_width 85 | tmp4 := (tmp3 % __tm_h_width) * 60 86 | s = tmp4 / __tm_h_width 87 | t = tmp4 % __tm_h_width 88 | t /= __tm_t_width 89 | st = float64(tmp4) / __tm_h_width 90 | } 91 | _ = s 92 | _ = t 93 | return fmt.Sprintf("%02d:%02d:%06.3f", h, m, st) 94 | } 95 | 96 | func GetDatetimeFromUint32(category int, zipday uint32, tminutes uint16) (year int, month int, day int, hour int, minute int) { 97 | hour = 15 98 | if category < 4 || category == 7 || category == 8 { 99 | year = int((zipday >> 11) + 2004) 100 | month = int((zipday % 2048) / 100) 101 | day = int((zipday % 2048) % 100) 102 | hour = int(tminutes / 60) 103 | minute = int(tminutes % 60) 104 | } else { 105 | year = int(zipday / 10000) 106 | month = int((zipday % 10000) / 100) 107 | day = int(zipday % 100) 108 | } 109 | return 110 | } 111 | 112 | func getDatetimeNow(category int, lasttime string) (year int, month int, day int, hour int, minute int) { 113 | utime, _ := time.Parse("2006-01-02 15:04:05", lasttime) 114 | switch category { 115 | case proto.KLINE_TYPE_5MIN: 116 | utime = utime.Add(time.Minute * 5) 117 | case proto.KLINE_TYPE_15MIN: 118 | utime = utime.Add(time.Minute * 15) 119 | case proto.KLINE_TYPE_30MIN: 120 | utime = utime.Add(time.Minute * 30) 121 | case proto.KLINE_TYPE_1HOUR: 122 | utime = utime.Add(time.Hour) 123 | case proto.KLINE_TYPE_DAILY: 124 | utime = utime.AddDate(0, 0, 1) 125 | case proto.KLINE_TYPE_WEEKLY: 126 | utime = utime.Add(time.Hour * 24 * 7) 127 | case proto.KLINE_TYPE_MONTHLY: 128 | utime = utime.AddDate(0, 1, 0) 129 | case proto.KLINE_TYPE_EXHQ_1MIN: 130 | utime = utime.Add(time.Minute) 131 | case proto.KLINE_TYPE_1MIN: 132 | utime = utime.Add(time.Minute) 133 | case proto.KLINE_TYPE_RI_K: 134 | utime = utime.AddDate(0, 0, 1) 135 | case proto.KLINE_TYPE_3MONTH: 136 | utime = utime.AddDate(0, 3, 0) 137 | case proto.KLINE_TYPE_YEARLY: 138 | utime = utime.AddDate(1, 0, 0) 139 | } 140 | 141 | if category < 4 || category == 7 || category == 8 { 142 | if (utime.Hour() >= 15 && utime.Minute() > 0) || (utime.Hour() > 15) { 143 | utime = utime.AddDate(0, 0, 1) 144 | utime = utime.Add(time.Minute * 30) 145 | hour = (utime.Hour() + 18) % 24 146 | } else { 147 | hour = utime.Hour() 148 | } 149 | minute = utime.Minute() 150 | } else { 151 | if utime.Unix() > time.Now().Unix() { 152 | utime = time.Now() 153 | } 154 | hour = utime.Hour() 155 | minute = utime.Minute() 156 | if utime.Hour() > 15 { 157 | hour = 15 158 | minute = 0 159 | } 160 | } 161 | year = utime.Year() 162 | month = int(utime.Month()) 163 | day = utime.Day() 164 | return 165 | } 166 | 167 | func GetTime(b []byte, pos *int) (h uint16, m uint16) { 168 | var sec uint16 169 | _ = binary.Read(bytes.NewBuffer(b[*pos:*pos+2]), binary.LittleEndian, &sec) 170 | h = sec / 60 171 | m = sec % 60 172 | *pos += 2 173 | return 174 | } 175 | 176 | func GetDatetime(category int, b []byte, pos *int) (year int, month int, day int, hour int, minute int) { 177 | hour = 15 178 | if category < 4 || category == 7 || category == 8 { 179 | var zipday, tminutes uint16 180 | _ = binary.Read(bytes.NewBuffer(b[*pos:*pos+2]), binary.LittleEndian, &zipday) 181 | *pos += 2 182 | _ = binary.Read(bytes.NewBuffer(b[*pos:*pos+2]), binary.LittleEndian, &tminutes) 183 | *pos += 2 184 | 185 | year = int((zipday >> 11) + 2004) 186 | month = int((zipday % 2048) / 100) 187 | day = int((zipday % 2048) % 100) 188 | hour = int(tminutes / 60) 189 | minute = int(tminutes % 60) 190 | } else { 191 | var zipday uint32 192 | _ = binary.Read(bytes.NewBuffer(b[*pos:*pos+4]), binary.LittleEndian, &zipday) 193 | *pos += 4 194 | year = int(zipday / 10000) 195 | month = int((zipday % 10000) / 100) 196 | day = int(zipday % 100) 197 | } 198 | return 199 | } 200 | -------------------------------------------------------------------------------- /internal/time_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestTimeFromInt(t *testing.T) { 10 | nTime := 14986367 11 | nTime = 14986967 12 | nTime = 11026532 13 | //nTime = 11295421 14 | t1 := time.UnixMilli(int64(nTime)) 15 | fmt.Println(t1) 16 | //nTime = 8 17 | s := TimeFromInt(nTime) 18 | fmt.Println(s) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /internal/unit.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "gitee.com/quant1x/exchange" 4 | 5 | // BaseUnit 交易单位 6 | // 7 | // A股、债券交易和债券买断式回购交易的申报价格最小变动单位为0.01元人民币 8 | // 基金、权证交易为0.001元人民币 9 | // B股交易为0.001美元 10 | // 债券质押式回购交易为0.005元 11 | func defaultBaseUnit(marketId exchange.MarketType, code string) float64 { 12 | _ = marketId 13 | c := code[:2] 14 | switch c { 15 | case "60", "68", "00", "30", "39": 16 | return 100.0 17 | } 18 | c = code[:3] 19 | switch c { 20 | case "510": 21 | return 1000.0 22 | } 23 | return 100.00 24 | } 25 | 26 | type unitHandler func(marketId exchange.MarketType, code string) float64 27 | 28 | var ( 29 | BaseUnit unitHandler = defaultBaseUnit 30 | ) 31 | 32 | func RegisterBaseUnitFunction(f unitHandler) { 33 | BaseUnit = f 34 | } 35 | -------------------------------------------------------------------------------- /internal/varints.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // DecodeVarint pytdx : 类似utf-8的编码方式保存有符号数字 4 | func DecodeVarint(b []byte, pos *int) int { 5 | 6 | //0x7f与常量做与运算实质是保留常量(转换为二进制形式)的后7位数,既取值区间为[0,127] 7 | //0x3f与常量做与运算实质是保留常量(转换为二进制形式)的后6位数,既取值区间为[0,63] 8 | // 9 | //0x80 1000 0000 10 | //0x7f 0111 1111 11 | //0x40 100 0000 12 | //0x3f 011 1111 13 | 14 | posByte := 6 15 | bData := b[*pos] 16 | data := int(bData & 0x3f) 17 | bSign := false 18 | if (bData & 0x40) > 0 { 19 | bSign = true 20 | } 21 | 22 | if (bData & 0x80) > 0 { 23 | for { 24 | *pos += 1 25 | bData = b[*pos] 26 | data += (int(bData&0x7f) << posByte) 27 | 28 | posByte += 7 29 | 30 | if (bData & 0x80) <= 0 { 31 | break 32 | } 33 | } 34 | } 35 | *pos++ 36 | 37 | if bSign { 38 | data = -data 39 | } 40 | return data 41 | } 42 | -------------------------------------------------------------------------------- /proto/TdxProtocol.md: -------------------------------------------------------------------------------- 1 | 2 | API 3 | 4 | ``` 5 | 头部数据包含 流水号、命令字、包类型、压缩包类型、包长度、数据长度、数据内容 6 | 响应数据包含 流水号、命令字、包类型、压缩包类型、包长度、数据长度、数据内容 7 | ``` 8 | 9 | 解析 10 | ``` 11 | 通过协议头的解析,获取长度、获取数据,数据解压成标准的byte数据,二次封装为标准对象。 12 | 数据的格式是 小端在前的GBK格式。 13 | 根据 命令字 以及流水号 实现多线程异步处理,命令字可知道是什么请求,流水号可以进行业务处理。 14 | 压缩包的解压方式为 Inflater 类解压响应内容会携带通达信标准协议字段,用来区分协议的类型。 15 | 16 | ``` 17 | 18 | 连接 19 | ``` 20 | socket连接上后需要进行2次连接 21 | 发送内容为监听招商证券的连接的二进制数据 22 | 连接成功后需要发送心跳连接(用来判断连接是否正常) 23 | ``` 24 | 25 | 26 | 通信 27 | ``` 28 | 正式建立连接后可以通信,可以建立多个socket同时通信 29 | socket的端口和地址 在通达信的主站行情中可以获取命令字 30 | ``` 31 | 32 | 33 | ``` 34 | public int LOGIN_ONE = 0x000d;//第一次登录 35 | public int LOGIN_TWO = 0x0fdb;//第二次登录 36 | public int HEART = 0x0004;//心跳维持 37 | public int STOCK_COUNT = 0x044e;//股票数目 38 | public int STOCK_LIST = 0x0450;//股票列表 39 | public int KMINUTE = 0x0537;//当天分时K线 40 | public int KMINUTE_OLD = 0x0fb4;//指定日期分时K线 41 | public int KLINE = 0x052d;//股票K线 42 | public int BIDD = 0x056a;//当日的竞价 43 | public int QUOTE = 0x053e;//实时五笔报价 44 | public int QUOTE_SORT = 0x053e;//沪深排序 45 | public int TRANSACTION = 0x0fc5;//分笔成交明细 46 | public int TRANSACTION_OLD = 0x0fb5;//历史分笔成交明细 47 | public int FINANCE = 0x0010;//财务数据 48 | public int COMPANY = 0x02d0;//公司数据 F10 49 | public int EXDIVIDEND = 0x000f;//除权除息 50 | public int FILE_DIRECTORY = 0x02cf;//公司文件目录 51 | public int FILE_CONTENT = 0x02d0;//公司文件内容 52 | ``` 53 | -------------------------------------------------------------------------------- /proto/command.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | // 标准行情 4 | 5 | // 标准行情-命令字 6 | const ( 7 | STD_MSG_HEARTBEAT = 0x0004 // 心跳维持 8 | STD_MSG_LOGIN1 = 0x000d // 第一次登录 9 | STD_MSG_LOGIN2 = 0x0fdb // 第二次登录 10 | STD_MSG_XDXR_INFO = 0x000f // 除权除息信息 11 | STD_MSG_FINANCE_INFO = 0x0010 // 财务信息 12 | STD_MSG_PING = 0x0015 // 测试连接 13 | STD_MSG_COMPANY_CATEGORY = 0x02cf // 公司信息文件信息 14 | STD_MSG_COMPANY_CONTENT = 0x02d0 // 公司信息描述 15 | STD_MSG_SECURITY_COUNT = 0x044e // 证券数量 16 | STD_MSG_SECURITY_LIST = 0x0450 // 证券列表 17 | STD_MSG_INDEXBARS = 0x052d // 指数K线 18 | STD_MSG_SECURITY_BARS = 0x052d // 股票K线 19 | STD_MSG_SECURITY_QUOTES_old = 0x053e // 行情信息 20 | STD_MSG_SECURITY_QUOTES_new = 0x054c // 行情信息 21 | STD_MSG_MINUTETIME_DATA = 0x051d // 分时数据 22 | STD_MSG_BLOCK_META = 0x02c5 // 板块文件信息 23 | STD_MSG_BLOCK_DATA = 0x06b9 // 板块文件数据 24 | STD_MSG_TRANSACTION_DATA = 0x0fc5 // 分笔成交信息 25 | STD_MSG_HISTORY_MINUTETIME_DATA = 0x0fb4 // 历史分时信息 26 | STD_MSG_HISTORY_TRANSACTION_DATA = 0x0fb5 // 历史分笔成交信息 27 | //CMD_INFO_EX = 0x000f 28 | //CMD_STOCK_LIST = 0x0524 29 | //CMD_INSTANT_TRANS = 0x0fc5 30 | //CMD_HIS_TRANS = 0x0fb5 31 | //CMD_HEART_BEAT = 0x0523 32 | ) 33 | 34 | // K线种类 35 | const ( 36 | KLINE_TYPE_5MIN = 0 // 5 分钟 K线 37 | KLINE_TYPE_15MIN = 1 // 15 分钟 K线 38 | KLINE_TYPE_30MIN = 2 // 30 分钟 K线 39 | KLINE_TYPE_1HOUR = 3 // 1 小时 K线 40 | KLINE_TYPE_DAILY = 4 // 日 K线 41 | KLINE_TYPE_WEEKLY = 5 // 周 K线 42 | KLINE_TYPE_MONTHLY = 6 // 月 K线 43 | KLINE_TYPE_EXHQ_1MIN = 7 // 1分钟 44 | KLINE_TYPE_1MIN = 8 // 1 分钟 K线 45 | KLINE_TYPE_RI_K = 9 // 日 K线 46 | KLINE_TYPE_3MONTH = 10 // 季 K线 47 | KLINE_TYPE_YEARLY = 11 // 年 K线 48 | ) 49 | 50 | const ( 51 | Compressed = uint8(0x10) // 压缩标志 52 | FlagNotZipped = uint8(0x0c) // zip未压缩 53 | FlagZipped = uint8(Compressed | FlagNotZipped) // zip已压缩 消息头标志 0x789C 54 | ) 55 | -------------------------------------------------------------------------------- /proto/command_test.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestZipFlag(t *testing.T) { 9 | fmt.Println(FlagZipped) 10 | } 11 | -------------------------------------------------------------------------------- /proto/ext/ex_cmd1.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | import ( 4 | "gitee.com/quant1x/gotdx/internal" 5 | "gitee.com/quant1x/gotdx/proto/std" 6 | ) 7 | 8 | // ExCmd1Request 请求包结构 9 | type ExCmd1Request struct { 10 | Cmd []byte `struc:"[92]byte" json:"cmd"` 11 | } 12 | 13 | // Marshal 请求包序列化输出 14 | func (req *ExCmd1Request) Marshal() ([]byte, error) { 15 | return std.DefaultMarshal(req) 16 | } 17 | 18 | // ExCmd1Response 响应包结构 19 | type ExCmd1Response struct { 20 | Unknown []byte `json:"unknown"` 21 | Reply string `json:"reply"` 22 | } 23 | 24 | func (resp *ExCmd1Response) Unmarshal(data []byte) error { 25 | //resp.Unknown = data 26 | resp.Reply = internal.Utf8ToGbk(data[3:53]) 27 | return nil 28 | } 29 | 30 | // NewExCmd1Request 创建ExCmd1请求包 31 | func NewExCmd1Request() (*ExCmd1Request, error) { 32 | hexString := "01 01 48 65 00 01 52 00 52 00 54 24 1f 32 c6 e5 d5 3d fb 41 1f 32 c6 e5 d5 3d fb 41 1f 32 c6 e5 d5 3d fb 41 1f 32 c6 e5 d5 3d fb 41 1f 32 c6 e5 d5 3d fb 41 1f 32 c6 e5 d5 3d fb 41 1f 32 c6 e5 d5 3d fb 41 1f 32 c6 e5 d5 3d fb 41 cc e1 6d ff d5 ba 3f b8 cb c5 7a 05 4f 77 48 ea" 33 | //hexString := "01 01 48 65 00 01 02 00 02 00 55 24" 34 | request := &ExCmd1Request{ 35 | Cmd: internal.HexString2Bytes(hexString), 36 | } 37 | return request, nil 38 | } 39 | 40 | func NewExCmd1() (*ExCmd1Request, *ExCmd1Response, error) { 41 | var response ExCmd1Response 42 | var request, err = NewExCmd1Request() 43 | return request, &response, err 44 | } 45 | -------------------------------------------------------------------------------- /proto/std/finance_info.go: -------------------------------------------------------------------------------- 1 | package std 2 | 3 | // 获取股票列表 4 | import ( 5 | "gitee.com/quant1x/exchange" 6 | "gitee.com/quant1x/gotdx/internal" 7 | ) 8 | 9 | // 请求包结构 10 | type FinanceInfoRequest struct { 11 | // struc不允许slice解析,只允许包含长度的array,该长度可根据hex字符串计算 12 | Unknown1 []byte `struc:"[14]byte"` 13 | // pytdx中使用struct.Pack进行反序列化 14 | // 其中 bestIpCount { 108 | maxCap = bestIpCount 109 | } 110 | 111 | maxIdle := maxCap 112 | 113 | halfCpuCount := runtime.NumCPU() / 2 114 | if maxIdle > halfCpuCount { 115 | maxIdle = halfCpuCount 116 | } 117 | 118 | cp, err := NewConnPool(maxCap, maxIdle, _factory, _close, _ping) 119 | if err != nil { 120 | return nil, err 121 | } 122 | stdApi.connPool = cp 123 | return &stdApi, nil 124 | } 125 | 126 | func (this *StdApi) Len() int { 127 | return len(this.servers) 128 | } 129 | 130 | func (this *StdApi) init() { 131 | for _, v := range this.servers { 132 | this.ch <- v 133 | } 134 | } 135 | 136 | // AcquireAddress 获取一个地址 137 | func (this *StdApi) AcquireAddress() *Server { 138 | this.once.Do(this.init) 139 | // 非阻塞获取 140 | //srv, ok := <-this.ch 141 | logger.Warnf("获取一个服务器地址...begin") 142 | // 阻塞获取一个地址 143 | server := <-this.ch 144 | logger.Warnf("获取一个服务器地址...end") 145 | if len(server.Host) == 0 || server.Port == 0 { 146 | logger.Warnf("获取一个服务器地址...failed: nil") 147 | return nil 148 | } 149 | logger.Warnf("获取一个服务器地址...server=%s", server) 150 | return &server 151 | } 152 | 153 | // ReleaseAddress 返还一个地址 154 | func (this *StdApi) ReleaseAddress(srv *Server) { 155 | logger.Warnf("返回一个服务器地址...") 156 | if srv == nil || len(srv.Host) == 0 || srv.Port == 0 { 157 | logger.Warnf("返回一个服务器地址...failed: nil") 158 | return 159 | } 160 | this.once.Do(this.init) 161 | logger.Warnf("返回一个服务器地址...server=%s, begin", *srv) 162 | // 阻塞返还一个地址 163 | this.ch <- *srv 164 | logger.Warnf("返回一个服务器地址...server=%s, end", *srv) 165 | } 166 | 167 | // NumOfServers 增加返回服务器IP数量 168 | func (this *StdApi) NumOfServers() int { 169 | return len(this.servers) 170 | } 171 | 172 | func (this *StdApi) GetMaxIdleCount() int { 173 | return this.connPool.GetMaxIdleCount() 174 | } 175 | 176 | // Close 关闭 177 | func (this *StdApi) Close() { 178 | this.connPool.CloseAll() 179 | } 180 | 181 | // 通过池关闭连接 182 | func (this *StdApi) poolClose(cli *TcpClient) error { 183 | return this.connPool.CloseConn(cli) 184 | } 185 | -------------------------------------------------------------------------------- /quotes/api_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "fmt" 5 | "gitee.com/quant1x/exchange" 6 | "gitee.com/quant1x/gotdx/proto" 7 | "gitee.com/quant1x/gox/logger" 8 | "testing" 9 | ) 10 | 11 | func TestStdApi_ALL(t *testing.T) { 12 | logger.SetLevel(logger.DEBUG) 13 | //quotesSrv := Server{Host: "119.147.212.81", Port: 7709} 14 | //stdApi := NewStdApi(quotesSrv) 15 | stdApi, err := NewStdApi() 16 | if err != nil { 17 | panic(err) 18 | } 19 | defer stdApi.Close() 20 | 21 | // 1. hello1 22 | hello1, err := stdApi.Hello1() 23 | if err != nil { 24 | fmt.Printf("%+v\n", err) 25 | } 26 | fmt.Printf("%+v\n", hello1) 27 | 28 | // 2. hello2 29 | hello2, err := stdApi.Hello2() 30 | if err != nil { 31 | fmt.Printf("%+v\n", err) 32 | } 33 | fmt.Printf("%+v\n", hello2) 34 | 35 | // 2.1 heartbeat 36 | heartBeat, err := stdApi.HeartBeat() 37 | if err != nil { 38 | fmt.Printf("%+v\n", err) 39 | } 40 | 41 | fmt.Printf("%+v\n", heartBeat) 42 | 43 | // 3. finance_info 44 | fi, err := stdApi.GetFinanceInfo("sh600600") 45 | if err != nil { 46 | fmt.Printf("%+v\n", err) 47 | } 48 | fmt.Printf("%+v\n", fi) 49 | 50 | // 4. kline 51 | kl, err := stdApi.GetKLine("sz002528", proto.KLINE_TYPE_RI_K, 0, 800) 52 | if err != nil { 53 | fmt.Printf("%+v\n", err) 54 | } 55 | fmt.Printf("GetKLine: %+v\n", kl) 56 | 57 | // 5. stock list 58 | sl, err := stdApi.GetSecurityList(exchange.MarketIdShangHai, 0) 59 | if err != nil { 60 | fmt.Printf("%+v\n", err) 61 | } 62 | fmt.Printf("GetSecurityList: %+v\n", sl) 63 | 64 | // 6 index kline 65 | ikl, err := stdApi.GetIndexBars("sh000001", proto.KLINE_TYPE_RI_K, 0, 800) 66 | if err != nil { 67 | fmt.Printf("%+v\n", err) 68 | } 69 | fmt.Printf("GetIndexBars: %+v\n", ikl) 70 | // 7. 获取指定市场内的证券数目 71 | sc, err := stdApi.GetSecurityCount(exchange.MarketIdShangHai) 72 | if err != nil { 73 | fmt.Printf("%+v\n", err) 74 | } 75 | fmt.Printf("%+v\n", sc) 76 | 77 | //time.Sleep(time.Second * 15) 78 | // 8.1 获取5档行情 79 | //sq, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai, proto.MarketIdShangHai}, []string{"600030", "600600"}) 80 | //sq1, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai}, []string{"600600"}) 81 | sq1, err := stdApi.GetSecurityQuotes([]uint8{exchange.MarketIdShangHai}, []string{"688981"}) 82 | if err != nil { 83 | fmt.Printf("%+v\n", err) 84 | } 85 | fmt.Printf("%+v\n", sq1) 86 | 87 | // 8.2 获取5档行情 88 | sq2, err := stdApi.V2GetSecurityQuotes([]uint8{exchange.MarketIdShenZhen}, []string{"002423"}) 89 | if err != nil { 90 | fmt.Printf("%+v\n", err) 91 | } 92 | fmt.Printf("%+v\n", sq2) 93 | 94 | // 9. 分时数据 95 | //mt, err := stdApi.GetMinuteTimeData("sz159607") // 数据异常 96 | mt, err := stdApi.GetMinuteTimeData("sh510050") 97 | if err != nil { 98 | fmt.Printf("%+v\n", err) 99 | } 100 | fmt.Printf("%+v\n", mt) 101 | // 10. 历史分时 102 | hmt, err := stdApi.GetHistoryMinuteTimeData("sh600600", 20230113) 103 | if err != nil { 104 | fmt.Printf("%+v\n", err) 105 | } 106 | fmt.Printf("%+v\n", hmt) 107 | // 11. 分笔成交 108 | td, err := stdApi.GetTransactionData("sz000629", 0, 3800) 109 | if err != nil { 110 | fmt.Printf("%+v\n", err) 111 | } 112 | fmt.Printf("%+v\n", td) 113 | // 12. 历史分笔成交 114 | htd, err := stdApi.GetHistoryTransactionData("sh600105", 20230421, 0, 1800) 115 | if err != nil { 116 | fmt.Printf("%+v\n", err) 117 | } 118 | fmt.Printf("%+v\n", htd) 119 | // 13. 除权除息 120 | xdxr, err := stdApi.GetXdxrInfo("sz002528") 121 | if err != nil { 122 | fmt.Printf("%+v\n", err) 123 | } 124 | fmt.Printf("%+v\n", xdxr) 125 | // 14. 板块meta信息 126 | blkMeta, err := stdApi.GetBlockMeta(BLOCK_DEFAULT) 127 | if err != nil { 128 | fmt.Printf("%+v\n", err) 129 | } 130 | fmt.Printf("%+v\n", blkMeta) 131 | 132 | // 15. 板块信息 133 | blkInfo, err := stdApi.GetBlockInfo(BLOCK_DEFAULT) 134 | if err != nil { 135 | fmt.Printf("%+v\n", err) 136 | } 137 | fmt.Printf("%+v\n", blkInfo) 138 | } 139 | -------------------------------------------------------------------------------- /quotes/base_client.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "gitee.com/quant1x/gox/runtime" 5 | "io" 6 | "net" 7 | "sync" 8 | "sync/atomic" 9 | "time" 10 | 11 | "gitee.com/quant1x/gox/api" 12 | "gitee.com/quant1x/gox/exception" 13 | "gitee.com/quant1x/gox/logger" 14 | ) 15 | 16 | var ( 17 | ErrTimeOut = exception.New(1, "connect timeout") 18 | ) 19 | 20 | type TcpClient struct { 21 | sync.Mutex // 匿名属性 22 | conn net.Conn // tcp连接 23 | server *Server // 服务器信息 24 | opt *Options // 参数 25 | done chan bool // connection done 26 | completedTime time.Time // 时间戳 27 | closed uint32 // 关闭次数 28 | } 29 | 30 | func NewClient(opt *Options) *TcpClient { 31 | client := &TcpClient{} 32 | if opt.MaxRetryTimes <= 0 { 33 | opt.MaxRetryTimes = DefaultRetryTimes 34 | } 35 | if opt.ConnectionTimeout <= 0 { 36 | opt.ConnectionTimeout = CONN_TIMEOUT * time.Second 37 | } 38 | if opt.ReadTimeout <= 0 { 39 | opt.ReadTimeout = RECV_TIMEOUT * time.Second 40 | } 41 | if opt.WriteTimeout <= 0 { 42 | opt.WriteTimeout = RECV_TIMEOUT * time.Second 43 | } 44 | 45 | client.opt = opt 46 | client.done = make(chan bool, 1) 47 | client.updateCompletedTimestamp() 48 | return client 49 | } 50 | 51 | // 更新最后一次成功send/recv的时间戳 52 | func (client *TcpClient) updateCompletedTimestamp() { 53 | client.completedTime = time.Now() 54 | } 55 | 56 | // 过去了多少秒 57 | func (client *TcpClient) crossTime() (elapsedTime float64) { 58 | seconds := time.Since(client.completedTime).Seconds() 59 | return seconds 60 | } 61 | 62 | // 是否超时 63 | func (client *TcpClient) hasTimedOut() bool { 64 | elapsedTime := client.crossTime() 65 | timeout := client.opt.ConnectionTimeout.Seconds() 66 | return elapsedTime >= timeout 67 | } 68 | 69 | // Command 执行通达信指令 70 | func (client *TcpClient) Command(msg Message) error { 71 | client.Lock() 72 | defer client.Unlock() 73 | if client.conn == nil { 74 | logger.Error("tcp连接失效") 75 | return io.EOF 76 | } 77 | err := process(client, msg) 78 | //errors.Is(err, net.OpError) 79 | //if _,ok:=err.( *net.OpError) ;ok{ 80 | // return nil,err 81 | //} 82 | if err != nil { 83 | logger.Error("业务处理失败", err) 84 | return err 85 | } 86 | client.updateCompletedTimestamp() 87 | return nil 88 | } 89 | 90 | func (client *TcpClient) heartbeat() { 91 | defer runtime.IgnorePanic("heartbeat.done") 92 | ticker := time.NewTicker(time.Second * 1) 93 | defer ticker.Stop() 94 | for { 95 | select { 96 | case <-ticker.C: 97 | client.Lock() 98 | timedOut := client.hasTimedOut() 99 | client.Unlock() 100 | if timedOut { 101 | msg := NewSecurityCountPackage() 102 | msg.SetParams(&SecurityCountRequest{ 103 | Market: uint16(1), 104 | }) 105 | err := client.Command(msg) 106 | if err != nil { 107 | logger.Warnf("client -> server[%s]: error > shutdown", client.server) 108 | _ = client.Close() 109 | return 110 | } else { 111 | client.updateCompletedTimestamp() 112 | logger.Warnf("client -> server[%s]: heartbeat", client.server) 113 | } 114 | // 模拟服务器主动断开或者网络断开 115 | //logger.Warnf("client -> server[%s]: test force > shutdown", client.Addr) 116 | //_ = client.Close() 117 | //return 118 | } 119 | case <-client.done: 120 | logger.Warnf("client -> server[%s]: done > shutdown", client.server) 121 | return 122 | } 123 | } 124 | } 125 | 126 | // Connect 连接服务器 127 | func (client *TcpClient) Connect(server *Server) error { 128 | addr := server.Addr() 129 | conn, err := net.DialTimeout("tcp", addr, client.opt.ConnectionTimeout) // net.DialTimeout() 130 | state := "connected" 131 | if err != nil { 132 | state = err.Error() 133 | } 134 | logger.Warnf("client -> server[%s]: %s", addr, state) 135 | if err == nil { 136 | client.conn = conn 137 | client.server = server 138 | client.updateCompletedTimestamp() 139 | go client.heartbeat() 140 | } 141 | 142 | if client.conn == nil { 143 | return ErrTimeOut 144 | } 145 | return nil 146 | } 147 | 148 | // Close 断开服务器 149 | func (client *TcpClient) Close() error { 150 | defer runtime.IgnorePanic("TcpClient.Close") 151 | if atomic.LoadUint32(&client.closed) > 0 { 152 | return io.EOF 153 | } 154 | client.done <- true 155 | close(client.done) 156 | client.opt.releaseAddress(client.server) 157 | api.CloseQuietly(client.conn) 158 | atomic.AddUint32(&client.closed, 1) 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /quotes/base_client_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestTcpClient_heartbeat(t *testing.T) { 9 | stdApi, err := NewStdApi() 10 | if err != nil { 11 | panic(err) 12 | } 13 | defer stdApi.Close() 14 | // 休眠20秒触发超时流程 15 | time.Sleep(time.Second * 2000) 16 | } 17 | -------------------------------------------------------------------------------- /quotes/base_consts.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import "errors" 4 | 5 | type TdxMarket int 6 | 7 | const ( 8 | DefaultRetryTimes = 3 // 重试次数 9 | MessageHeaderBytes = 0x10 10 | MessageMaxBytes = 1 << 15 11 | ) 12 | 13 | var ( 14 | ErrBadData = errors.New("more than 8M data") 15 | ) 16 | -------------------------------------------------------------------------------- /quotes/base_message.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gox/api" 10 | "gitee.com/quant1x/gox/logger" 11 | "io" 12 | "time" 13 | ) 14 | 15 | // StdRequestHeader 标准行情-请求-消息头 16 | type StdRequestHeader struct { 17 | ZipFlag uint8 `struc:"uint8,little"` // ZipFlag 18 | SeqID uint32 `struc:"uint32,little"` // 请求编号 19 | PacketType uint8 `struc:"uint8,little"` // 包类型 20 | PkgLen1 uint16 `struc:"uint16,little"` // 消息体长度1 21 | PkgLen2 uint16 `struc:"uint16,little"` // 消息体长度2 22 | Method uint16 `struc:"uint16,little"` // method 请求方法 23 | } 24 | 25 | // StdResponseHeader 标准行情-响应-消息头 26 | type StdResponseHeader struct { 27 | I1 uint32 `struc:"uint32,little"` 28 | ZipFlag uint8 `struc:"uint8,little"` // ZipFlag 29 | SeqID uint32 `struc:"uint32,little"` // 请求编号 30 | I3 uint8 `struc:"uint8,little"` 31 | Method uint16 `struc:"uint16,little"` // method 32 | ZipSize uint16 `struc:"uint16,little"` // 长度 33 | UnZipSize uint16 `struc:"uint16,little"` // 未压缩长度 34 | } 35 | 36 | // Message 消息接口 37 | type Message interface { 38 | // Serialize 编码 39 | Serialize() ([]byte, error) 40 | // UnSerialize 解码 41 | UnSerialize(head interface{}, in []byte) error 42 | // Reply 获取返回值 43 | Reply() interface{} 44 | } 45 | 46 | // 消息处理 47 | func process(client *TcpClient, msg Message) error { 48 | defer client.updateCompletedTimestamp() 49 | conn := client.conn 50 | opt := client.opt 51 | // 1. 序列化 52 | sendData, err := msg.Serialize() 53 | if err != nil { 54 | logger.Errorf("数据包编码失败: %+v", err) 55 | return err 56 | } 57 | 58 | // 2. 发送指令 59 | retryTimes := 0 60 | if logger.IsDebug() { 61 | logger.Debug(internal.Bytes2HexString(sendData)) 62 | } 63 | for { 64 | // 设置写timeout 65 | err = conn.SetWriteDeadline(time.Now().Add(opt.WriteTimeout)) 66 | if err != nil { 67 | return err 68 | } 69 | n, err := conn.Write(sendData) 70 | if n < len(sendData) { 71 | retryTimes++ 72 | if retryTimes <= opt.MaxRetryTimes { 73 | logger.Warnf("第%d次重试\n", retryTimes) 74 | } else { 75 | logger.Errorf("发送指令失败-1, %+v", err) 76 | return err 77 | } 78 | } else { 79 | if err != nil { 80 | logger.Errorf("发送指令失败-2", err) 81 | return err 82 | } 83 | break 84 | } 85 | } 86 | 87 | // 3. 读取响应 88 | // 3.1 读取响应的消息头 89 | headerBytes := make([]byte, MessageHeaderBytes) 90 | // 设置读timeout 91 | err = conn.SetReadDeadline(time.Now().Add(opt.ReadTimeout)) 92 | if err != nil { 93 | return err 94 | } 95 | _, err = io.ReadFull(conn, headerBytes) 96 | if err != nil { 97 | logger.Error("读取数据指令失败-1", err) 98 | return err 99 | } 100 | if logger.IsDebug() { 101 | logger.Debug("response header: ", hex.EncodeToString(headerBytes)) 102 | } 103 | 104 | // 3.2 响应的消息头, 反序列化 105 | headerBuf := bytes.NewReader(headerBytes) 106 | var header StdResponseHeader 107 | if err := binary.Read(headerBuf, binary.LittleEndian, &header); err != nil { 108 | logger.Error("读取数据指令失败-2", err) 109 | return err 110 | } 111 | if logger.IsDebug() { 112 | logger.Debugf("response header: %+v", header) 113 | } 114 | // 3.3 处理超长信息的异常 115 | if header.ZipSize > MessageMaxBytes { 116 | logger.Warnf("msgData has bytes(%d) beyond max %d\n", header.ZipSize, MessageMaxBytes) 117 | return ErrBadData 118 | } 119 | // 3.4 读取响应的消息体 120 | msgData := make([]byte, header.ZipSize) 121 | // 设置读timeout 122 | err = conn.SetReadDeadline(time.Now().Add(opt.ReadTimeout)) 123 | if err != nil { 124 | return err 125 | } 126 | _, err = io.ReadFull(conn, msgData) 127 | if err != nil { 128 | logger.Error("读取数据指令失败-3", err) 129 | return err 130 | } 131 | // 3.5 反序列化响应的消息体 132 | var out bytes.Buffer 133 | if logger.IsDebug() { 134 | logger.Debugf("response body: %+v", hex.EncodeToString(msgData)) 135 | } 136 | var respBody []byte 137 | if header.ZipSize != header.UnZipSize { 138 | b := bytes.NewReader(msgData) 139 | r, _ := zlib.NewReader(b) 140 | defer api.CloseQuietly(r) 141 | _, _ = io.Copy(&out, r) 142 | respBody = out.Bytes() 143 | } else { 144 | respBody = msgData 145 | } 146 | if logger.IsDebug() { 147 | logger.Debugf("response body: %+v", hex.EncodeToString(respBody)) 148 | } 149 | err = msg.UnSerialize(&header, respBody) 150 | // 4. 返回 151 | return err 152 | } 153 | -------------------------------------------------------------------------------- /quotes/base_message_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "fmt" 9 | "gitee.com/quant1x/gox/logger" 10 | "io" 11 | "testing" 12 | ) 13 | 14 | func parseResponseHeader(data []byte) (*StdResponseHeader, []byte, error) { 15 | var header StdResponseHeader 16 | //err := cstruct.Unpack(data, &header) 17 | headerBuf := bytes.NewReader(data) 18 | err := binary.Read(headerBuf, binary.LittleEndian, &header) 19 | if err != nil { 20 | return nil, nil, err 21 | } 22 | fmt.Println(headerBuf.Len(), headerBuf.Size()) 23 | pos := int(headerBuf.Size()) - headerBuf.Len() 24 | if header.ZipSize > MessageMaxBytes { 25 | logger.Debugf("msgData has bytes(%d) beyond max %d\n", header.ZipSize, MessageMaxBytes) 26 | return &header, nil, ErrBadData 27 | } 28 | var out bytes.Buffer 29 | var body []byte 30 | if header.ZipSize != header.UnZipSize { 31 | b := bytes.NewReader(data[pos:]) 32 | r, _ := zlib.NewReader(b) 33 | _, _ = io.Copy(&out, r) 34 | body = out.Bytes() 35 | _ = r.Close() 36 | } else { 37 | body = data[pos:] 38 | } 39 | return &header, body, err 40 | 41 | } 42 | 43 | func TestProcess(t *testing.T) { 44 | hexString := "b1cb74000c760028000004000a000a0000000000000000000000" 45 | data, err := hex.DecodeString(hexString) 46 | if err != nil { 47 | fmt.Println(err) 48 | } 49 | respHeader, respBody, err := parseResponseHeader(data) 50 | if err != nil { 51 | fmt.Println(err) 52 | } 53 | fmt.Printf("%+v\n", respHeader) 54 | fmt.Printf("%+v\n", respBody) 55 | bodyBuff := bytes.NewReader(respBody) 56 | var resp HeartBeatReply 57 | err = binary.Read(bodyBuff, binary.LittleEndian, &resp) 58 | if err != nil { 59 | fmt.Println(err) 60 | } 61 | fmt.Printf("%+v\n", resp) 62 | } 63 | -------------------------------------------------------------------------------- /quotes/base_pool.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "gitee.com/quant1x/gox/logger" 5 | "gitee.com/quant1x/gox/pool" 6 | "time" 7 | ) 8 | 9 | const ( 10 | // POOL_INITED 连接池初始化 11 | POOL_INITED = 1 12 | // POOL_MAX 连接池最大 2 13 | POOL_MAX = 10 14 | // CONN_TIMEOUT 链接超时 10 s 15 | CONN_TIMEOUT = 10 16 | // RECV_TIMEOUT 接收数据超时 17 | RECV_TIMEOUT = 5 18 | ) 19 | 20 | // ConnPool 连接池 21 | type ConnPool struct { 22 | addr string 23 | pool pool.Pool 24 | maxIdle int 25 | } 26 | 27 | // NewConnPool 创新一个新连接池 28 | func NewConnPool(maxCap, maxIdle int, factory func() (any, error), close func(any) error, ping func(any) error) (*ConnPool, error) { 29 | initialCap := POOL_INITED 30 | if maxIdle < POOL_INITED { 31 | maxIdle = POOL_INITED 32 | } 33 | maxIdle = maxCap 34 | // 创建一个连接池: 初始化5,最大连接30 35 | poolConfig := &pool.Config{ 36 | InitialCap: initialCap, 37 | MaxCap: maxCap, 38 | MaxIdle: maxIdle, 39 | Factory: factory, 40 | Close: close, 41 | Ping: ping, 42 | //连接最大空闲时间,超过该时间的连接 将会关闭,可避免空闲时连接EOF,自动失效的问题 43 | IdleTimeout: CONN_TIMEOUT * time.Second, 44 | } 45 | _pool, err := pool.NewChannelPool(poolConfig) 46 | if err != nil { 47 | logger.Errorf("create channel pool failed, error=%+v", err) 48 | return nil, err 49 | } 50 | cp := &ConnPool{ 51 | pool: _pool, 52 | maxIdle: maxIdle, 53 | } 54 | return cp, nil 55 | } 56 | 57 | func (p *ConnPool) GetMaxIdleCount() int { 58 | return p.maxIdle 59 | } 60 | 61 | func (p *ConnPool) GetConn() any { 62 | conn, err := p.pool.Get() 63 | if err != nil { 64 | logger.Errorf("获取连接失败, error=%+v", err) 65 | return nil 66 | } 67 | return conn 68 | } 69 | 70 | func (p *ConnPool) CloseConn(conn any) error { 71 | return p.pool.Close(conn) 72 | } 73 | 74 | func (p *ConnPool) ReturnConn(conn any) { 75 | _ = p.pool.Put(conn) 76 | } 77 | 78 | func (p *ConnPool) CloseAll() { 79 | p.pool.CloseAll() 80 | } 81 | 82 | func (p *ConnPool) Close() { 83 | p.pool.Release() 84 | } 85 | -------------------------------------------------------------------------------- /quotes/base_timer.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "time" 8 | ) 9 | 10 | const ( 11 | defaultPingInterval = 10 12 | ) 13 | 14 | func pinger(ctx context.Context, w io.Writer, reset <-chan time.Duration) { 15 | var interval time.Duration 16 | select { 17 | case <-ctx.Done(): 18 | return 19 | case interval = <-reset: //读取更新的心跳间隔时间 20 | default: 21 | } 22 | if interval < 0 { 23 | interval = defaultPingInterval 24 | } 25 | timer := time.NewTimer(interval) 26 | defer func() { 27 | if !timer.Stop() { 28 | <-timer.C 29 | } 30 | }() 31 | for { 32 | select { 33 | case <-ctx.Done(): 34 | return 35 | case newInterval := <-reset: 36 | if !timer.Stop() { 37 | <-timer.C 38 | } 39 | if newInterval > 0 { 40 | interval = newInterval 41 | } 42 | case <-timer.C: 43 | if _, err := w.Write([]byte("ping")); err != nil { 44 | //在此跟踪并执行连续超时 45 | return 46 | } 47 | } 48 | _ = timer.Reset(interval) //重制心跳上报时间间隔 49 | } 50 | } 51 | 52 | func ExamplePinger() { 53 | ctx, cancelFunc := context.WithCancel(context.Background()) 54 | r, w := io.Pipe() //代替网络连接net.Conn 55 | done := make(chan struct{}) 56 | resetTimer := make(chan time.Duration, 1) 57 | resetTimer <- time.Second //ping间隔初始值 58 | 59 | go func() { 60 | pinger(ctx, w, resetTimer) 61 | close(done) 62 | }() 63 | receivePing := func(d time.Duration, r io.Reader) { 64 | if d >= 0 { 65 | fmt.Printf("resetting time (%s)\n", d) 66 | resetTimer <- d 67 | } 68 | 69 | now := time.Now() 70 | buf := make([]byte, 1024) 71 | n, err := r.Read(buf) 72 | if err != nil { 73 | fmt.Println(err) 74 | } 75 | fmt.Printf("received %q (%s)\n", buf[:n], time.Since(now).Round(100*time.Millisecond)) 76 | } 77 | for i, v := range []int64{0, 200, 300, 0, -1, -1, -1} { 78 | fmt.Printf("Run %d\n", i+1) 79 | receivePing(time.Duration(v)*time.Millisecond, r) 80 | } 81 | cancelFunc() //取消context使pinger退出 82 | <-done 83 | } 84 | -------------------------------------------------------------------------------- /quotes/base_timer_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import "testing" 4 | 5 | func TestExamplePinger(t *testing.T) { 6 | ExamplePinger() 7 | } 8 | -------------------------------------------------------------------------------- /quotes/bestip.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/gotdx/proto/ext" 7 | "gitee.com/quant1x/gotdx/proto/std" 8 | "gitee.com/quant1x/gox/api" 9 | "math" 10 | "slices" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | const ( 18 | TDX_HOST_HQ = "HQ" 19 | TDX_HOST_EX = "EX" 20 | TDX_HOST_GP = "GP" 21 | ) 22 | 23 | const ( 24 | maxCrossTime = 50 // 最大耗时 25 | ) 26 | 27 | // ServerGroup 主机组 28 | type ServerGroup struct { 29 | HQ []Server `json:"HQ"` 30 | EX []Server `json:"EX"` 31 | GP []Server `json:"GP"` 32 | } 33 | 34 | // AllServers 全部主机 35 | type AllServers struct { 36 | //Server ServerGroup `json:"Server"` 37 | BestIP ServerGroup `json:"BestIP"` 38 | } 39 | 40 | // ProfileBestIPList 测试最快的服务器 41 | func ProfileBestIPList() *AllServers { 42 | var as AllServers 43 | 44 | // HQ-servers 45 | dst := cleanServers(StandardServerList, testHQ) 46 | as.BestIP.HQ = dst 47 | 48 | // EX-server, reply提示版本不一致, 扩展服务暂不可用 49 | dst = cleanServers(ExtensionServerList, testEX) 50 | as.BestIP.EX = dst 51 | 52 | //// SP-servers 53 | //dst = cleanServers(GP_HOSTS, testEX) 54 | //as.BestIP.GP = dst 55 | str, _ := json.Marshal(as) 56 | fmt.Println(string(str)) 57 | return &as 58 | } 59 | 60 | func cleanServers(src []Server, test func(addr string) error) (dst []Server) { 61 | //err := json.Unmarshal([]byte(str), &src) 62 | //if err != nil { 63 | // return src, dst 64 | //} 65 | //fmt.Printf("%+v\n", src) 66 | 67 | dst = slices.Clone(src) 68 | for i, _ := range dst { 69 | v := &dst[i] 70 | fmt.Printf("%d: %+v\n", i, v) 71 | _ = detect(v, test) 72 | fmt.Printf("%d: %+v\n", i, v) 73 | } 74 | 75 | sort.Slice(dst, func(i, j int) bool { 76 | return dst[i].CrossTime < dst[j].CrossTime 77 | }) 78 | dst = api.Filter(dst, func(e Server) bool { 79 | return e.CrossTime < maxCrossTime 80 | }) 81 | num := len(dst) 82 | if num > POOL_MAX { 83 | num = POOL_MAX 84 | } 85 | dst = dst[0:num] 86 | fmt.Println(dst) 87 | return 88 | } 89 | 90 | // 检测, 返回毫秒 91 | func detect(srv *Server, test func(addr string) error) int64 { 92 | var crossTime int64 = math.MaxInt64 93 | addr := strings.Join([]string{srv.Host, strconv.Itoa(srv.Port)}, ":") 94 | start := time.Now() 95 | err := test(addr) 96 | if err != nil { 97 | srv.CrossTime = crossTime 98 | return crossTime 99 | } 100 | // 计算耗时, 纳秒 101 | crossTime = int64(time.Since(start)) 102 | // 转成毫秒 103 | srv.CrossTime = crossTime / int64(time.Millisecond) 104 | return crossTime 105 | } 106 | 107 | // 标准服务器测试 108 | func testHQ(addr string) error { 109 | cli, err := NewClientForTest(addr) 110 | if err != nil { 111 | return err 112 | } 113 | // CMD信令 1 114 | data, err := CommandWithConn(cli, func() (req std.Marshaler, resp std.Unmarshaler, err error) { 115 | req, resp, err = std.NewSetupCmd1() 116 | return 117 | }) 118 | fmt.Printf("%+v\n", data) 119 | _ = cli.Close() 120 | return err 121 | } 122 | 123 | // 扩展服务器测试 124 | func testEX(addr string) error { 125 | cli, err := NewClientForTest(addr) 126 | if err != nil { 127 | return err 128 | } 129 | // CMD信令 1 130 | data, err := CommandWithConn(cli, func() (req std.Marshaler, resp std.Unmarshaler, err error) { 131 | req, resp, err = ext.NewExCmd1() 132 | return 133 | }) 134 | fmt.Printf("%+v\n", data) 135 | _ = cli.Close() 136 | return err 137 | } 138 | -------------------------------------------------------------------------------- /quotes/bestip.txt: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | var ( 4 | StandardServer = []Server{ 5 | {Name: "通达信深圳双线主站1", Host: "110.41.147.114", Port: 7709, CrossTime: 0}, 6 | {Name: "通达信深圳双线主站2", Host: "110.41.2.72", Port: 7709, CrossTime: 0}, 7 | {Name: "通达信深圳双线主站3", Host: "110.41.4.4", Port: 7709, CrossTime: 0}, 8 | {Name: "通达信深圳双线主站4", Host: "47.113.94.204", Port: 7709, CrossTime: 0}, 9 | {Name: "通达信深圳双线主站5", Host: "8.129.174.169", Port: 7709, CrossTime: 0}, 10 | {Name: "通达信深圳双线主站6", Host: "110.41.154.219", Port: 7709, CrossTime: 0}, 11 | {Name: "通达信上海双线主站1", Host: "124.70.176.52", Port: 7709, CrossTime: 0}, 12 | {Name: "通达信上海双线主站2", Host: "47.100.236.28", Port: 7709, CrossTime: 0}, 13 | {Name: "通达信上海双线主站3", Host: "123.60.186.45", Port: 7709, CrossTime: 0}, 14 | {Name: "通达信上海双线主站4", Host: "123.60.164.122", Port: 7709, CrossTime: 0}, 15 | {Name: "通达信上海双线主站5", Host: "47.116.105.28", Port: 7709, CrossTime: 0}, 16 | {Name: "通达信上海双线主站6", Host: "124.70.199.56", Port: 7709, CrossTime: 0}, 17 | {Name: "通达信北京双线主站1", Host: "121.36.54.217", Port: 7709, CrossTime: 0}, 18 | {Name: "通达信北京双线主站2", Host: "121.36.81.195", Port: 7709, CrossTime: 0}, 19 | {Name: "通达信北京双线主站3", Host: "123.249.15.60", Port: 7709, CrossTime: 0}, 20 | {Name: "通达信广州双线主站1", Host: "124.71.85.110", Port: 7709, CrossTime: 0}, 21 | {Name: "通达信广州双线主站2", Host: "139.9.51.18", Port: 7709, CrossTime: 0}, 22 | {Name: "通达信广州双线主站3", Host: "139.159.239.163", Port: 7709, CrossTime: 0}, 23 | {Name: "通达信上海双线主站7", Host: "106.14.201.131", Port: 7709, CrossTime: 0}, 24 | {Name: "通达信上海双线主站8", Host: "106.14.190.242", Port: 7709, CrossTime: 0}, 25 | {Name: "通达信上海双线主站9", Host: "121.36.225.169", Port: 7709, CrossTime: 0}, 26 | {Name: "通达信上海双线主站10", Host: "123.60.70.228", Port: 7709, CrossTime: 0}, 27 | {Name: "通达信上海双线主站11", Host: "123.60.73.44", Port: 7709, CrossTime: 0}, 28 | {Name: "通达信上海双线主站12", Host: "124.70.133.119", Port: 7709, CrossTime: 0}, 29 | {Name: "通达信上海双线主站13", Host: "124.71.187.72", Port: 7709, CrossTime: 0}, 30 | {Name: "通达信上海双线主站14", Host: "124.71.187.122", Port: 7709, CrossTime: 0}, 31 | {Name: "通达信武汉电信主站1", Host: "119.97.185.59", Port: 7709, CrossTime: 0}, 32 | {Name: "通达信深圳双线主站7", Host: "47.107.64.168", Port: 7709, CrossTime: 0}, 33 | {Name: "通达信北京双线主站4", Host: "124.70.75.113", Port: 7709, CrossTime: 0}, 34 | {Name: "通达信广州双线主站4", Host: "124.71.9.153", Port: 7709, CrossTime: 0}, 35 | {Name: "通达信上海双线主站15", Host: "123.60.84.66", Port: 7709, CrossTime: 0}, 36 | {Name: "通达信深圳双线主站8", Host: "47.107.228.47", Port: 7719, CrossTime: 0}, 37 | {Name: "通达信北京双线主站5", Host: "120.46.186.223", Port: 7709, CrossTime: 0}, 38 | {Name: "通达信北京双线主站6", Host: "124.70.22.210", Port: 7709, CrossTime: 0}, 39 | {Name: "通达信北京双线主站7", Host: "139.9.133.247", Port: 7709, CrossTime: 0}, 40 | {Name: "通达信广州双线主站5", Host: "116.205.163.254", Port: 7709, CrossTime: 0}, 41 | {Name: "通达信广州双线主站6", Host: "116.205.171.132", Port: 7709, CrossTime: 0}, 42 | {Name: "通达信广州双线主站7", Host: "116.205.183.150", Port: 7709, CrossTime: 0}, 43 | 44 | //{Name: "杭州电信主站J1", Host: "60.191.117.167", Port: 7709}, 45 | //{Name: "杭州电信主站J2", Host: "115.238.56.198", Port: 7709}, 46 | //{Name: "杭州电信主站J3", Host: "218.75.126.9", Port: 7709}, 47 | //{Name: "杭州电信主站J4", Host: "115.238.90.165", Port: 7709}, 48 | //{Name: "杭州联通主站J2", Host: "60.12.136.250", Port: 7709}, 49 | //{Name: "云行情上海电信Z1", Host: "114.80.63.12", Port: 7709}, 50 | //{Name: "云行情上海电信Z2", Host: "114.80.63.35", Port: 7709}, 51 | //{Name: "上海电信主站Z3", Host: "180.153.39.51", Port: 7709}, 52 | //{Name: "招商证券深圳行情", Host: "39.108.28.83", Port: 7709}, 53 | //{Name: "招商证券深圳行情", Host: "119.147.212.81", Port: 7709}, 54 | //{Name: "招商证券深圳行情", Host: "58.249.119.236", Port: 7709}, 55 | //{Name: "招商证券深圳行情", Host: "183.240.166.230", Port: 7709}, 56 | //{Name: "招商证券北京行情", Host: "61.49.50.190", Port: 7709}, 57 | //{Name: "招商证券北京行情", Host: "39.105.251.234", Port: 7709}, 58 | //{Name: "安信", Host: "59.36.5.11", Port: 7709}, 59 | //{Name: "广发", Host: "119.29.19.242", Port: 7709}, 60 | //{Name: "广发", Host: "183.60.224.177", Port: 7709}, 61 | //{Name: "广发", Host: "183.60.224.178", Port: 7709}, 62 | //{Name: "国泰君安", Host: "117.34.114.13", Port: 7709}, 63 | //{Name: "国泰君安", Host: "117.34.114.14", Port: 7709}, 64 | //{Name: "国泰君安", Host: "117.34.114.15", Port: 7709}, 65 | //{Name: "国泰君安", Host: "117.34.114.16", Port: 7709}, 66 | //{Name: "国泰君安", Host: "117.34.114.17", Port: 7709}, 67 | //{Name: "国泰君安", Host: "117.34.114.18", Port: 7709}, 68 | //{Name: "国泰君安", Host: "117.34.114.20", Port: 7709}, 69 | //{Name: "国泰君安", Host: "117.34.114.27", Port: 7709}, 70 | //{Name: "国泰君安", Host: "117.34.114.30", Port: 7709}, 71 | //{Name: "国信", Host: "58.63.254.247", Port: 7709}, 72 | //{Name: "海通", Host: "123.125.108.90", Port: 7709}, 73 | //{Name: "海通", Host: "175.6.5.153", Port: 7709}, 74 | //{Name: "海通", Host: "182.118.47.151", Port: 7709}, 75 | //{Name: "海通", Host: "182.131.3.245", Port: 7709}, 76 | //{Name: "海通", Host: "202.100.166.27", Port: 7709}, 77 | //{Name: "海通", Host: "58.63.254.191", Port: 7709}, 78 | //{Name: "海通", Host: "58.63.254.217", Port: 7709}, 79 | //{Name: "华林", Host: "202.100.166.21", Port: 7709}, 80 | //{Name: "华林", Host: "202.96.138.90", Port: 7709}, 81 | //{Name: "华林", Host: "218.106.92.182", Port: 7709}, 82 | //{Name: "华林", Host: "218.106.92.183", Port: 7709}, 83 | //{Name: "华林", Host: "220.178.55.71", Port: 7709}, 84 | //{Name: "华林", Host: "220.178.55.86", Port: 7709}, 85 | } 86 | 87 | ExtensionServer = []Server{ 88 | {Name: "扩展市场深圳双线1", Host: "112.74.214.43", Port: 7727, CrossTime: 0}, 89 | {Name: "扩展市场深圳双线2", Host: "120.25.218.6", Port: 7727, CrossTime: 0}, 90 | {Name: "扩展市场深圳双线3", Host: "47.107.75.159", Port: 7727, CrossTime: 0}, 91 | {Name: "扩展市场深圳双线4", Host: "47.106.204.218", Port: 7727, CrossTime: 0}, 92 | {Name: "扩展市场深圳双线5", Host: "47.106.209.131", Port: 7727, CrossTime: 0}, 93 | {Name: "扩展市场武汉主站1", Host: "119.97.185.5", Port: 7727, CrossTime: 0}, 94 | {Name: "扩展市场深圳双线6", Host: "47.115.94.72", Port: 7727, CrossTime: 0}, 95 | {Name: "扩展市场上海双线1", Host: "106.14.95.149", Port: 7727, CrossTime: 0}, 96 | {Name: "扩展市场上海双线2", Host: "47.102.108.214", Port: 7727, CrossTime: 0}, 97 | {Name: "扩展市场上海双线3", Host: "47.103.86.229", Port: 7727, CrossTime: 0}, 98 | {Name: "扩展市场上海双线4", Host: "47.103.88.146", Port: 7727, CrossTime: 0}, 99 | {Name: "扩展市场广州双线1", Host: "116.205.143.214", Port: 7727, CrossTime: 0}, 100 | {Name: "扩展市场广州双线2", Host: "124.71.223.19", Port: 7727, CrossTime: 0}, 101 | } 102 | ) 103 | 104 | //const ( 105 | // // HQ_HOSTS 标准市场 主机列表 106 | // HQ_HOSTS = `[ 107 | //{"name":"北京双线主站1", "host":"121.36.54.217", "port": 7709}, 108 | //{"name":"北京双线主站2", "host":"121.36.81.195", "port": 7709}, 109 | //{"name":"北京双线主站3", "host":"123.249.15.60", "port": 7709}, 110 | //{"name":"北京双线主站4", "host":"124.70.75.113", "port": 7709}, 111 | //{"name":"北京双线主站5", "host":"120.46.186.223", "port": 7709}, 112 | //{"name":"北京双线主站6", "host":"124.70.22.210", "port": 7709}, 113 | //{"name":"上海双线主站1", "host":"124.70.176.52", "port": 7709}, 114 | //{"name":"上海双线主站2", "host":"47.100.236.28", "port": 7709}, 115 | //{"name":"上海双线主站5", "host":"47.116.105.28", "port": 7709}, 116 | //{"name":"上海双线主站6", "host":"124.70.199.56", "port": 7709}, 117 | //{"name":"上海双线主站7", "host":"106.14.201.131", "port": 7709}, 118 | //{"name":"上海双线主站8", "host":"106.14.190.242", "port": 7709}, 119 | //{"name":"上海双线主站9", "host":"121.36.225.169", "port": 7709}, 120 | //{"name":"上海双线主站10", "host":"123.60.70.228", "port": 7709}, 121 | //{"name":"上海双线主站11", "host":"123.60.73.44", "port": 7709}, 122 | //{"name":"上海双线主站12", "host":"124.70.133.119", "port": 7709}, 123 | //{"name":"上海双线主站13", "host":"124.71.187.72", "port": 7709}, 124 | //{"name":"上海双线主站14", "host":"124.71.187.122", "port": 7709}, 125 | //{"name":"上海双线主站15", "host":"123.60.84.66", "port": 7709}, 126 | //{"name":"深圳双线主站1", "host":"110.41.147.114", "port": 7709}, 127 | //{"name":"深圳双线主站4", "host":"47.113.94.204", "port": 7709}, 128 | //{"name":"深圳双线主站5", "host":"8.129.174.169", "port": 7709}, 129 | //{"name":"深圳双线主站6", "host":"110.41.154.219", "port": 7709}, 130 | //{"name":"深圳双线主站7", "host":"47.107.64.168", "port": 7709}, 131 | //{"name":"深圳双线主站8", "host":"47.107.228.47", "port": 7719}, 132 | //{"name":"广州双线主站1", "host":"124.71.85.110", "port": 7709}, 133 | //{"name":"广州双线主站2", "host":"139.9.51.18", "port": 7709}, 134 | //{"name":"广州双线主站3", "host":"139.159.239.163", "port": 7709}, 135 | //{"name":"广州双线主站4", "host":"124.71.9.153", "port": 7709}, 136 | //{"name":"广州双线主站5", "host":"116.205.163.254", "port": 7709}, 137 | //{"name":"广州双线主站6", "host":"116.205.171.132", "port": 7709}, 138 | //{"name":"广州双线主站7", "host":"116.205.183.150", "port": 7709}, 139 | //{"name":"杭州电信主站J1", "host":"60.191.117.167", "port":7709}, 140 | //{"name":"杭州电信主站J2", "host":"115.238.56.198", "port":7709}, 141 | //{"name":"杭州电信主站J3", "host":"218.75.126.9", "port":7709}, 142 | //{"name":"杭州电信主站J4", "host":"115.238.90.165", "port":7709}, 143 | //{"name":"杭州联通主站J2", "host":"60.12.136.250", "port":7709}, 144 | //{"name":"云行情上海电信Z1", "host":"114.80.63.12", "port":7709}, 145 | //{"name":"云行情上海电信Z2", "host":"114.80.63.35", "port":7709}, 146 | //{"name":"上海电信主站Z3", "host":"180.153.39.51", "port":7709}, 147 | //{"name":"招商证券深圳行情", "host":"39.108.28.83", "port":7709}, 148 | //{"name":"招商证券深圳行情", "host":"119.147.212.81", "port":7709}, 149 | //{"name":"招商证券深圳行情", "host":"58.249.119.236", "port":7709}, 150 | //{"name":"招商证券深圳行情", "host":"183.240.166.230", "port":7709}, 151 | //{"name":"招商证券北京行情", "host":"61.49.50.190", "port":7709}, 152 | //{"name":"招商证券北京行情", "host":"39.105.251.234", "port":7709}, 153 | //{"name":"安信", "host":"59.36.5.11", "port":7709}, 154 | //{"name":"广发", "host":"119.29.19.242", "port":7709}, 155 | //{"name":"广发", "host":"183.60.224.177", "port":7709}, 156 | //{"name":"广发", "host":"183.60.224.178", "port":7709}, 157 | //{"name":"国泰君安", "host":"117.34.114.13", "port":7709}, 158 | //{"name":"国泰君安", "host":"117.34.114.14", "port":7709}, 159 | //{"name":"国泰君安", "host":"117.34.114.15", "port":7709}, 160 | //{"name":"国泰君安", "host":"117.34.114.16", "port":7709}, 161 | //{"name":"国泰君安", "host":"117.34.114.17", "port":7709}, 162 | //{"name":"国泰君安", "host":"117.34.114.18", "port":7709}, 163 | //{"name":"国泰君安", "host":"117.34.114.20", "port":7709}, 164 | //{"name":"国泰君安", "host":"117.34.114.27", "port":7709}, 165 | //{"name":"国泰君安", "host":"117.34.114.30", "port":7709}, 166 | //{"name":"国信", "host":"58.63.254.247", "port":7709}, 167 | //{"name":"海通", "host":"123.125.108.90", "port":7709}, 168 | //{"name":"海通", "host":"175.6.5.153", "port":7709}, 169 | //{"name":"海通", "host":"182.118.47.151", "port":7709}, 170 | //{"name":"海通", "host":"182.131.3.245", "port":7709}, 171 | //{"name":"海通", "host":"202.100.166.27", "port":7709}, 172 | //{"name":"海通", "host":"58.63.254.191", "port":7709}, 173 | //{"name":"海通", "host":"58.63.254.217", "port":7709}, 174 | //{"name":"华林", "host":"202.100.166.21", "port":7709}, 175 | //{"name":"华林", "host":"202.96.138.90", "port":7709}, 176 | //{"name":"华林", "host":"218.106.92.182", "port":7709}, 177 | //{"name":"华林", "host":"218.106.92.183", "port":7709}, 178 | //{"name":"华林", "host":"220.178.55.71", "port":7709}, 179 | //{"name":"华林", "host":"220.178.55.86", "port":7709} 180 | //]` 181 | // 182 | // // EX_HOSTS 扩展市场主机列表 183 | // EX_HOSTS = `[ 184 | //{"name":"扩展市场深圳双线1", "host":"112.74.214.43", "port": 7727}, 185 | //{"name":"扩展市场深圳双线3", "host":"47.107.75.159", "port": 7727}, 186 | //{"name":"扩展市场武汉主站1", "host":"119.97.185.5", "port": 7727}, 187 | //{"name":"扩展市场上海双线0", "host":"106.14.95.149", "port": 7727} 188 | //]` 189 | // // GP_HOSTS 财务数据 主机列表 190 | // GP_HOSTS = `[ 191 | //{"name":"默认财务数据线路", "host":"120.76.152.87", "port": 7709} 192 | //]` 193 | //) 194 | -------------------------------------------------------------------------------- /quotes/bestip_cache.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "gitee.com/quant1x/exchange" 6 | "gitee.com/quant1x/exchange/cache" 7 | "gitee.com/quant1x/gox/api" 8 | "gitee.com/quant1x/gox/coroutine" 9 | "gitee.com/quant1x/gox/logger" 10 | "gitee.com/quant1x/gox/timestamp" 11 | "os" 12 | "path/filepath" 13 | "time" 14 | ) 15 | 16 | var ( 17 | DefaultHQServer = Server{ 18 | Name: "临时主机", 19 | Host: "119.147.212.81", 20 | Port: 7709, 21 | CrossTime: 0, 22 | } 23 | DefaultEXServer = DefaultHQServer 24 | ) 25 | 26 | const ( 27 | serverListFilename = "tdx.json" 28 | serverResetOffsetHours = 8 29 | serverResetOffsetMinutes = 55 30 | ) 31 | 32 | var ( 33 | onceSortServers coroutine.RollingOnce 34 | cacheAllServers AllServers 35 | ) 36 | 37 | func loadSortedServerList(configPath string) *AllServers { 38 | f, err := os.Open(configPath) 39 | if err != nil { 40 | return nil 41 | } 42 | decoder := json.NewDecoder(f) 43 | var as AllServers 44 | err = decoder.Decode(&as) 45 | if err != nil { 46 | return nil 47 | } 48 | return &as 49 | } 50 | 51 | func saveSortedServerList(as *AllServers, configPath string) error { 52 | data, err := json.Marshal(as) 53 | if err != nil { 54 | return err 55 | } 56 | err = os.WriteFile(configPath, data, 0644) 57 | return err 58 | } 59 | 60 | func GetFastHost(key string) []Server { 61 | onceSortServers.Do(lazyCachedSortedServerList) 62 | bestIp := cacheAllServers.BestIP 63 | if key == TDX_HOST_HQ { 64 | if len(bestIp.HQ) > 0 { 65 | return bestIp.HQ 66 | } else { 67 | return []Server{DefaultHQServer} 68 | } 69 | } else if key == TDX_HOST_EX { 70 | if len(bestIp.EX) > 0 { 71 | return bestIp.EX 72 | } else { 73 | return []Server{DefaultHQServer} 74 | } 75 | } 76 | return []Server{DefaultHQServer} 77 | } 78 | 79 | func lazyCachedSortedServerList() { 80 | // 0. 确定更新时间, 08:55:00, 服务器列表先于其它服务更新 81 | onceSortServers.SetOffsetTime(serverResetOffsetHours, serverResetOffsetMinutes) 82 | // 1. 组织文件路径 83 | filename := filepath.Join(cache.GetMetaPath(), serverListFilename) 84 | 85 | // 2. 检查缓存文件是否存在 86 | var lastModified time.Time 87 | fs, err := api.GetFileStat(filename) 88 | if err == nil { 89 | lastModified = fs.LastWriteTime 90 | } 91 | // 3. 尝试更新服务器列表 92 | allServers := updateBestIpList(lastModified) 93 | // 4. 更新内存 94 | cacheAllServers = *allServers 95 | } 96 | 97 | func updateBestIpList(lastModified time.Time) *AllServers { 98 | filename := filepath.Join(cache.GetMetaPath(), serverListFilename) 99 | // 2.2 转换缓存文件最后修改日期, 时间格式和日历格式对齐 100 | cacheLastDay := lastModified.Format(exchange.TradingDayDateFormat) 101 | 102 | observerTimestamp := onceSortServers.GetCurrentAnchorPoint() 103 | observerTime := timestamp.Time(observerTimestamp) 104 | now := timestamp.Now() 105 | var allServers *AllServers 106 | needUpdate := false 107 | // 3. 比较缓存日期和最后一个交易日 108 | latestDay := exchange.LastTradeDate() 109 | c1 := now >= observerTimestamp 110 | c2 := cacheLastDay < latestDay 111 | c3 := lastModified.Before(observerTime) 112 | if c1 && (c2 && c3) { 113 | // 缓存过时,重新生成 114 | allServers = ProfileBestIPList() 115 | needUpdate = true 116 | } else { 117 | // 缓存有效,尝试加载 118 | allServers = loadSortedServerList(filename) 119 | } 120 | // 4. 数据有效, 则缓存文件 121 | ok := allServers != nil && len(allServers.BestIP.HQ) > 0 /*&& len(allServers.BestIP.EX) > 0*/ 122 | if needUpdate && ok { 123 | // 保存有效缓存 124 | _ = saveSortedServerList(allServers, filename) 125 | } else if !ok { 126 | logger.Fatalf("服务器列表为空") 127 | } 128 | return allServers 129 | } 130 | 131 | // BestIP 网络测速, 更新本地服务器列表配置文件 132 | // 133 | // 强制刷新 134 | func BestIP() { 135 | var lastModified time.Time 136 | updateBestIpList(lastModified) 137 | } 138 | -------------------------------------------------------------------------------- /quotes/bestip_cache_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestOpenConfig(t *testing.T) { 9 | list := GetFastHost(TDX_HOST_HQ) 10 | fmt.Printf("%+v\n", list) 11 | } 12 | -------------------------------------------------------------------------------- /quotes/bestip_client.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "gitee.com/quant1x/gotdx/internal" 7 | "gitee.com/quant1x/gotdx/proto/std" 8 | "net" 9 | "time" 10 | ) 11 | 12 | type LabClient struct { 13 | conn net.Conn 14 | addr string 15 | //Host string 16 | //Port int 17 | Timeout time.Duration 18 | MaxRetryTimes int 19 | RetryDuration time.Duration 20 | } 21 | 22 | func NewClientForTest(addr string) (*LabClient, error) { 23 | conn, err := net.DialTimeout("tcp", addr, 1*time.Second) // net.DialTimeout() 24 | if err != nil { 25 | fmt.Printf("connect %s, %+v\n", addr, err) 26 | return nil, err 27 | } 28 | return &LabClient{ 29 | conn: conn, 30 | addr: addr, 31 | //Host: host, 32 | //Port: port, 33 | MaxRetryTimes: 5, 34 | Timeout: 1 * time.Second, 35 | RetryDuration: time.Millisecond * 200, 36 | }, nil 37 | } 38 | 39 | func (cli *LabClient) Do(request std.Marshaler, response std.Unmarshaler) error { 40 | // 序列化请求 41 | req, err := request.Marshal() 42 | if err != nil { 43 | return err 44 | } 45 | // 发送请求 46 | retryTimes := 0 47 | SEND: 48 | n, err := cli.conn.Write(req) 49 | // 重试 50 | if n < len(req) { 51 | retryTimes += 1 52 | if retryTimes <= cli.MaxRetryTimes { 53 | fmt.Printf("第%d次重试\n", retryTimes) 54 | goto SEND 55 | } else { 56 | return errors.New("数据未完整发送") 57 | } 58 | } 59 | if err != nil { 60 | return err 61 | } 62 | // 解析响应包头 63 | var header std.PacketHeader 64 | // 读取包头 大小为16个字节 65 | // 单次获取的字列流 66 | headerLength := 0x10 67 | headerBytes := make([]byte, headerLength) 68 | // 调用socket获取字节流并保存到data中 69 | headerBytes, err = cli.receive(headerLength) 70 | if err != nil { 71 | return err 72 | } 73 | err = header.Unmarshal(headerBytes) 74 | if err != nil { 75 | return err 76 | } 77 | // 根据获取响应体结构 78 | // 调用socket获取字节流并保存到data中 79 | bodyBytes, err := cli.receive(header.ZipSize) 80 | if err != nil { 81 | return err 82 | } 83 | // zlib解压缩 84 | if header.Compressed() { 85 | bodyBytes, err = internal.ZlibUnCompress(bodyBytes) 86 | } 87 | // 反序列化为响应体结构 88 | err = response.Unmarshal(bodyBytes) 89 | if err != nil { 90 | return err 91 | } 92 | return nil 93 | } 94 | 95 | func (cli *LabClient) receive(length int) (data []byte, err error) { 96 | var ( 97 | receivedSize int 98 | ) 99 | READ: 100 | tmp := make([]byte, length) 101 | // 设置读timeout 102 | err = cli.conn.SetReadDeadline(time.Now().Add(cli.Timeout)) 103 | if err != nil { 104 | fmt.Println("setReadDeadline failed:", err) 105 | } 106 | // 调用socket获取字节流并保存到data中 107 | receivedSize, err = cli.conn.Read(tmp) 108 | // socket错误,可能为EOF 109 | if err != nil { 110 | return nil, err 111 | } 112 | // 数据添加到总输出,由于tmp申请内存时使用了length的长度, 113 | // 所以直接全部复制到data中会使得未完全传输的部分被填充为0导致数据获取不完整, 114 | // 故使用tmp[:receivedSize] 115 | data = append(data, tmp[:receivedSize]...) 116 | // 数据读满就可以返回了 117 | if len(data) == length { 118 | return 119 | } 120 | // 读取小于标准尺寸,说明到文件尾或者读取出现了问题没读满,可以返回了 121 | if receivedSize < length { 122 | goto READ 123 | } 124 | return 125 | } 126 | 127 | func (cli *LabClient) Close() error { 128 | return cli.conn.Close() 129 | } 130 | -------------------------------------------------------------------------------- /quotes/bestip_command.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "fmt" 5 | "gitee.com/quant1x/gotdx/proto/std" 6 | ) 7 | 8 | func CommandWithConn(cli *LabClient, callback std.Factory) (std.Unmarshaler, error) { 9 | req, resp, err := callback() 10 | if err != nil { 11 | fmt.Println(err) 12 | return nil, err 13 | } 14 | err = cli.Do(req, resp) 15 | if err != nil { 16 | fmt.Println(err) 17 | _ = cli.Close() 18 | return nil, err 19 | } 20 | return resp, nil 21 | } 22 | -------------------------------------------------------------------------------- /quotes/bestip_embed.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "gitee.com/quant1x/gotdx/internal" 7 | "gitee.com/quant1x/gox/api" 8 | "gitee.com/quant1x/gox/logger" 9 | "gitee.com/quant1x/pkg/ini" 10 | "io" 11 | "os" 12 | "slices" 13 | "strings" 14 | "text/template" 15 | ) 16 | 17 | var ( 18 | // ResourcesPath 资源路径 19 | ResourcesPath = "resources" 20 | ) 21 | 22 | //go:embed resources/* 23 | var resources embed.FS 24 | 25 | const ( 26 | sectionStandardServer = "HQHOST" 27 | defaultStandardPort = 7709 28 | keyHostNum = "HostNum" 29 | sectionExtensionServer = "DSHOST" 30 | defaultExtensionPort = 7727 31 | ) 32 | 33 | var ( 34 | ignoreStandardPortList = []int{80} // 标准行情需要忽略的端口号 35 | ignoreExtensionPortList = []int{} // 扩展行情需要忽略的端口号 36 | ) 37 | 38 | type tdxConfig struct { 39 | source string 40 | filename string 41 | } 42 | 43 | var ( 44 | tdxServerList = []tdxConfig{ 45 | tdxConfig{source: "通达信", filename: "tdx.cfg"}, 46 | tdxConfig{source: "中信证券", filename: "zhongxin.cfg"}, 47 | tdxConfig{source: "华泰证券", filename: "huatai.cfg"}, 48 | tdxConfig{source: "国泰君安", filename: "guotaijunan.cfg"}, 49 | } 50 | ) 51 | 52 | var ( 53 | templateAddress = `package quotes 54 | 55 | var ( 56 | // StandardServerList 标准行情服务器列表 57 | StandardServerList = []Server{ 58 | {{- range .Std}} 59 | {Source: "{{.Source}}", Name: "{{.Name}}", Host: "{{.Host}}", Port: {{.Port}}, CrossTime: 0}, 60 | {{- end}} 61 | } 62 | // ExtensionServerList 扩展行情服务器列表 63 | ExtensionServerList = []Server{ 64 | {{- range .Ext}} 65 | {Source: "{{.Source}}", Name: "{{.Name}}", Host: "{{.Host}}", Port: {{.Port}}, CrossTime: 0}, 66 | {{- end}} 67 | } 68 | ) 69 | 70 | ` 71 | ) 72 | 73 | func loadAllConfig() { 74 | var standardServers, extensionServers []Server 75 | for _, config := range tdxServerList { 76 | std, ext := loadTdxConfig(config) 77 | if len(std) > 0 { 78 | standardServers = append(standardServers, std...) 79 | } 80 | if len(ext) > 0 { 81 | extensionServers = append(extensionServers, ext...) 82 | } 83 | } 84 | fmt.Println("----------<" + sectionStandardServer + ">----------") 85 | for _, v := range standardServers { 86 | fmt.Printf(`{Source: "%s", Name: "%s", Host: "%s", Port: %d, CrossTime: 0},`+"\n", v.Source, v.Name, v.Host, v.Port) 87 | } 88 | fmt.Println("----------<" + sectionExtensionServer + ">----------") 89 | for _, v := range extensionServers { 90 | fmt.Printf(`{Source: "%s", Name: "%s", Host: "%s", Port: %d, CrossTime: 0},`+"\n", v.Source, v.Name, v.Host, v.Port) 91 | } 92 | tmpl, err := template.New("address").Parse(templateAddress) 93 | if err != nil { 94 | logger.Fatalf("解析服务器地址模版失败, error=%+v", err) 95 | } 96 | data := struct { 97 | Std []Server 98 | Ext []Server 99 | }{ 100 | Std: standardServers, 101 | Ext: extensionServers, 102 | } 103 | writer, err := os.Create("bestip_address.go") 104 | if err != nil { 105 | logger.Fatalf("创建bestip_address.go源文件失败, error=%+v", err) 106 | } 107 | err = tmpl.Execute(writer, data) 108 | if err != nil { 109 | logger.Fatalf("执行服务器地址模版失败, error=%+v", err) 110 | } 111 | } 112 | 113 | func loadTdxConfig(config tdxConfig) (std, ext []Server) { 114 | name := config.filename 115 | source := config.source 116 | fs, err := api.OpenEmbed(resources, ResourcesPath+"/"+name) 117 | if err != nil { 118 | logger.Fatalf("%+v", err) 119 | } 120 | data, err := io.ReadAll(fs) 121 | if err != nil { 122 | logger.Fatalf("%+v", err) 123 | } 124 | cfg, err := ini.Load(data) 125 | if err != nil { 126 | logger.Fatalf("%+v", err) 127 | } 128 | //fmt.Println("----------<" + sectionStandardServer + ">----------") 129 | section := cfg.Section(sectionStandardServer) 130 | if section == nil { 131 | return 132 | } 133 | v := section.Key(keyHostNum).Value() 134 | hostNum := int(api.ParseInt(v)) 135 | for i := 0; i < hostNum; i++ { 136 | hostName := section.Key(fmt.Sprintf("HostName%02d", i+1)).Value() 137 | hostName = internal.Utf8ToGbk(api.String2Bytes(hostName)) 138 | ipAddress := section.Key(fmt.Sprintf("IPAddress%02d", i+1)).Value() 139 | if isIPV6(ipAddress) { 140 | continue 141 | } 142 | tmpPort := section.Key(fmt.Sprintf("Port%02d", i+1)).Value() 143 | port := int(api.ParseInt(tmpPort)) 144 | if slices.Contains(ignoreStandardPortList, port) { 145 | continue 146 | } 147 | srv := Server{Source: source, Name: hostName, Host: ipAddress, Port: port} 148 | std = append(std, srv) 149 | } 150 | //fmt.Println("----------<" + sectionExtensionServer + ">----------") 151 | section = cfg.Section(sectionExtensionServer) 152 | if section == nil { 153 | return 154 | } 155 | v = section.Key(keyHostNum).Value() 156 | hostNum = int(api.ParseInt(v)) 157 | for i := 0; i < hostNum; i++ { 158 | hostName := section.Key(fmt.Sprintf("HostName%02d", i+1)).Value() 159 | hostName = internal.Utf8ToGbk(api.String2Bytes(hostName)) 160 | ipAddress := section.Key(fmt.Sprintf("IPAddress%02d", i+1)).Value() 161 | if isIPV6(ipAddress) { 162 | continue 163 | } 164 | tmpPort := section.Key(fmt.Sprintf("Port%02d", i+1)).Value() 165 | port := int(api.ParseInt(tmpPort)) 166 | if slices.Contains(ignoreExtensionPortList, port) { 167 | continue 168 | } 169 | srv := Server{Source: source, Name: hostName, Host: ipAddress, Port: port} 170 | ext = append(ext, srv) 171 | } 172 | return 173 | } 174 | 175 | func isIPV6(address string) bool { 176 | arr := strings.Split(address, ":") 177 | return len(arr) > 2 178 | } 179 | -------------------------------------------------------------------------------- /quotes/bestip_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBestIP(t *testing.T) { 8 | BestIP() 9 | } 10 | 11 | func TestReadTdxConfig(t *testing.T) { 12 | loadAllConfig() 13 | } 14 | -------------------------------------------------------------------------------- /quotes/block_info.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/gotdx/internal" 8 | "gitee.com/quant1x/gotdx/proto" 9 | "gitee.com/quant1x/gox/encoding/binary/struc" 10 | ) 11 | 12 | // BlockInfoPackage 板块信息 13 | type BlockInfoPackage struct { 14 | reqHeader *StdRequestHeader 15 | respHeader *StdResponseHeader 16 | request *BlockInfoRequest 17 | response *BlockInfoResponse 18 | contentHex string 19 | } 20 | 21 | // BlockInfoRequest 请求包 22 | type BlockInfoRequest struct { 23 | Start uint32 `struc:"uint32,little"` 24 | Size uint32 `struc:"uint32,little"` 25 | BlockFile [100]byte `struc:"[100]byte,little"` // 板块文件名 26 | } 27 | 28 | type BlockInfo struct { 29 | BlockName string 30 | BlockType uint16 31 | StockCount uint16 32 | Codelist []string 33 | } 34 | 35 | type BlockInfoResponse struct { 36 | Size uint32 `struc:"uint32,little"` 37 | Data []byte `struc:"sizefrom=Size"` 38 | } 39 | 40 | type BlockInfoReply struct { 41 | BlockNum uint16 `struc:"uint16,little"` // 板块个数 42 | Block []BlockInfo // 板块列表 43 | } 44 | 45 | func NewBlockInfoPackage() *BlockInfoPackage { 46 | pkg := new(BlockInfoPackage) 47 | pkg.reqHeader = new(StdRequestHeader) 48 | pkg.respHeader = new(StdResponseHeader) 49 | pkg.request = new(BlockInfoRequest) 50 | pkg.response = new(BlockInfoResponse) 51 | 52 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 53 | //0c 54 | pkg.reqHeader.ZipFlag = 0x0c 55 | //1f 18 76 00 56 | pkg.reqHeader.SeqID = internal.SequenceId() 57 | //01 58 | pkg.reqHeader.PacketType = 0x01 59 | //0b 00 60 | //PkgLen1 uint16 61 | pkg.reqHeader.PkgLen1 = 0x006e 62 | //0b 00 63 | //PkgLen2 uint16 64 | pkg.reqHeader.PkgLen2 = 0x006e 65 | //10 00 66 | pkg.reqHeader.Method = proto.STD_MSG_BLOCK_DATA 67 | //pkg.contentHex = "0100" // 未解 68 | return pkg 69 | } 70 | 71 | func (obj *BlockInfoPackage) SetParams(req *BlockInfoRequest) { 72 | obj.request = req 73 | } 74 | 75 | func (obj *BlockInfoPackage) Serialize() ([]byte, error) { 76 | buf := new(bytes.Buffer) 77 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 78 | b, err := hex.DecodeString(obj.contentHex) 79 | buf.Write(b) 80 | err = binary.Write(buf, binary.LittleEndian, obj.request) 81 | return buf.Bytes(), err 82 | } 83 | 84 | func (obj *BlockInfoPackage) UnSerialize(header interface{}, data []byte) error { 85 | obj.respHeader = header.(*StdResponseHeader) 86 | // 构造流 87 | buf := bytes.NewBuffer(data) 88 | var reply BlockInfoResponse 89 | err := struc.Unpack(buf, &reply) 90 | if err != nil { 91 | return err 92 | } 93 | obj.response = &reply 94 | return nil 95 | } 96 | 97 | func (obj *BlockInfoPackage) Reply() interface{} { 98 | return obj.response 99 | } 100 | -------------------------------------------------------------------------------- /quotes/block_meta.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/gotdx/internal" 8 | "gitee.com/quant1x/gotdx/proto" 9 | "gitee.com/quant1x/gox/encoding/binary/struc" 10 | ) 11 | 12 | // 板块相关参数 13 | const ( 14 | BLOCK_ZHISHU = "block_zs.dat" // 指数 15 | BLOCK_FENGGE = "block_fg.dat" // 风格 16 | BLOCK_GAINIAN = "block_gn.dat" // 概念 17 | BLOCK_DEFAULT = "block.dat" // 早期的板块数据文件, 与block_zs.dat 18 | BLOCK_CHUNKS_SIZE = 0x7530 // 板块文件默认一个请求包最大数据 19 | ) 20 | 21 | // BlockMetaPackage 板块信息 22 | type BlockMetaPackage struct { 23 | reqHeader *StdRequestHeader 24 | respHeader *StdResponseHeader 25 | request *BlockMetaRequest 26 | response *BlockMeta 27 | contentHex string 28 | } 29 | 30 | // BlockMetaRequest 请求包 31 | type BlockMetaRequest struct { 32 | BlockFile [40]byte // 板块文件名 33 | } 34 | 35 | // BlockMeta 响应包结构 36 | type BlockMeta struct { 37 | Size uint32 `struc:"uint32,little"` // 尺寸 38 | C1 byte `struc:"byte,little"` // C1 39 | HashValue [32]byte `struc:"[32]byte,little"` // hash值 40 | C2 byte `struc:"byte,little"` // C2 41 | } 42 | 43 | func NewBlockMetaPackage() *BlockMetaPackage { 44 | pkg := new(BlockMetaPackage) 45 | pkg.reqHeader = new(StdRequestHeader) 46 | pkg.respHeader = new(StdResponseHeader) 47 | pkg.request = new(BlockMetaRequest) 48 | pkg.response = new(BlockMeta) 49 | 50 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 51 | //0c 52 | pkg.reqHeader.ZipFlag = 0x0c 53 | //1f 18 76 00 54 | pkg.reqHeader.SeqID = internal.SequenceId() 55 | //01 56 | pkg.reqHeader.PacketType = 0x01 57 | //0b 00 58 | //PkgLen1 uint16 59 | pkg.reqHeader.PkgLen1 = 0x002a 60 | //0b 00 61 | //PkgLen2 uint16 62 | pkg.reqHeader.PkgLen2 = 0x002a 63 | //10 00 64 | pkg.reqHeader.Method = proto.STD_MSG_BLOCK_META 65 | //pkg.contentHex = "0100" // 未解 66 | return pkg 67 | } 68 | 69 | func (obj *BlockMetaPackage) SetParams(req *BlockMetaRequest) { 70 | obj.request = req 71 | } 72 | 73 | func (obj *BlockMetaPackage) Serialize() ([]byte, error) { 74 | buf := new(bytes.Buffer) 75 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 76 | b, err := hex.DecodeString(obj.contentHex) 77 | buf.Write(b) 78 | err = binary.Write(buf, binary.LittleEndian, obj.request) 79 | return buf.Bytes(), err 80 | } 81 | 82 | func (obj *BlockMetaPackage) UnSerialize(header interface{}, data []byte) error { 83 | obj.respHeader = header.(*StdResponseHeader) 84 | // 构造流 85 | buf := bytes.NewBuffer(data) 86 | var reply BlockMeta 87 | err := struc.Unpack(buf, &reply) 88 | if err != nil { 89 | return err 90 | } 91 | obj.response = &reply 92 | return nil 93 | } 94 | 95 | func (obj *BlockMetaPackage) Reply() interface{} { 96 | return obj.response 97 | } 98 | -------------------------------------------------------------------------------- /quotes/cmd_hearbeat.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/gotdx/internal" 8 | "gitee.com/quant1x/gotdx/proto" 9 | ) 10 | 11 | // 心跳包, command: 0004 12 | // 0c76002800 02 0200 0200 0400 13 | // b1cb74000c760028000004000a000a00 00000000000000000000 14 | 15 | type HeartBeatPackage struct { 16 | reqHeader *StdRequestHeader 17 | request *HeartBeatRequest 18 | respHeader *StdResponseHeader 19 | reply *HeartBeatReply 20 | 21 | contentHex string 22 | } 23 | 24 | type HeartBeatRequest struct { 25 | } 26 | 27 | type HeartBeatReply struct { 28 | Info string // 10个字节的消息, 未解 29 | } 30 | 31 | func NewHeartBeat() *HeartBeatPackage { 32 | obj := new(HeartBeatPackage) 33 | obj.reqHeader = new(StdRequestHeader) 34 | obj.respHeader = new(StdResponseHeader) 35 | obj.request = new(HeartBeatRequest) 36 | obj.reply = new(HeartBeatReply) 37 | 38 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 39 | obj.reqHeader.SeqID = internal.SequenceId() 40 | obj.reqHeader.PacketType = 0x02 41 | obj.reqHeader.Method = proto.STD_MSG_HEARTBEAT 42 | return obj 43 | } 44 | 45 | func (obj *HeartBeatPackage) Serialize() ([]byte, error) { 46 | b, err := hex.DecodeString(obj.contentHex) 47 | 48 | obj.reqHeader.PkgLen1 = 2 + uint16(len(b)) 49 | obj.reqHeader.PkgLen2 = 2 + uint16(len(b)) 50 | 51 | buf := new(bytes.Buffer) 52 | err = binary.Write(buf, binary.LittleEndian, obj.reqHeader) 53 | 54 | buf.Write(b) 55 | return buf.Bytes(), err 56 | } 57 | 58 | func (obj *HeartBeatPackage) UnSerialize(header interface{}, data []byte) error { 59 | obj.respHeader = header.(*StdResponseHeader) 60 | serverInfo := internal.Utf8ToGbk(data[:]) 61 | obj.reply.Info = serverInfo 62 | return nil 63 | } 64 | 65 | func (obj *HeartBeatPackage) Reply() interface{} { 66 | return obj.reply 67 | } 68 | -------------------------------------------------------------------------------- /quotes/cmd_hello1.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/gotdx/internal" 8 | "gitee.com/quant1x/gotdx/proto" 9 | ) 10 | 11 | type Hello1Package struct { 12 | reqHeader *StdRequestHeader 13 | request *Hello1Request 14 | respHeader *StdResponseHeader 15 | reply *Hello1Reply 16 | 17 | contentHex string 18 | } 19 | 20 | type Hello1Request struct { 21 | } 22 | 23 | type Hello1Reply struct { 24 | Info string 25 | serverTime string 26 | } 27 | 28 | func NewHello1() *Hello1Package { 29 | obj := new(Hello1Package) 30 | obj.reqHeader = new(StdRequestHeader) 31 | obj.respHeader = new(StdResponseHeader) 32 | obj.request = new(Hello1Request) 33 | obj.reply = new(Hello1Reply) 34 | 35 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 36 | obj.reqHeader.SeqID = internal.SequenceId() 37 | obj.reqHeader.PacketType = 0x01 38 | obj.reqHeader.Method = proto.STD_MSG_LOGIN1 39 | obj.contentHex = "01" 40 | return obj 41 | } 42 | 43 | func (obj *Hello1Package) Serialize() ([]byte, error) { 44 | b, err := hex.DecodeString(obj.contentHex) 45 | 46 | obj.reqHeader.PkgLen1 = 2 + uint16(len(b)) 47 | obj.reqHeader.PkgLen2 = 2 + uint16(len(b)) 48 | 49 | buf := new(bytes.Buffer) 50 | err = binary.Write(buf, binary.LittleEndian, obj.reqHeader) 51 | 52 | buf.Write(b) 53 | return buf.Bytes(), err 54 | } 55 | 56 | // 00e60708051 50 f0 00 d3 a02b2020c03840384038403840384033a02b2020c0384038403840384038403 00 5a8a3401 f94a0100 5a8a3401 fd4a0100ff00e 700000101013f 57 | // 58 | // 分 时 秒 日期 59 | func (obj *Hello1Package) UnSerialize(header interface{}, data []byte) error { 60 | obj.respHeader = header.(*StdResponseHeader) 61 | serverInfo := internal.Utf8ToGbk(data[68:]) 62 | obj.reply.Info = serverInfo 63 | return nil 64 | } 65 | 66 | func (obj *Hello1Package) Reply() interface{} { 67 | return obj.reply 68 | } 69 | -------------------------------------------------------------------------------- /quotes/cmd_hello2.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/gotdx/internal" 8 | "gitee.com/quant1x/gotdx/proto" 9 | ) 10 | 11 | type Hello2Package struct { 12 | reqHeader *StdRequestHeader 13 | respHeader *StdResponseHeader 14 | request *Hello2Request 15 | reply *Hello2Reply 16 | 17 | contentHex string 18 | } 19 | 20 | type Hello2Request struct { 21 | } 22 | 23 | type Hello2Reply struct { 24 | Info string 25 | serverTime string 26 | } 27 | 28 | func NewHello2() *Hello2Package { 29 | obj := new(Hello2Package) 30 | obj.reqHeader = new(StdRequestHeader) 31 | obj.respHeader = new(StdResponseHeader) 32 | obj.request = new(Hello2Request) 33 | obj.reply = new(Hello2Reply) 34 | 35 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 36 | obj.reqHeader.SeqID = internal.SequenceId() 37 | obj.reqHeader.PacketType = 0x01 38 | obj.reqHeader.Method = proto.STD_MSG_LOGIN2 39 | obj.contentHex = "d5d0c9ccd6a4a8af0000008fc22540130000d500c9ccbdf0d7ea00000002" 40 | return obj 41 | } 42 | 43 | func (obj *Hello2Package) Serialize() ([]byte, error) { 44 | b, err := hex.DecodeString(obj.contentHex) 45 | 46 | obj.reqHeader.PkgLen1 = 2 + uint16(len(b)) 47 | obj.reqHeader.PkgLen2 = 2 + uint16(len(b)) 48 | 49 | buf := new(bytes.Buffer) 50 | err = binary.Write(buf, binary.LittleEndian, obj.reqHeader) 51 | buf.Write(b) 52 | return buf.Bytes(), err 53 | } 54 | 55 | /* 56 | 0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011f85e34068747470733a2f2f626967352e6e65776f6e652e636f6d2e636e2f7a797968742f7a645f7a737a712e7a6970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004150503a414c4c0d0a54494d453a303a30312d31353a30352c31353a30362d32333a35390d0a20202020c4facab9d3c3b5c4b0e6b1bebcb4bdabcda3d3c3a3acceaac1cbc4fab5c4d5fdb3a3cab9d3c32cc7ebbea1bfecc9fdd6c1d5d0c9ccd6a4c8af5043b0e6a1a30d0a20202020c8e7b9fbb2bbc4dcd7d4b6afc9fdbcb6a3acc7ebb5bdb9d9cdf868747470733a2f2f7777772e636d736368696e612e636f6d2fcfc2d4d8b0b2d7b0a3acd0bbd0bbc4fab5c4d6a7b3d6a3a100 年月日 年月日 57 | */ 58 | func (obj *Hello2Package) UnSerialize(header interface{}, data []byte) error { 59 | obj.respHeader = header.(*StdResponseHeader) 60 | 61 | serverInfo := internal.Utf8ToGbk(data[58:]) 62 | //fmt.Println(hex.EncodeToString(data)) 63 | obj.reply.Info = serverInfo 64 | return nil 65 | } 66 | 67 | func (obj *Hello2Package) Reply() interface{} { 68 | return obj.reply 69 | } 70 | -------------------------------------------------------------------------------- /quotes/index_bars.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | ) 11 | 12 | // IndexBarsPackage 指数K线 13 | type IndexBarsPackage struct { 14 | reqHeader *StdRequestHeader 15 | respHeader *StdResponseHeader 16 | //request *IndexBarsRequest 17 | //reply *IndexBarsReply 18 | request *SecurityBarsRequest 19 | reply *SecurityBarsReply 20 | 21 | contentHex string 22 | } 23 | 24 | //type IndexBarsRequest struct { 25 | // MarketType uint16 26 | // Code [6]byte 27 | // Category uint16 // 种类 5分钟 10分钟 28 | // I uint16 // 未知 填充 29 | // Start uint16 30 | // Count uint16 31 | //} 32 | // 33 | //type IndexBarsReply struct { 34 | // Count uint16 35 | // List []IndexBar 36 | //} 37 | // 38 | //type IndexBar struct { 39 | // Open float64 40 | // Close float64 41 | // High float64 42 | // Low float64 43 | // Vol float64 44 | // Amount float64 45 | // Year int 46 | // Month int 47 | // Day int 48 | // Hour int 49 | // Minute int 50 | // DateTime string 51 | // UpCount uint16 52 | // DownCount uint16 53 | //} 54 | 55 | func NewIndexBarsPackage() *IndexBarsPackage { 56 | obj := new(IndexBarsPackage) 57 | obj.reqHeader = new(StdRequestHeader) 58 | obj.respHeader = new(StdResponseHeader) 59 | obj.request = new(SecurityBarsRequest) 60 | obj.reply = new(SecurityBarsReply) 61 | 62 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 63 | obj.reqHeader.SeqID = internal.SequenceId() 64 | obj.reqHeader.PacketType = 0x00 65 | //obj.reqHeader.PkgLen1 = 66 | //obj.reqHeader.PkgLen2 = 67 | obj.reqHeader.Method = proto.STD_MSG_INDEXBARS 68 | obj.contentHex = "00000000000000000000" 69 | return obj 70 | } 71 | func (obj *IndexBarsPackage) SetParams(req *SecurityBarsRequest) { 72 | obj.request = req 73 | obj.request.I = 1 74 | } 75 | 76 | func (obj *IndexBarsPackage) Serialize() ([]byte, error) { 77 | obj.reqHeader.PkgLen1 = 0x1c 78 | obj.reqHeader.PkgLen2 = 0x1c 79 | 80 | buf := new(bytes.Buffer) 81 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 82 | err = binary.Write(buf, binary.LittleEndian, obj.request) 83 | b, err := hex.DecodeString(obj.contentHex) 84 | buf.Write(b) 85 | 86 | //b, err := hex.DecodeString(obj.contentHex) 87 | //buf.Write(b) 88 | 89 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 90 | 91 | return buf.Bytes(), err 92 | } 93 | 94 | func (obj *IndexBarsPackage) UnSerialize(header interface{}, data []byte) error { 95 | obj.respHeader = header.(*StdResponseHeader) 96 | 97 | pos := 0 98 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 99 | pos += 2 100 | 101 | pre_diff_base := 0 102 | for index := uint16(0); index < obj.reply.Count; index++ { 103 | ele := SecurityBar{} 104 | 105 | ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute = internal.GetDatetime(int(obj.request.Category), data, &pos) 106 | 107 | //if index == 0 { 108 | // ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute = getDatetime(int(obj.request.Category), data, &pos) 109 | //} else { 110 | // ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute = getDatetimeNow(int(obj.request.Category), lasttime) 111 | //} 112 | ele.DateTime = fmt.Sprintf("%d-%02d-%02d %02d:%02d:00", ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute) 113 | 114 | price_open_diff := internal.DecodeVarint(data, &pos) 115 | price_close_diff := internal.DecodeVarint(data, &pos) 116 | 117 | price_high_diff := internal.DecodeVarint(data, &pos) 118 | price_low_diff := internal.DecodeVarint(data, &pos) 119 | 120 | var ivol uint32 121 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &ivol) 122 | ele.Vol = internal.IntToFloat64(int(ivol)) 123 | pos += 4 124 | 125 | var dbvol uint32 126 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &dbvol) 127 | ele.Amount = internal.IntToFloat64(int(dbvol)) 128 | pos += 4 129 | 130 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.UpCount) 131 | pos += 2 132 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.DownCount) 133 | pos += 2 134 | 135 | ele.Open = float64(price_open_diff+pre_diff_base) / 1000.0 136 | price_open_diff += pre_diff_base 137 | 138 | ele.Close = float64(price_open_diff+price_close_diff) / 1000.0 139 | ele.High = float64(price_open_diff+price_high_diff) / 1000.0 140 | ele.Low = float64(price_open_diff+price_low_diff) / 1000.0 141 | 142 | pre_diff_base = price_open_diff + price_close_diff 143 | 144 | obj.reply.List = append(obj.reply.List, ele) 145 | } 146 | return err 147 | } 148 | 149 | func (obj *IndexBarsPackage) Reply() interface{} { 150 | return obj.reply 151 | } 152 | -------------------------------------------------------------------------------- /quotes/resources/guotaijunan.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/0fb82b6c2f8ecff70466504eba29241a9fd00e15/quotes/resources/guotaijunan.cfg -------------------------------------------------------------------------------- /quotes/resources/huatai.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/0fb82b6c2f8ecff70466504eba29241a9fd00e15/quotes/resources/huatai.cfg -------------------------------------------------------------------------------- /quotes/resources/tdx.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/0fb82b6c2f8ecff70466504eba29241a9fd00e15/quotes/resources/tdx.cfg -------------------------------------------------------------------------------- /quotes/resources/zhongxin.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/0fb82b6c2f8ecff70466504eba29241a9fd00e15/quotes/resources/zhongxin.cfg -------------------------------------------------------------------------------- /quotes/stock_company_info_category.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/gotdx/internal" 8 | "gitee.com/quant1x/gotdx/proto" 9 | "gitee.com/quant1x/gox/encoding/binary/struc" 10 | ) 11 | 12 | // CompanyInfoCategoryPackage 企业基本信息 13 | type CompanyInfoCategoryPackage struct { 14 | reqHeader *StdRequestHeader 15 | respHeader *StdResponseHeader 16 | request *CompanyInfoCategoryRequest 17 | reply []CompanyInfoCategory 18 | contentHex string 19 | } 20 | 21 | type CompanyInfoCategoryRequest struct { 22 | Market uint16 // 市场代码 23 | Code [6]byte // 股票代码 24 | Unknown uint32 // 未知数据 25 | } 26 | 27 | // CompanyInfoCategoryReply 响应包结构, 28 | type CompanyInfoCategoryReply struct { 29 | Count uint16 `struc:"uint16,little,sizeof=Data"` // 词条总数 30 | Data []RawCompanyInfoCategory `struc:"[152]byte, little"` // 词条数据 31 | } 32 | 33 | // RawCompanyInfoCategory 响应包结构 34 | type RawCompanyInfoCategory struct { 35 | Name []byte `struc:"[64]byte,little"` // 名称 36 | Filename []byte `struc:"[80]byte,little"` // 文件名 37 | Offset uint32 `struc:"uint32,little"` // 偏移量 38 | Length uint32 `struc:"uint32,little"` // 长度 39 | } 40 | 41 | type CompanyInfoCategory struct { 42 | Name string `struc:"[64]byte,little" dataframe:"name"` // 名称 43 | Filename string `struc:"[80]byte,little" dataframe:"filename"` // 文件名 44 | Offset uint32 `struc:"uint32,little" dataframe:"offset"` // 偏移量 45 | Length uint32 `struc:"uint32,little" dataframe:"length"` // 长度 46 | } 47 | 48 | func NewCompanyInfoCategoryPackage() *CompanyInfoCategoryPackage { 49 | pkg := new(CompanyInfoCategoryPackage) 50 | pkg.reqHeader = new(StdRequestHeader) 51 | pkg.respHeader = new(StdResponseHeader) 52 | //pkg.request = new(CompanyInfoCategoryRequest) 53 | //pkg.reply = new(CompanyInfoCategory) 54 | 55 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 56 | //0c 57 | pkg.reqHeader.ZipFlag = proto.FlagNotZipped 58 | //1f 18 76 00 59 | pkg.reqHeader.SeqID = internal.SequenceId() 60 | //01 61 | pkg.reqHeader.PacketType = 0x01 62 | //0b 00 63 | //PkgLen1 uint16 64 | pkg.reqHeader.PkgLen1 = 0x000e 65 | //0b 00 66 | //PkgLen2 uint16 67 | pkg.reqHeader.PkgLen2 = 0x000e 68 | //10 00 69 | pkg.reqHeader.Method = proto.STD_MSG_COMPANY_CATEGORY 70 | //pkg.contentHex = "0100" // 未解 71 | return pkg 72 | } 73 | 74 | func (obj *CompanyInfoCategoryPackage) SetParams(req *CompanyInfoCategoryRequest) { 75 | obj.request = req 76 | } 77 | 78 | func (obj *CompanyInfoCategoryPackage) Serialize() ([]byte, error) { 79 | buf := new(bytes.Buffer) 80 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 81 | b, err := hex.DecodeString(obj.contentHex) 82 | buf.Write(b) 83 | err = binary.Write(buf, binary.LittleEndian, obj.request) 84 | return buf.Bytes(), err 85 | } 86 | 87 | func (obj *CompanyInfoCategoryPackage) UnSerialize(header interface{}, data []byte) error { 88 | obj.respHeader = header.(*StdResponseHeader) 89 | 90 | var reply CompanyInfoCategoryReply 91 | buf := bytes.NewBuffer(data) 92 | err := struc.Unpack(buf, &reply) 93 | if err != nil { 94 | return err 95 | } 96 | //category := make(map[string]CompanyInfoCategory) 97 | list := []CompanyInfoCategory{} 98 | for _, v := range reply.Data { 99 | info := CompanyInfoCategory{ 100 | Name: internal.Utf8ToGbk(v.Name[:]), 101 | Filename: internal.Utf8ToGbk(v.Filename[:]), 102 | Offset: v.Offset, 103 | Length: v.Length, 104 | } 105 | //category[info.Name] = info 106 | list = append(list, info) 107 | } 108 | obj.reply = list 109 | return nil 110 | } 111 | 112 | func (obj *CompanyInfoCategoryPackage) Reply() interface{} { 113 | return obj.reply 114 | } 115 | -------------------------------------------------------------------------------- /quotes/stock_company_info_category_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/gox/api" 7 | "testing" 8 | ) 9 | 10 | func TestCompanyInfoCategoryPackage(t *testing.T) { 11 | stdApi, err := NewStdApi() 12 | if err != nil { 13 | panic(err) 14 | } 15 | defer stdApi.Close() 16 | reply, err := stdApi.GetCompanyInfoCategory("sh600977") 17 | if err != nil { 18 | fmt.Printf("%+v\n", err) 19 | } 20 | fmt.Printf("%+v\n", reply) 21 | data, _ := json.Marshal(reply) 22 | text := api.Bytes2String(data) 23 | fmt.Println(text) 24 | } 25 | -------------------------------------------------------------------------------- /quotes/stock_company_info_content.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/exchange" 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | "gitee.com/quant1x/gox/encoding/binary/struc" 11 | "gitee.com/quant1x/gox/util/linkedhashmap" 12 | "strings" 13 | ) 14 | 15 | // CompanyInfoContentPackage 企业基本信息 16 | type CompanyInfoContentPackage struct { 17 | reqHeader *StdRequestHeader 18 | respHeader *StdResponseHeader 19 | request *CompanyInfoContentRequest 20 | reply *CompanyInfoContent 21 | contentHex string 22 | } 23 | 24 | type CompanyInfoContentRequest struct { 25 | Market uint16 // 市场代码 26 | Code [6]byte // 股票代码 27 | Unknown1 uint16 // 未知数据 28 | Filename [80]byte // 文件名 29 | Offset uint32 // 偏移量 30 | Length uint32 // 长度 31 | Unknown2 uint32 // 未知数据 32 | } 33 | 34 | // CompanyInfoContentReply 响应包结构, 35 | type CompanyInfoContentReply struct { 36 | Market uint16 `struc:"uint16,little"` // 市场代码 37 | Code string `struc:"[6]byte,little"` // 股票代码 38 | Unknown1 []byte `struc:"[2]byte,little"` // 未知 39 | Length uint16 `struc:"uint16,little"` // 词条总数 40 | Data []byte `struc:"sizefrom=Length"` 41 | } 42 | 43 | type CompanyInfoContent struct { 44 | Market exchange.MarketType `dataframe:"market"` // 市场代码 45 | Code string `dataframe:"code"` // 短码 46 | Name string `dataframe:"name"` // 名称 47 | Length uint32 `dataframe:"length"` // 长度 48 | Content string `dataframe:"content"` // 内容 49 | } 50 | 51 | func (this *CompanyInfoContent) Map(unit string) *linkedhashmap.Map { 52 | mapInfo := linkedhashmap.New() 53 | c := strings.ReplaceAll(this.Content, "-\\u003e", "->") 54 | //arr := strings.Split(c, "\\r\\n\\r\\n") 55 | arr := strings.Split(c, "\r\n\r\n") 56 | for i, block := range arr { 57 | block = strings.TrimSpace(block) 58 | //v = strings.ReplaceAll(v, " ", "") 59 | //v = strings.ReplaceAll(v, "│\\r\\n││", "") 60 | //v = strings.Trim(v, "│") 61 | //fmt.Println(i, block) 62 | if i > 0 && strings.Index(block, unit) >= 0 { 63 | arr := strings.Split(block, "\r\n") 64 | block = "" 65 | for _, v := range arr { 66 | if strings.Index(v, unit) >= 0 { 67 | continue 68 | } 69 | if strings.Index(v, "┌") >= 0 && strings.Index(v, "┐") >= 0 { 70 | continue 71 | } 72 | if strings.Index(v, "└") >= 0 && strings.Index(v, "┘") >= 0 { 73 | continue 74 | } 75 | if strings.Index(v, "├") >= 0 && strings.Index(v, "┤") >= 0 { 76 | continue 77 | } 78 | v = strings.TrimLeft(v, "│") 79 | v = strings.TrimRight(v, "│") 80 | v = strings.TrimSpace(v) 81 | v = strings.ReplaceAll(v, "│", "|") 82 | //v = strings.TrimLeft(v, "|") 83 | if v[0] == '|' { 84 | block += v[1:] 85 | } else { 86 | block += "|" + v 87 | } 88 | } 89 | list := strings.Split(block[1:], "|") 90 | 91 | for k := 0; k < len(list); k += 2 { 92 | key := strings.TrimSpace(list[k]) 93 | value := strings.TrimSpace(list[k+1]) 94 | mapInfo.Put(key, value) 95 | } 96 | //mapInfo.Each(func(key interface{}, value interface{}) { 97 | // fmt.Println(key, value) 98 | //}) 99 | 100 | break 101 | } 102 | } 103 | return mapInfo 104 | } 105 | 106 | func NewCompanyInfoContentPackage() *CompanyInfoContentPackage { 107 | pkg := new(CompanyInfoContentPackage) 108 | pkg.reqHeader = new(StdRequestHeader) 109 | pkg.respHeader = new(StdResponseHeader) 110 | //pkg.request = new(CompanyInfoContentRequest) 111 | //pkg.reply = new(CompanyInfoContent) 112 | 113 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 114 | //0c 115 | pkg.reqHeader.ZipFlag = proto.FlagNotZipped 116 | //1f 18 76 00 117 | pkg.reqHeader.SeqID = internal.SequenceId() 118 | //01 119 | pkg.reqHeader.PacketType = 0x01 120 | //0b 00 121 | //PkgLen1 uint16 122 | pkg.reqHeader.PkgLen1 = 0x0068 123 | //0b 00 124 | //PkgLen2 uint16 125 | pkg.reqHeader.PkgLen2 = 0x0068 126 | //10 00 127 | pkg.reqHeader.Method = proto.STD_MSG_COMPANY_CONTENT 128 | //pkg.contentHex = "0100" // 未解 129 | return pkg 130 | } 131 | 132 | func (obj *CompanyInfoContentPackage) SetParams(req *CompanyInfoContentRequest) { 133 | obj.request = req 134 | } 135 | 136 | func (obj *CompanyInfoContentPackage) Serialize() ([]byte, error) { 137 | buf := new(bytes.Buffer) 138 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 139 | b, err := hex.DecodeString(obj.contentHex) 140 | buf.Write(b) 141 | err = binary.Write(buf, binary.LittleEndian, obj.request) 142 | return buf.Bytes(), err 143 | } 144 | 145 | func (obj *CompanyInfoContentPackage) UnSerialize(header interface{}, data []byte) error { 146 | obj.respHeader = header.(*StdResponseHeader) 147 | 148 | var reply CompanyInfoContentReply 149 | buf := bytes.NewBuffer(data) 150 | err := struc.Unpack(buf, &reply) 151 | if err != nil { 152 | return err 153 | } 154 | response := CompanyInfoContent{ 155 | Market: exchange.MarketType(reply.Market), 156 | Code: reply.Code, 157 | Length: uint32(reply.Length), 158 | Content: internal.Utf8ToGbk(reply.Data), 159 | } 160 | obj.reply = &response 161 | return nil 162 | } 163 | 164 | func (obj *CompanyInfoContentPackage) Reply() interface{} { 165 | return obj.reply 166 | } 167 | -------------------------------------------------------------------------------- /quotes/stock_finance_info_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/gox/api" 7 | "testing" 8 | ) 9 | 10 | func TestNewFinanceInfoPackage(t *testing.T) { 11 | stdApi, err := NewStdApi() 12 | if err != nil { 13 | panic(err) 14 | } 15 | defer stdApi.Close() 16 | sq1, err := stdApi.GetFinanceInfo("sh600115") 17 | if err != nil { 18 | fmt.Printf("%+v\n", err) 19 | } 20 | fmt.Printf("%+v\n", sq1) 21 | data, _ := json.Marshal(sq1) 22 | text := api.Bytes2String(data) 23 | fmt.Println(text) 24 | } 25 | -------------------------------------------------------------------------------- /quotes/stock_minute_time_data.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | // todo API未有效解析 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "gitee.com/quant1x/exchange" 10 | "gitee.com/quant1x/gotdx/internal" 11 | "gitee.com/quant1x/gotdx/proto" 12 | "gitee.com/quant1x/gox/api" 13 | ) 14 | 15 | type MinuteTimePackage struct { 16 | reqHeader *StdRequestHeader 17 | respHeader *StdResponseHeader 18 | request *MinuteTimeRequest 19 | reply *MinuteTimeReply 20 | 21 | contentHex string 22 | } 23 | 24 | type MinuteTimeRequest struct { 25 | Market uint16 26 | Code [6]byte 27 | Date uint32 28 | } 29 | 30 | type MinuteTimeReply struct { 31 | Count uint16 32 | List []MinuteTime 33 | } 34 | 35 | type MinuteTime struct { 36 | Price float32 37 | Vol int 38 | } 39 | 40 | func NewMinuteTimePackage() *MinuteTimePackage { 41 | obj := new(MinuteTimePackage) 42 | obj.reqHeader = new(StdRequestHeader) 43 | obj.respHeader = new(StdResponseHeader) 44 | obj.request = new(MinuteTimeRequest) 45 | obj.reply = new(MinuteTimeReply) 46 | 47 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 48 | obj.reqHeader.SeqID = internal.SequenceId() 49 | obj.reqHeader.PacketType = 0x00 50 | //obj.reqHeader.PkgLen1 = 51 | //obj.reqHeader.PkgLen2 = 52 | //obj.reqHeader.Method = 0x051d 53 | obj.reqHeader.Method = proto.STD_MSG_MINUTETIME_DATA 54 | obj.contentHex = "" 55 | return obj 56 | } 57 | func (obj *MinuteTimePackage) SetParams(req *MinuteTimeRequest) { 58 | obj.request = req 59 | } 60 | 61 | func (obj *MinuteTimePackage) Serialize() ([]byte, error) { 62 | obj.reqHeader.PkgLen1 = 0x0e 63 | obj.reqHeader.PkgLen2 = 0x0e 64 | 65 | buf := new(bytes.Buffer) 66 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 67 | err = binary.Write(buf, binary.LittleEndian, obj.request) 68 | b, err := hex.DecodeString(obj.contentHex) 69 | buf.Write(b) 70 | 71 | //b, err := hex.DecodeString(obj.contentHex) 72 | //buf.Write(b) 73 | 74 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 75 | 76 | return buf.Bytes(), err 77 | } 78 | 79 | func (obj *MinuteTimePackage) UnSerialize(header interface{}, data []byte) error { 80 | obj.respHeader = header.(*StdResponseHeader) 81 | 82 | market := exchange.MarketType(obj.request.Market) 83 | code := api.Bytes2String(obj.request.Code[:]) 84 | 85 | pos := 0 86 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 87 | pos += 2 88 | // 跳过4个字节 89 | pos += 6 90 | 91 | pos += 3 92 | 93 | baseUnit := internal.BaseUnit(market, code) 94 | lastPrice := 0 95 | //TODO: ETF的数据不对需要进一步处理 96 | for index := uint16(0); index < obj.reply.Count; index++ { 97 | rawPrice := internal.DecodeVarint(data, &pos) 98 | reversed1 := internal.DecodeVarint(data, &pos) 99 | _ = reversed1 100 | vol := internal.DecodeVarint(data, &pos) 101 | lastPrice += rawPrice 102 | 103 | p := float32(lastPrice) / float32(baseUnit) 104 | 105 | ele := MinuteTime{p, vol} 106 | obj.reply.List = append(obj.reply.List, ele) 107 | } 108 | return err 109 | } 110 | 111 | func (obj *MinuteTimePackage) Reply() interface{} { 112 | return obj.reply 113 | } 114 | -------------------------------------------------------------------------------- /quotes/stock_minute_time_data_history.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/exchange" 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | "gitee.com/quant1x/gox/api" 11 | ) 12 | 13 | type HistoryMinuteTimePackage struct { 14 | reqHeader *StdRequestHeader 15 | respHeader *StdResponseHeader 16 | request *HistoryMinuteTimeRequest 17 | reply *MinuteTimeReply 18 | 19 | contentHex string 20 | } 21 | 22 | type HistoryMinuteTimeRequest struct { 23 | Date uint32 24 | Market uint8 25 | Code [6]byte 26 | } 27 | 28 | //type HistoryMinuteTimeReply struct { 29 | // Count uint16 30 | // List []HistoryMinuteTime 31 | //} 32 | // 33 | //type HistoryMinuteTime struct { 34 | // Price float32 35 | // Vol int 36 | //} 37 | 38 | func NewHistoryMinuteTimePackage() *HistoryMinuteTimePackage { 39 | obj := new(HistoryMinuteTimePackage) 40 | obj.reqHeader = new(StdRequestHeader) 41 | obj.respHeader = new(StdResponseHeader) 42 | obj.request = new(HistoryMinuteTimeRequest) 43 | obj.reply = new(MinuteTimeReply) 44 | 45 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 46 | obj.reqHeader.SeqID = internal.SequenceId() 47 | obj.reqHeader.PacketType = 0x00 48 | //obj.reqHeader.PkgLen1 = 49 | //obj.reqHeader.PkgLen2 = 50 | obj.reqHeader.Method = proto.STD_MSG_HISTORY_MINUTETIME_DATA 51 | obj.contentHex = "" 52 | return obj 53 | } 54 | 55 | // SetParams 设置参数 56 | func (obj *HistoryMinuteTimePackage) SetParams(req *HistoryMinuteTimeRequest) { 57 | obj.request = req 58 | } 59 | 60 | func (obj *HistoryMinuteTimePackage) Serialize() ([]byte, error) { 61 | obj.reqHeader.PkgLen1 = 0x0d 62 | obj.reqHeader.PkgLen2 = 0x0d 63 | 64 | buf := new(bytes.Buffer) 65 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 66 | err = binary.Write(buf, binary.LittleEndian, obj.request) 67 | b, err := hex.DecodeString(obj.contentHex) 68 | buf.Write(b) 69 | 70 | //b, err := hex.DecodeString(obj.contentHex) 71 | //buf.Write(b) 72 | 73 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 74 | 75 | return buf.Bytes(), err 76 | } 77 | 78 | func (obj *HistoryMinuteTimePackage) UnSerialize(header interface{}, data []byte) error { 79 | obj.respHeader = header.(*StdResponseHeader) 80 | 81 | market := exchange.MarketType(obj.request.Market) 82 | code := api.Bytes2String(obj.request.Code[:]) 83 | dataLen := len(data) 84 | if dataLen < 2 { 85 | return nil 86 | } 87 | 88 | pos := 0 89 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 90 | pos += 2 91 | if obj.reply.Count == 0 { 92 | return nil 93 | } 94 | // 跳过4个字节 功能未解析 95 | if dataLen < 6 { 96 | return nil 97 | } 98 | _, _, _, bType := data[pos], data[pos+1], data[pos+2], data[pos+3] 99 | pos += 4 100 | baseUnit := internal.BaseUnit(market, code) 101 | //var baseUnit float32 102 | //if bType > 0x40 { 103 | // baseUnit = 100.0 104 | //} else { 105 | // baseUnit = 1000.0 106 | //} 107 | lastPrice := 0 108 | for index := uint16(0); index < obj.reply.Count; index++ { 109 | rawPrice := internal.DecodeVarint(data, &pos) 110 | reversed1 := internal.DecodeVarint(data, &pos) 111 | _ = reversed1 112 | vol := internal.DecodeVarint(data, &pos) 113 | lastPrice += rawPrice 114 | 115 | p := float32(lastPrice) / float32(baseUnit) 116 | ele := MinuteTime{Price: p, Vol: vol} 117 | obj.reply.List = append(obj.reply.List, ele) 118 | } 119 | _ = bType 120 | return err 121 | } 122 | 123 | func (obj *HistoryMinuteTimePackage) Reply() interface{} { 124 | return obj.reply 125 | } 126 | -------------------------------------------------------------------------------- /quotes/stock_minute_time_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/gox/api" 7 | "testing" 8 | ) 9 | 10 | func TestStockMinuteTime(t *testing.T) { 11 | stdApi, err := NewStdApi() 12 | if err != nil { 13 | panic(err) 14 | } 15 | defer stdApi.Close() 16 | code := "sh600105" 17 | reply, err := stdApi.GetMinuteTimeData(code) 18 | if err != nil { 19 | fmt.Printf("%+v\n", err) 20 | } 21 | fmt.Printf("%+v\n", reply) 22 | data, _ := json.Marshal(reply) 23 | text := api.Bytes2String(data) 24 | fmt.Println(text) 25 | } 26 | 27 | func TestStockMinuteTimeHistory(t *testing.T) { 28 | stdApi, err := NewStdApi() 29 | if err != nil { 30 | panic(err) 31 | } 32 | defer stdApi.Close() 33 | code := "sz000666" 34 | code = "sh000001" 35 | code = "sh510050" 36 | code = "sz159915" 37 | code = "sh600178" 38 | var date uint32 = 20240118 39 | reply, err := stdApi.GetHistoryMinuteTimeData(code, date) 40 | if err != nil { 41 | fmt.Printf("%+v\n", err) 42 | } 43 | fmt.Printf("%+v\n", reply) 44 | data, _ := json.Marshal(reply) 45 | text := api.Bytes2String(data) 46 | fmt.Println(text) 47 | } 48 | -------------------------------------------------------------------------------- /quotes/stock_security_bars.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | ) 11 | 12 | const ( 13 | TDX_SECURITY_BARS_MAX = 800 // 单次最大获取800条K线数据 14 | ) 15 | 16 | // SecurityBars K线 17 | type SecurityBarsPackage struct { 18 | reqHeader *StdRequestHeader 19 | respHeader *StdResponseHeader 20 | request *SecurityBarsRequest 21 | response *SecurityBarsReply 22 | 23 | contentHex string 24 | } 25 | 26 | type SecurityBarsRequest struct { 27 | Market uint16 28 | Code [6]byte 29 | Category uint16 // 种类 5分钟 10分钟 30 | I uint16 // 未知 填充 31 | Start uint16 32 | Count uint16 33 | } 34 | 35 | type SecurityBarsReply struct { 36 | Count uint16 37 | List []SecurityBar 38 | } 39 | 40 | // SecurityBar K线数据 41 | type SecurityBar struct { 42 | Open float64 43 | Close float64 44 | High float64 45 | Low float64 46 | Vol float64 47 | Amount float64 48 | Year int 49 | Month int 50 | Day int 51 | Hour int 52 | Minute int 53 | DateTime string 54 | UpCount uint16 // 指数有效, 上涨家数 55 | DownCount uint16 // 指数有效, 下跌家数 56 | } 57 | 58 | func NewSecurityBarsPackage() *SecurityBarsPackage { 59 | obj := new(SecurityBarsPackage) 60 | obj.reqHeader = new(StdRequestHeader) 61 | obj.respHeader = new(StdResponseHeader) 62 | obj.request = new(SecurityBarsRequest) 63 | obj.response = new(SecurityBarsReply) 64 | 65 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 66 | obj.reqHeader.SeqID = internal.SequenceId() 67 | obj.reqHeader.PacketType = 0x00 68 | //obj.reqHeader.PkgLen1 = 69 | //obj.reqHeader.PkgLen2 = 70 | obj.reqHeader.Method = proto.STD_MSG_SECURITY_BARS 71 | obj.contentHex = "00000000000000000000" 72 | return obj 73 | } 74 | 75 | func (obj *SecurityBarsPackage) SetParams(req *SecurityBarsRequest) { 76 | obj.request = req 77 | obj.request.I = 1 78 | } 79 | 80 | func (obj *SecurityBarsPackage) Serialize() ([]byte, error) { 81 | obj.reqHeader.PkgLen1 = 0x1c 82 | obj.reqHeader.PkgLen2 = 0x1c 83 | 84 | buf := new(bytes.Buffer) 85 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 86 | err = binary.Write(buf, binary.LittleEndian, obj.request) 87 | b, err := hex.DecodeString(obj.contentHex) 88 | buf.Write(b) 89 | 90 | //b, err := hex.DecodeString(obj.contentHex) 91 | //buf.Write(b) 92 | 93 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 94 | 95 | return buf.Bytes(), err 96 | } 97 | 98 | func (obj *SecurityBarsPackage) UnSerialize(header interface{}, data []byte) error { 99 | obj.respHeader = header.(*StdResponseHeader) 100 | 101 | pos := 0 102 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.response.Count) 103 | pos += 2 104 | 105 | pre_diff_base := 0 106 | 107 | for index := uint16(0); index < obj.response.Count; index++ { 108 | ele := SecurityBar{} 109 | ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute = internal.GetDatetime(int(obj.request.Category), data, &pos) 110 | 111 | ele.DateTime = fmt.Sprintf("%d-%02d-%02d %02d:%02d:00", ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute) 112 | 113 | price_open_diff := internal.DecodeVarint(data, &pos) 114 | price_close_diff := internal.DecodeVarint(data, &pos) 115 | 116 | price_high_diff := internal.DecodeVarint(data, &pos) 117 | price_low_diff := internal.DecodeVarint(data, &pos) 118 | 119 | var ivol uint32 120 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &ivol) 121 | ele.Vol = internal.IntToFloat64(ivol) 122 | pos += 4 123 | 124 | var dbvol uint32 125 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &dbvol) 126 | ele.Amount = internal.IntToFloat64(int(dbvol)) 127 | pos += 4 128 | 129 | ele.Open = float64(price_open_diff+pre_diff_base) / 1000.0 130 | price_open_diff += pre_diff_base 131 | 132 | ele.Close = float64(price_open_diff+price_close_diff) / 1000.0 133 | ele.High = float64(price_open_diff+price_high_diff) / 1000.0 134 | ele.Low = float64(price_open_diff+price_low_diff) / 1000.0 135 | 136 | pre_diff_base = price_open_diff + price_close_diff 137 | 138 | obj.response.List = append(obj.response.List, ele) 139 | } 140 | return err 141 | } 142 | 143 | func (obj *SecurityBarsPackage) Reply() interface{} { 144 | return obj.response 145 | } 146 | -------------------------------------------------------------------------------- /quotes/stock_security_bars_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/gotdx/proto" 7 | "gitee.com/quant1x/gox/api" 8 | "testing" 9 | ) 10 | 11 | func TestSecurityBarsPackage(t *testing.T) { 12 | stdApi, err := NewStdApiWithServers([]Server{{Host: "123.125.108.14", Port: 7709, Name: "test"}}) 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer stdApi.Close() 17 | //sq1, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShenZhen}, []string{"600275", "600455", "600086", "300742"}) 18 | sq1, err := stdApi.GetKLine("sh510050", proto.KLINE_TYPE_DAILY, 0, 5) 19 | if err != nil { 20 | fmt.Printf("%+v\n", err) 21 | } 22 | fmt.Printf("%+v\n", sq1) 23 | data, _ := json.Marshal(sq1) 24 | text := api.Bytes2String(data) 25 | fmt.Println(text) 26 | } 27 | -------------------------------------------------------------------------------- /quotes/stock_security_count.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/gotdx/internal" 8 | "gitee.com/quant1x/gotdx/proto" 9 | ) 10 | 11 | // SecurityCountPackage 市场股票数量 12 | type SecurityCountPackage struct { 13 | reqHeader *StdRequestHeader 14 | respHeader *StdResponseHeader 15 | request *SecurityCountRequest 16 | reply *SecurityCountReply 17 | contentHex string 18 | } 19 | 20 | type SecurityCountRequest struct { 21 | Market uint16 22 | } 23 | 24 | type SecurityCountReply struct { 25 | Count uint16 26 | } 27 | 28 | func NewSecurityCountPackage() *SecurityCountPackage { 29 | obj := new(SecurityCountPackage) 30 | obj.reqHeader = new(StdRequestHeader) 31 | obj.respHeader = new(StdResponseHeader) 32 | obj.request = new(SecurityCountRequest) 33 | obj.reply = new(SecurityCountReply) 34 | 35 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 36 | obj.reqHeader.SeqID = internal.SequenceId() 37 | obj.reqHeader.PacketType = 0x01 38 | obj.reqHeader.Method = proto.STD_MSG_SECURITY_COUNT 39 | obj.contentHex = "75c73301" // 未解 40 | return obj 41 | } 42 | 43 | func (obj *SecurityCountPackage) SetParams(req *SecurityCountRequest) { 44 | obj.request = req 45 | } 46 | 47 | func (obj *SecurityCountPackage) Serialize() ([]byte, error) { 48 | obj.reqHeader.PkgLen1 = 2 + 4 + 2 49 | obj.reqHeader.PkgLen2 = 2 + 4 + 2 50 | 51 | buf := new(bytes.Buffer) 52 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 53 | err = binary.Write(buf, binary.LittleEndian, obj.request) 54 | b, err := hex.DecodeString(obj.contentHex) 55 | buf.Write(b) 56 | return buf.Bytes(), err 57 | } 58 | 59 | func (obj *SecurityCountPackage) UnSerialize(header interface{}, data []byte) error { 60 | obj.respHeader = header.(*StdResponseHeader) 61 | 62 | obj.reply.Count = binary.LittleEndian.Uint16(data[:2]) 63 | return nil 64 | } 65 | 66 | func (obj *SecurityCountPackage) Reply() interface{} { 67 | return obj.reply 68 | } 69 | -------------------------------------------------------------------------------- /quotes/stock_security_list.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "gitee.com/quant1x/gotdx/internal" 7 | "gitee.com/quant1x/gotdx/proto" 8 | ) 9 | 10 | const ( 11 | TDX_SECURITY_LIST_MAX = 1000 // 单次最大获取多少条股票数据 12 | ) 13 | 14 | // SecurityListPackage 股票列表 15 | type SecurityListPackage struct { 16 | reqHeader *StdRequestHeader 17 | respHeader *StdResponseHeader 18 | request *SecurityListRequest 19 | reply *SecurityListReply 20 | 21 | contentHex string 22 | } 23 | 24 | type SecurityListRequest struct { 25 | Market uint16 26 | Start uint16 27 | } 28 | 29 | type SecurityListReply struct { 30 | Count uint16 31 | List []Security 32 | } 33 | 34 | type Security struct { 35 | Code string 36 | VolUnit uint16 37 | Reversed1 [4]byte `dataframe:"-"` 38 | //R1 uint32 39 | //P1 float64 40 | DecimalPoint int8 41 | Name string 42 | PreClose float64 43 | Reversed2 [4]byte `dataframe:"-"` 44 | //R2 uint32 45 | //P2 float64 46 | } 47 | 48 | func NewSecurityListPackage() *SecurityListPackage { 49 | obj := new(SecurityListPackage) 50 | obj.reqHeader = new(StdRequestHeader) 51 | obj.respHeader = new(StdResponseHeader) 52 | obj.request = new(SecurityListRequest) 53 | obj.reply = new(SecurityListReply) 54 | 55 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 56 | obj.reqHeader.SeqID = internal.SequenceId() 57 | obj.reqHeader.PacketType = 0x01 58 | obj.reqHeader.Method = proto.STD_MSG_SECURITY_LIST 59 | return obj 60 | } 61 | 62 | func (obj *SecurityListPackage) SetParams(req *SecurityListRequest) { 63 | obj.request = req 64 | } 65 | 66 | func (obj *SecurityListPackage) Serialize() ([]byte, error) { 67 | obj.reqHeader.PkgLen1 = 2 + 4 68 | obj.reqHeader.PkgLen2 = 2 + 4 69 | 70 | buf := new(bytes.Buffer) 71 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 72 | err = binary.Write(buf, binary.LittleEndian, obj.request) 73 | 74 | //b, err := hex.DecodeString(obj.contentHex) 75 | //buf.Write(b) 76 | 77 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 78 | 79 | return buf.Bytes(), err 80 | } 81 | 82 | func (obj *SecurityListPackage) UnSerialize(header interface{}, data []byte) error { 83 | obj.respHeader = header.(*StdResponseHeader) 84 | 85 | pos := 0 86 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 87 | pos += 2 88 | for index := uint16(0); index < obj.reply.Count; index++ { 89 | ele := Security{} 90 | var code [6]byte 91 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+6]), binary.LittleEndian, &code) 92 | pos += 6 93 | ele.Code = string(code[:]) 94 | 95 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.VolUnit) 96 | pos += 2 97 | 98 | var name [8]byte 99 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+8]), binary.LittleEndian, &name) 100 | ele.Name = internal.Utf8ToGbk(name[:]) 101 | pos += 8 102 | 103 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &ele.Reversed1) 104 | //_ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &ele.R1) 105 | //ele.P1 = getVolume(int(ele.R1)) 106 | pos += 4 107 | 108 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+1]), binary.LittleEndian, &ele.DecimalPoint) 109 | pos += 1 110 | var rawPreClose uint32 111 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &rawPreClose) 112 | ele.PreClose = internal.IntToFloat64(int(rawPreClose)) 113 | pos += 4 114 | 115 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &ele.Reversed2) 116 | //_ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &ele.R2) 117 | //ele.P2 = getVolume(int(ele.R2)) 118 | pos += 4 119 | 120 | obj.reply.List = append(obj.reply.List, ele) 121 | } 122 | return err 123 | } 124 | 125 | func (obj *SecurityListPackage) Reply() interface{} { 126 | return obj.reply 127 | } 128 | -------------------------------------------------------------------------------- /quotes/stock_security_list_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/exchange" 7 | "gitee.com/quant1x/gox/api" 8 | "testing" 9 | ) 10 | 11 | func TestSecurityListPackage(t *testing.T) { 12 | stdApi, err := NewStdApi() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer stdApi.Close() 17 | reply, err := stdApi.GetSecurityList(exchange.MarketIdShangHai, TDX_SECURITY_LIST_MAX*19) 18 | if err != nil { 19 | fmt.Printf("%+v\n", err) 20 | } 21 | fmt.Printf("%+v\n", reply) 22 | fmt.Println("==========") 23 | data, _ := json.Marshal(reply) 24 | text := api.Bytes2String(data) 25 | fmt.Println(text) 26 | } 27 | -------------------------------------------------------------------------------- /quotes/stock_security_quotes_new.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "gitee.com/quant1x/exchange" 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | "gitee.com/quant1x/gox/logger" 11 | ) 12 | 13 | const ( 14 | TDX_SECURITY_QUOTES_MAX_V2 = 80 // 单次最大获取80条实时数据 15 | ) 16 | 17 | // V2SecurityQuotesPackage 盘口五档报价 18 | type V2SecurityQuotesPackage struct { 19 | reqHeader *StdRequestHeader 20 | respHeader *StdResponseHeader 21 | request *V2SecurityQuotesRequest 22 | reply *V2SecurityQuotesReply 23 | 24 | contentHex string 25 | } 26 | 27 | type V2Stock struct { 28 | Market uint8 29 | Code string 30 | } 31 | 32 | type V2SecurityQuotesRequest struct { 33 | Count uint16 34 | StockList []V2Stock 35 | } 36 | 37 | type V2SecurityQuotesReply struct { 38 | Count uint16 39 | List []V2SecurityQuote 40 | } 41 | 42 | type V2SecurityQuote struct { 43 | Market uint8 // 市场 44 | Code string // 代码 45 | Active1 uint16 // 活跃度 46 | Price float64 // 现价 47 | LastClose float64 // 昨收 48 | Open float64 // 开盘 49 | High float64 // 最高 50 | Low float64 // 最低 51 | ServerTime string // 时间 52 | ReversedBytes0 int // 保留(时间 ServerTime) 53 | ReversedBytes1 int // 保留 54 | Vol int // 总量 55 | CurVol int // 现量 56 | Amount float64 // 总金额 57 | SVol int // 内盘 58 | BVol int // 外盘 59 | ReversedBytes2 int // 保留 60 | ReversedBytes3 int // 保留 61 | Bid1 float64 62 | Ask1 float64 63 | BidVol1 int 64 | AskVol1 int 65 | //Bid2 float64 66 | //Ask2 float64 67 | //BidVol2 int 68 | //AskVol2 int 69 | //Bid3 float64 70 | //Ask3 float64 71 | //BidVol3 int 72 | //AskVol3 int 73 | //Bid4 float64 74 | //Ask4 float64 75 | //BidVol4 int 76 | //AskVol4 int 77 | //Bid5 float64 78 | //Ask5 float64 79 | //BidVol5 int 80 | //AskVol5 int 81 | ReversedBytes4 uint16 // 保留 82 | ReversedBytes5 int // 保留 83 | ReversedBytes6 int // 保留 84 | ReversedBytes7 int // 保留 85 | ReversedBytes8 int // 保留 86 | Rate float64 // 涨速 87 | Active2 uint16 // 活跃度 88 | } 89 | 90 | type V2Level struct { 91 | Price float64 92 | Vol int 93 | } 94 | 95 | func NewV2SecurityQuotesPackage() *V2SecurityQuotesPackage { 96 | obj := new(V2SecurityQuotesPackage) 97 | obj.reqHeader = new(StdRequestHeader) 98 | obj.respHeader = new(StdResponseHeader) 99 | obj.request = new(V2SecurityQuotesRequest) 100 | obj.reply = new(V2SecurityQuotesReply) 101 | 102 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 103 | obj.reqHeader.SeqID = internal.SequenceId() 104 | obj.reqHeader.PacketType = 0x01 105 | obj.reqHeader.Method = proto.STD_MSG_SECURITY_QUOTES_new 106 | obj.contentHex = "0500000000000000" // 1.3.5以前的版本 107 | return obj 108 | } 109 | 110 | func (obj *V2SecurityQuotesPackage) SetParams(req *V2SecurityQuotesRequest) { 111 | req.Count = uint16(len(req.StockList)) 112 | obj.request = req 113 | } 114 | 115 | func (obj *V2SecurityQuotesPackage) Serialize() ([]byte, error) { 116 | obj.reqHeader.PkgLen1 = 2 + uint16(len(obj.request.StockList)*7) + 10 117 | obj.reqHeader.PkgLen2 = 2 + uint16(len(obj.request.StockList)*7) + 10 118 | 119 | buf := new(bytes.Buffer) 120 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 121 | b, err := hex.DecodeString(obj.contentHex) 122 | buf.Write(b) 123 | 124 | err = binary.Write(buf, binary.LittleEndian, obj.request.Count) 125 | for _, stock := range obj.request.StockList { 126 | code := make([]byte, 6) 127 | copy(code, stock.Code) 128 | tmp := []byte{stock.Market} 129 | tmp = append(tmp, code...) 130 | buf.Write(tmp) 131 | } 132 | return buf.Bytes(), err 133 | } 134 | 135 | func (obj *V2SecurityQuotesPackage) UnSerialize(header interface{}, data []byte) error { 136 | obj.respHeader = header.(*StdResponseHeader) 137 | 138 | if logger.IsDebug() { 139 | logger.Debugf(hex.EncodeToString(data)) 140 | } 141 | pos := 0 142 | 143 | pos += 2 // 跳过两个字节 144 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 145 | pos += 2 146 | for index := uint16(0); index < obj.reply.Count; index++ { 147 | ele := V2SecurityQuote{} 148 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+1]), binary.LittleEndian, &ele.Market) 149 | pos += 1 150 | var code [6]byte 151 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+6]), binary.LittleEndian, &code) 152 | //enc := mahonia.NewDecoder("gbk") 153 | //ele.Code = enc.ConvertString(string(code[:])) 154 | ele.Code = internal.Utf8ToGbk(code[:]) 155 | pos += 6 156 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.Active1) 157 | pos += 2 158 | 159 | price := internal.DecodeVarint(data, &pos) 160 | ele.Price = obj.getPrice(price, 0) 161 | ele.LastClose = obj.getPrice(price, internal.DecodeVarint(data, &pos)) 162 | ele.Open = obj.getPrice(price, internal.DecodeVarint(data, &pos)) 163 | ele.High = obj.getPrice(price, internal.DecodeVarint(data, &pos)) 164 | ele.Low = obj.getPrice(price, internal.DecodeVarint(data, &pos)) 165 | 166 | ele.ReversedBytes0 = internal.DecodeVarint(data, &pos) 167 | if ele.ReversedBytes0 > 0 { 168 | //ele.ServerTime = timeFromStr(fmt.Sprintf("%d", ele.ReversedBytes0)) 169 | ele.ServerTime = internal.TimeFromInt(ele.ReversedBytes0) 170 | } else { 171 | ele.ServerTime = "0" 172 | // 如果出现这种情况, 可能是退市或者其实交易状态异常的数据, 摘牌的情况下, 证券代码是错的 173 | ele.Code = exchange.StockDelisting 174 | } 175 | 176 | ele.ReversedBytes1 = internal.DecodeVarint(data, &pos) 177 | 178 | ele.Vol = internal.DecodeVarint(data, &pos) 179 | ele.CurVol = internal.DecodeVarint(data, &pos) 180 | 181 | var amountraw uint32 182 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &amountraw) 183 | pos += 4 184 | ele.Amount = internal.IntToFloat64(int(amountraw)) 185 | 186 | ele.SVol = internal.DecodeVarint(data, &pos) 187 | ele.BVol = internal.DecodeVarint(data, &pos) 188 | 189 | ele.ReversedBytes2 = internal.DecodeVarint(data, &pos) 190 | ele.ReversedBytes3 = internal.DecodeVarint(data, &pos) 191 | //fmt.Printf("pos: %d\n", pos) 192 | //fmt.Println(hex.EncodeToString(data[:pos])) 193 | 194 | var bidLevels []V2Level 195 | var askLevels []V2Level 196 | //baNum := 5 197 | baNum := 1 198 | for i := 0; i < baNum; i++ { 199 | bidele := V2Level{Price: obj.getPrice(internal.DecodeVarint(data, &pos), price)} 200 | offerele := V2Level{Price: obj.getPrice(internal.DecodeVarint(data, &pos), price)} 201 | bidele.Vol = internal.DecodeVarint(data, &pos) 202 | offerele.Vol = internal.DecodeVarint(data, &pos) 203 | bidLevels = append(bidLevels, bidele) 204 | askLevels = append(askLevels, offerele) 205 | } 206 | ele.Bid1 = bidLevels[0].Price 207 | //ele.Bid2 = bidLevels[1].Price 208 | //ele.Bid3 = bidLevels[2].Price 209 | //ele.Bid4 = bidLevels[3].Price 210 | //ele.Bid5 = bidLevels[4].Price 211 | ele.Ask1 = askLevels[0].Price 212 | //ele.Ask2 = askLevels[1].Price 213 | //ele.Ask3 = askLevels[2].Price 214 | //ele.Ask4 = askLevels[3].Price 215 | //ele.Ask5 = askLevels[4].Price 216 | 217 | ele.BidVol1 = bidLevels[0].Vol 218 | //ele.BidVol2 = bidLevels[1].Vol 219 | //ele.BidVol3 = bidLevels[2].Vol 220 | //ele.BidVol4 = bidLevels[3].Vol 221 | //ele.BidVol5 = bidLevels[4].Vol 222 | 223 | ele.AskVol1 = askLevels[0].Vol 224 | //ele.AskVol2 = askLevels[1].Vol 225 | //ele.AskVol3 = askLevels[2].Vol 226 | //ele.AskVol4 = askLevels[3].Vol 227 | //ele.AskVol5 = askLevels[4].Vol 228 | //fmt.Printf("pos: %d\n", pos) 229 | //fmt.Println(hex.EncodeToString(data[:pos])) 230 | 231 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.ReversedBytes4) 232 | pos += 2 233 | //ele.ReversedBytes5 = getPrice(data, &pos) 234 | //ele.ReversedBytes6 = getPrice(data, &pos) 235 | //ele.ReversedBytes7 = getPrice(data, &pos) 236 | //ele.ReversedBytes8 = getPrice(data, &pos) 237 | 238 | var reversedbytes9 int16 239 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &reversedbytes9) 240 | pos += 2 241 | ele.Rate = float64(reversedbytes9) / 100.0 242 | 243 | // 保留 2个字节 244 | //_r1 := data[pos : pos+2] 245 | //_pos := 0 246 | //_price1 := getPrice(_r1, &_pos) 247 | pos += 2 248 | //pos += _pos 249 | 250 | // 保留 12x4字节 251 | _lenth := 12 * 4 252 | _r2 := data[pos : pos+_lenth] 253 | _pos2 := 0 254 | for { 255 | _p2 := obj.getPrice(internal.DecodeVarint(_r2, &_pos2), price) 256 | //_p2 := getPrice(_r2, &_pos2) 257 | if logger.IsDebug() { 258 | logger.Debug(_p2) 259 | } 260 | if _pos2 >= _lenth { 261 | break 262 | } 263 | } 264 | //_ = _price 265 | pos += _lenth 266 | 267 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.Active2) 268 | pos += 2 269 | 270 | obj.reply.List = append(obj.reply.List, ele) 271 | } 272 | return nil 273 | } 274 | 275 | func (obj *V2SecurityQuotesPackage) Reply() interface{} { 276 | return obj.reply 277 | } 278 | 279 | func (obj *V2SecurityQuotesPackage) getPrice(price int, diff int) float64 { 280 | return float64(price+diff) / 100.0 281 | } 282 | -------------------------------------------------------------------------------- /quotes/stock_security_quotes_new_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestV2SecurityQuotesPackage_UnSerialize(t *testing.T) { 10 | hexString := "b1cb74001c3b2f8800004c051105d107789c75957d4c137718c79fdfddf5ee7a6da1050aa29597a9d36cce1d2db4b02d2d5704c4524944d0a9c16c0b71cecd652ff125cb16c69497511c600b5840878522182ca0e86801d14dc117d4b86936b904984ee79c6e6ed1c539c3aef80ff7c7fdfeff3d9f7c9ee7fb7b7e0011002ccb1af529835480cab06269fe669e7942b9bc8bd4aef762ed4dd5ea3ee72ccefd3902545e8cca9fab54de9bc2a068f15016cc387addefe61fc25a4e82c419a4048a5ecfa6e413adea4c8ec9aadacb33bfa82b7ec54d05f2a736d7083ab61f833639071a1f5212e140c150e297cb6696d89b32c762bd775612914f0411294909d730bf3a3f479e13a8e7997fd5bda7b0105d6187cdb7ade43c82432a40b39d48093a888689dceb2289e41db254d9cb999c14e11a36dd2a43f20d6c3f63c5c88c0e37cf8c33ee49fc95bace265b4f2b763080609f82034329a1c400645014d08b241efdbcc8b279af4b52e24610a14fd227dfa29bc36d79d16b9cb53c3319defc94f3609737c4afa81889abeb8d075f0f09a89ae8a5ed918fa734006b8a96cfacb2755b93f97e53ac558a728b0e5212f58602aa5fb1b19029ec6be299ff14dee1387ad5f7667bd569a6ae38129c1f0b23475e952ac23ba505d8a7153126d61db08ccfad933429a09091650d498957c9800296a956150773a568dc19f962f42798dd7354d977490ece1121574708373d4a8d4e8540d1f8b86822cb7df32c556baa252157c96908ab3f891ae9a51c6e1d708c313cbd731821f3922bcb4b0b07df003705e83072a1cac86777a8f49915521765bcf6e89cfe9414e1249a1e8889adc3eb298411694d0778e647aa9d57a81aff599cfd8d93ee1ec6913b9483da591df917901648803287c8c2467b2cfafba5921675f874725936047744903a5dd64015cf5c88f0f810d6b0b0c0d6feb63f03952de0a007d508c9550302584c67ceaca07dd86ee6cedc944c6ec83421c998c8135574decac8bcbe469eb94c7b476675a95aeacf669fb849b7dd08852fc23918c43aa21df8fd291c8aca178a34e22d6bcd0f9ff4486af0c107c89a0cc66ed217b5ce4eae2ca9e1993fa2765fa435d7df3f9f5dd346f47b09f07e294c5ceb56286136d090aa528852b57a81ce72cbe9911c4737399ddc64c308f6359d8ee14b5d42aa26687f00a78e1cde6c6bbb83f65c417010717002edc695c20d0c86c6e2451e85c7f7584ae6570f493146b060a84cacc14f1f8ab1a531ab6b76f3ccdd987df5f35ce87533b9c2f39ba6bf5303bd3b69409dd8f125768d2cf8d273626c33ab90ebdfb4c4b98a254dfc74909268326da77651e91c2c6b6ce399d394ab42d749bc3af774f6d19f94cddda1b0ab567069d71e4a75a048215aa99b9e17f5ebddb756598cf5d2fb647b70efb2c986840d74a9f1c334d5d6c37f8f31c346e777aaf8fe40afbdac94ac1ca0c0fb44d859c588568629810088558918b9ef5c33b736e44a866b030d06019294f2801e8c3d87b6c8b61c2fe71968bcd8833cb2973e65733abd595d6d9bacbde75f005421938f8684821c8a720744fd9abcd465193b7257d2e441b05f0683d1741baf8c92a974e9f50e9e198d2a398385d714dfb3d50ea1ae5d08396200a110a5f07d087fd45f0745bbb76fc7b7e6f4890f24b7e2ed678f24214143bac3ac8432b3a18e67ae8735b490ea3cefc6ec7e3fbeff0e062d9c15e447c90bc4e3291ae2d61f13452bd33537352ae13349090d196c95d16430e1adb319ad2eadea2ba153be1e2c7c8f09b21d2790e723e85f0bf89cf92ae191d3007f968b147c936596be4287a48209ff1fbd25ad8b" 11 | hexString = "b1cb74000c15000000004c05690069000000010000303032343233000fbf0b6a68096882fba60eff0b9dc73884710b36a74d9ebd14808a240194744100900d9d1816140d001100805fa94a0000000000000000000000005ba59c3fd71b0fc2000000000000000000000000000000000000000000000000000f" 12 | data, err := hex.DecodeString(hexString) 13 | if err != nil { 14 | fmt.Println(err) 15 | } 16 | 17 | header, respBody, err := parseResponseHeader(data) 18 | if err != nil { 19 | fmt.Println(err) 20 | } 21 | fmt.Printf("%+v\n", header) 22 | quotes := NewV2SecurityQuotesPackage() 23 | err = quotes.UnSerialize(header, respBody) 24 | if err != nil { 25 | fmt.Println(err) 26 | } 27 | fmt.Printf("%+v\n", quotes) 28 | } 29 | -------------------------------------------------------------------------------- /quotes/stock_security_quotes_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/exchange" 7 | "gitee.com/quant1x/gox/api" 8 | "testing" 9 | ) 10 | 11 | func TestSecurityQuotesPackage(t *testing.T) { 12 | stdApi, err := NewStdApi() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer stdApi.Close() 17 | //sq1, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShenZhen}, []string{"600275", "600455", "600086", "300742"}) 18 | sq1, err := stdApi.GetSecurityQuotes( 19 | []uint8{exchange.MarketIdShangHai, exchange.MarketIdShangHai, exchange.MarketIdShangHai, exchange.MarketIdShangHai, exchange.MarketIdShenZhen}, 20 | []string{"000001", "000002", "880005", "880656", "399107"}) 21 | if err != nil { 22 | fmt.Printf("%+v\n", err) 23 | } 24 | fmt.Printf("%+v\n", sq1) 25 | data, _ := json.Marshal(sq1) 26 | text := api.Bytes2String(data) 27 | fmt.Println(text) 28 | } 29 | -------------------------------------------------------------------------------- /quotes/stock_security_snapshot.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import "gitee.com/quant1x/exchange" 4 | 5 | type ExchangeState int8 6 | 7 | const ( 8 | TDX_EXCHANGE_STATE_DELISTING ExchangeState = iota - 1 // 终止上市 9 | TDX_EXCHANGE_STATE_CLOSING // 收盘 10 | TDX_EXCHANGE_STATE_NORMAL // 正常交易 11 | TDX_EXCHANGE_STATE_PAUSE // 暂停交易 12 | ) 13 | 14 | // Snapshot L1 行情快照 15 | type Snapshot struct { 16 | Date string // 交易日期 17 | SecurityCode string // 证券代码 18 | ExchangeState ExchangeState // 交易状态 19 | State TradeState // 上市公司状态 20 | Market uint8 // 市场 21 | Code string // 代码 22 | Active uint16 // 活跃度 23 | Price float64 // 现价 24 | LastClose float64 // 昨收 25 | Open float64 // 开盘 26 | High float64 // 最高 27 | Low float64 // 最低 28 | ServerTime string // 时间 29 | ReversedBytes0 int // 保留(时间 ServerTime) 30 | ReversedBytes1 int // 保留 31 | Vol int // 总量 32 | CurVol int // 个股-现成交量,板块指数-现成交额 33 | Amount float64 // 总金额 34 | SVol int // 个股有效-内盘 35 | BVol int // 个股有效-外盘 36 | IndexOpenAmount int // 指数有效-集合竞价成交金额=开盘成交金额 37 | StockOpenAmount int // 个股有效-集合竞价成交金额=开盘成交金额 38 | OpenVolume int // 集合竞价-开盘量, 单位是股 39 | CloseVolume int // 集合竞价-收盘量, 单位是股 40 | IndexUp int // 指数有效-上涨数 41 | IndexUpLimit int // 指数有效-涨停数 42 | IndexDown int // 指数有效-下跌数 43 | IndexDownLimit int // 指数有效-跌停数 44 | Bid1 float64 // 个股-委买价1 45 | Ask1 float64 // 个股-委卖价1 46 | BidVol1 int // 个股-委买量1 板块-上涨数 47 | AskVol1 int // 个股-委卖量1 板块-下跌数 48 | Bid2 float64 // 个股-委买价2 49 | Ask2 float64 // 个股-委卖价2 50 | BidVol2 int // 个股-委买量2 板块-涨停数 51 | AskVol2 int // 个股-委卖量2 板块-跌停数 52 | Bid3 float64 // 个股-委买价3 53 | Ask3 float64 // 个股-委卖价3 54 | BidVol3 int // 个股-委买量3 55 | AskVol3 int // 个股-委卖量3 56 | Bid4 float64 // 个股-委买价4 57 | Ask4 float64 // 个股-委卖价4 58 | BidVol4 int // 个股-委买量4 59 | AskVol4 int // 个股-委卖量4 60 | Bid5 float64 // 个股-委买价5 61 | Ask5 float64 // 个股-委卖价5 62 | BidVol5 int // 个股-委买量5 63 | AskVol5 int // 个股-委卖量5 64 | ReversedBytes4 uint16 // 保留 65 | ReversedBytes5 int // 保留 66 | ReversedBytes6 int // 保留 67 | ReversedBytes7 int // 保留 68 | ReversedBytes8 int // 保留 69 | Rate float64 // 涨速 70 | Active2 uint16 // 活跃度, 如果是指数则为0, 个股同Active1 71 | TimeStamp string // 本地当前时间戳 72 | } 73 | 74 | // CheckDirection 检测当前交易方向 75 | // 76 | // todo: 只能检测即时行情数据, 对于历史数据无效 77 | func (this *Snapshot) CheckDirection() (biddingDirection, volumeDirection int) { 78 | if this.Price == this.Bid1 { 79 | biddingDirection = -1 80 | } else if this.Price == this.Ask1 { 81 | biddingDirection = 1 82 | } 83 | bidVol := this.BidVol1 + this.BidVol2 + this.BidVol3 + this.BidVol4 + this.BidVol5 84 | askVol := this.AskVol1 + this.AskVol2 + this.AskVol3 + this.AskVol4 + this.AskVol5 85 | volumeDirection = bidVol - askVol 86 | return 87 | } 88 | 89 | // AverageBiddingVolume 平均竞量 90 | func (this *Snapshot) AverageBiddingVolume() int { 91 | bidVol := this.BidVol1 + this.BidVol2 + this.BidVol3 + this.BidVol4 + this.BidVol5 92 | askVol := this.AskVol1 + this.AskVol2 + this.AskVol3 + this.AskVol4 + this.AskVol5 93 | return (bidVol + askVol) / 10 94 | } 95 | 96 | // DetectBiddingPhase 检测竞价阶段 97 | // 如果5档行情 98 | func (this *Snapshot) DetectBiddingPhase() (head, tail bool) { 99 | head = false 100 | tail = false 101 | kind := exchange.AssertCode(this.SecurityCode) 102 | switch kind { 103 | case exchange.STOCK, exchange.ETF: 104 | // 个股竞价阶段, 竞价3-5的数据都是0 105 | bidPrice := int(this.Bid3 + this.Bid4 + this.Bid5) 106 | bidVol := this.BidVol3 + this.BidVol4 + this.BidVol5 107 | if bidPrice+bidVol == 0 { 108 | // 早盘竞价时开盘等于0 109 | if this.Open == 0 { 110 | head = true 111 | } else { 112 | tail = true 113 | } 114 | } 115 | case exchange.INDEX: 116 | // 指数 117 | head = this.Active == 0 118 | tail = this.Active > 0 119 | case exchange.BLOCK: 120 | // 板块 121 | head = this.Active == 0 122 | tail = this.Active > 0 123 | } 124 | 125 | return 126 | } 127 | -------------------------------------------------------------------------------- /quotes/stock_security_snapshot_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/gox/api" 7 | "testing" 8 | ) 9 | 10 | func TestSnapshotPackage(t *testing.T) { 11 | stdApi, err := NewStdApiWithServers([]Server{Server{Host: "123.125.108.14", Port: 7709, Name: "test"}}) 12 | //stdApi, err := NewStdApi() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer stdApi.Close() 17 | //sq1, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShenZhen}, []string{"600275", "600455", "600086", "300742"}) 18 | sq1, err := stdApi.GetSnapshot([]string{"sh000001", "600105", "880656", "880367", "510050", "000666"}) 19 | if err != nil { 20 | fmt.Printf("%+v\n", err) 21 | } 22 | fmt.Printf("%+v\n", sq1) 23 | data, _ := json.Marshal(sq1) 24 | text := api.Bytes2String(data) 25 | fmt.Println(text) 26 | } 27 | 28 | func TestSnapshot_DetectBiddingPhase(t *testing.T) { 29 | cacheList := []Snapshot{} 30 | filename := "600903.csv" 31 | err := api.CsvToSlices(filename, &cacheList) 32 | fmt.Println(err) 33 | } 34 | -------------------------------------------------------------------------------- /quotes/stock_transaction_data.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "gitee.com/quant1x/exchange" 9 | "gitee.com/quant1x/gotdx/internal" 10 | "gitee.com/quant1x/gotdx/proto" 11 | "gitee.com/quant1x/gox/api" 12 | ) 13 | 14 | type TradeType = int32 15 | 16 | const ( 17 | TDX_TICK_BUY TradeType = iota // 买入 18 | TDX_TICK_SELL TradeType = 1 // 卖出 19 | TDX_TICK_NEUTRAL TradeType = 2 // 中性盘 20 | TDX_TICK_UNKNOWN TradeType = 3 // 未知类型, 出现在09:27分的历史数据中, 暂时确定为中性盘 21 | ) 22 | 23 | const ( 24 | TDX_TRANSACTION_MAX = 1800 // 单次最多获取多少条分笔成交记录 25 | ) 26 | 27 | // TransactionPackage 当日分笔成交信息 28 | type TransactionPackage struct { 29 | reqHeader *StdRequestHeader 30 | respHeader *StdResponseHeader 31 | request *TransactionRequest 32 | reply *TransactionReply 33 | 34 | contentHex string 35 | } 36 | 37 | type TransactionRequest struct { 38 | Market uint16 39 | Code [6]byte 40 | Start uint16 41 | Count uint16 42 | } 43 | 44 | type TransactionReply struct { 45 | Count uint16 46 | List []TickTransaction 47 | } 48 | 49 | type TickTransaction struct { 50 | Time string `dataframe:"time"` 51 | Price float64 `dataframe:"price"` 52 | Vol int `dataframe:"vol"` 53 | Num int `dataframe:"num"` // 历史成交数据中并无这个字段 54 | Amount float64 `dataframe:"amount"` // 新增金额字段 55 | BuyOrSell int `dataframe:"buyorsell"` 56 | } 57 | 58 | func NewTransactionPackage() *TransactionPackage { 59 | obj := new(TransactionPackage) 60 | obj.reqHeader = new(StdRequestHeader) 61 | obj.respHeader = new(StdResponseHeader) 62 | obj.request = new(TransactionRequest) 63 | obj.reply = new(TransactionReply) 64 | 65 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 66 | obj.reqHeader.SeqID = internal.SequenceId() 67 | obj.reqHeader.PacketType = 0x00 68 | //obj.reqHeader.PkgLen1 = 69 | //obj.reqHeader.PkgLen2 = 70 | obj.reqHeader.Method = proto.STD_MSG_TRANSACTION_DATA 71 | obj.contentHex = "" 72 | return obj 73 | } 74 | func (obj *TransactionPackage) SetParams(req *TransactionRequest) { 75 | obj.request = req 76 | } 77 | 78 | func (obj *TransactionPackage) Serialize() ([]byte, error) { 79 | obj.reqHeader.PkgLen1 = 0x0e 80 | obj.reqHeader.PkgLen2 = 0x0e 81 | 82 | buf := new(bytes.Buffer) 83 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 84 | err = binary.Write(buf, binary.LittleEndian, obj.request) 85 | b, err := hex.DecodeString(obj.contentHex) 86 | buf.Write(b) 87 | 88 | //b, err := hex.DecodeString(obj.contentHex) 89 | //buf.Write(b) 90 | 91 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 92 | 93 | return buf.Bytes(), err 94 | } 95 | 96 | func (obj *TransactionPackage) UnSerialize(header interface{}, data []byte) error { 97 | obj.respHeader = header.(*StdResponseHeader) 98 | 99 | market := exchange.MarketType(obj.request.Market) 100 | code := api.Bytes2String(obj.request.Code[:]) 101 | baseUnit := internal.BaseUnit(market, code) 102 | isIndex := exchange.AssertIndexByMarketAndCode(market, code) 103 | 104 | pos := 0 105 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 106 | pos += 2 107 | 108 | lastPrice := 0 109 | for index := uint16(0); index < obj.reply.Count; index++ { 110 | ele := TickTransaction{} 111 | hour, minute := internal.GetTime(data, &pos) 112 | ele.Time = fmt.Sprintf("%02d:%02d", hour, minute) 113 | rawPrice := internal.DecodeVarint(data, &pos) 114 | ele.Vol = internal.DecodeVarint(data, &pos) 115 | ele.Num = internal.DecodeVarint(data, &pos) 116 | ele.BuyOrSell = internal.DecodeVarint(data, &pos) 117 | lastPrice += rawPrice 118 | ele.Price = float64(lastPrice) / baseUnit 119 | if isIndex { 120 | amount := ele.Vol * 100 121 | ele.Amount = float64(amount) 122 | ele.Vol = int(float64(amount) / ele.Price) 123 | } else { 124 | ele.Vol *= 100 125 | ele.Amount = float64(ele.Vol) * ele.Price 126 | } 127 | tmp := internal.DecodeVarint(data, &pos) 128 | _ = tmp 129 | obj.reply.List = append(obj.reply.List, ele) 130 | } 131 | return err 132 | } 133 | 134 | func (obj *TransactionPackage) Reply() interface{} { 135 | return obj.reply 136 | } 137 | -------------------------------------------------------------------------------- /quotes/stock_transaction_data_history.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "gitee.com/quant1x/exchange" 9 | "gitee.com/quant1x/gotdx/internal" 10 | "gitee.com/quant1x/gotdx/proto" 11 | "gitee.com/quant1x/gox/api" 12 | ) 13 | 14 | type HistoryTransactionPackage struct { 15 | reqHeader *StdRequestHeader 16 | respHeader *StdResponseHeader 17 | request *HistoryTransactionRequest 18 | reply *TransactionReply 19 | 20 | contentHex string 21 | } 22 | 23 | type HistoryTransactionRequest struct { 24 | Date uint32 25 | Market uint16 26 | Code [6]byte 27 | Start uint16 28 | Count uint16 29 | } 30 | 31 | //type HistoryTransactionReply struct { 32 | // Count uint16 33 | // List []HistoryTransaction 34 | //} 35 | // 36 | //type HistoryTransaction struct { 37 | // Time string 38 | // Price float64 39 | // Vol int 40 | // Num int 41 | // BuyOrSell int 42 | //} 43 | 44 | func NewHistoryTransactionPackage() *HistoryTransactionPackage { 45 | obj := new(HistoryTransactionPackage) 46 | obj.reqHeader = new(StdRequestHeader) 47 | obj.respHeader = new(StdResponseHeader) 48 | obj.request = new(HistoryTransactionRequest) 49 | obj.reply = new(TransactionReply) 50 | 51 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 52 | obj.reqHeader.SeqID = internal.SequenceId() 53 | obj.reqHeader.PacketType = 0x00 54 | //obj.reqHeader.PkgLen1 = 55 | //obj.reqHeader.PkgLen2 = 56 | obj.reqHeader.Method = proto.STD_MSG_HISTORY_TRANSACTION_DATA 57 | obj.contentHex = "" 58 | return obj 59 | } 60 | 61 | // SetParams 设置参数 62 | func (obj *HistoryTransactionPackage) SetParams(req *HistoryTransactionRequest) { 63 | obj.request = req 64 | } 65 | 66 | func (obj *HistoryTransactionPackage) Serialize() ([]byte, error) { 67 | obj.reqHeader.PkgLen1 = 0x12 68 | obj.reqHeader.PkgLen2 = 0x12 69 | 70 | buf := new(bytes.Buffer) 71 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 72 | err = binary.Write(buf, binary.LittleEndian, obj.request) 73 | b, err := hex.DecodeString(obj.contentHex) 74 | buf.Write(b) 75 | 76 | //b, err := hex.DecodeString(obj.contentHex) 77 | //buf.Write(b) 78 | 79 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 80 | 81 | return buf.Bytes(), err 82 | } 83 | 84 | func (obj *HistoryTransactionPackage) UnSerialize(header interface{}, data []byte) error { 85 | obj.respHeader = header.(*StdResponseHeader) 86 | 87 | market := exchange.MarketType(obj.request.Market) 88 | code := api.Bytes2String(obj.request.Code[:]) 89 | isIndex := exchange.AssertIndexByMarketAndCode(market, code) 90 | 91 | pos := 0 92 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 93 | pos += 2 94 | //var a uint16 95 | //var b uint32 96 | //binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &a) 97 | //binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &b) 98 | // 跳过4个字节 99 | pos += 4 100 | baseUnit := internal.BaseUnit(market, code) 101 | lastPrice := 0 102 | for index := uint16(0); index < obj.reply.Count; index++ { 103 | ele := TickTransaction{} 104 | h, m := internal.GetTime(data, &pos) 105 | ele.Time = fmt.Sprintf("%02d:%02d", h, m) 106 | rawPrice := internal.DecodeVarint(data, &pos) 107 | ele.Vol = internal.DecodeVarint(data, &pos) 108 | ele.BuyOrSell = internal.DecodeVarint(data, &pos) 109 | internal.DecodeVarint(data, &pos) 110 | 111 | lastPrice = lastPrice + rawPrice 112 | ele.Price = float64(lastPrice) / baseUnit 113 | 114 | if isIndex { 115 | amount := ele.Vol * 100 116 | ele.Amount = float64(amount) 117 | ele.Vol = int(float64(amount) / ele.Price) 118 | } else { 119 | ele.Vol *= 100 120 | ele.Amount = float64(ele.Vol) * ele.Price 121 | } 122 | 123 | obj.reply.List = append(obj.reply.List, ele) 124 | } 125 | return err 126 | } 127 | 128 | func (obj *HistoryTransactionPackage) Reply() interface{} { 129 | return obj.reply 130 | } 131 | -------------------------------------------------------------------------------- /quotes/stock_transaction_data_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/gox/api" 7 | "testing" 8 | ) 9 | 10 | func TestTransaction(t *testing.T) { 11 | stdApi, err := NewStdApi() 12 | if err != nil { 13 | panic(err) 14 | } 15 | defer stdApi.Close() 16 | reply, err := stdApi.GetTransactionData("sh600010", 0, 2) 17 | if err != nil { 18 | fmt.Printf("%+v\n", err) 19 | } 20 | fmt.Printf("%+v\n", reply) 21 | data, _ := json.Marshal(reply) 22 | text := api.Bytes2String(data) 23 | fmt.Println(text) 24 | } 25 | 26 | func TestHistoryTransaction(t *testing.T) { 27 | stdApi, err := NewStdApi() 28 | if err != nil { 29 | panic(err) 30 | } 31 | defer stdApi.Close() 32 | code := "sh880534" 33 | date := 20240110 34 | reply, err := stdApi.GetHistoryTransactionData(code, uint32(date), 0, 2) 35 | if err != nil { 36 | fmt.Printf("%+v\n", err) 37 | } 38 | fmt.Printf("%+v\n", reply) 39 | data, _ := json.Marshal(reply) 40 | text := api.Bytes2String(data) 41 | fmt.Println(text) 42 | } 43 | -------------------------------------------------------------------------------- /quotes/stock_xdxr_info.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | "gitee.com/quant1x/gox/encoding/binary/struc" 11 | ) 12 | 13 | var ( 14 | XDXR_CATEGORY_MAPPING = map[int]string{ 15 | 1: "除权除息", 16 | 2: "送配股上市", 17 | 3: "非流通股上市", 18 | 4: "未知股本变动", 19 | 5: "股本变化", 20 | 6: "增发新股", 21 | 7: "股份回购", 22 | 8: "增发新股上市", 23 | 9: "转配股上市", 24 | 10: "可转债上市", 25 | 11: "扩缩股", 26 | 12: "非流通股缩股", 27 | 13: "送认购权证", 28 | 14: "送认沽权证", 29 | } 30 | ) 31 | 32 | // XdxrInfoPackage 除权除息 33 | type XdxrInfoPackage struct { 34 | reqHeader *StdRequestHeader 35 | respHeader *StdResponseHeader 36 | request *XdxrInfoRequest 37 | response *XdxrInfoReply 38 | reply []XdxrInfo 39 | contentHex string 40 | } 41 | 42 | type XdxrInfoRequest struct { 43 | //Count uint16 // 总数 44 | Market uint8 // 市场代码 45 | Code [6]byte // 股票代码 46 | } 47 | 48 | // XdxrInfoReply 响应包结构 49 | type XdxrInfoReply struct { 50 | Unknown []byte `struc:"[9]byte,little"` // 未知 51 | Count uint16 `struc:"uint16,little,sizeof=List"` // 总数 52 | List []RawXdxrInfo `struc:"[29]byte, little"` // [29]byte和title中间必须要有一个空格 53 | } 54 | 55 | type RawXdxrInfo struct { 56 | Market int `struc:"uint8,little"` // 市场代码 57 | Code string `struc:"[6]byte,little"` // 股票代码 58 | Unknown int `struc:"uint8,little"` // 未知 59 | Date uint32 `struc:"uint32,little"` // 日期 60 | Category int `struc:"uint8,little"` // 类型 61 | Data []byte `struc:"[16]byte,little"` 62 | } 63 | 64 | type XdxrInfo struct { 65 | Date string // 日期 66 | Category int // 类型 67 | Name string // 类型名称 68 | FenHong float64 // 分红 69 | PeiGuJia float64 // 配股价 70 | SongZhuanGu float64 // 送转股 71 | PeiGu float64 // 配股 72 | SuoGu float64 // 缩股 73 | QianLiuTong float64 // 前流通 74 | HouLiuTong float64 // 后流通 75 | QianZongGuBen float64 // 前总股本 76 | HouZongGuBen float64 // 后总股本 77 | FenShu float64 // 份数 78 | XingGuanJia float64 // 行权价 79 | } 80 | 81 | // IsCapitalChange 是否股本变化 82 | func (x *XdxrInfo) IsCapitalChange() bool { 83 | switch x.Category { 84 | case 1, 11, 12, 13, 14: 85 | return false 86 | default: 87 | if x.HouLiuTong > 0 && x.HouZongGuBen > 0 { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | // Adjust 返回复权回调函数 factor 95 | func (x *XdxrInfo) Adjust() func(p float64) float64 { 96 | songZhuangu := x.SongZhuanGu 97 | peiGu := x.PeiGu 98 | suoGu := x.SuoGu 99 | xdxrGuShu := (songZhuangu + peiGu - suoGu) / 10 100 | fenHong := x.FenHong 101 | peiGuJia := x.PeiGuJia 102 | xdxrFenHong := (peiGuJia*peiGu - fenHong) / 10 103 | 104 | factor := func(p float64) float64 { 105 | v := (p + xdxrFenHong) / (1 + xdxrGuShu) 106 | //return num.Decimal(v) 107 | return v 108 | } 109 | return factor 110 | } 111 | 112 | func NewXdxrInfoPackage() *XdxrInfoPackage { 113 | pkg := new(XdxrInfoPackage) 114 | pkg.reqHeader = new(StdRequestHeader) 115 | pkg.respHeader = new(StdResponseHeader) 116 | pkg.request = new(XdxrInfoRequest) 117 | pkg.response = new(XdxrInfoReply) 118 | 119 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 120 | //0c 121 | pkg.reqHeader.ZipFlag = proto.FlagNotZipped 122 | //1f 18 76 00 123 | pkg.reqHeader.SeqID = internal.SequenceId() 124 | //01 125 | pkg.reqHeader.PacketType = 0x01 126 | //0b 00 127 | //PkgLen1 uint16 128 | pkg.reqHeader.PkgLen1 = 0x000b 129 | //0b 00 130 | //PkgLen2 uint16 131 | pkg.reqHeader.PkgLen2 = 0x000b 132 | //10 00 133 | pkg.reqHeader.Method = proto.STD_MSG_XDXR_INFO 134 | pkg.contentHex = "0100" // 未解 135 | return pkg 136 | } 137 | 138 | func (obj *XdxrInfoPackage) SetParams(req *XdxrInfoRequest) { 139 | //req.Count = 1 140 | obj.request = req 141 | } 142 | 143 | func (obj *XdxrInfoPackage) Serialize() ([]byte, error) { 144 | buf := new(bytes.Buffer) 145 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 146 | b, err := hex.DecodeString(obj.contentHex) 147 | buf.Write(b) 148 | err = binary.Write(buf, binary.LittleEndian, obj.request) 149 | return buf.Bytes(), err 150 | } 151 | 152 | func (obj *XdxrInfoPackage) UnSerialize(header interface{}, data []byte) error { 153 | obj.respHeader = header.(*StdResponseHeader) 154 | // 构造流 155 | buf := bytes.NewBuffer(data) 156 | var reply XdxrInfoReply 157 | err := struc.Unpack(buf, &reply) 158 | if err != nil { 159 | return err 160 | } 161 | var list = []XdxrInfo{} 162 | for _, v := range reply.List { 163 | year, month, day, hour, minute := internal.GetDatetimeFromUint32(9, v.Date, 0) 164 | xdxr := XdxrInfo{ 165 | //Date string // 日期 166 | Date: fmt.Sprintf("%04d-%02d-%02d", year, month, day), 167 | //Category int // 类型 168 | Category: v.Category, 169 | //Name string // 类型名称 170 | Name: XDXR_CATEGORY_MAPPING[v.Category], 171 | //FenHong int // 分红 172 | //PeiGuJia int // 配股价 173 | //SongZhuanGu int // 送转股 174 | //PeiGu int // 配股 175 | //SuoGu int // 锁骨 176 | //QianLiuTong int // 盘前流通 177 | //HouLiuTong int // 盘后流通 178 | //QianZongGuBen int // 前总股本 179 | //HouZongGuBen int // 后总股本 180 | //FenShu int // 份数 181 | //XingGuanJia int // 行权价 182 | } 183 | switch xdxr.Category { 184 | case 1: 185 | var f float32 186 | pos := 0 187 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 188 | xdxr.FenHong = float64(f) 189 | pos += 4 190 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 191 | xdxr.PeiGuJia = float64(f) 192 | pos += 4 193 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 194 | xdxr.SongZhuanGu = float64(f) 195 | pos += 4 196 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 197 | xdxr.PeiGu = float64(f) 198 | case 11, 12: 199 | var f float32 200 | pos := 8 201 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 202 | xdxr.SuoGu = float64(f) 203 | case 13, 14: 204 | var f float32 205 | pos := 0 206 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 207 | xdxr.XingGuanJia = float64(f) 208 | pos = 8 209 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 210 | xdxr.FenShu = float64(f) 211 | default: 212 | var i uint32 213 | pos := 0 214 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &i) 215 | xdxr.QianLiuTong = __get_v(i) 216 | pos += 4 217 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &i) 218 | xdxr.QianZongGuBen = __get_v(i) 219 | pos += 4 220 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &i) 221 | xdxr.HouLiuTong = __get_v(i) 222 | pos += 4 223 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &i) 224 | xdxr.HouZongGuBen = __get_v(i) 225 | } 226 | list = append(list, xdxr) 227 | _ = hour 228 | _ = minute 229 | } 230 | obj.reply = list 231 | return nil 232 | } 233 | 234 | func (obj *XdxrInfoPackage) Reply() interface{} { 235 | return obj.reply 236 | } 237 | 238 | func __get_v(v uint32) float64 { 239 | if v == 0 { 240 | return 0 241 | } 242 | return internal.IntToFloat64(int(v)) 243 | } 244 | -------------------------------------------------------------------------------- /quotes/stock_xdxr_info_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/gox/api" 7 | "testing" 8 | ) 9 | 10 | func TestXdxrInfoPackage(t *testing.T) { 11 | stdApi, err := NewStdApi() 12 | if err != nil { 13 | panic(err) 14 | } 15 | defer stdApi.Close() 16 | //sq1, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShenZhen}, []string{"600275", "600455", "600086", "300742"}) 17 | sq1, err := stdApi.GetXdxrInfo("sh600115") 18 | if err != nil { 19 | fmt.Printf("%+v\n", err) 20 | } 21 | fmt.Printf("%+v\n", sq1) 22 | data, _ := json.Marshal(sq1) 23 | text := api.Bytes2String(data) 24 | fmt.Println(text) 25 | } 26 | -------------------------------------------------------------------------------- /securities/block.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "gitee.com/quant1x/exchange" 5 | "gitee.com/quant1x/gox/api" 6 | "gitee.com/quant1x/gox/coroutine" 7 | "slices" 8 | ) 9 | 10 | var ( 11 | __onceBlockFiles coroutine.PeriodicOnce 12 | __global_block_list = []BlockInfo{} 13 | __mapBlock = map[string]BlockInfo{} 14 | ) 15 | 16 | // BlockInfo 板块信息 17 | type BlockInfo struct { 18 | Name string `dataframe:"name"` // 名称 19 | Code string `dataframe:"code"` // 代码 20 | Type int `dataframe:"type"` // 类型 21 | Count int `dataframe:"count"` // 个股数量 22 | Block string `dataframe:"block"` // 通达信板块编码 23 | ConstituentStocks []string `dataframe:"ConstituentStocks"` // 板块成份股 24 | } 25 | 26 | func loadCacheBlockInfos() { 27 | syncBlockFiles() 28 | bkFilename := SectorFilename() 29 | list := []BlockInfo{} 30 | err := api.CsvToSlices(bkFilename, &list) 31 | if err != nil { 32 | return 33 | } 34 | if len(list) > 0 { 35 | __global_block_list = []BlockInfo{} 36 | for _, v := range list { 37 | // 对齐板块代码 38 | blockCode := exchange.CorrectSecurityCode(v.Code) 39 | v.Code = blockCode 40 | for i := 0; i < len(v.ConstituentStocks); i++ { 41 | // 对齐个股代码 42 | stockCode := exchange.CorrectSecurityCode(v.ConstituentStocks[i]) 43 | v.ConstituentStocks[i] = stockCode 44 | } 45 | // 缓存列表 46 | __global_block_list = append(__global_block_list, v) 47 | // 缓存板块映射关系 48 | __mapBlock[v.Code] = v 49 | } 50 | } 51 | } 52 | 53 | // BlockList 板块列表 54 | func BlockList() (list []BlockInfo) { 55 | __onceBlockFiles.Do(loadCacheBlockInfos) 56 | return slices.Clone(__global_block_list) 57 | } 58 | 59 | func GetBlockInfo(code string) *BlockInfo { 60 | __onceBlockFiles.Do(loadCacheBlockInfos) 61 | securityCode := code 62 | if !exchange.AssertBlockBySecurityCode(&securityCode) { 63 | return nil 64 | } 65 | blockInfo, ok := __mapBlock[securityCode] 66 | if ok { 67 | return &blockInfo 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /securities/block_config.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "bufio" 5 | "gitee.com/quant1x/exchange/cache" 6 | "gitee.com/quant1x/gox/api" 7 | "gitee.com/quant1x/gox/text/encoding" 8 | "io" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | // 加载板块和板块名称对应 14 | func loadIndexBlockInfos() []BlockInfo { 15 | bks := []string{"tdxzs.cfg", "tdxzs3.cfg"} 16 | bis := []BlockInfo{} 17 | tmpMap := map[string]BlockInfo{} 18 | for _, v := range bks { 19 | bi := getBlockInfoFromConfig(v) 20 | if len(bi) == 0 { 21 | continue 22 | } 23 | for _, info := range bi { 24 | if bv, ok := tmpMap[info.Code]; !ok { 25 | bis = append(bis, info) 26 | tmpMap[info.Code] = info 27 | } else { 28 | _ = bv 29 | } 30 | } 31 | } 32 | return bis 33 | } 34 | 35 | func getBlockInfoFromConfig(name string) []BlockInfo { 36 | cacheFilename := cache.GetBlockPath() + "/" + name 37 | if !api.FileExist(cacheFilename) { 38 | // 如果文件不存在, 导出内嵌资源 39 | err := export(cacheFilename, name) 40 | if err != nil { 41 | return nil 42 | } 43 | } 44 | file, err := os.Open(cacheFilename) 45 | if err != nil { 46 | return nil 47 | } 48 | defer api.CloseQuietly(file) 49 | reader := bufio.NewReader(file) 50 | // 按行处理txt 51 | decoder := encoding.NewDecoder("GBK") 52 | var blocks = []BlockInfo{} 53 | for { 54 | data, _, err := reader.ReadLine() 55 | if err == io.EOF { 56 | break 57 | } 58 | line := decoder.ConvertString(string(data)) 59 | arr := strings.Split(line, "|") 60 | bk := BlockInfo{ 61 | Name: arr[0], 62 | Code: arr[1], 63 | Type: int(api.ParseInt(arr[2])), 64 | Block: arr[5], 65 | } 66 | blocks = append(blocks, bk) 67 | } 68 | return blocks 69 | } 70 | -------------------------------------------------------------------------------- /securities/block_data.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "gitee.com/quant1x/exchange" 5 | "gitee.com/quant1x/gotdx/quotes" 6 | "gitee.com/quant1x/gox/api" 7 | "os" 8 | ) 9 | 10 | // 同步板块数据 11 | func syncBlockFiles() { 12 | downloadBlockRawData(quotes.BLOCK_DEFAULT) 13 | downloadBlockRawData(quotes.BLOCK_GAINIAN) 14 | downloadBlockRawData(quotes.BLOCK_FENGGE) 15 | downloadBlockRawData(quotes.BLOCK_ZHISHU) 16 | updateCacheBlockFile() 17 | } 18 | 19 | // 更新缓存csv数据文件 20 | func updateCacheBlockFile() { 21 | // 如果板块数据不存在, 从应用内导出 22 | blockFile := SectorFilename() 23 | createOrUpdate := false 24 | if !api.FileExist(blockFile) { 25 | createOrUpdate = true 26 | } else { 27 | dataStat, err := os.Stat(blockFile) 28 | if err == nil || os.IsExist(err) { 29 | dataModifyTime := dataStat.ModTime() 30 | toInit := exchange.CanInitialize(dataModifyTime) 31 | if toInit { 32 | createOrUpdate = true 33 | } 34 | } else { 35 | createOrUpdate = true 36 | } 37 | } 38 | if createOrUpdate { 39 | parseAndGenerateBlockFile() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /securities/block_embed.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "time" 10 | ) 11 | 12 | var ( 13 | // ResourcesPath 资源路径 14 | ResourcesPath = "resources" 15 | ) 16 | 17 | //go:embed resources/* 18 | var resources embed.FS 19 | 20 | // OpenEmbed 打开嵌入式文件 21 | func OpenEmbed(name string) (fs.File, error) { 22 | filename := fmt.Sprintf("%s/%s", ResourcesPath, name) 23 | reader, err := resources.Open(filename) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return reader, nil 28 | } 29 | 30 | // 导出内嵌资源文件 31 | func export(dest, source string) error { 32 | src, err := OpenEmbed(source) 33 | if err != nil { 34 | return err 35 | } 36 | output, err := os.Create(dest) 37 | if err != nil { 38 | return err 39 | } 40 | //const ( 41 | // BUFFERSIZE = 8192 42 | //) 43 | //buf := make([]byte, BUFFERSIZE) 44 | //for { 45 | // n, err := src.Read(buf) 46 | // if err != nil && err != io.EOF { 47 | // return err 48 | // } 49 | // if n == 0 { 50 | // break 51 | // } 52 | // 53 | // if _, err := output.Write(buf[:n]); err != nil { 54 | // return err 55 | // } 56 | //} 57 | _, err = io.Copy(output, src) 58 | if err != nil { 59 | return err 60 | } 61 | mtime := time.Now() 62 | err = os.Chtimes(dest, mtime, mtime) 63 | return err 64 | } 65 | -------------------------------------------------------------------------------- /securities/block_industry.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "bufio" 5 | "gitee.com/quant1x/exchange/cache" 6 | "gitee.com/quant1x/gox/api" 7 | "gitee.com/quant1x/gox/text/encoding" 8 | "io" 9 | "os" 10 | "slices" 11 | "strings" 12 | ) 13 | 14 | // IndustryInfo 行业板块对应 15 | type IndustryInfo struct { 16 | MarketId int // 市场代码 17 | Code string // 股票代码 18 | Block string // 行业板块代码 19 | Block5 string // 二级行业板块代码 20 | XBlock string // x行业代码 21 | XBlock5 string // x二级行业代码 22 | } 23 | 24 | // 获取行业板块 25 | func loadIndustryBlocks() []IndustryInfo { 26 | hyfile := "tdxhy.cfg" 27 | name := hyfile 28 | cacheFilename := cache.GetBlockPath() + "/" + name 29 | if !api.FileExist(cacheFilename) { 30 | // 如果文件不存在, 导出内嵌资源 31 | err := export(cacheFilename, name) 32 | if err != nil { 33 | return nil 34 | } 35 | } 36 | file, err := os.Open(cacheFilename) 37 | if err != nil { 38 | return nil 39 | } 40 | defer api.CloseQuietly(file) 41 | reader := bufio.NewReader(file) 42 | // 按行处理txt 43 | decoder := encoding.NewDecoder("GBK") 44 | var hys = []IndustryInfo{} 45 | for { 46 | data, _, err := reader.ReadLine() 47 | if err == io.EOF { 48 | break 49 | } 50 | line := decoder.ConvertString(string(data)) 51 | arr := strings.Split(line, "|") 52 | bc := arr[2] 53 | bc5 := bc 54 | if len(bc5) >= 5 { 55 | bc5 = bc5[0:5] 56 | } 57 | var xbc, xbc5 string 58 | if len(arr) >= 6 { 59 | xbc5 = arr[5] 60 | if len(xbc5) >= 6 { 61 | xbc = xbc5[:5] 62 | } 63 | } 64 | 65 | hy := IndustryInfo{ 66 | MarketId: int(api.ParseInt(arr[0])), 67 | Code: arr[1], 68 | Block: bc, 69 | Block5: bc5, 70 | XBlock: xbc, 71 | XBlock5: xbc5, 72 | } 73 | hys = append(hys, hy) 74 | } 75 | return hys 76 | } 77 | 78 | // 从行业信息中提取股票代码列表 79 | func industryConstituentStockList(hys []IndustryInfo, block string) []string { 80 | list := []string{} 81 | for _, v := range hys { 82 | if len(block) == 5 { 83 | //if v.Block5 == block { 84 | if strings.HasPrefix(v.Block5, block) || strings.HasPrefix(v.XBlock5, block) { 85 | list = append(list, v.Code) 86 | } 87 | } else { 88 | if v.Block == block || v.XBlock == block { 89 | list = append(list, v.Code) 90 | } 91 | } 92 | } 93 | if len(list) > 0 { 94 | slices.Sort(list) 95 | } 96 | return list 97 | } 98 | -------------------------------------------------------------------------------- /securities/block_parse.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "fmt" 5 | "gitee.com/quant1x/exchange" 6 | "gitee.com/quant1x/exchange/cache" 7 | "gitee.com/quant1x/gox/api" 8 | ) 9 | 10 | // SectorFilename 板块缓存文件名 11 | func SectorFilename(date ...string) string { 12 | name := "blocks" 13 | cacheDate := exchange.LastTradeDate() 14 | if len(date) > 0 { 15 | cacheDate = exchange.FixTradeDate(date[0]) 16 | } 17 | filename := fmt.Sprintf("%s/%s.%s", cache.GetMetaPath(), name, cacheDate) 18 | return filename 19 | } 20 | 21 | // 读取板块数据 22 | func parseAndGenerateBlockFile() { 23 | blockInfos := loadIndexBlockInfos() 24 | block2Name := map[string]string{} 25 | for _, v := range blockInfos { 26 | block2Name[v.Block] = v.Name 27 | } 28 | bks := []string{"block.dat", "block_gn.dat", "block_fg.dat", "block_zs.dat"} 29 | //bks := []string{"block_gn.dat", "block_fg.dat", "block_zs.dat"} 30 | name2block := map[string]__raw_block_info{} 31 | for _, v := range bks { 32 | bi := parseRawBlockData(v) 33 | if bi != nil { 34 | for _, bk := range (*bi).Data { 35 | blockName := bk.BlockName 36 | if bn, ok := block2Name[blockName]; ok { 37 | blockName = bn 38 | } 39 | name2block[blockName] = bk 40 | } 41 | } 42 | } 43 | // 行业代码映射 44 | code2hy := map[string]string{} 45 | for _, v := range blockInfos { 46 | if v.Name != v.Block { 47 | code2hy[v.Block] = v.Name 48 | } 49 | } 50 | // 行业板块数据 51 | hys := loadIndustryBlocks() 52 | for i, v := range blockInfos { 53 | bn := v.Name 54 | __info, ok := name2block[bn] 55 | if ok { 56 | list := []string{} 57 | for _, sc := range __info.List { 58 | if len(sc.Code) < 5 { 59 | continue 60 | } 61 | marketId, _, _ := exchange.DetectMarket(sc.Code) 62 | if marketId == exchange.MarketIdBeiJing { 63 | continue 64 | } 65 | list = append(list, sc.Code) 66 | } 67 | blockInfos[i].Count = int(__info.Num) 68 | blockInfos[i].ConstituentStocks = list 69 | continue 70 | } 71 | bc := v.Block 72 | stockList := industryConstituentStockList(hys, bc) 73 | if len(stockList) > 0 { 74 | blockInfos[i].Count = len(stockList) 75 | blockInfos[i].ConstituentStocks = stockList 76 | } 77 | } 78 | blockInfos = api.Filter(blockInfos, func(info BlockInfo) bool { 79 | return len(info.ConstituentStocks) > 0 80 | }) 81 | if len(blockInfos) > 0 { 82 | filename := SectorFilename() 83 | _ = api.SlicesToCsv(filename, blockInfos) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /securities/block_raw.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "gitee.com/quant1x/exchange" 5 | "gitee.com/quant1x/exchange/cache" 6 | "gitee.com/quant1x/gotdx" 7 | "gitee.com/quant1x/gox/api" 8 | "gitee.com/quant1x/gox/encoding/binary/struc" 9 | "gitee.com/quant1x/gox/text/encoding" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | // 下载板块原始数据文件 15 | func downloadBlockRawData(filename string) { 16 | tdxApi := gotdx.GetTdxApi() 17 | fn := cache.GetBlockPath() + "/" + filename 18 | fileInfo, err := os.Stat(fn) 19 | if err == nil || os.IsExist(err) { 20 | toInit := exchange.CanInitialize(fileInfo.ModTime()) 21 | if !toInit { 22 | return 23 | } 24 | } 25 | resp, err := tdxApi.GetBlockInfo(filename) 26 | if err == nil { 27 | fn := cache.GetBlockPath() + "/" + filename 28 | _ = api.CheckFilepath(fn, true) 29 | fp, err := os.Create(fn) 30 | if err == nil { 31 | _, _ = fp.Write(resp.Data) 32 | _ = fp.Close() 33 | } 34 | } 35 | } 36 | 37 | type __raw_block_info struct { 38 | BlockName string `struc:"[9]byte,little"` // 板块名称 39 | Num uint16 `struc:"uint16,little"` // 个股数量 40 | BlockType uint16 `struc:"uint16,little"` // 板块类型 41 | List [400]__block_stock `struct:"[400]__block_stock,little"` // 个股列表 42 | } 43 | 44 | type __block_stock struct { 45 | Code string `struc:"[7]byte,little"` // 证券代码 46 | } 47 | 48 | type __raw_block_data struct { 49 | //Header blockHeader `struc:"[386]byte,little"` 50 | Unknown [384]byte `struc:"[384]byte,little"` // 头信息, 忽略 51 | Count uint16 `struc:"uint16,little,sizeof=Data"` // 板块数量 52 | Data []__raw_block_info `struc:"[2813]byte, little"` // 板块数据 53 | } 54 | 55 | func parseRawBlockData(blockFilename string) *__raw_block_data { 56 | fn := cache.GetBlockPath() + "/" + blockFilename 57 | _ = api.CheckFilepath(fn, true) 58 | file, err := os.Open(fn) 59 | if err != nil { 60 | return nil 61 | } 62 | defer api.CloseQuietly(file) 63 | var block __raw_block_data 64 | err = struc.Unpack(file, &block) 65 | if err != nil { 66 | return nil 67 | } 68 | decoder := encoding.NewDecoder("GBK") 69 | for i, v := range block.Data { 70 | name := decoder.ConvertString(v.BlockName) 71 | block.Data[i].BlockName = strings.ReplaceAll(name, string([]byte{0x00}), "") 72 | for j, s := range v.List { 73 | block.Data[i].List[j].Code = strings.ReplaceAll(s.Code, string([]byte{0x00}), "") 74 | } 75 | } 76 | return &block 77 | } 78 | -------------------------------------------------------------------------------- /securities/block_test.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestBlockList(t *testing.T) { 9 | v := BlockList() 10 | fmt.Println(v) 11 | } 12 | 13 | func TestParseAndGenerateBlockFile(t *testing.T) { 14 | parseAndGenerateBlockFile() 15 | } 16 | 17 | func TestGetBlockInfo(t *testing.T) { 18 | code := "880818" 19 | code = "881432" 20 | v := GetBlockInfo(code) 21 | fmt.Println(v) 22 | } 23 | -------------------------------------------------------------------------------- /securities/block_type.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | type BlockType = int 4 | 5 | const ( 6 | BK_UNKNOWN BlockType = 0 // 未知类型 7 | BK_HANGYE BlockType = 2 // 行业 8 | BK_DIQU BlockType = 3 // 地区 9 | BK_GAINIAN BlockType = 4 // 概念 10 | BK_FENGGE BlockType = 5 // 风格 11 | BK_ZHISHU BlockType = 6 // 指数 12 | BK_YJHY BlockType = 12 // 研究行业 13 | 14 | BKN_HANGYE = "行业" 15 | BKN_DIQU = "地区" 16 | BKN_GAINIAN = "概念" 17 | BKN_FENGGE = "风格" 18 | BKN_ZHISHU = "指数" 19 | BKN_YJHY = "研究行业" 20 | ) 21 | 22 | var ( 23 | mapBlockType = map[BlockType]string{ 24 | BK_HANGYE: BKN_HANGYE, 25 | BK_DIQU: BKN_DIQU, 26 | BK_GAINIAN: BKN_GAINIAN, 27 | BK_FENGGE: BKN_FENGGE, 28 | BK_ZHISHU: BKN_ZHISHU, 29 | BK_YJHY: BKN_YJHY, 30 | } 31 | ) 32 | 33 | // BlockTypeNameByCode 通过板块类型代码获取板块类型名称 34 | func BlockTypeNameByCode(blockCode int) (name string, ok bool) { 35 | blockType := BlockType(blockCode) 36 | return BlockTypeNameByTypeCode(blockType) 37 | } 38 | 39 | // BlockTypeNameByTypeCode 通过板块类型代码获取板块类型名称 40 | func BlockTypeNameByTypeCode(blockType BlockType) (string, bool) { 41 | bkName, found := mapBlockType[blockType] 42 | return bkName, found 43 | } 44 | -------------------------------------------------------------------------------- /securities/margin_trading.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gitee.com/quant1x/exchange" 7 | "gitee.com/quant1x/exchange/cache" 8 | "gitee.com/quant1x/gox/api" 9 | "gitee.com/quant1x/gox/coroutine" 10 | "gitee.com/quant1x/gox/http" 11 | urlpkg "net/url" 12 | "slices" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // 两融配置文件 18 | marginTradingFilename = "margin-trading.csv" 19 | // https://data.eastmoney.com/rzrq/detail/all.html 20 | ) 21 | 22 | type FinancingAndSecuritiesLendingTarget struct { 23 | Code string `name:"证券代码" dataframe:"code"` 24 | } 25 | 26 | var ( 27 | onceMarginTrading coroutine.RollingOnce 28 | cacheMarginTradingList []string 29 | mapMarginTrading = map[string]bool{} 30 | ) 31 | 32 | const ( 33 | // https://data.eastmoney.com/rzrq/detail/all.html 34 | // https://datacenter-web.eastmoney.com/api/data/v1/get?reportName=RPTA_WEB_RZRQ_GGMX&columns=ALL&source=WEB&pageNumber=1&pageSize=10&sortColumns=rzjme&sortTypes=-1&filter=(DATE%3D%272023-12-28%27)&callback=jQuery112303199655251283524_1703887938254&_=1703887938257 35 | urlEastMoneyApiRZRQ = "https://datacenter-web.eastmoney.com/api/data/v1/get" 36 | rzrqPageSize = 500 37 | ) 38 | 39 | // SecurityMarginTrading 融资融券 40 | type SecurityMarginTrading struct { 41 | DATE string `name:"日期" json:"DATE"` 42 | MARKET string `name:"市场" json:"MARKET"` 43 | SCODE string `name:"代码" json:"SCODE"` 44 | SecName string `name:"证券名称" json:"SECNAME"` 45 | RZYE float64 `name:"融资余额(元)" json:"RZYE"` 46 | RQYL float64 `name:"融券余量(股)" json:"RQYL"` 47 | RZRQYE float64 `name:"融资融券余额(元)" json:"RZRQYE"` 48 | RQYE float64 `name:"融券余额(元)" json:"RQYE"` 49 | RQMCL float64 `name:"融券卖出量(股)" json:"RQMCL"` 50 | RZRQYECZ float64 `name:"融资融券余额差值(元)" json:"RZRQYECZ"` 51 | RZMRE float64 `name:"融资买入额(元)" json:"RZMRE"` 52 | SZ float64 `name:"SZ" json:"SZ"` 53 | RZYEZB float64 `name:"融资余额占流通市值比(%)" json:"RZYEZB"` 54 | RZMRE3D float64 `name:"3日融资买入额(元)" json:"RZMRE3D"` 55 | RZMRE5D float64 `name:"5日融资买入额(元)" json:"RZMRE5D"` 56 | RZMRE10D float64 `name:"10日融资买入额(元)" json:"RZMRE10D"` 57 | RZCHE float64 `name:"融资偿还额(元)" json:"RZCHE"` 58 | RZCHE3D float64 `name:"3日融资偿还额(元)" json:"RZCHE3D"` 59 | RZCHE5D float64 `name:"5日融资偿还额(元)" json:"RZCHE5D"` 60 | RZCHE10D float64 `name:"10日融资偿还额(元)" json:"RZCHE10D"` 61 | RZJME float64 `name:"融资净买额(元)" json:"RZJME"` 62 | RZJME3D float64 `name:"3日融资净买额(元)" json:"RZJME3D"` 63 | RZJME5D float64 `name:"5日融资净买额(元)" json:"RZJME5D"` 64 | RZJME10D float64 `name:"10日融资净买额(元)" json:"RZJME10D"` 65 | RQMCL3D float64 `name:"3日融券卖出量(股)" json:"RQMCL3D"` 66 | RQMCL5D float64 `name:"5日融券卖出量(股)" json:"RQMCL5D"` 67 | RQMCL10D float64 `name:"10日融券卖出量(股)" json:"RQMCL10D"` 68 | RQCHL float64 `name:"融券偿还量(股)" json:"RQCHL"` 69 | RQCHL3D float64 `name:"3日融券偿还量(股)" json:"RQCHL3D"` 70 | RQCHL5D float64 `name:"5日融券偿还量(股)" json:"RQCHL5D"` 71 | RQCHL10D float64 `name:"10日融券偿还量(股)" json:"RQCHL10D"` 72 | RQJMG float64 `name:"融券净卖出(股)" json:"RQJMG"` 73 | RQJMG3D float64 `name:"3日融券净卖出(股)" json:"RQJMG3D"` 74 | RQJMG5D float64 `name:"5日融券净卖出(股)" json:"RQJMG5D"` 75 | RQJMG10D float64 `name:"10日融券净卖出(股)" json:"RQJMG10D"` 76 | SPJ float64 `name:"收盘价" json:"SPJ"` 77 | ZDF float64 `name:"涨跌幅" json:"ZDF"` 78 | RChange3DCP float64 `name:"3日未识别" json:"RCHANGE3DCP"` 79 | RChange5DCP float64 `name:"5日未识别" json:"RCHANGE5DCP"` 80 | RChange10DCP float64 `name:"10日未识别" json:"RCHANGE10DCP"` 81 | KCB int `name:"科创板" json:"KCB"` 82 | TradeMarketCode string `name:"二级市场代码" json:"TRADE_MARKET_CODE"` 83 | TradeMarket string `name:"二级市场" json:"TRADE_MARKET"` 84 | FinBalanceGr float64 `json:"FIN_BALANCE_GR"` 85 | SecuCode string `name:"证券代码" json:"SECUCODE"` 86 | } 87 | 88 | type rawMarginTrading struct { 89 | Version string `json:"version"` 90 | Result struct { 91 | Pages int `json:"pages"` 92 | Data []SecurityMarginTrading `json:"data"` 93 | Count int `json:"count"` 94 | } `json:"result"` 95 | Success bool `json:"success"` 96 | Message string `json:"message"` 97 | Code int `json:"code"` 98 | } 99 | 100 | func rawMarginTradingList(date string, pageNumber int) ([]SecurityMarginTrading, int, error) { 101 | tradeDate := exchange.FixTradeDate(date) 102 | params := urlpkg.Values{ 103 | "reportName": {"RPTA_WEB_RZRQ_GGMX"}, 104 | "columns": {"ALL"}, 105 | "source": {"WEB"}, 106 | "sortColumns": {"scode"}, 107 | "sortTypes": {"1"}, 108 | "pageSize": {fmt.Sprintf("%d", rzrqPageSize)}, 109 | "pageNumber": {fmt.Sprintf("%d", pageNumber)}, 110 | "client": {"WEB"}, 111 | "filter": {fmt.Sprintf(`(DATE='%s')`, tradeDate)}, 112 | } 113 | 114 | url := urlEastMoneyApiRZRQ + "?" + params.Encode() 115 | data, err := http.Get(url) 116 | if err != nil { 117 | return nil, 0, err 118 | } 119 | var raw rawMarginTrading 120 | err = json.Unmarshal(data, &raw) 121 | if err != nil { 122 | return nil, 0, err 123 | } 124 | return raw.Result.Data, raw.Result.Pages, nil 125 | } 126 | 127 | func getMarginTradingDate() string { 128 | return exchange.GetFrontTradeDay() 129 | } 130 | 131 | // GetMarginTradingList 获取两融列表 132 | func GetMarginTradingList() []SecurityMarginTrading { 133 | date := getMarginTradingDate() 134 | var list []SecurityMarginTrading 135 | pages := 1 136 | for i := 0; i < pages; i++ { 137 | tmpList, tmpPages, err := rawMarginTradingList(date, i+1) 138 | if err != nil { 139 | break 140 | } 141 | list = append(list, tmpList...) 142 | if len(tmpList) < rzrqPageSize { 143 | break 144 | } 145 | if pages == 1 { 146 | pages = tmpPages 147 | } 148 | } 149 | return list 150 | } 151 | 152 | func lazyLoadMarginTrading() { 153 | target := cache.GetMetaPath() + "/" + marginTradingFilename 154 | // 1. 获取缓存文件状态 155 | var lastModified time.Time 156 | fs, err := api.GetFileStat(target) 157 | if err == nil { 158 | lastModified = fs.LastWriteTime 159 | } 160 | // 2. 临时两融列表 161 | var tempList []FinancingAndSecuritiesLendingTarget 162 | // 3. 比较缓存日期和最新的时间 163 | cacheLastDay := lastModified.Format(exchange.TradingDayDateFormat) 164 | if cacheLastDay < exchange.LastTradeDate() { 165 | // 过时, 下载 166 | list := GetMarginTradingList() 167 | for _, v := range list { 168 | securityCode := exchange.CorrectSecurityCode(v.SecuCode) 169 | tempList = append(tempList, FinancingAndSecuritiesLendingTarget{Code: securityCode}) 170 | } 171 | // 刷新本地缓存文件 172 | if len(tempList) > 0 { 173 | _ = api.SlicesToCsv(target, tempList) 174 | } 175 | } 176 | // 4. 如果文件不存在, 则从内置资源文件导出 177 | if len(tempList) == 0 && !api.FileExist(target) { 178 | filename := fmt.Sprintf("%s/%s", ResourcesPath, marginTradingFilename) 179 | _ = api.Export(resources, filename, target) 180 | } 181 | // 5. 如果没有更新, 则从缓存中加载 182 | if len(tempList) == 0 && api.FileExist(target) { 183 | _ = api.CsvToSlices(target, &tempList) 184 | } 185 | // 6. 准备加载两融标的代码列表到内存 186 | var codes []string 187 | for _, v := range tempList { 188 | code := v.Code 189 | securityCode := exchange.CorrectSecurityCode(code) 190 | codes = append(codes, securityCode) 191 | } 192 | if len(codes) > 0 { 193 | codes = api.SliceUnique(codes, func(a string, b string) int { 194 | if a < b { 195 | return -1 196 | } else if a > b { 197 | return 1 198 | } else { 199 | return 0 200 | } 201 | }) 202 | cacheMarginTradingList = slices.Clone(codes) 203 | clear(mapMarginTrading) 204 | for _, v := range cacheMarginTradingList { 205 | mapMarginTrading[v] = true 206 | } 207 | } 208 | } 209 | 210 | // MarginTradingList 获取两融标的列表 211 | func MarginTradingList() []string { 212 | onceMarginTrading.Do(lazyLoadMarginTrading) 213 | return cacheMarginTradingList 214 | } 215 | 216 | // IsMarginTradingTarget 是否两融标的 217 | func IsMarginTradingTarget(code string) bool { 218 | onceMarginTrading.Do(lazyLoadMarginTrading) 219 | securityCode := exchange.CorrectSecurityCode(code) 220 | _, ok := mapMarginTrading[securityCode] 221 | return ok 222 | } 223 | -------------------------------------------------------------------------------- /securities/margin_trading_test.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestMarginTradingList(t *testing.T) { 9 | v1 := MarginTradingList() 10 | fmt.Println(v1) 11 | v2 := MarginTradingList() 12 | fmt.Println(v2) 13 | } 14 | 15 | func TestIsMarginTradingTarget(t *testing.T) { 16 | type args struct { 17 | code string 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want bool 23 | }{ 24 | { 25 | name: "600178", 26 | args: args{code: "600178"}, 27 | want: false, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | if got := IsMarginTradingTarget(tt.args.code); got != tt.want { 33 | t.Errorf("IsMarginTradingTarget() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /securities/resources/README.md: -------------------------------------------------------------------------------- 1 | 通达信数据维护 2 | === 3 | 4 | ```shell 5 | cp -rp ~/workspace/data/tdx/T0002/hq_cache/tdxhy.cfg securities/resources/ 6 | cp -rp ~/workspace/data/tdx/T0002/hq_cache/tdxzs.cfg securities/resources/ 7 | cp -rp ~/workspace/data/tdx/T0002/hq_cache/tdxzs3.cfg securities/resources/ 8 | ``` -------------------------------------------------------------------------------- /securities/resources/tdxzs.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/0fb82b6c2f8ecff70466504eba29241a9fd00e15/securities/resources/tdxzs.cfg -------------------------------------------------------------------------------- /securities/resources/tdxzs3.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/0fb82b6c2f8ecff70466504eba29241a9fd00e15/securities/resources/tdxzs3.cfg -------------------------------------------------------------------------------- /securities/security_list.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "gitee.com/quant1x/exchange" 5 | "gitee.com/quant1x/exchange/cache" 6 | "gitee.com/quant1x/gotdx" 7 | "gitee.com/quant1x/gotdx/internal" 8 | "gitee.com/quant1x/gotdx/quotes" 9 | "gitee.com/quant1x/gox/api" 10 | "gitee.com/quant1x/gox/coroutine" 11 | "math" 12 | "os" 13 | "path/filepath" 14 | "slices" 15 | ) 16 | 17 | var ( 18 | cacheSecurityCodeList = filepath.Join(cache.GetMetaPath(), "securities.csv") 19 | ) 20 | 21 | var ( 22 | __mapStockList = map[string]quotes.Security{} // 股票列表缓存 23 | __onceStockList coroutine.PeriodicOnce 24 | __stock_list = []string{} 25 | ) 26 | 27 | // 读取股票列表缓存 28 | func readCacheSecurityList() { 29 | filename := cacheSecurityCodeList 30 | if !api.FileExist(filename) { 31 | return 32 | } 33 | list := []quotes.Security{} 34 | err := api.CsvToSlices(filename, &list) 35 | if err != nil || len(list) == 0 { 36 | return 37 | } 38 | for _, v := range list { 39 | code := v.Code 40 | __mapStockList[code] = v 41 | } 42 | reloadCodeList() 43 | } 44 | 45 | func reloadCodeList() { 46 | list := api.Keys(__mapStockList) 47 | list = api.Unique(list) 48 | __stock_list = slices.Clone(list) 49 | } 50 | 51 | func writeCacheSecurityList(list []quotes.Security) { 52 | filename := cacheSecurityCodeList 53 | api.SliceSort(list, func(a, b quotes.Security) bool { 54 | return a.Code < b.Code 55 | }) 56 | _ = api.SlicesToCsv(filename, list) 57 | } 58 | 59 | func lazyLoadStockList() { 60 | filename := cacheSecurityCodeList 61 | bUpdated := false 62 | if !api.FileExist(filename) { 63 | // 不存在需要创建 64 | bUpdated = true 65 | } else { 66 | // 存在, 先加载一次 67 | readCacheSecurityList() 68 | // 获取文件创建时间 69 | finfo, _ := os.Stat(filename) 70 | bUpdated = exchange.CanInitialize(finfo.ModTime()) 71 | } 72 | if !bUpdated { 73 | return 74 | } 75 | list := getSecurityList() 76 | if len(list) == 0 { 77 | return 78 | } 79 | // 覆盖当前缓存 80 | for _, v := range list { 81 | code := v.Code 82 | __mapStockList[code] = v 83 | } 84 | // 更新缓存 85 | list = api.Values(__mapStockList) 86 | // 更新代码列表 87 | reloadCodeList() 88 | writeCacheSecurityList(list) 89 | } 90 | 91 | // getSecurityList 证券列表 92 | func getSecurityList() (allList []quotes.Security) { 93 | stdApi := gotdx.GetTdxApi() 94 | offset := uint16(quotes.TDX_SECURITY_LIST_MAX) 95 | start := uint16(0) 96 | for { 97 | reply, err := stdApi.GetSecurityList(exchange.MarketIdShangHai, start) 98 | if err != nil { 99 | return 100 | } 101 | for i := 0; i < int(reply.Count); i++ { 102 | security := &reply.List[i] 103 | security.Code = "sh" + security.Code 104 | if exchange.AssertBlockBySecurityCode(&(security.Code)) { 105 | blk := GetBlockInfo(security.Code) 106 | if blk != nil { 107 | security.Name = blk.Name 108 | } 109 | } 110 | } 111 | list := api.Filter(reply.List, checkIndexAndStock) 112 | if len(list) > 0 { 113 | allList = append(allList, list...) 114 | } 115 | if reply.Count < offset { 116 | break 117 | } 118 | start += reply.Count 119 | } 120 | start = uint16(0) 121 | for { 122 | reply, err := stdApi.GetSecurityList(exchange.MarketIdShenZhen, start) 123 | if err != nil { 124 | return 125 | } 126 | for i := 0; i < int(reply.Count); i++ { 127 | reply.List[i].Code = "sz" + reply.List[i].Code 128 | } 129 | list := api.Filter(reply.List, checkIndexAndStock) 130 | if len(list) > 0 { 131 | allList = append(allList, list...) 132 | } 133 | if reply.Count < offset { 134 | break 135 | } 136 | start += reply.Count 137 | } 138 | 139 | return 140 | } 141 | 142 | // CheckoutSecurityInfo 获取证券信息 143 | func CheckoutSecurityInfo(securityCode string) (*quotes.Security, bool) { 144 | __onceStockList.Do(lazyLoadStockList) 145 | securityCode = exchange.CorrectSecurityCode(securityCode) 146 | security, ok := __mapStockList[securityCode] 147 | if ok { 148 | return &security, true 149 | } 150 | return nil, false 151 | } 152 | 153 | // 检查指数和个股 154 | func checkIndexAndStock(security quotes.Security) bool { 155 | if exchange.AssertIndexBySecurityCode(security.Code) { 156 | return true 157 | } 158 | if exchange.AssertStockBySecurityCode(security.Code) { 159 | return true 160 | } 161 | return false 162 | } 163 | 164 | // GetStockName 获取证券名称 165 | func GetStockName(securityCode string) string { 166 | security, ok := CheckoutSecurityInfo(securityCode) 167 | if ok { 168 | return security.Name 169 | } 170 | return "Unknown" 171 | } 172 | 173 | // AllCodeList 获取全部证券代码 174 | func AllCodeList() []string { 175 | __onceStockList.Do(lazyLoadStockList) 176 | return __stock_list 177 | } 178 | 179 | // SecurityBaseUnit 获取证券标价格的最小变动单位, 0.01返回100, 0.001返回1000 180 | func SecurityBaseUnit(marketId exchange.MarketType, code string) float64 { 181 | securityCode := exchange.GetSecurityCode(marketId, code) 182 | securityInfo, ok := CheckoutSecurityInfo(securityCode) 183 | if !ok { 184 | return 100.00 185 | } 186 | return math.Pow10(int(securityInfo.DecimalPoint)) 187 | } 188 | 189 | // SecurityPriceDigits 获取证券标的价格保留小数点后几位 190 | // 191 | // 默认范围2, 即小数点后2位 192 | func SecurityPriceDigits(marketId exchange.MarketType, code string) int { 193 | securityCode := exchange.GetSecurityCode(marketId, code) 194 | securityInfo, ok := CheckoutSecurityInfo(securityCode) 195 | if !ok { 196 | return 2 197 | } 198 | return int(securityInfo.DecimalPoint) 199 | } 200 | 201 | func init() { 202 | internal.RegisterBaseUnitFunction(SecurityBaseUnit) 203 | } 204 | -------------------------------------------------------------------------------- /securities/security_list_test.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "fmt" 5 | "gitee.com/quant1x/exchange" 6 | "gitee.com/quant1x/gotdx/internal" 7 | "testing" 8 | ) 9 | 10 | func TestGetStockName(t *testing.T) { 11 | code := "sh880635" 12 | v := GetStockName(code) 13 | fmt.Println(v) 14 | } 15 | 16 | func TestAllCodeList(t *testing.T) { 17 | v := AllCodeList() 18 | fmt.Println(v) 19 | } 20 | 21 | func TestBaseUnit(t *testing.T) { 22 | marketId := exchange.MarketIdShangHai 23 | code := "000001" 24 | v := internal.BaseUnit(marketId, code) 25 | fmt.Println(v) 26 | } 27 | -------------------------------------------------------------------------------- /tdx-client.go: -------------------------------------------------------------------------------- 1 | package gotdx 2 | 3 | import ( 4 | "gitee.com/quant1x/gotdx/quotes" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | stdApi *quotes.StdApi = nil 10 | tdxMutex sync.Mutex 11 | ) 12 | 13 | func initTdxApi() { 14 | if stdApi == nil { 15 | api_, err := quotes.NewStdApi() 16 | if err != nil { 17 | return 18 | } 19 | stdApi = api_ 20 | } 21 | } 22 | 23 | func GetTdxApi() *quotes.StdApi { 24 | tdxMutex.Lock() 25 | defer tdxMutex.Unlock() 26 | initTdxApi() 27 | return stdApi 28 | } 29 | 30 | func ReOpen() { 31 | tdxMutex.Lock() 32 | defer tdxMutex.Unlock() 33 | if stdApi != nil { 34 | stdApi.Close() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tdx-client_test.go: -------------------------------------------------------------------------------- 1 | package gotdx 2 | 3 | import ( 4 | "fmt" 5 | "gitee.com/quant1x/gotdx/proto" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestReOpen(t *testing.T) { 11 | api := GetTdxApi() 12 | v, _ := api.GetXdxrInfo("sh600072") 13 | fmt.Println(v) 14 | time.Sleep(20 * time.Second) 15 | ReOpen() 16 | v, _ = api.GetXdxrInfo("sh600072") 17 | fmt.Println(v) 18 | fmt.Println(api.NumOfServers()) 19 | klines, _ := api.GetKLine("sh600600", proto.KLINE_TYPE_RI_K, 0, 1) 20 | fmt.Println(klines) 21 | } 22 | --------------------------------------------------------------------------------