├── .gitignore ├── quotes ├── resources │ ├── tdx.cfg │ ├── huatai.cfg │ ├── zhongxin.cfg │ └── guotaijunan.cfg ├── base_timer_test.go ├── bestip_cache_test.go ├── bestip_test.go ├── base_consts.go ├── base_client_test.go ├── bestip_command.go ├── stock_finance_info_test.go ├── stock_company_info_category_test.go ├── stock_security_list_test.go ├── stock_xdxr_info_test.go ├── stock_security_bars_test.go ├── stock_security_quotes_test.go ├── stock_transaction_data_test.go ├── stock_security_snapshot_test.go ├── stock_minute_time_test.go ├── base_message_test.go ├── cmd_hearbeat.go ├── cmd_hello1.go ├── base_timer.go ├── stock_security_count.go ├── base_pool.go ├── cmd_hello2.go ├── block_meta.go ├── block_info.go ├── bestip_client.go ├── stock_minute_time_data.go ├── bestip.go ├── stock_security_quotes_new_test.go ├── stock_minute_time_data_history.go ├── bestip_cache.go ├── stock_company_info_category.go ├── api_test.go ├── stock_security_list.go ├── stock_transaction_data_history.go ├── stock_transaction_data.go ├── base_message.go ├── base_client.go ├── targets.go ├── stock_security_bars.go ├── stock_security_snapshot.go ├── index_bars.go ├── api.go ├── stock_company_info_content.go ├── bestip_embed.go ├── stock_xdxr_info.go ├── stock_security_quotes_new.go └── bestip.txt ├── securities ├── resources │ ├── tdxzs.cfg │ ├── tdxzs3.cfg │ └── README.md ├── block_test.go ├── security_list_test.go ├── margin_trading_test.go ├── block_data.go ├── block_type.go ├── block_embed.go ├── block_config.go ├── block.go ├── block_industry.go ├── block_parse.go ├── block_raw.go ├── security_list.go └── margin_trading.go ├── proto ├── command_test.go ├── std │ ├── setup_cmd2.go │ ├── setup_cmd1.go │ ├── setup_cmd3.go │ ├── security_count.go │ ├── protocol.go │ ├── security_list.go │ └── finance_info.go ├── ext │ └── ex_cmd1.go ├── TdxProtocol.md └── command.go ├── internal ├── sequence_test.go ├── sequence.go ├── hex_test.go ├── time_test.go ├── hex.go ├── varints_test.go ├── varints.go ├── unit.go ├── compress.go ├── number_test.go ├── number.go ├── strings.go └── time.go ├── client_test.go ├── README.md ├── client.go ├── go.mod ├── LICENSE └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | *.csv -------------------------------------------------------------------------------- /quotes/resources/tdx.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/HEAD/quotes/resources/tdx.cfg -------------------------------------------------------------------------------- /quotes/resources/huatai.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/HEAD/quotes/resources/huatai.cfg -------------------------------------------------------------------------------- /quotes/resources/zhongxin.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/HEAD/quotes/resources/zhongxin.cfg -------------------------------------------------------------------------------- /quotes/resources/guotaijunan.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/HEAD/quotes/resources/guotaijunan.cfg -------------------------------------------------------------------------------- /securities/resources/tdxzs.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/HEAD/securities/resources/tdxzs.cfg -------------------------------------------------------------------------------- /securities/resources/tdxzs3.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quant1x/gotdx/HEAD/securities/resources/tdxzs3.cfg -------------------------------------------------------------------------------- /quotes/base_timer_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import "testing" 4 | 5 | func TestExamplePinger(t *testing.T) { 6 | ExamplePinger() 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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(HOST_HQ) 10 | fmt.Printf("%+v\n", list) 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ``` -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | nTime = 10100682 15 | nTime = 8836243 16 | nTime = 150006364 17 | t1 := time.UnixMilli(int64(nTime)) 18 | fmt.Println(t1) 19 | //nTime = 8 20 | s := TimeFromInt(nTime) 21 | fmt.Println(s) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /quotes/bestip_command.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gitee.com/quant1x/gotdx/proto/std" 7 | ) 8 | 9 | func CommandWithConn(cli *LabClient, callback std.Factory) (std.Unmarshaler, error) { 10 | req, resp, err := callback() 11 | if err != nil { 12 | fmt.Println(err) 13 | return nil, err 14 | } 15 | err = cli.Do(req, resp) 16 | if err != nil { 17 | fmt.Println(err) 18 | _ = cli.Close() 19 | return nil, err 20 | } 21 | return resp, nil 22 | } 23 | -------------------------------------------------------------------------------- /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 | code = "880978" 21 | code = "881003" 22 | v := GetBlockInfo(code) 23 | fmt.Println(v) 24 | } 25 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package gotdx 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "gitee.com/quant1x/gotdx/proto" 9 | ) 10 | 11 | func TestReOpen(t *testing.T) { 12 | api := GetTdxApi() 13 | v, _ := api.GetXdxrInfo("sh600072") 14 | fmt.Println(v) 15 | time.Sleep(20 * time.Second) 16 | ReOpen() 17 | v, _ = api.GetXdxrInfo("sh600072") 18 | fmt.Println(v) 19 | fmt.Println(api.NumOfServers()) 20 | klines, _ := api.GetKLine("sh600600", proto.KLINE_TYPE_RI_K, 0, 1) 21 | fmt.Println(klines) 22 | } 23 | -------------------------------------------------------------------------------- /quotes/stock_finance_info_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | func TestNewFinanceInfoPackage(t *testing.T) { 12 | stdApi, err := NewStdApi() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer stdApi.Close() 17 | sq1, err := stdApi.GetFinanceInfo("bj920116") 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 | -------------------------------------------------------------------------------- /quotes/stock_company_info_category_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | func TestCompanyInfoCategoryPackage(t *testing.T) { 12 | stdApi, err := NewStdApi() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer stdApi.Close() 17 | reply, err := stdApi.GetCompanyInfoCategory("sh600977") 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 | -------------------------------------------------------------------------------- /securities/security_list_test.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "gitee.com/quant1x/exchange" 8 | "gitee.com/quant1x/gotdx/internal" 9 | ) 10 | 11 | func TestGetStockName(t *testing.T) { 12 | code := "sh880635" 13 | v := GetStockName(code) 14 | fmt.Println(v) 15 | } 16 | 17 | func TestAllCodeList(t *testing.T) { 18 | v := AllCodeList() 19 | fmt.Println(v) 20 | } 21 | 22 | func TestBaseUnit(t *testing.T) { 23 | marketId := exchange.MarketIdShangHai 24 | code := "000001" 25 | v := internal.BaseUnit(marketId, code) 26 | fmt.Println(v) 27 | } 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package gotdx 2 | 3 | import ( 4 | "sync" 5 | 6 | "gitee.com/quant1x/gotdx/quotes" 7 | ) 8 | 9 | var ( 10 | stdApi *quotes.StdApi = nil 11 | tdxMutex sync.Mutex 12 | ) 13 | 14 | func initTdxApi() { 15 | if stdApi == nil { 16 | api_, err := quotes.NewStdApi() 17 | if err != nil { 18 | return 19 | } 20 | stdApi = api_ 21 | } 22 | } 23 | 24 | func GetTdxApi() *quotes.StdApi { 25 | tdxMutex.Lock() 26 | defer tdxMutex.Unlock() 27 | initTdxApi() 28 | return stdApi 29 | } 30 | 31 | func ReOpen() { 32 | tdxMutex.Lock() 33 | defer tdxMutex.Unlock() 34 | if stdApi != nil { 35 | stdApi.Close() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /quotes/stock_security_list_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/exchange" 9 | "gitee.com/quant1x/gox/api" 10 | ) 11 | 12 | func TestSecurityListPackage(t *testing.T) { 13 | stdApi, err := NewStdApi() 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer stdApi.Close() 18 | reply, err := stdApi.GetSecurityList(exchange.MarketIdBeiJing, 0) 19 | if err != nil { 20 | fmt.Printf("%+v\n", err) 21 | } 22 | fmt.Printf("%+v\n", reply) 23 | fmt.Println("==========") 24 | data, _ := json.Marshal(reply) 25 | text := api.Bytes2String(data) 26 | fmt.Println(text) 27 | } 28 | -------------------------------------------------------------------------------- /internal/varints_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "testing" 4 | 5 | func TestDecodeVarint(t *testing.T) { 6 | pos := 0 7 | type args struct { 8 | b []byte 9 | pos *int 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want int 15 | }{ 16 | { 17 | name: "1", 18 | args: args{ 19 | b: []byte{255, 136, 15}, 20 | pos: &pos, 21 | }, 22 | want: -123456, 23 | }, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | if got := DecodeVarint(tt.args.b, tt.args.pos); got != tt.want { 28 | t.Errorf("DecodeVarint() = %v, want %v", got, tt.want) 29 | } 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gitee.com/quant1x/gotdx 2 | 3 | go 1.25.0 4 | 5 | require ( 6 | gitee.com/quant1x/asio v1.1.5 7 | gitee.com/quant1x/exchange v0.8.11 8 | gitee.com/quant1x/gox v1.25.1 9 | gitee.com/quant1x/num v0.7.9 10 | golang.org/x/text v0.29.0 11 | gopkg.in/ini.v1 v1.67.0 12 | ) 13 | 14 | require ( 15 | gitee.com/quant1x/pkg v0.8.3 // indirect 16 | github.com/dlclark/regexp2 v1.11.5 // indirect 17 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 18 | github.com/google/pprof v0.0.0-20250919162441-8b542baf5bcf // indirect 19 | github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect 20 | golang.org/x/sys v0.36.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /quotes/stock_xdxr_info_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | func TestXdxrInfoPackage(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.GetXdxrInfo("sh600115") 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | unit := 100.00 13 | if marketId == exchange.MarketIdShangHai { 14 | c := code[:2] 15 | switch c { 16 | case "51": 17 | unit = 1000.00 18 | } 19 | } else if marketId == exchange.MarketIdShenZhen { 20 | c := code[:3] 21 | switch c { 22 | case "159": 23 | unit = 1000.0 24 | } 25 | } 26 | 27 | return unit 28 | } 29 | 30 | type unitHandler func(marketId exchange.MarketType, code string) float64 31 | 32 | var ( 33 | BaseUnit unitHandler = defaultBaseUnit 34 | ) 35 | 36 | func RegisterBaseUnitFunction(f unitHandler) { 37 | BaseUnit = f 38 | } 39 | -------------------------------------------------------------------------------- /internal/compress.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "io" 7 | 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | // ZlibCompress 进行zlib压缩 12 | func ZlibCompress(src []byte) ([]byte, error) { 13 | var in bytes.Buffer 14 | w := zlib.NewWriter(&in) 15 | _, err := w.Write(src) 16 | if err != nil { 17 | return nil, err 18 | } 19 | err = w.Close() 20 | if err != nil { 21 | return nil, err 22 | } 23 | return in.Bytes(), nil 24 | } 25 | 26 | // ZlibUnCompress 进行zlib解压缩 27 | func ZlibUnCompress(compressSrc []byte) ([]byte, error) { 28 | b := bytes.NewReader(compressSrc) 29 | var out bytes.Buffer 30 | r, err := zlib.NewReader(b) 31 | if err != nil { 32 | return nil, err 33 | } 34 | defer api.CloseQuietly(r) 35 | _, err = io.Copy(&out, r) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return out.Bytes(), nil 40 | } 41 | -------------------------------------------------------------------------------- /quotes/stock_security_bars_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/gotdx/proto" 9 | "gitee.com/quant1x/gox/api" 10 | ) 11 | 12 | func TestSecurityBarsPackage(t *testing.T) { 13 | stdApi, err := NewStdApiWithServers([]Server{{Host: "123.125.108.14", Port: 7709, Name: "test"}}) 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer stdApi.Close() 18 | //sq1, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShenZhen}, []string{"600275", "600455", "600086", "300742"}) 19 | sq1, err := stdApi.GetKLine("sz000001", proto.KLINE_TYPE_1MIN, 0, 5) 20 | if err != nil { 21 | fmt.Printf("%+v\n", err) 22 | } 23 | fmt.Printf("%+v\n", sq1) 24 | data, _ := json.Marshal(sq1) 25 | text := api.Bytes2String(data) 26 | fmt.Println(text) 27 | } 28 | -------------------------------------------------------------------------------- /quotes/stock_security_quotes_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/exchange" 9 | "gitee.com/quant1x/gox/api" 10 | ) 11 | 12 | func TestSecurityQuotesPackage(t *testing.T) { 13 | stdApi, err := NewStdApi() 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer stdApi.Close() 18 | //sq1, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShenZhen}, []string{"600275", "600455", "600086", "300742"}) 19 | sq1, err := stdApi.GetSecurityQuotes( 20 | []uint8{exchange.MarketIdShangHai, exchange.MarketIdShangHai, exchange.MarketIdShangHai, exchange.MarketIdShangHai, exchange.MarketIdShenZhen}, 21 | []string{"000001", "000002", "880005", "880656", "399107"}) 22 | if err != nil { 23 | fmt.Printf("%+v\n", err) 24 | } 25 | fmt.Printf("%+v\n", sq1) 26 | data, _ := json.Marshal(sq1) 27 | text := api.Bytes2String(data) 28 | fmt.Println(text) 29 | } 30 | -------------------------------------------------------------------------------- /proto/std/setup_cmd2.go: -------------------------------------------------------------------------------- 1 | package std 2 | 3 | import ( 4 | "gitee.com/quant1x/gotdx/internal" 5 | ) 6 | 7 | // SetupCmd2Request 请求包结构 8 | type SetupCmd2Request struct { 9 | Cmd []byte `struc:"[13]byte" json:"cmd"` 10 | } 11 | 12 | // Marshal 请求包序列化输出 13 | func (req *SetupCmd2Request) Marshal() ([]byte, error) { 14 | return DefaultMarshal(req) 15 | } 16 | 17 | // SetupCmd2Response 响应包结构 18 | type SetupCmd2Response struct { 19 | Unknown []byte `json:"unknown"` 20 | } 21 | 22 | func (resp *SetupCmd2Response) Unmarshal(data []byte) error { 23 | resp.Unknown = data 24 | return nil 25 | } 26 | 27 | // NewSetupCmd2Request 创建SetupCmd2请求包 28 | func NewSetupCmd2Request() (*SetupCmd2Request, error) { 29 | request := &SetupCmd2Request{ 30 | Cmd: internal.HexString2Bytes("0c 02 18 94 00 01 03 00 03 00 0d 00 02"), 31 | } 32 | return request, nil 33 | } 34 | 35 | func NewSetupCmd2() (*SetupCmd2Request, *SetupCmd2Response, error) { 36 | var response SetupCmd2Response 37 | var request, err = NewSetupCmd2Request() 38 | return request, &response, err 39 | } 40 | -------------------------------------------------------------------------------- /securities/block_data.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "os" 5 | 6 | "gitee.com/quant1x/exchange" 7 | "gitee.com/quant1x/gotdx/quotes" 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | // 同步板块数据 12 | func syncBlockFiles() { 13 | downloadBlockRawData(quotes.BLOCK_DEFAULT) 14 | downloadBlockRawData(quotes.BLOCK_GAINIAN) 15 | downloadBlockRawData(quotes.BLOCK_FENGGE) 16 | downloadBlockRawData(quotes.BLOCK_ZHISHU) 17 | updateCacheBlockFile() 18 | } 19 | 20 | // 更新缓存csv数据文件 21 | func updateCacheBlockFile() { 22 | // 如果板块数据不存在, 从应用内导出 23 | blockFile := SectorFilename() 24 | createOrUpdate := false 25 | if !api.FileExist(blockFile) { 26 | createOrUpdate = true 27 | } else { 28 | dataStat, err := os.Stat(blockFile) 29 | if err == nil || os.IsExist(err) { 30 | dataModifyTime := dataStat.ModTime() 31 | toInit := exchange.CanInitialize(dataModifyTime) 32 | if toInit { 33 | createOrUpdate = true 34 | } 35 | } else { 36 | createOrUpdate = true 37 | } 38 | } 39 | if createOrUpdate { 40 | parseAndGenerateBlockFile() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /quotes/stock_transaction_data_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | func TestTransaction(t *testing.T) { 12 | stdApi, err := NewStdApi() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer stdApi.Close() 17 | reply, err := stdApi.GetTransactionData("sh600010", 0, 2) 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 TestHistoryTransaction(t *testing.T) { 28 | stdApi, err := NewStdApi() 29 | if err != nil { 30 | panic(err) 31 | } 32 | defer stdApi.Close() 33 | code := "sh880534" 34 | date := 20240110 35 | code = "sh600010" 36 | date = 20250417 37 | reply, err := stdApi.GetHistoryTransactionData(code, uint32(date), 0, 2) 38 | if err != nil { 39 | fmt.Printf("%+v\n", err) 40 | } 41 | fmt.Printf("%+v\n", reply) 42 | data, _ := json.Marshal(reply) 43 | text := api.Bytes2String(data) 44 | fmt.Println(text) 45 | } 46 | -------------------------------------------------------------------------------- /quotes/stock_security_snapshot_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | func TestSnapshotPackage(t *testing.T) { 12 | stdApi, err := NewStdApiWithServers([]Server{Server{Host: "123.125.108.14", Port: 7709, Name: "test"}}) 13 | //stdApi, err := NewStdApi() 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer stdApi.Close() 18 | //sq1, err := stdApi.GetSecurityQuotes([]uint8{proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShangHai, proto.MarketIdShenZhen}, []string{"600275", "600455", "600086", "300742"}) 19 | sq1, err := stdApi.GetSnapshot([]string{"sh000001", "600105", "880656", "880367", "510050", "000666", "bj833171"}) 20 | if err != nil { 21 | fmt.Printf("%+v\n", err) 22 | } 23 | fmt.Printf("%+v\n", sq1) 24 | data, _ := json.Marshal(sq1) 25 | text := api.Bytes2String(data) 26 | fmt.Println(text) 27 | } 28 | 29 | func TestSnapshot_DetectBiddingPhase(t *testing.T) { 30 | cacheList := []Snapshot{} 31 | filename := "600903.csv" 32 | err := api.CsvToSlices(filename, &cacheList) 33 | fmt.Println(err) 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /proto/std/setup_cmd1.go: -------------------------------------------------------------------------------- 1 | package std 2 | 3 | import ( 4 | "gitee.com/quant1x/gotdx/internal" 5 | ) 6 | 7 | // SetupCmd1Request 请求包结构 8 | type SetupCmd1Request struct { 9 | Cmd []byte `struc:"[13]byte" json:"cmd"` 10 | } 11 | 12 | // Marshal 请求包序列化输出 13 | func (req *SetupCmd1Request) Marshal() ([]byte, error) { 14 | return DefaultMarshal(req) 15 | } 16 | 17 | // SetupCmd1Response 响应包结构 18 | // serverInfo := Utf8ToGbk(data[68:]) 19 | type SetupCmd1Response struct { 20 | Unknown []byte `json:"unknown"` 21 | Reply string `json:"reply"` 22 | } 23 | 24 | func (resp *SetupCmd1Response) Unmarshal(data []byte) error { 25 | //resp.Unknown = data 26 | resp.Reply = internal.Utf8ToGbk(data[68:]) 27 | return nil 28 | } 29 | 30 | // 创建SetupCmd1请求包 31 | func NewSetupCmd1Request() (*SetupCmd1Request, error) { 32 | request := &SetupCmd1Request{ 33 | Cmd: internal.HexString2Bytes("0c 02 18 93 00 01 03 00 03 00 0d 00 01"), 34 | } 35 | return request, nil 36 | } 37 | 38 | func NewSetupCmd1() (*SetupCmd1Request, *SetupCmd1Response, error) { 39 | var response SetupCmd1Response 40 | var request, err = NewSetupCmd1Request() 41 | return request, &response, err 42 | } 43 | -------------------------------------------------------------------------------- /proto/std/setup_cmd3.go: -------------------------------------------------------------------------------- 1 | package std 2 | 3 | import ( 4 | "gitee.com/quant1x/gotdx/internal" 5 | ) 6 | 7 | // SetupCmd3Request 请求包结构 8 | type SetupCmd3Request struct { 9 | Cmd []byte `struc:"[42]byte" json:"cmd"` 10 | } 11 | 12 | // Marshal 请求包序列化输出 13 | func (req *SetupCmd3Request) Marshal() ([]byte, error) { 14 | return DefaultMarshal(req) 15 | } 16 | 17 | // SetupCmd3Response 响应包结构 18 | type SetupCmd3Response struct { 19 | Unknown []byte `json:"unknown"` 20 | } 21 | 22 | func (resp *SetupCmd3Response) Unmarshal(data []byte) error { 23 | resp.Unknown = data 24 | return nil 25 | } 26 | 27 | // NewSetupCmd3Request 创建SetupCmd3请求包 28 | func NewSetupCmd3Request() (*SetupCmd3Request, error) { 29 | request := &SetupCmd3Request{ 30 | Cmd: internal.HexString2Bytes("0c 03 18 99 00 01 20 00 20 00 db 0f d5" + 31 | "d0 c9 cc d6 a4 a8 af 00 00 00 8f c2 25" + 32 | "40 13 00 00 d5 00 c9 cc bd f0 d7 ea 00" + 33 | "00 00 02"), 34 | } 35 | return request, nil 36 | } 37 | 38 | func NewSetupCmd3() (*SetupCmd3Request, *SetupCmd3Response, error) { 39 | var response SetupCmd3Response 40 | var request, err = NewSetupCmd3Request() 41 | return request, &response, err 42 | } 43 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /proto/std/security_count.go: -------------------------------------------------------------------------------- 1 | package std 2 | 3 | import ( 4 | "gitee.com/quant1x/exchange" 5 | "gitee.com/quant1x/gotdx/internal" 6 | ) 7 | 8 | // SecurityCountRequest 请求包结构 9 | type SecurityCountRequest struct { 10 | Unknown1 []byte `struc:"[12]byte"` 11 | Market exchange.MarketType `struc:"uint16,little" json:"market"` 12 | Unknown2 []byte `struc:"[4]byte"` 13 | } 14 | 15 | // Marshal 请求包序列化输出 16 | func (req *SecurityCountRequest) Marshal() ([]byte, error) { 17 | return DefaultMarshal(req) 18 | } 19 | 20 | // SecurityCountResponse 响应包结构 21 | type SecurityCountResponse struct { 22 | Count uint `struc:"uint16,little" json:"count"` 23 | } 24 | 25 | func (resp *SecurityCountResponse) Unmarshal(data []byte) error { 26 | return DefaultUnmarshal(data, resp) 27 | } 28 | 29 | // todo: 检测market是否为合法值 30 | func NewSecurityCountRequest(market exchange.MarketType) (*SecurityCountRequest, error) { 31 | request := &SecurityCountRequest{ 32 | Unknown1: internal.HexString2Bytes("0c 0c 18 6c 00 01 08 00 08 00 4e 04"), 33 | Market: market, 34 | Unknown2: internal.HexString2Bytes("75 c7 33 01"), 35 | } 36 | return request, nil 37 | } 38 | 39 | func NewSecurityCount(market exchange.MarketType) (*SecurityCountRequest, *SecurityCountResponse, error) { 40 | var response SecurityCountResponse 41 | var request, err = NewSecurityCountRequest(market) 42 | return request, &response, err 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /quotes/stock_minute_time_test.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | func TestStockMinuteTime(t *testing.T) { 12 | stdApi, err := NewStdApi() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer stdApi.Close() 17 | code := "sh510050" 18 | reply, err := stdApi.GetMinuteTimeData(code) 19 | if err != nil { 20 | fmt.Printf("%+v\n", err) 21 | } 22 | fmt.Printf("%+v\n", reply) 23 | data, _ := json.Marshal(reply) 24 | text := api.Bytes2String(data) 25 | fmt.Println(text) 26 | } 27 | 28 | func TestStockMinuteTimeHistory(t *testing.T) { 29 | stdApi, err := NewStdApi() 30 | if err != nil { 31 | panic(err) 32 | } 33 | defer stdApi.Close() 34 | code := "sz000666" 35 | code = "sh000001" 36 | code = "sh510050" 37 | code = "sz159915" 38 | //code = "sh600178" 39 | //code = "sh513100" 40 | code = "sh563210" 41 | var date uint32 = 20250805 42 | reply, err := stdApi.GetHistoryMinuteTimeData(code, date) 43 | if err != nil { 44 | fmt.Printf("%+v\n", err) 45 | } 46 | fmt.Printf("%+v\n", reply) 47 | data, _ := json.Marshal(reply) 48 | text := api.Bytes2String(data) 49 | fmt.Println(text) 50 | 51 | reply, err = stdApi.GetHistoryMinuteTimeData(code, date) 52 | if err != nil { 53 | fmt.Printf("%+v\n", err) 54 | } 55 | fmt.Printf("%+v\n", reply) 56 | data, _ = json.Marshal(reply) 57 | text = api.Bytes2String(data) 58 | fmt.Println(text) 59 | } 60 | -------------------------------------------------------------------------------- /securities/block_config.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "gitee.com/quant1x/exchange/cache" 10 | "gitee.com/quant1x/gox/api" 11 | "gitee.com/quant1x/gox/text/encoding" 12 | ) 13 | 14 | // 加载板块和板块名称对应 15 | func loadIndexBlockInfos() []BlockInfo { 16 | bks := []string{"tdxzs.cfg", "tdxzs3.cfg"} 17 | bis := []BlockInfo{} 18 | tmpMap := map[string]BlockInfo{} 19 | for _, v := range bks { 20 | bi := getBlockInfoFromConfig(v) 21 | if len(bi) == 0 { 22 | continue 23 | } 24 | for _, info := range bi { 25 | if bv, ok := tmpMap[info.Code]; !ok { 26 | bis = append(bis, info) 27 | tmpMap[info.Code] = info 28 | } else { 29 | _ = bv 30 | } 31 | } 32 | } 33 | return bis 34 | } 35 | 36 | func getBlockInfoFromConfig(name string) []BlockInfo { 37 | cacheFilename := cache.GetBlockPath() + "/" + name 38 | if !api.FileExist(cacheFilename) { 39 | // 如果文件不存在, 导出内嵌资源 40 | err := export(cacheFilename, name) 41 | if err != nil { 42 | return nil 43 | } 44 | } 45 | file, err := os.Open(cacheFilename) 46 | if err != nil { 47 | return nil 48 | } 49 | defer api.CloseQuietly(file) 50 | reader := bufio.NewReader(file) 51 | // 按行处理txt 52 | decoder := encoding.NewDecoder("GBK") 53 | var blocks = []BlockInfo{} 54 | for { 55 | data, _, err := reader.ReadLine() 56 | if err == io.EOF { 57 | break 58 | } 59 | line := decoder.ConvertString(string(data)) 60 | arr := strings.Split(line, "|") 61 | bk := BlockInfo{ 62 | Name: arr[0], 63 | Code: arr[1], 64 | Type: int(api.ParseInt(arr[2])), 65 | Block: arr[5], 66 | } 67 | blocks = append(blocks, bk) 68 | } 69 | return blocks 70 | } 71 | -------------------------------------------------------------------------------- /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 | "io" 10 | "testing" 11 | 12 | "gitee.com/quant1x/gox/logger" 13 | ) 14 | 15 | func parseResponseHeader(data []byte) (*StdResponseHeader, []byte, error) { 16 | var header StdResponseHeader 17 | //err := cstruct.Unpack(data, &header) 18 | headerBuf := bytes.NewReader(data) 19 | err := binary.Read(headerBuf, binary.LittleEndian, &header) 20 | if err != nil { 21 | return nil, nil, err 22 | } 23 | fmt.Println(headerBuf.Len(), headerBuf.Size()) 24 | pos := int(headerBuf.Size()) - headerBuf.Len() 25 | if header.ZipSize > MessageMaxBytes { 26 | logger.Debugf("msgData has bytes(%d) beyond max %d\n", header.ZipSize, MessageMaxBytes) 27 | return &header, nil, ErrBadData 28 | } 29 | var out bytes.Buffer 30 | var body []byte 31 | if header.ZipSize != header.UnZipSize { 32 | b := bytes.NewReader(data[pos:]) 33 | r, _ := zlib.NewReader(b) 34 | _, _ = io.Copy(&out, r) 35 | body = out.Bytes() 36 | _ = r.Close() 37 | } else { 38 | body = data[pos:] 39 | } 40 | return &header, body, err 41 | 42 | } 43 | 44 | func TestProcess(t *testing.T) { 45 | hexString := "b1cb74000c760028000004000a000a0000000000000000000000" 46 | data, err := hex.DecodeString(hexString) 47 | if err != nil { 48 | fmt.Println(err) 49 | } 50 | respHeader, respBody, err := parseResponseHeader(data) 51 | if err != nil { 52 | fmt.Println(err) 53 | } 54 | fmt.Printf("%+v\n", respHeader) 55 | fmt.Printf("%+v\n", respBody) 56 | bodyBuff := bytes.NewReader(respBody) 57 | var resp HeartBeatReply 58 | err = binary.Read(bodyBuff, binary.LittleEndian, &resp) 59 | if err != nil { 60 | fmt.Println(err) 61 | } 62 | fmt.Printf("%+v\n", resp) 63 | } 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /quotes/cmd_hearbeat.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | ) 11 | 12 | // 心跳包, command: 0004 13 | // 0c76002800 02 0200 0200 0400 14 | // b1cb74000c760028000004000a000a00 00000000000000000000 15 | 16 | type HeartBeatPackage struct { 17 | reqHeader *StdRequestHeader 18 | request *HeartBeatRequest 19 | respHeader *StdResponseHeader 20 | reply *HeartBeatReply 21 | 22 | contentHex string 23 | } 24 | 25 | type HeartBeatRequest struct { 26 | } 27 | 28 | type HeartBeatReply struct { 29 | Info string // 10个字节的消息, 未解 30 | } 31 | 32 | func NewHeartBeat() *HeartBeatPackage { 33 | obj := new(HeartBeatPackage) 34 | obj.reqHeader = new(StdRequestHeader) 35 | obj.respHeader = new(StdResponseHeader) 36 | obj.request = new(HeartBeatRequest) 37 | obj.reply = new(HeartBeatReply) 38 | 39 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 40 | obj.reqHeader.SeqID = internal.SequenceId() 41 | obj.reqHeader.PacketType = 0x02 42 | obj.reqHeader.Method = proto.STD_MSG_HEARTBEAT 43 | return obj 44 | } 45 | 46 | func (obj *HeartBeatPackage) Serialize() ([]byte, error) { 47 | b, err := hex.DecodeString(obj.contentHex) 48 | 49 | obj.reqHeader.PkgLen1 = 2 + uint16(len(b)) 50 | obj.reqHeader.PkgLen2 = 2 + uint16(len(b)) 51 | 52 | buf := new(bytes.Buffer) 53 | err = binary.Write(buf, binary.LittleEndian, obj.reqHeader) 54 | 55 | buf.Write(b) 56 | return buf.Bytes(), err 57 | } 58 | 59 | func (obj *HeartBeatPackage) UnSerialize(header interface{}, data []byte) error { 60 | obj.respHeader = header.(*StdResponseHeader) 61 | serverInfo := internal.Utf8ToGbk(data[:]) 62 | obj.reply.Info = serverInfo 63 | return nil 64 | } 65 | 66 | func (obj *HeartBeatPackage) Reply() interface{} { 67 | return obj.reply 68 | } 69 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /proto/std/protocol.go: -------------------------------------------------------------------------------- 1 | package std 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | 7 | "gitee.com/quant1x/gox/encoding/binary/struc" 8 | ) 9 | 10 | type Factory func() (Marshaler, Unmarshaler, error) 11 | 12 | type Marshaler interface { 13 | Marshal() ([]byte, error) 14 | } 15 | 16 | type Unmarshaler interface { 17 | Unmarshal([]byte) error 18 | } 19 | 20 | // PacketHeader 行情服务器发送第一条指令的返回数据 21 | type PacketHeader struct { 22 | raw []byte 23 | Unknown1 uint `struc:"uint32,little" json:"unknown_1"` 24 | 25 | // B will be encoded/decoded as a 16-bit int (a "short") 26 | // but is stored as a native int in the struct 27 | Unknown2 uint `struc:"uint32,little" json:"unknown_2"` 28 | 29 | // the sizeof key links a buffer's size to any int field 30 | Unknown3 uint `struc:"uint32,little" json:"unknown_3"` 31 | ZipSize int `struc:"uint16,little" json:"zip_size"` 32 | 33 | // you can get freaky if you want 34 | UnzipSize int `struc:"uint16,little" json:"unzip_size"` 35 | } 36 | 37 | func (h *PacketHeader) Bytes() []byte { 38 | return h.raw 39 | } 40 | 41 | func (h *PacketHeader) Compressed() bool { 42 | return h.ZipSize != h.UnzipSize 43 | } 44 | 45 | func (h *PacketHeader) Size() int { 46 | return h.UnzipSize 47 | } 48 | func (h *PacketHeader) Unmarshal(data []byte) error { 49 | h.raw = data 50 | return DefaultUnmarshal(data, h) 51 | } 52 | 53 | // DefaultUnmarshal 基于struc包的反序列化方案 54 | func DefaultUnmarshal(data []byte, v interface{}) error { 55 | // 构造流 56 | buf := bytes.NewBuffer(data) 57 | // 使用struc解析到struct 58 | err := struc.Unpack(buf, v) 59 | if err != nil { 60 | log.Println(err) 61 | return err 62 | } 63 | return nil 64 | } 65 | 66 | // DefaultMarshal 基于struc包的序列化方案 67 | func DefaultMarshal(v interface{}) ([]byte, error) { 68 | // 构造流 69 | buf := bytes.Buffer{} 70 | // 使用struc解析到struct 71 | err := struc.Pack(&buf, v) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return buf.Bytes(), nil 76 | } 77 | -------------------------------------------------------------------------------- /quotes/cmd_hello1.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | ) 11 | 12 | type Hello1Package struct { 13 | reqHeader *StdRequestHeader 14 | request *Hello1Request 15 | respHeader *StdResponseHeader 16 | reply *Hello1Reply 17 | 18 | contentHex string 19 | } 20 | 21 | type Hello1Request struct { 22 | } 23 | 24 | type Hello1Reply struct { 25 | Info string 26 | serverTime string 27 | } 28 | 29 | func NewHello1() *Hello1Package { 30 | obj := new(Hello1Package) 31 | obj.reqHeader = new(StdRequestHeader) 32 | obj.respHeader = new(StdResponseHeader) 33 | obj.request = new(Hello1Request) 34 | obj.reply = new(Hello1Reply) 35 | 36 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 37 | obj.reqHeader.SeqID = internal.SequenceId() 38 | obj.reqHeader.PacketType = 0x01 39 | obj.reqHeader.Method = proto.STD_MSG_LOGIN1 40 | obj.contentHex = "01" 41 | return obj 42 | } 43 | 44 | func (obj *Hello1Package) Serialize() ([]byte, error) { 45 | b, err := hex.DecodeString(obj.contentHex) 46 | 47 | obj.reqHeader.PkgLen1 = 2 + uint16(len(b)) 48 | obj.reqHeader.PkgLen2 = 2 + uint16(len(b)) 49 | 50 | buf := new(bytes.Buffer) 51 | err = binary.Write(buf, binary.LittleEndian, obj.reqHeader) 52 | 53 | buf.Write(b) 54 | return buf.Bytes(), err 55 | } 56 | 57 | // 00e60708051 50 f0 00 d3 a02b2020c03840384038403840384033a02b2020c0384038403840384038403 00 5a8a3401 f94a0100 5a8a3401 fd4a0100ff00e 700000101013f 58 | // 59 | // 分 时 秒 日期 60 | func (obj *Hello1Package) UnSerialize(header interface{}, data []byte) error { 61 | obj.respHeader = header.(*StdResponseHeader) 62 | serverInfo := internal.Utf8ToGbk(data[68:]) 63 | obj.reply.Info = serverInfo 64 | return nil 65 | } 66 | 67 | func (obj *Hello1Package) Reply() interface{} { 68 | return obj.reply 69 | } 70 | -------------------------------------------------------------------------------- /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/stock_security_count.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | ) 11 | 12 | // SecurityCountPackage 市场股票数量 13 | type SecurityCountPackage struct { 14 | reqHeader *StdRequestHeader 15 | respHeader *StdResponseHeader 16 | request *SecurityCountRequest 17 | reply *SecurityCountReply 18 | contentHex string 19 | } 20 | 21 | type SecurityCountRequest struct { 22 | Market uint16 23 | } 24 | 25 | type SecurityCountReply struct { 26 | Count uint16 27 | } 28 | 29 | func NewSecurityCountPackage() *SecurityCountPackage { 30 | obj := new(SecurityCountPackage) 31 | obj.reqHeader = new(StdRequestHeader) 32 | obj.respHeader = new(StdResponseHeader) 33 | obj.request = new(SecurityCountRequest) 34 | obj.reply = new(SecurityCountReply) 35 | 36 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 37 | obj.reqHeader.SeqID = internal.SequenceId() 38 | obj.reqHeader.PacketType = 0x01 39 | obj.reqHeader.Method = proto.STD_MSG_SECURITY_COUNT 40 | obj.contentHex = "75c73301" // 未解 41 | return obj 42 | } 43 | 44 | func (obj *SecurityCountPackage) SetParams(req *SecurityCountRequest) { 45 | obj.request = req 46 | } 47 | 48 | func (obj *SecurityCountPackage) Serialize() ([]byte, error) { 49 | obj.reqHeader.PkgLen1 = 2 + 4 + 2 50 | obj.reqHeader.PkgLen2 = 2 + 4 + 2 51 | 52 | buf := new(bytes.Buffer) 53 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 54 | err = binary.Write(buf, binary.LittleEndian, obj.request) 55 | b, err := hex.DecodeString(obj.contentHex) 56 | buf.Write(b) 57 | return buf.Bytes(), err 58 | } 59 | 60 | func (obj *SecurityCountPackage) UnSerialize(header interface{}, data []byte) error { 61 | obj.respHeader = header.(*StdResponseHeader) 62 | 63 | obj.reply.Count = binary.LittleEndian.Uint16(data[:2]) 64 | return nil 65 | } 66 | 67 | func (obj *SecurityCountPackage) Reply() interface{} { 68 | return obj.reply 69 | } 70 | -------------------------------------------------------------------------------- /quotes/base_pool.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "time" 5 | 6 | "gitee.com/quant1x/asio" 7 | "gitee.com/quant1x/gox/logger" 8 | ) 9 | 10 | const ( 11 | // POOL_INITED 连接池初始化 12 | POOL_INITED = 1 13 | // POOL_MAX 连接池最大 2 14 | POOL_MAX = 10 15 | // CONN_TIMEOUT 链接超时 10 s 16 | CONN_TIMEOUT = 10 17 | // RECV_TIMEOUT 接收数据超时 18 | RECV_TIMEOUT = 5 19 | ) 20 | 21 | // ConnPool 连接池 22 | type ConnPool struct { 23 | addr string 24 | pool asio.ConnectionPool 25 | maxIdle int 26 | } 27 | 28 | // NewConnPool 创新一个新连接池 29 | func NewConnPool(maxCap, maxIdle int, factory func() (any, error), close func(any) error, ping func(any) error) (*ConnPool, error) { 30 | initialCap := POOL_INITED 31 | if maxIdle < POOL_INITED { 32 | maxIdle = POOL_INITED 33 | } 34 | maxIdle = maxCap 35 | // 创建一个连接池: 初始化5,最大连接30 36 | poolConfig := &asio.Config{ 37 | InitialCap: initialCap, 38 | MaxCap: maxCap, 39 | MaxIdle: maxIdle, 40 | Factory: factory, 41 | Close: close, 42 | Ping: ping, 43 | //连接最大空闲时间,超过该时间的连接 将会关闭,可避免空闲时连接EOF,自动失效的问题 44 | IdleTimeout: CONN_TIMEOUT * time.Second, 45 | } 46 | _pool, err := asio.NewConnectionPool(poolConfig) 47 | if err != nil { 48 | logger.Errorf("create channel pool failed, error=%+v", err) 49 | return nil, err 50 | } 51 | cp := &ConnPool{ 52 | pool: _pool, 53 | maxIdle: maxIdle, 54 | } 55 | return cp, nil 56 | } 57 | 58 | func (p *ConnPool) GetMaxIdleCount() int { 59 | return p.maxIdle 60 | } 61 | 62 | func (p *ConnPool) GetConn() any { 63 | conn, err := p.pool.Acquire() 64 | if err != nil { 65 | logger.Errorf("获取连接失败, error=%+v", err) 66 | return nil 67 | } 68 | return conn 69 | } 70 | 71 | func (p *ConnPool) CloseConn(conn any) error { 72 | return p.pool.CloseConnection(conn) 73 | } 74 | 75 | func (p *ConnPool) ReturnConn(conn any) { 76 | _ = p.pool.Release(conn) 77 | } 78 | 79 | func (p *ConnPool) CloseAll() { 80 | p.pool.CloseAll() 81 | } 82 | 83 | func (p *ConnPool) Close() { 84 | _ = p.pool.Close() 85 | } 86 | -------------------------------------------------------------------------------- /securities/block.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "slices" 5 | 6 | "gitee.com/quant1x/exchange" 7 | "gitee.com/quant1x/gox/api" 8 | "gitee.com/quant1x/gox/coroutine" 9 | ) 10 | 11 | var ( 12 | __onceBlockFiles coroutine.PeriodicOnce 13 | __global_block_list = []BlockInfo{} 14 | __mapBlock = map[string]BlockInfo{} 15 | ) 16 | 17 | // BlockInfo 板块信息 18 | type BlockInfo struct { 19 | Name string `dataframe:"name"` // 名称 20 | Code string `dataframe:"code"` // 代码 21 | Type int `dataframe:"type"` // 类型 22 | Count int `dataframe:"count"` // 个股数量 23 | Block string `dataframe:"block"` // 通达信板块编码 24 | ConstituentStocks []string `dataframe:"ConstituentStocks"` // 板块成份股 25 | } 26 | 27 | func loadCacheBlockInfos() { 28 | syncBlockFiles() 29 | bkFilename := SectorFilename() 30 | list := []BlockInfo{} 31 | err := api.CsvToSlices(bkFilename, &list) 32 | if err != nil { 33 | return 34 | } 35 | if len(list) > 0 { 36 | __global_block_list = []BlockInfo{} 37 | for _, v := range list { 38 | // 对齐板块代码 39 | blockCode := exchange.CorrectSecurityCode(v.Code) 40 | v.Code = blockCode 41 | for i := 0; i < len(v.ConstituentStocks); i++ { 42 | // 对齐个股代码 43 | stockCode := exchange.CorrectSecurityCode(v.ConstituentStocks[i]) 44 | v.ConstituentStocks[i] = stockCode 45 | } 46 | // 缓存列表 47 | __global_block_list = append(__global_block_list, v) 48 | // 缓存板块映射关系 49 | __mapBlock[v.Code] = v 50 | } 51 | } 52 | } 53 | 54 | // BlockList 板块列表 55 | func BlockList() (list []BlockInfo) { 56 | __onceBlockFiles.Do(loadCacheBlockInfos) 57 | return slices.Clone(__global_block_list) 58 | } 59 | 60 | func GetBlockInfo(code string) *BlockInfo { 61 | __onceBlockFiles.Do(loadCacheBlockInfos) 62 | securityCode := code 63 | if !exchange.AssertBlockBySecurityCode(&securityCode) { 64 | return nil 65 | } 66 | blockInfo, ok := __mapBlock[securityCode] 67 | if ok { 68 | return &blockInfo 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/strings.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | 8 | "golang.org/x/text/encoding/simplifiedchinese" 9 | "golang.org/x/text/encoding/traditionalchinese" 10 | "golang.org/x/text/transform" 11 | ) 12 | 13 | func Utf8ToGbk(text []byte) string { 14 | pos := bytes.IndexByte(text, 0x00) 15 | if pos >= 0 { 16 | text = text[:pos] 17 | } 18 | r := bytes.NewReader(text) 19 | decoder := transform.NewReader(r, simplifiedchinese.GBK.NewDecoder()) //GB18030 20 | content, _ := io.ReadAll(decoder) 21 | return strings.ReplaceAll(string(content), string([]byte{0x00}), "") 22 | } 23 | 24 | // Utf8ToGbk utf8 转gbk 25 | func v1Utf8ToGbk(text []byte) string { 26 | r := bytes.NewReader(text) 27 | decoder := transform.NewReader(r, simplifiedchinese.GBK.NewDecoder()) //GB18030 28 | content, _ := io.ReadAll(decoder) 29 | return strings.ReplaceAll(string(content), string([]byte{0x00}), "") 30 | } 31 | 32 | // DecodeGBK convert GBK to UTF-8 33 | func DecodeGBK(s []byte) ([]byte, error) { 34 | I := bytes.NewReader(s) 35 | decoder := transform.NewReader(I, simplifiedchinese.GBK.NewDecoder()) 36 | d, err := io.ReadAll(decoder) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return d, nil 41 | } 42 | 43 | // EncodeGBK convert UTF-8 to GBK 44 | func EncodeGBK(s []byte) ([]byte, error) { 45 | I := bytes.NewReader(s) 46 | encoder := transform.NewReader(I, simplifiedchinese.GBK.NewEncoder()) 47 | d, err := io.ReadAll(encoder) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return d, nil 52 | } 53 | 54 | // DecodeBig5 convert BIG5 to UTF-8 55 | func DecodeBig5(s []byte) ([]byte, error) { 56 | I := bytes.NewReader(s) 57 | O := transform.NewReader(I, traditionalchinese.Big5.NewDecoder()) 58 | d, e := io.ReadAll(O) 59 | if e != nil { 60 | return nil, e 61 | } 62 | return d, nil 63 | } 64 | 65 | // EncodeBig5 convert UTF-8 to BIG5 66 | func EncodeBig5(s []byte) ([]byte, error) { 67 | I := bytes.NewReader(s) 68 | O := transform.NewReader(I, traditionalchinese.Big5.NewEncoder()) 69 | d, e := io.ReadAll(O) 70 | if e != nil { 71 | return nil, e 72 | } 73 | return d, nil 74 | } 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /securities/block_industry.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "slices" 8 | "strings" 9 | 10 | "gitee.com/quant1x/exchange/cache" 11 | "gitee.com/quant1x/gox/api" 12 | "gitee.com/quant1x/gox/text/encoding" 13 | ) 14 | 15 | // IndustryInfo 行业板块对应 16 | type IndustryInfo struct { 17 | MarketId int // 市场代码 18 | Code string // 股票代码 19 | Block string // 行业板块代码 20 | Block5 string // 二级行业板块代码 21 | XBlock string // x行业代码 22 | XBlock5 string // x二级行业代码 23 | } 24 | 25 | // 获取行业板块 26 | func loadIndustryBlocks() []IndustryInfo { 27 | hyfile := "tdxhy.cfg" 28 | name := hyfile 29 | cacheFilename := cache.GetBlockPath() + "/" + name 30 | if !api.FileExist(cacheFilename) { 31 | // 如果文件不存在, 导出内嵌资源 32 | err := export(cacheFilename, name) 33 | if err != nil { 34 | return nil 35 | } 36 | } 37 | file, err := os.Open(cacheFilename) 38 | if err != nil { 39 | return nil 40 | } 41 | defer api.CloseQuietly(file) 42 | reader := bufio.NewReader(file) 43 | // 按行处理txt 44 | decoder := encoding.NewDecoder("GBK") 45 | var hys = []IndustryInfo{} 46 | for { 47 | data, _, err := reader.ReadLine() 48 | if err == io.EOF { 49 | break 50 | } 51 | line := decoder.ConvertString(string(data)) 52 | arr := strings.Split(line, "|") 53 | bc := arr[2] 54 | bc5 := bc 55 | if len(bc5) >= 5 { 56 | bc5 = bc5[0:5] 57 | } 58 | var xbc, xbc5 string 59 | if len(arr) >= 6 { 60 | xbc5 = arr[5] 61 | if len(xbc5) >= 6 { 62 | xbc = xbc5[:5] 63 | } 64 | } 65 | 66 | hy := IndustryInfo{ 67 | MarketId: int(api.ParseInt(arr[0])), 68 | Code: arr[1], 69 | Block: bc, 70 | Block5: bc5, 71 | XBlock: xbc, 72 | XBlock5: xbc5, 73 | } 74 | hys = append(hys, hy) 75 | } 76 | return hys 77 | } 78 | 79 | // 从行业信息中提取股票代码列表 80 | func industryConstituentStockList(hys []IndustryInfo, block string) []string { 81 | list := []string{} 82 | for _, v := range hys { 83 | if strings.HasPrefix(v.Block5, block) || strings.HasPrefix(v.XBlock5, block) { 84 | list = append(list, v.Code) 85 | } else if v.Block5 == block || v.Block == block || v.XBlock5 == block || v.XBlock == block { 86 | list = append(list, v.Code) 87 | } 88 | } 89 | if len(list) > 0 { 90 | slices.Sort(list) 91 | } 92 | return list 93 | } 94 | -------------------------------------------------------------------------------- /securities/block_parse.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gitee.com/quant1x/exchange" 7 | "gitee.com/quant1x/exchange/cache" 8 | "gitee.com/quant1x/gox/api" 9 | ) 10 | 11 | // SectorFilename 板块缓存文件名 12 | func SectorFilename(date ...string) string { 13 | name := "blocks" 14 | cacheDate := exchange.LastTradeDate() 15 | if len(date) > 0 { 16 | cacheDate = exchange.FixTradeDate(date[0]) 17 | } 18 | filename := fmt.Sprintf("%s/%s.%s", cache.GetMetaPath(), name, cacheDate) 19 | return filename 20 | } 21 | 22 | // 读取板块数据 23 | func parseAndGenerateBlockFile() { 24 | blockInfos := loadIndexBlockInfos() 25 | block2Name := map[string]string{} 26 | for _, v := range blockInfos { 27 | block2Name[v.Block] = v.Name 28 | } 29 | bks := []string{"block.dat", "block_gn.dat", "block_fg.dat", "block_zs.dat"} 30 | //bks := []string{"block_gn.dat", "block_fg.dat", "block_zs.dat"} 31 | name2block := map[string]__raw_block_info{} 32 | for _, v := range bks { 33 | bi := parseRawBlockData(v) 34 | if bi != nil { 35 | for _, bk := range (*bi).Data { 36 | blockName := bk.BlockName 37 | if bn, ok := block2Name[blockName]; ok { 38 | blockName = bn 39 | } 40 | name2block[blockName] = bk 41 | } 42 | } 43 | } 44 | // 行业代码映射 45 | code2hy := map[string]string{} 46 | for _, v := range blockInfos { 47 | if v.Name != v.Block { 48 | code2hy[v.Block] = v.Name 49 | } 50 | } 51 | // 行业板块数据 52 | hys := loadIndustryBlocks() 53 | for i, v := range blockInfos { 54 | bn := v.Name 55 | __info, ok := name2block[bn] 56 | if ok { 57 | list := []string{} 58 | for _, sc := range __info.List { 59 | if len(sc.Code) < 5 { 60 | continue 61 | } 62 | marketId, _, _ := exchange.DetectMarket(sc.Code) 63 | if marketId == exchange.MarketIdBeiJing { 64 | continue 65 | } 66 | list = append(list, sc.Code) 67 | } 68 | blockInfos[i].Count = int(__info.Num) 69 | blockInfos[i].ConstituentStocks = list 70 | continue 71 | } 72 | bc := v.Block 73 | stockList := industryConstituentStockList(hys, bc) 74 | if len(stockList) > 0 { 75 | blockInfos[i].Count = len(stockList) 76 | blockInfos[i].ConstituentStocks = stockList 77 | } 78 | } 79 | blockInfos = api.Filter(blockInfos, func(info BlockInfo) bool { 80 | return len(info.ConstituentStocks) > 0 81 | }) 82 | if len(blockInfos) > 0 { 83 | filename := SectorFilename() 84 | _ = api.SlicesToCsv(filename, blockInfos) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /securities/block_raw.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "gitee.com/quant1x/exchange" 8 | "gitee.com/quant1x/exchange/cache" 9 | "gitee.com/quant1x/gotdx" 10 | "gitee.com/quant1x/gox/api" 11 | "gitee.com/quant1x/gox/encoding/binary/struc" 12 | "gitee.com/quant1x/gox/text/encoding" 13 | ) 14 | 15 | // 下载板块原始数据文件 16 | func downloadBlockRawData(filename string) { 17 | tdxApi := gotdx.GetTdxApi() 18 | fn := cache.GetBlockPath() + "/" + filename 19 | fileInfo, err := os.Stat(fn) 20 | if err == nil || os.IsExist(err) { 21 | toInit := exchange.CanInitialize(fileInfo.ModTime()) 22 | if !toInit { 23 | return 24 | } 25 | } 26 | resp, err := tdxApi.GetBlockInfo(filename) 27 | if err == nil { 28 | fn := cache.GetBlockPath() + "/" + filename 29 | _ = api.CheckFilepath(fn, true) 30 | fp, err := os.Create(fn) 31 | if err == nil { 32 | _, _ = fp.Write(resp.Data) 33 | _ = fp.Close() 34 | } 35 | } 36 | } 37 | 38 | type __raw_block_info struct { 39 | BlockName string `struc:"[9]byte,little"` // 板块名称 40 | Num uint16 `struc:"uint16,little"` // 个股数量 41 | BlockType uint16 `struc:"uint16,little"` // 板块类型 42 | List [400]__block_stock `struct:"[400]__block_stock,little"` // 个股列表 43 | } 44 | 45 | type __block_stock struct { 46 | Code string `struc:"[7]byte,little"` // 证券代码 47 | } 48 | 49 | type __raw_block_data struct { 50 | //Header blockHeader `struc:"[386]byte,little"` 51 | Unknown [384]byte `struc:"[384]byte,little"` // 头信息, 忽略 52 | Count uint16 `struc:"uint16,little,sizeof=Data"` // 板块数量 53 | Data []__raw_block_info `struc:"[2813]byte, little"` // 板块数据 54 | } 55 | 56 | func parseRawBlockData(blockFilename string) *__raw_block_data { 57 | fn := cache.GetBlockPath() + "/" + blockFilename 58 | _ = api.CheckFilepath(fn, true) 59 | file, err := os.Open(fn) 60 | if err != nil { 61 | return nil 62 | } 63 | defer api.CloseQuietly(file) 64 | var block __raw_block_data 65 | err = struc.Unpack(file, &block) 66 | if err != nil { 67 | return nil 68 | } 69 | decoder := encoding.NewDecoder("GBK") 70 | for i, v := range block.Data { 71 | name := decoder.ConvertString(v.BlockName) 72 | block.Data[i].BlockName = strings.ReplaceAll(name, string([]byte{0x00}), "") 73 | for j, s := range v.List { 74 | block.Data[i].List[j].Code = strings.ReplaceAll(s.Code, string([]byte{0x00}), "") 75 | } 76 | } 77 | return &block 78 | } 79 | -------------------------------------------------------------------------------- /quotes/cmd_hello2.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | ) 11 | 12 | type Hello2Package struct { 13 | reqHeader *StdRequestHeader 14 | respHeader *StdResponseHeader 15 | request *Hello2Request 16 | reply *Hello2Reply 17 | 18 | contentHex string 19 | } 20 | 21 | type Hello2Request struct { 22 | } 23 | 24 | type Hello2Reply struct { 25 | Info string 26 | serverTime string 27 | } 28 | 29 | func NewHello2() *Hello2Package { 30 | obj := new(Hello2Package) 31 | obj.reqHeader = new(StdRequestHeader) 32 | obj.respHeader = new(StdResponseHeader) 33 | obj.request = new(Hello2Request) 34 | obj.reply = new(Hello2Reply) 35 | 36 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 37 | obj.reqHeader.SeqID = internal.SequenceId() 38 | obj.reqHeader.PacketType = 0x01 39 | obj.reqHeader.Method = proto.STD_MSG_LOGIN2 40 | obj.contentHex = "d5d0c9ccd6a4a8af0000008fc22540130000d500c9ccbdf0d7ea00000002" 41 | return obj 42 | } 43 | 44 | func (obj *Hello2Package) Serialize() ([]byte, error) { 45 | b, err := hex.DecodeString(obj.contentHex) 46 | 47 | obj.reqHeader.PkgLen1 = 2 + uint16(len(b)) 48 | obj.reqHeader.PkgLen2 = 2 + uint16(len(b)) 49 | 50 | buf := new(bytes.Buffer) 51 | err = binary.Write(buf, binary.LittleEndian, obj.reqHeader) 52 | buf.Write(b) 53 | return buf.Bytes(), err 54 | } 55 | 56 | /* 57 | 0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011f85e34068747470733a2f2f626967352e6e65776f6e652e636f6d2e636e2f7a797968742f7a645f7a737a712e7a6970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004150503a414c4c0d0a54494d453a303a30312d31353a30352c31353a30362d32333a35390d0a20202020c4facab9d3c3b5c4b0e6b1bebcb4bdabcda3d3c3a3acceaac1cbc4fab5c4d5fdb3a3cab9d3c32cc7ebbea1bfecc9fdd6c1d5d0c9ccd6a4c8af5043b0e6a1a30d0a20202020c8e7b9fbb2bbc4dcd7d4b6afc9fdbcb6a3acc7ebb5bdb9d9cdf868747470733a2f2f7777772e636d736368696e612e636f6d2fcfc2d4d8b0b2d7b0a3acd0bbd0bbc4fab5c4d6a7b3d6a3a100 年月日 年月日 58 | */ 59 | func (obj *Hello2Package) UnSerialize(header interface{}, data []byte) error { 60 | obj.respHeader = header.(*StdResponseHeader) 61 | 62 | serverInfo := internal.Utf8ToGbk(data[58:]) 63 | //fmt.Println(hex.EncodeToString(data)) 64 | obj.reply.Info = serverInfo 65 | return nil 66 | } 67 | 68 | func (obj *Hello2Package) Reply() interface{} { 69 | return obj.reply 70 | } 71 | -------------------------------------------------------------------------------- /quotes/block_meta.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | "gitee.com/quant1x/gox/encoding/binary/struc" 11 | ) 12 | 13 | // 板块相关参数 14 | const ( 15 | BLOCK_ZHISHU = "block_zs.dat" // 指数 16 | BLOCK_FENGGE = "block_fg.dat" // 风格 17 | BLOCK_GAINIAN = "block_gn.dat" // 概念 18 | BLOCK_DEFAULT = "block.dat" // 早期的板块数据文件, 与block_zs.dat 19 | BLOCK_CHUNKS_SIZE = 0x7530 // 板块文件默认一个请求包最大数据 20 | ) 21 | 22 | // BlockMetaPackage 板块信息 23 | type BlockMetaPackage struct { 24 | reqHeader *StdRequestHeader 25 | respHeader *StdResponseHeader 26 | request *BlockMetaRequest 27 | response *BlockMeta 28 | contentHex string 29 | } 30 | 31 | // BlockMetaRequest 请求包 32 | type BlockMetaRequest struct { 33 | BlockFile [40]byte // 板块文件名 34 | } 35 | 36 | // BlockMeta 响应包结构 37 | type BlockMeta struct { 38 | Size uint32 `struc:"uint32,little"` // 尺寸 39 | C1 byte `struc:"byte,little"` // C1 40 | HashValue [32]byte `struc:"[32]byte,little"` // hash值 41 | C2 byte `struc:"byte,little"` // C2 42 | } 43 | 44 | func NewBlockMetaPackage() *BlockMetaPackage { 45 | pkg := new(BlockMetaPackage) 46 | pkg.reqHeader = new(StdRequestHeader) 47 | pkg.respHeader = new(StdResponseHeader) 48 | pkg.request = new(BlockMetaRequest) 49 | pkg.response = new(BlockMeta) 50 | 51 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 52 | //0c 53 | pkg.reqHeader.ZipFlag = 0x0c 54 | //1f 18 76 00 55 | pkg.reqHeader.SeqID = internal.SequenceId() 56 | //01 57 | pkg.reqHeader.PacketType = 0x01 58 | //0b 00 59 | //PkgLen1 uint16 60 | pkg.reqHeader.PkgLen1 = 0x002a 61 | //0b 00 62 | //PkgLen2 uint16 63 | pkg.reqHeader.PkgLen2 = 0x002a 64 | //10 00 65 | pkg.reqHeader.Method = proto.STD_MSG_BLOCK_META 66 | //pkg.contentHex = "0100" // 未解 67 | return pkg 68 | } 69 | 70 | func (obj *BlockMetaPackage) SetParams(req *BlockMetaRequest) { 71 | obj.request = req 72 | } 73 | 74 | func (obj *BlockMetaPackage) Serialize() ([]byte, error) { 75 | buf := new(bytes.Buffer) 76 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 77 | b, err := hex.DecodeString(obj.contentHex) 78 | buf.Write(b) 79 | err = binary.Write(buf, binary.LittleEndian, obj.request) 80 | return buf.Bytes(), err 81 | } 82 | 83 | func (obj *BlockMetaPackage) UnSerialize(header interface{}, data []byte) error { 84 | obj.respHeader = header.(*StdResponseHeader) 85 | // 构造流 86 | buf := bytes.NewBuffer(data) 87 | var reply BlockMeta 88 | err := struc.Unpack(buf, &reply) 89 | if err != nil { 90 | return err 91 | } 92 | obj.response = &reply 93 | return nil 94 | } 95 | 96 | func (obj *BlockMetaPackage) Reply() interface{} { 97 | return obj.response 98 | } 99 | -------------------------------------------------------------------------------- /quotes/block_info.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | "gitee.com/quant1x/gox/encoding/binary/struc" 11 | ) 12 | 13 | // BlockInfoPackage 板块信息 14 | type BlockInfoPackage struct { 15 | reqHeader *StdRequestHeader 16 | respHeader *StdResponseHeader 17 | request *BlockInfoRequest 18 | response *BlockInfoResponse 19 | contentHex string 20 | } 21 | 22 | // BlockInfoRequest 请求包 23 | type BlockInfoRequest struct { 24 | Start uint32 `struc:"uint32,little"` 25 | Size uint32 `struc:"uint32,little"` 26 | BlockFile [100]byte `struc:"[100]byte,little"` // 板块文件名 27 | } 28 | 29 | type BlockInfo struct { 30 | BlockName string 31 | BlockType uint16 32 | StockCount uint16 33 | Codelist []string 34 | } 35 | 36 | type BlockInfoResponse struct { 37 | Size uint32 `struc:"uint32,little"` 38 | Data []byte `struc:"sizefrom=Size"` 39 | } 40 | 41 | type BlockInfoReply struct { 42 | BlockNum uint16 `struc:"uint16,little"` // 板块个数 43 | Block []BlockInfo // 板块列表 44 | } 45 | 46 | func NewBlockInfoPackage() *BlockInfoPackage { 47 | pkg := new(BlockInfoPackage) 48 | pkg.reqHeader = new(StdRequestHeader) 49 | pkg.respHeader = new(StdResponseHeader) 50 | pkg.request = new(BlockInfoRequest) 51 | pkg.response = new(BlockInfoResponse) 52 | 53 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 54 | //0c 55 | pkg.reqHeader.ZipFlag = 0x0c 56 | //1f 18 76 00 57 | pkg.reqHeader.SeqID = internal.SequenceId() 58 | //01 59 | pkg.reqHeader.PacketType = 0x01 60 | //0b 00 61 | //PkgLen1 uint16 62 | pkg.reqHeader.PkgLen1 = 0x006e 63 | //0b 00 64 | //PkgLen2 uint16 65 | pkg.reqHeader.PkgLen2 = 0x006e 66 | //10 00 67 | pkg.reqHeader.Method = proto.STD_MSG_BLOCK_DATA 68 | //pkg.contentHex = "0100" // 未解 69 | return pkg 70 | } 71 | 72 | func (obj *BlockInfoPackage) SetParams(req *BlockInfoRequest) { 73 | obj.request = req 74 | } 75 | 76 | func (obj *BlockInfoPackage) Serialize() ([]byte, error) { 77 | buf := new(bytes.Buffer) 78 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 79 | b, err := hex.DecodeString(obj.contentHex) 80 | buf.Write(b) 81 | err = binary.Write(buf, binary.LittleEndian, obj.request) 82 | return buf.Bytes(), err 83 | } 84 | 85 | func (obj *BlockInfoPackage) UnSerialize(header interface{}, data []byte) error { 86 | obj.respHeader = header.(*StdResponseHeader) 87 | // 构造流 88 | buf := bytes.NewBuffer(data) 89 | var reply BlockInfoResponse 90 | err := struc.Unpack(buf, &reply) 91 | if err != nil { 92 | return err 93 | } 94 | obj.response = &reply 95 | return nil 96 | } 97 | 98 | func (obj *BlockInfoPackage) Reply() interface{} { 99 | return obj.response 100 | } 101 | -------------------------------------------------------------------------------- /quotes/bestip_client.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "gitee.com/quant1x/gotdx/internal" 10 | "gitee.com/quant1x/gotdx/proto/std" 11 | ) 12 | 13 | type LabClient struct { 14 | conn net.Conn 15 | addr string 16 | //Host string 17 | //Port int 18 | Timeout time.Duration 19 | MaxRetryTimes int 20 | RetryDuration time.Duration 21 | } 22 | 23 | func NewClientForTest(addr string) (*LabClient, error) { 24 | conn, err := net.DialTimeout("tcp", addr, 1*time.Second) // net.DialTimeout() 25 | if err != nil { 26 | fmt.Printf("connect %s, %+v\n", addr, err) 27 | return nil, err 28 | } 29 | return &LabClient{ 30 | conn: conn, 31 | addr: addr, 32 | //Host: host, 33 | //Port: port, 34 | MaxRetryTimes: 5, 35 | Timeout: 1 * time.Second, 36 | RetryDuration: time.Millisecond * 200, 37 | }, nil 38 | } 39 | 40 | func (cli *LabClient) Do(request std.Marshaler, response std.Unmarshaler) error { 41 | // 序列化请求 42 | req, err := request.Marshal() 43 | if err != nil { 44 | return err 45 | } 46 | // 发送请求 47 | retryTimes := 0 48 | SEND: 49 | n, err := cli.conn.Write(req) 50 | // 重试 51 | if n < len(req) { 52 | retryTimes += 1 53 | if retryTimes <= cli.MaxRetryTimes { 54 | fmt.Printf("第%d次重试\n", retryTimes) 55 | goto SEND 56 | } else { 57 | return errors.New("数据未完整发送") 58 | } 59 | } 60 | if err != nil { 61 | return err 62 | } 63 | // 解析响应包头 64 | var header std.PacketHeader 65 | // 读取包头 大小为16个字节 66 | // 单次获取的字列流 67 | headerLength := 0x10 68 | headerBytes := make([]byte, headerLength) 69 | // 调用socket获取字节流并保存到data中 70 | headerBytes, err = cli.receive(headerLength) 71 | if err != nil { 72 | return err 73 | } 74 | err = header.Unmarshal(headerBytes) 75 | if err != nil { 76 | return err 77 | } 78 | // 根据获取响应体结构 79 | // 调用socket获取字节流并保存到data中 80 | bodyBytes, err := cli.receive(header.ZipSize) 81 | if err != nil { 82 | return err 83 | } 84 | // zlib解压缩 85 | if header.Compressed() { 86 | bodyBytes, err = internal.ZlibUnCompress(bodyBytes) 87 | } 88 | // 反序列化为响应体结构 89 | err = response.Unmarshal(bodyBytes) 90 | if err != nil { 91 | return err 92 | } 93 | return nil 94 | } 95 | 96 | func (cli *LabClient) receive(length int) (data []byte, err error) { 97 | var ( 98 | receivedSize int 99 | ) 100 | READ: 101 | tmp := make([]byte, length) 102 | // 设置读timeout 103 | err = cli.conn.SetReadDeadline(time.Now().Add(cli.Timeout)) 104 | if err != nil { 105 | fmt.Println("setReadDeadline failed:", err) 106 | } 107 | // 调用socket获取字节流并保存到data中 108 | receivedSize, err = cli.conn.Read(tmp) 109 | // socket错误,可能为EOF 110 | if err != nil { 111 | return nil, err 112 | } 113 | // 数据添加到总输出,由于tmp申请内存时使用了length的长度, 114 | // 所以直接全部复制到data中会使得未完全传输的部分被填充为0导致数据获取不完整, 115 | // 故使用tmp[:receivedSize] 116 | data = append(data, tmp[:receivedSize]...) 117 | // 数据读满就可以返回了 118 | if len(data) == length { 119 | return 120 | } 121 | // 读取小于标准尺寸,说明到文件尾或者读取出现了问题没读满,可以返回了 122 | if receivedSize < length { 123 | goto READ 124 | } 125 | return 126 | } 127 | 128 | func (cli *LabClient) Close() error { 129 | return cli.conn.Close() 130 | } 131 | -------------------------------------------------------------------------------- /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 | 10 | "gitee.com/quant1x/exchange" 11 | "gitee.com/quant1x/gotdx/internal" 12 | "gitee.com/quant1x/gotdx/proto" 13 | "gitee.com/quant1x/gox/api" 14 | ) 15 | 16 | type MinuteTimePackage struct { 17 | reqHeader *StdRequestHeader 18 | respHeader *StdResponseHeader 19 | request *MinuteTimeRequest 20 | reply *MinuteTimeReply 21 | 22 | contentHex string 23 | } 24 | 25 | type MinuteTimeRequest struct { 26 | Market uint16 27 | Code [6]byte 28 | Date uint32 29 | } 30 | 31 | type MinuteTimeReply struct { 32 | Count uint16 33 | List []MinuteTime 34 | } 35 | 36 | type MinuteTime struct { 37 | Price float32 38 | Vol int 39 | } 40 | 41 | // NewMinuteTimePackage 获取分时数据 42 | // 43 | // Deprecated: 废弃, ETF的数据不对需要进一步处理, 推荐 NewHistoryMinuteTimePackage 44 | func NewMinuteTimePackage() *MinuteTimePackage { 45 | obj := new(MinuteTimePackage) 46 | obj.reqHeader = new(StdRequestHeader) 47 | obj.respHeader = new(StdResponseHeader) 48 | obj.request = new(MinuteTimeRequest) 49 | obj.reply = new(MinuteTimeReply) 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 = 0x051d 57 | obj.reqHeader.Method = proto.STD_MSG_MINUTETIME_DATA 58 | obj.contentHex = "" 59 | return obj 60 | } 61 | func (obj *MinuteTimePackage) SetParams(req *MinuteTimeRequest) { 62 | obj.request = req 63 | } 64 | 65 | func (obj *MinuteTimePackage) Serialize() ([]byte, error) { 66 | obj.reqHeader.PkgLen1 = 0x0e 67 | obj.reqHeader.PkgLen2 = 0x0e 68 | 69 | buf := new(bytes.Buffer) 70 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 71 | err = binary.Write(buf, binary.LittleEndian, obj.request) 72 | b, err := hex.DecodeString(obj.contentHex) 73 | buf.Write(b) 74 | 75 | //b, err := hex.DecodeString(obj.contentHex) 76 | //buf.Write(b) 77 | 78 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 79 | 80 | return buf.Bytes(), err 81 | } 82 | 83 | func (obj *MinuteTimePackage) UnSerialize(header interface{}, data []byte) error { 84 | obj.respHeader = header.(*StdResponseHeader) 85 | 86 | market := exchange.MarketType(obj.request.Market) 87 | code := api.Bytes2String(obj.request.Code[:]) 88 | 89 | pos := 0 90 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 91 | pos += 2 92 | // 跳过4个字节 93 | pos += 6 94 | 95 | pos += 3 96 | 97 | baseUnit := internal.BaseUnit(market, code) 98 | lastPrice := 0 99 | //TODO: ETF的数据不对需要进一步处理 100 | for index := uint16(0); index < obj.reply.Count; index++ { 101 | rawPrice := internal.DecodeVarint(data, &pos) 102 | reversed1 := internal.DecodeVarint(data, &pos) 103 | _ = reversed1 104 | vol := internal.DecodeVarint(data, &pos) 105 | lastPrice += rawPrice 106 | 107 | p := float32(lastPrice) / float32(baseUnit) 108 | 109 | ele := MinuteTime{p, vol} 110 | obj.reply.List = append(obj.reply.List, ele) 111 | } 112 | return err 113 | } 114 | 115 | func (obj *MinuteTimePackage) Reply() interface{} { 116 | return obj.reply 117 | } 118 | -------------------------------------------------------------------------------- /quotes/bestip.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "slices" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "gitee.com/quant1x/gotdx/proto/ext" 14 | "gitee.com/quant1x/gotdx/proto/std" 15 | "gitee.com/quant1x/gox/api" 16 | ) 17 | 18 | const ( 19 | HOST_HQ = "HQ" 20 | HOST_EX = "EX" 21 | HOST_GP = "GP" 22 | ) 23 | 24 | const ( 25 | maxCrossTime = 1000 // 最大耗时 26 | ) 27 | 28 | // ServerGroup 主机组 29 | type ServerGroup struct { 30 | HQ []Server `json:"HQ"` 31 | EX []Server `json:"EX"` 32 | GP []Server `json:"GP"` 33 | } 34 | 35 | // AllServers 全部主机 36 | type AllServers struct { 37 | //Server ServerGroup `json:"Server"` 38 | BestIP ServerGroup `json:"BestIP"` 39 | } 40 | 41 | // ProfileBestIPList 测试最快的服务器 42 | func ProfileBestIPList() *AllServers { 43 | var as AllServers 44 | 45 | // HQ-servers 46 | dst := cleanServers(StandardServerList, testHQ) 47 | as.BestIP.HQ = dst 48 | 49 | // EX-server, reply提示版本不一致, 扩展服务暂不可用 50 | dst = cleanServers(ExtensionServerList, testEX) 51 | as.BestIP.EX = dst 52 | 53 | //// SP-servers 54 | //dst = cleanServers(GP_HOSTS, testEX) 55 | //as.BestIP.GP = dst 56 | str, _ := json.Marshal(as) 57 | fmt.Println(string(str)) 58 | return &as 59 | } 60 | 61 | func cleanServers(src []Server, test func(addr string) error) (dst []Server) { 62 | //err := json.Unmarshal([]byte(str), &src) 63 | //if err != nil { 64 | // return src, dst 65 | //} 66 | //fmt.Printf("%+v\n", src) 67 | 68 | dst = slices.Clone(src) 69 | for i, _ := range dst { 70 | v := &dst[i] 71 | fmt.Printf("%d: %+v\n", i, v) 72 | _ = detect(v, test) 73 | fmt.Printf("%d: %+v\n", i, v) 74 | } 75 | 76 | sort.Slice(dst, func(i, j int) bool { 77 | return dst[i].CrossTime < dst[j].CrossTime 78 | }) 79 | dst = api.Filter(dst, func(e Server) bool { 80 | return e.CrossTime < maxCrossTime 81 | }) 82 | num := len(dst) 83 | if num > POOL_MAX { 84 | num = POOL_MAX 85 | } 86 | dst = dst[0:num] 87 | fmt.Println(dst) 88 | return 89 | } 90 | 91 | // 检测, 返回毫秒 92 | func detect(srv *Server, test func(addr string) error) int64 { 93 | var crossTime int64 = math.MaxInt64 94 | addr := strings.Join([]string{srv.Host, strconv.Itoa(srv.Port)}, ":") 95 | start := time.Now() 96 | err := test(addr) 97 | if err != nil { 98 | srv.CrossTime = crossTime 99 | return crossTime 100 | } 101 | // 计算耗时, 纳秒 102 | crossTime = int64(time.Since(start)) 103 | // 转成毫秒 104 | srv.CrossTime = crossTime / int64(time.Millisecond) 105 | return crossTime 106 | } 107 | 108 | // 标准服务器测试 109 | func testHQ(addr string) error { 110 | cli, err := NewClientForTest(addr) 111 | if err != nil { 112 | return err 113 | } 114 | // CMD信令 1 115 | data, err := CommandWithConn(cli, func() (req std.Marshaler, resp std.Unmarshaler, err error) { 116 | req, resp, err = std.NewSetupCmd1() 117 | return 118 | }) 119 | fmt.Printf("%+v\n", data) 120 | _ = cli.Close() 121 | return err 122 | } 123 | 124 | // 扩展服务器测试 125 | func testEX(addr string) error { 126 | cli, err := NewClientForTest(addr) 127 | if err != nil { 128 | return err 129 | } 130 | // CMD信令 1 131 | data, err := CommandWithConn(cli, func() (req std.Marshaler, resp std.Unmarshaler, err error) { 132 | req, resp, err = ext.NewExCmd1() 133 | return 134 | }) 135 | fmt.Printf("%+v\n", data) 136 | _ = cli.Close() 137 | return err 138 | } 139 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gitee.com/quant1x/asio v1.1.5 h1:CNKG81mS0U6nV1hVskZVHFbkB3IA4pBd73z1AgFrj/8= 2 | gitee.com/quant1x/asio v1.1.5/go.mod h1:cHKuUzwDJyhQYKuimnE2fbjnXzr7IRXllUjMLkXGzrM= 3 | gitee.com/quant1x/exchange v0.8.11 h1:Ozi7uqbp5TZKrwxh6zK1IL5NHCbuyKDNxf/UHC9+/HE= 4 | gitee.com/quant1x/exchange v0.8.11/go.mod h1:o3NlZ8sD0xOI3uWD/ooNJusxhb27ltor3ICM3WhV0uo= 5 | gitee.com/quant1x/gox v1.25.1 h1:daOlQK8I7iXcaZNFXHj1PYq7TwEcLVM+LjeUQiSRtg8= 6 | gitee.com/quant1x/gox v1.25.1/go.mod h1:5KDJRyeY2tnAmqe4OOVRBEW6j/cB40PfDISTB3wThuQ= 7 | gitee.com/quant1x/num v0.7.9 h1:3u+7zNS8GHyMmCoSfVr1tvt61CTpq2RrEkRBhx17vOU= 8 | gitee.com/quant1x/num v0.7.9/go.mod h1:vSXTdXpwD2zDieH17LA5elGf4WhIjMaGHNKCt0MAbHs= 9 | gitee.com/quant1x/pkg v0.8.3 h1:npJkpNvhfbe7uwAs+kAODz0POd1HVSlLzYT/hPPM46o= 10 | gitee.com/quant1x/pkg v0.8.3/go.mod h1:NwWkHtUEV+UePFVbFPwbf/Y2kO1UDT3M9GWe+PF0NrY= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 14 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 15 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= 16 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 17 | github.com/google/pprof v0.0.0-20250919162441-8b542baf5bcf h1:ekSWtXn05ARVCly6JvQLXzAqHfVR4Jq14KjVL1f+1JY= 18 | github.com/google/pprof v0.0.0-20250919162441-8b542baf5bcf/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= 19 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 22 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 23 | github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4= 24 | github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 28 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 29 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 30 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 31 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 32 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 33 | golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 34 | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 35 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 36 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 37 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 38 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /quotes/stock_minute_time_data_history.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 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 HistoryMinuteTimePackage struct { 15 | reqHeader *StdRequestHeader 16 | respHeader *StdResponseHeader 17 | request *HistoryMinuteTimeRequest 18 | reply *MinuteTimeReply 19 | 20 | contentHex string 21 | } 22 | 23 | type HistoryMinuteTimeRequest struct { 24 | Date uint32 25 | Market uint8 26 | Code [6]byte 27 | } 28 | 29 | //type HistoryMinuteTimeReply struct { 30 | // Count uint16 31 | // List []HistoryMinuteTime 32 | //} 33 | // 34 | //type HistoryMinuteTime struct { 35 | // Price float32 36 | // Vol int 37 | //} 38 | 39 | func NewHistoryMinuteTimePackage() *HistoryMinuteTimePackage { 40 | obj := new(HistoryMinuteTimePackage) 41 | obj.reqHeader = new(StdRequestHeader) 42 | obj.respHeader = new(StdResponseHeader) 43 | obj.request = new(HistoryMinuteTimeRequest) 44 | obj.reply = new(MinuteTimeReply) 45 | 46 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 47 | obj.reqHeader.SeqID = internal.SequenceId() 48 | obj.reqHeader.PacketType = 0x00 49 | //obj.reqHeader.PkgLen1 = 50 | //obj.reqHeader.PkgLen2 = 51 | obj.reqHeader.Method = proto.STD_MSG_HISTORY_MINUTETIME_DATA 52 | obj.contentHex = "" 53 | return obj 54 | } 55 | 56 | // SetParams 设置参数 57 | func (obj *HistoryMinuteTimePackage) SetParams(req *HistoryMinuteTimeRequest) { 58 | obj.request = req 59 | } 60 | 61 | func (obj *HistoryMinuteTimePackage) Serialize() ([]byte, error) { 62 | obj.reqHeader.PkgLen1 = 0x0d 63 | obj.reqHeader.PkgLen2 = 0x0d 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 *HistoryMinuteTimePackage) 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 | dataLen := len(data) 85 | if dataLen < 2 { 86 | return nil 87 | } 88 | 89 | pos := 0 90 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 91 | pos += 2 92 | if obj.reply.Count == 0 { 93 | return nil 94 | } 95 | // 跳过4个字节 功能未解析 96 | if dataLen < 6 { 97 | return nil 98 | } 99 | _, _, _, bType := data[pos], data[pos+1], data[pos+2], data[pos+3] 100 | pos += 4 101 | baseUnit := internal.BaseUnit(market, code) 102 | //var baseUnit float32 103 | //if bType > 0x40 { 104 | // baseUnit = 100.0 105 | //} else { 106 | // baseUnit = 1000.0 107 | //} 108 | lastPrice := 0 109 | for index := uint16(0); index < obj.reply.Count; index++ { 110 | rawPrice := internal.DecodeVarint(data, &pos) 111 | reversed1 := internal.DecodeVarint(data, &pos) 112 | _ = reversed1 113 | vol := internal.DecodeVarint(data, &pos) 114 | lastPrice += rawPrice 115 | 116 | p := float32(lastPrice) / float32(baseUnit) 117 | ele := MinuteTime{Price: p, Vol: vol} 118 | obj.reply.List = append(obj.reply.List, ele) 119 | } 120 | _ = bType 121 | return err 122 | } 123 | 124 | func (obj *HistoryMinuteTimePackage) Reply() interface{} { 125 | return obj.reply 126 | } 127 | -------------------------------------------------------------------------------- /quotes/bestip_cache.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "gitee.com/quant1x/exchange" 10 | "gitee.com/quant1x/exchange/cache" 11 | "gitee.com/quant1x/gox/api" 12 | "gitee.com/quant1x/gox/coroutine" 13 | "gitee.com/quant1x/gox/logger" 14 | "gitee.com/quant1x/gox/timestamp" 15 | ) 16 | 17 | var ( 18 | DefaultHQServer = Server{ 19 | Name: "临时主机", 20 | Host: "119.147.212.81", 21 | Port: 7709, 22 | CrossTime: 0, 23 | } 24 | DefaultEXServer = DefaultHQServer 25 | ) 26 | 27 | const ( 28 | serverListFilename = "tdx.json" 29 | serverResetOffsetHours = 8 30 | serverResetOffsetMinutes = 55 31 | ) 32 | 33 | var ( 34 | onceSortServers coroutine.RollingOnce 35 | cacheAllServers AllServers 36 | ) 37 | 38 | func loadSortedServerList(configPath string) *AllServers { 39 | f, err := os.Open(configPath) 40 | if err != nil { 41 | return nil 42 | } 43 | decoder := json.NewDecoder(f) 44 | var as AllServers 45 | err = decoder.Decode(&as) 46 | if err != nil { 47 | return nil 48 | } 49 | return &as 50 | } 51 | 52 | func saveSortedServerList(as *AllServers, configPath string) error { 53 | data, err := json.Marshal(as) 54 | if err != nil { 55 | return err 56 | } 57 | err = os.WriteFile(configPath, data, 0644) 58 | return err 59 | } 60 | 61 | func GetFastHost(key string) []Server { 62 | onceSortServers.Do(lazyCachedSortedServerList) 63 | bestIp := cacheAllServers.BestIP 64 | if key == HOST_HQ { 65 | if len(bestIp.HQ) > 0 { 66 | return bestIp.HQ 67 | } else { 68 | return []Server{DefaultHQServer} 69 | } 70 | } else if key == HOST_EX { 71 | if len(bestIp.EX) > 0 { 72 | return bestIp.EX 73 | } else { 74 | return []Server{DefaultHQServer} 75 | } 76 | } 77 | return []Server{DefaultHQServer} 78 | } 79 | 80 | func lazyCachedSortedServerList() { 81 | // 0. 确定更新时间, 08:55:00, 服务器列表先于其它服务更新 82 | onceSortServers.SetOffsetTime(serverResetOffsetHours, serverResetOffsetMinutes) 83 | // 1. 组织文件路径 84 | filename := filepath.Join(cache.GetMetaPath(), serverListFilename) 85 | 86 | // 2. 检查缓存文件是否存在 87 | var lastModified time.Time 88 | fs, err := api.GetFileStat(filename) 89 | if err == nil { 90 | lastModified = fs.LastWriteTime 91 | } 92 | // 3. 尝试更新服务器列表 93 | allServers := updateBestIpList(lastModified) 94 | // 4. 更新内存 95 | cacheAllServers = *allServers 96 | } 97 | 98 | func updateBestIpList(lastModified time.Time) *AllServers { 99 | filename := filepath.Join(cache.GetMetaPath(), serverListFilename) 100 | // 2.2 转换缓存文件最后修改日期, 时间格式和日历格式对齐 101 | cacheLastDay := lastModified.Format(exchange.TradingDayDateFormat) 102 | 103 | observerTimestamp := onceSortServers.GetCurrentAnchorPoint() 104 | observerTime := timestamp.Time(observerTimestamp) 105 | now := timestamp.Now() 106 | var allServers *AllServers 107 | needUpdate := false 108 | // 3. 比较缓存日期和最后一个交易日 109 | latestDay := exchange.LastTradeDate() 110 | c1 := now >= observerTimestamp 111 | c2 := cacheLastDay < latestDay 112 | c3 := lastModified.Before(observerTime) 113 | if c1 && (c2 && c3) { 114 | // 缓存过时,重新生成 115 | allServers = ProfileBestIPList() 116 | needUpdate = true 117 | } else { 118 | // 缓存有效,尝试加载 119 | allServers = loadSortedServerList(filename) 120 | } 121 | // 4. 数据有效, 则缓存文件 122 | ok := allServers != nil && len(allServers.BestIP.HQ) > 0 /*&& len(allServers.BestIP.EX) > 0*/ 123 | if needUpdate && ok { 124 | // 保存有效缓存 125 | _ = saveSortedServerList(allServers, filename) 126 | } else if !ok { 127 | logger.Fatalf("服务器列表为空") 128 | } 129 | return allServers 130 | } 131 | 132 | // BestIP 网络测速, 更新本地服务器列表配置文件 133 | // 134 | // 强制刷新 135 | func BestIP() { 136 | var lastModified time.Time 137 | updateBestIpList(lastModified) 138 | } 139 | -------------------------------------------------------------------------------- /quotes/stock_company_info_category.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 8 | "gitee.com/quant1x/gotdx/internal" 9 | "gitee.com/quant1x/gotdx/proto" 10 | "gitee.com/quant1x/gox/encoding/binary/struc" 11 | ) 12 | 13 | // CompanyInfoCategoryPackage 企业基本信息 14 | type CompanyInfoCategoryPackage struct { 15 | reqHeader *StdRequestHeader 16 | respHeader *StdResponseHeader 17 | request *CompanyInfoCategoryRequest 18 | reply []CompanyInfoCategory 19 | contentHex string 20 | } 21 | 22 | type CompanyInfoCategoryRequest struct { 23 | Market uint16 // 市场代码 24 | Code [6]byte // 股票代码 25 | Unknown uint32 // 未知数据 26 | } 27 | 28 | // CompanyInfoCategoryReply 响应包结构, 29 | type CompanyInfoCategoryReply struct { 30 | Count uint16 `struc:"uint16,little,sizeof=Data"` // 词条总数 31 | Data []RawCompanyInfoCategory `struc:"[152]byte, little"` // 词条数据 32 | } 33 | 34 | // RawCompanyInfoCategory 响应包结构 35 | type RawCompanyInfoCategory struct { 36 | Name []byte `struc:"[64]byte,little"` // 名称 37 | Filename []byte `struc:"[80]byte,little"` // 文件名 38 | Offset uint32 `struc:"uint32,little"` // 偏移量 39 | Length uint32 `struc:"uint32,little"` // 长度 40 | } 41 | 42 | type CompanyInfoCategory struct { 43 | Name string `struc:"[64]byte,little" dataframe:"name"` // 名称 44 | Filename string `struc:"[80]byte,little" dataframe:"filename"` // 文件名 45 | Offset uint32 `struc:"uint32,little" dataframe:"offset"` // 偏移量 46 | Length uint32 `struc:"uint32,little" dataframe:"length"` // 长度 47 | } 48 | 49 | func NewCompanyInfoCategoryPackage() *CompanyInfoCategoryPackage { 50 | pkg := new(CompanyInfoCategoryPackage) 51 | pkg.reqHeader = new(StdRequestHeader) 52 | pkg.respHeader = new(StdResponseHeader) 53 | //pkg.request = new(CompanyInfoCategoryRequest) 54 | //pkg.reply = new(CompanyInfoCategory) 55 | 56 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 57 | //0c 58 | pkg.reqHeader.ZipFlag = proto.FlagNotZipped 59 | //1f 18 76 00 60 | pkg.reqHeader.SeqID = internal.SequenceId() 61 | //01 62 | pkg.reqHeader.PacketType = 0x01 63 | //0b 00 64 | //PkgLen1 uint16 65 | pkg.reqHeader.PkgLen1 = 0x000e 66 | //0b 00 67 | //PkgLen2 uint16 68 | pkg.reqHeader.PkgLen2 = 0x000e 69 | //10 00 70 | pkg.reqHeader.Method = proto.STD_MSG_COMPANY_CATEGORY 71 | //pkg.contentHex = "0100" // 未解 72 | return pkg 73 | } 74 | 75 | func (obj *CompanyInfoCategoryPackage) SetParams(req *CompanyInfoCategoryRequest) { 76 | obj.request = req 77 | } 78 | 79 | func (obj *CompanyInfoCategoryPackage) Serialize() ([]byte, error) { 80 | buf := new(bytes.Buffer) 81 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 82 | b, err := hex.DecodeString(obj.contentHex) 83 | buf.Write(b) 84 | err = binary.Write(buf, binary.LittleEndian, obj.request) 85 | return buf.Bytes(), err 86 | } 87 | 88 | func (obj *CompanyInfoCategoryPackage) UnSerialize(header interface{}, data []byte) error { 89 | obj.respHeader = header.(*StdResponseHeader) 90 | 91 | var reply CompanyInfoCategoryReply 92 | buf := bytes.NewBuffer(data) 93 | err := struc.Unpack(buf, &reply) 94 | if err != nil { 95 | return err 96 | } 97 | //category := make(map[string]CompanyInfoCategory) 98 | list := []CompanyInfoCategory{} 99 | for _, v := range reply.Data { 100 | info := CompanyInfoCategory{ 101 | Name: internal.Utf8ToGbk(v.Name[:]), 102 | Filename: internal.Utf8ToGbk(v.Filename[:]), 103 | Offset: v.Offset, 104 | Length: v.Length, 105 | } 106 | //category[info.Name] = info 107 | list = append(list, info) 108 | } 109 | obj.reply = list 110 | return nil 111 | } 112 | 113 | func (obj *CompanyInfoCategoryPackage) Reply() interface{} { 114 | return obj.reply 115 | } 116 | -------------------------------------------------------------------------------- /proto/std/security_list.go: -------------------------------------------------------------------------------- 1 | package std 2 | 3 | // 获取股票列表 4 | import ( 5 | "gitee.com/quant1x/exchange" 6 | "gitee.com/quant1x/gotdx/internal" 7 | ) 8 | 9 | // GetSecurityListRequest 请求包结构 10 | type GetSecurityListRequest struct { 11 | // struc不允许slice解析,只允许包含长度的array,该长度可根据hex字符串计算 12 | Unknown1 []byte `struc:"[12]byte"` 13 | // pytdx中使用struct.Pack进行反序列化 14 | // 其中 MessageMaxBytes { 117 | logger.Warnf("msgData has bytes(%d) beyond max %d\n", header.ZipSize, MessageMaxBytes) 118 | return ErrBadData 119 | } 120 | // 3.4 读取响应的消息体 121 | msgData := make([]byte, header.ZipSize) 122 | // 设置读timeout 123 | err = conn.SetReadDeadline(time.Now().Add(opt.ReadTimeout)) 124 | if err != nil { 125 | return err 126 | } 127 | _, err = io.ReadFull(conn, msgData) 128 | if err != nil { 129 | logger.Error("读取数据指令失败-3", err) 130 | return err 131 | } 132 | // 3.5 反序列化响应的消息体 133 | var out bytes.Buffer 134 | if logger.IsDebug() { 135 | logger.Debugf("response body: %+v", hex.EncodeToString(msgData)) 136 | } 137 | var respBody []byte 138 | if header.ZipSize != header.UnZipSize { 139 | b := bytes.NewReader(msgData) 140 | r, _ := zlib.NewReader(b) 141 | defer api.CloseQuietly(r) 142 | _, _ = io.Copy(&out, r) 143 | respBody = out.Bytes() 144 | } else { 145 | respBody = msgData 146 | } 147 | if logger.IsDebug() { 148 | logger.Debugf("response body: %+v", hex.EncodeToString(respBody)) 149 | } 150 | err = msg.UnSerialize(&header, respBody) 151 | // 4. 返回 152 | return err 153 | } 154 | -------------------------------------------------------------------------------- /quotes/base_client.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "gitee.com/quant1x/gox/runtime" 11 | 12 | "gitee.com/quant1x/gox/api" 13 | "gitee.com/quant1x/gox/exception" 14 | "gitee.com/quant1x/gox/logger" 15 | ) 16 | 17 | var ( 18 | ErrTimeOut = exception.New(1, "connect timeout") 19 | ) 20 | 21 | type TcpClient struct { 22 | sync.Mutex // 匿名属性 23 | conn net.Conn // tcp连接 24 | server *Server // 服务器信息 25 | opt *Options // 参数 26 | done chan bool // connection done 27 | completedTime time.Time // 时间戳 28 | closed uint32 // 关闭次数 29 | } 30 | 31 | func NewClient(opt *Options) *TcpClient { 32 | client := &TcpClient{} 33 | if opt.MaxRetryTimes <= 0 { 34 | opt.MaxRetryTimes = DefaultRetryTimes 35 | } 36 | if opt.ConnectionTimeout <= 0 { 37 | opt.ConnectionTimeout = CONN_TIMEOUT * time.Second 38 | } 39 | if opt.ReadTimeout <= 0 { 40 | opt.ReadTimeout = RECV_TIMEOUT * time.Second 41 | } 42 | if opt.WriteTimeout <= 0 { 43 | opt.WriteTimeout = RECV_TIMEOUT * time.Second 44 | } 45 | 46 | client.opt = opt 47 | client.done = make(chan bool, 1) 48 | client.updateCompletedTimestamp() 49 | return client 50 | } 51 | 52 | // 更新最后一次成功send/recv的时间戳 53 | func (client *TcpClient) updateCompletedTimestamp() { 54 | client.completedTime = time.Now() 55 | } 56 | 57 | // 过去了多少秒 58 | func (client *TcpClient) crossTime() (elapsedTime float64) { 59 | seconds := time.Since(client.completedTime).Seconds() 60 | return seconds 61 | } 62 | 63 | // 是否超时 64 | func (client *TcpClient) hasTimedOut() bool { 65 | elapsedTime := client.crossTime() 66 | timeout := client.opt.ConnectionTimeout.Seconds() 67 | return elapsedTime >= timeout 68 | } 69 | 70 | // Command 执行通达信指令 71 | func (client *TcpClient) Command(msg Message) error { 72 | client.Lock() 73 | defer client.Unlock() 74 | if client.conn == nil { 75 | logger.Error("tcp连接失效") 76 | return io.EOF 77 | } 78 | err := process(client, msg) 79 | //errors.Is(err, net.OpError) 80 | //if _,ok:=err.( *net.OpError) ;ok{ 81 | // return nil,err 82 | //} 83 | if err != nil { 84 | logger.Error("业务处理失败", err) 85 | return err 86 | } 87 | client.updateCompletedTimestamp() 88 | return nil 89 | } 90 | 91 | func (client *TcpClient) heartbeat() { 92 | defer runtime.IgnorePanic("heartbeat.done") 93 | ticker := time.NewTicker(time.Second * 1) 94 | defer ticker.Stop() 95 | for { 96 | select { 97 | case <-ticker.C: 98 | client.Lock() 99 | timedOut := client.hasTimedOut() 100 | client.Unlock() 101 | if timedOut { 102 | msg := NewSecurityCountPackage() 103 | msg.SetParams(&SecurityCountRequest{ 104 | Market: uint16(1), 105 | }) 106 | err := client.Command(msg) 107 | if err != nil { 108 | logger.Warnf("client -> server[%s]: error > shutdown", client.server) 109 | _ = client.Close() 110 | return 111 | } else { 112 | client.updateCompletedTimestamp() 113 | logger.Warnf("client -> server[%s]: heartbeat", client.server) 114 | } 115 | // 模拟服务器主动断开或者网络断开 116 | //logger.Warnf("client -> server[%s]: test force > shutdown", client.Addr) 117 | //_ = client.Close() 118 | //return 119 | } 120 | case <-client.done: 121 | logger.Warnf("client -> server[%s]: done > shutdown", client.server) 122 | return 123 | } 124 | } 125 | } 126 | 127 | // Connect 连接服务器 128 | func (client *TcpClient) Connect(server *Server) error { 129 | addr := server.Addr() 130 | conn, err := net.DialTimeout("tcp", addr, client.opt.ConnectionTimeout) // net.DialTimeout() 131 | state := "connected" 132 | if err != nil { 133 | state = err.Error() 134 | } 135 | logger.Warnf("client -> server[%s]: %s", addr, state) 136 | if err == nil { 137 | client.conn = conn 138 | client.server = server 139 | client.updateCompletedTimestamp() 140 | go client.heartbeat() 141 | } 142 | 143 | if client.conn == nil { 144 | return ErrTimeOut 145 | } 146 | return nil 147 | } 148 | 149 | // Close 断开服务器 150 | func (client *TcpClient) Close() error { 151 | defer runtime.IgnorePanic("TcpClient.Close") 152 | if atomic.LoadUint32(&client.closed) > 0 { 153 | return io.EOF 154 | } 155 | client.done <- true 156 | close(client.done) 157 | client.opt.releaseAddress(client.server) 158 | api.CloseQuietly(client.conn) 159 | atomic.AddUint32(&client.closed, 1) 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /quotes/targets.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "math" 5 | "os" 6 | "path/filepath" 7 | 8 | "gitee.com/quant1x/exchange" 9 | "gitee.com/quant1x/exchange/cache" 10 | "gitee.com/quant1x/gotdx/internal" 11 | "gitee.com/quant1x/gox/api" 12 | "gitee.com/quant1x/gox/coroutine" 13 | ) 14 | 15 | var ( 16 | cacheSecurityCodeList = filepath.Join(cache.GetMetaPath(), "targets.csv") 17 | ) 18 | 19 | type SecurityTarget struct { 20 | Code string 21 | VolUnit uint16 22 | DecimalPoint int8 23 | Name string 24 | } 25 | 26 | var ( 27 | __mapStockList = map[string]SecurityTarget{} // 股票列表缓存 28 | __onceStockList coroutine.PeriodicOnce 29 | ) 30 | 31 | // 读取股票列表缓存 32 | func readCacheSecurityList() { 33 | filename := cacheSecurityCodeList 34 | if !api.FileExist(filename) { 35 | return 36 | } 37 | list := []SecurityTarget{} 38 | err := api.CsvToSlices(filename, &list) 39 | if err != nil || len(list) == 0 { 40 | return 41 | } 42 | for _, v := range list { 43 | code := v.Code 44 | __mapStockList[code] = v 45 | } 46 | } 47 | 48 | func writeCacheSecurityList(list []SecurityTarget) { 49 | filename := cacheSecurityCodeList 50 | api.SliceSort(list, func(a, b SecurityTarget) bool { 51 | return a.Code < b.Code 52 | }) 53 | _ = api.SlicesToCsv(filename, list) 54 | } 55 | 56 | func lazyLoadStockList() { 57 | filename := cacheSecurityCodeList 58 | bUpdated := false 59 | if !api.FileExist(filename) { 60 | // 不存在需要创建 61 | bUpdated = true 62 | } else { 63 | // 存在, 先加载一次 64 | readCacheSecurityList() 65 | // 获取文件创建时间 66 | finfo, _ := os.Stat(filename) 67 | bUpdated = exchange.CanInitialize(finfo.ModTime()) 68 | } 69 | if !bUpdated { 70 | return 71 | } 72 | list := getSecurityList() 73 | if len(list) == 0 { 74 | return 75 | } 76 | // 覆盖当前缓存 77 | for _, v := range list { 78 | code := v.Code 79 | __mapStockList[code] = SecurityTarget{Code: v.Code, VolUnit: v.VolUnit, DecimalPoint: v.DecimalPoint, Name: v.Name} 80 | } 81 | // 更新缓存 82 | newList := api.Values(__mapStockList) 83 | // 更新代码列表 84 | writeCacheSecurityList(newList) 85 | } 86 | 87 | // getSecurityList 证券列表 88 | func getSecurityList() (allList []Security) { 89 | stdApi, err := NewStdApi() 90 | if err != nil { 91 | return 92 | } 93 | defer stdApi.Close() 94 | offset := uint16(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(reply.List) > 0 { 113 | allList = append(allList, reply.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(reply.List) > 0 { 131 | allList = append(allList, reply.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) (*SecurityTarget, 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 | // SecurityBaseUnit 获取证券标价格的最小变动单位, 0.01返回100, 0.001返回1000 154 | func SecurityBaseUnit(marketId exchange.MarketType, code string) float64 { 155 | securityCode := exchange.GetSecurityCode(marketId, code) 156 | securityInfo, ok := CheckoutSecurityInfo(securityCode) 157 | if !ok { 158 | return 100.00 159 | } 160 | return math.Pow10(int(securityInfo.DecimalPoint)) 161 | } 162 | 163 | func init() { 164 | internal.RegisterBaseUnitFunction(SecurityBaseUnit) 165 | } 166 | -------------------------------------------------------------------------------- /quotes/stock_security_bars.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | 9 | "gitee.com/quant1x/gotdx/internal" 10 | "gitee.com/quant1x/gotdx/proto" 11 | ) 12 | 13 | const ( 14 | SECURITY_BARS_MAX = 800 // 单次最大获取800条K线数据 15 | ) 16 | 17 | // SecurityBars K线 18 | type SecurityBarsPackage struct { 19 | reqHeader *StdRequestHeader 20 | respHeader *StdResponseHeader 21 | request *SecurityBarsRequest 22 | response *SecurityBarsReply 23 | 24 | contentHex string 25 | } 26 | 27 | type SecurityBarsRequest struct { 28 | Market uint16 29 | Code [6]byte 30 | Category uint16 // 种类 5分钟 10分钟 31 | I uint16 // 未知 填充, 间隔多少个Category 32 | Start uint16 33 | Count uint16 34 | } 35 | 36 | type SecurityBarsReply struct { 37 | Count uint16 38 | List []SecurityBar 39 | } 40 | 41 | // SecurityBar K线数据 42 | type SecurityBar struct { 43 | Open float64 44 | Close float64 45 | High float64 46 | Low float64 47 | Vol float64 48 | Amount float64 49 | Year int 50 | Month int 51 | Day int 52 | Hour int 53 | Minute int 54 | DateTime string 55 | UpCount uint16 // 指数有效, 上涨家数 56 | DownCount uint16 // 指数有效, 下跌家数 57 | } 58 | 59 | func NewSecurityBarsPackage() *SecurityBarsPackage { 60 | obj := new(SecurityBarsPackage) 61 | obj.reqHeader = new(StdRequestHeader) 62 | obj.respHeader = new(StdResponseHeader) 63 | obj.request = new(SecurityBarsRequest) 64 | obj.response = new(SecurityBarsReply) 65 | 66 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 67 | obj.reqHeader.SeqID = internal.SequenceId() 68 | obj.reqHeader.PacketType = 0x00 69 | //obj.reqHeader.PkgLen1 = 70 | //obj.reqHeader.PkgLen2 = 71 | obj.reqHeader.Method = proto.STD_MSG_SECURITY_BARS 72 | obj.contentHex = "00000000000000000000" 73 | return obj 74 | } 75 | 76 | func (obj *SecurityBarsPackage) SetParams(req *SecurityBarsRequest) { 77 | obj.request = req 78 | if req != nil && req.I < 1 { 79 | obj.request.I = 1 80 | } 81 | } 82 | 83 | func (obj *SecurityBarsPackage) Serialize() ([]byte, error) { 84 | obj.reqHeader.PkgLen1 = 0x1c 85 | obj.reqHeader.PkgLen2 = 0x1c 86 | 87 | buf := new(bytes.Buffer) 88 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 89 | err = binary.Write(buf, binary.LittleEndian, obj.request) 90 | b, err := hex.DecodeString(obj.contentHex) 91 | buf.Write(b) 92 | 93 | //b, err := hex.DecodeString(obj.contentHex) 94 | //buf.Write(b) 95 | 96 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 97 | 98 | return buf.Bytes(), err 99 | } 100 | 101 | func (obj *SecurityBarsPackage) UnSerialize(header interface{}, data []byte) error { 102 | obj.respHeader = header.(*StdResponseHeader) 103 | 104 | pos := 0 105 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.response.Count) 106 | pos += 2 107 | 108 | pre_diff_base := 0 109 | 110 | for index := uint16(0); index < obj.response.Count; index++ { 111 | ele := SecurityBar{} 112 | ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute = internal.GetDatetime(int(obj.request.Category), data, &pos) 113 | 114 | ele.DateTime = fmt.Sprintf("%d-%02d-%02d %02d:%02d:00.000", ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute) 115 | 116 | price_open_diff := internal.DecodeVarint(data, &pos) 117 | price_close_diff := internal.DecodeVarint(data, &pos) 118 | 119 | price_high_diff := internal.DecodeVarint(data, &pos) 120 | price_low_diff := internal.DecodeVarint(data, &pos) 121 | 122 | var ivol uint32 123 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &ivol) 124 | ele.Vol = internal.IntToFloat64(ivol) 125 | pos += 4 126 | 127 | var dbvol uint32 128 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &dbvol) 129 | ele.Amount = internal.IntToFloat64(int(dbvol)) 130 | pos += 4 131 | 132 | ele.Open = float64(price_open_diff+pre_diff_base) / 1000.0 133 | price_open_diff += pre_diff_base 134 | 135 | ele.Close = float64(price_open_diff+price_close_diff) / 1000.0 136 | ele.High = float64(price_open_diff+price_high_diff) / 1000.0 137 | ele.Low = float64(price_open_diff+price_low_diff) / 1000.0 138 | 139 | pre_diff_base = price_open_diff + price_close_diff 140 | 141 | obj.response.List = append(obj.response.List, ele) 142 | } 143 | return err 144 | } 145 | 146 | func (obj *SecurityBarsPackage) Reply() interface{} { 147 | return obj.response 148 | } 149 | -------------------------------------------------------------------------------- /quotes/stock_security_snapshot.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import "gitee.com/quant1x/exchange" 4 | 5 | type ExchangeState int8 6 | 7 | const ( 8 | EXCHANGE_STATE_DELISTING ExchangeState = iota - 1 // 终止上市 9 | EXCHANGE_STATE_CLOSING // 收盘 10 | EXCHANGE_STATE_NORMAL // 正常交易 11 | 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/index_bars.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | 9 | "gitee.com/quant1x/gotdx/internal" 10 | "gitee.com/quant1x/gotdx/proto" 11 | ) 12 | 13 | // IndexBarsPackage 指数K线 14 | type IndexBarsPackage struct { 15 | reqHeader *StdRequestHeader 16 | respHeader *StdResponseHeader 17 | //request *IndexBarsRequest 18 | //reply *IndexBarsReply 19 | request *SecurityBarsRequest 20 | reply *SecurityBarsReply 21 | 22 | contentHex string 23 | } 24 | 25 | //type IndexBarsRequest struct { 26 | // MarketType uint16 27 | // Code [6]byte 28 | // Category uint16 // 种类 5分钟 10分钟 29 | // I uint16 // 未知 填充 30 | // Start uint16 31 | // Count uint16 32 | //} 33 | // 34 | //type IndexBarsReply struct { 35 | // Count uint16 36 | // List []IndexBar 37 | //} 38 | // 39 | //type IndexBar struct { 40 | // Open float64 41 | // Close float64 42 | // High float64 43 | // Low float64 44 | // Vol float64 45 | // Amount float64 46 | // Year int 47 | // Month int 48 | // Day int 49 | // Hour int 50 | // Minute int 51 | // DateTime string 52 | // UpCount uint16 53 | // DownCount uint16 54 | //} 55 | 56 | func NewIndexBarsPackage() *IndexBarsPackage { 57 | obj := new(IndexBarsPackage) 58 | obj.reqHeader = new(StdRequestHeader) 59 | obj.respHeader = new(StdResponseHeader) 60 | obj.request = new(SecurityBarsRequest) 61 | obj.reply = new(SecurityBarsReply) 62 | 63 | obj.reqHeader.ZipFlag = proto.FlagNotZipped 64 | obj.reqHeader.SeqID = internal.SequenceId() 65 | obj.reqHeader.PacketType = 0x00 66 | //obj.reqHeader.PkgLen1 = 67 | //obj.reqHeader.PkgLen2 = 68 | obj.reqHeader.Method = proto.STD_MSG_INDEXBARS 69 | obj.contentHex = "00000000000000000000" 70 | return obj 71 | } 72 | func (obj *IndexBarsPackage) SetParams(req *SecurityBarsRequest) { 73 | obj.request = req 74 | obj.request.I = 1 75 | } 76 | 77 | func (obj *IndexBarsPackage) Serialize() ([]byte, error) { 78 | obj.reqHeader.PkgLen1 = 0x1c 79 | obj.reqHeader.PkgLen2 = 0x1c 80 | 81 | buf := new(bytes.Buffer) 82 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 83 | err = binary.Write(buf, binary.LittleEndian, obj.request) 84 | b, err := hex.DecodeString(obj.contentHex) 85 | buf.Write(b) 86 | 87 | //b, err := hex.DecodeString(obj.contentHex) 88 | //buf.Write(b) 89 | 90 | //err = binary.Write(buf, binary.LittleEndian, uint16(len(obj.stocks))) 91 | 92 | return buf.Bytes(), err 93 | } 94 | 95 | func (obj *IndexBarsPackage) UnSerialize(header interface{}, data []byte) error { 96 | obj.respHeader = header.(*StdResponseHeader) 97 | 98 | pos := 0 99 | err := binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &obj.reply.Count) 100 | pos += 2 101 | 102 | pre_diff_base := 0 103 | for index := uint16(0); index < obj.reply.Count; index++ { 104 | ele := SecurityBar{} 105 | 106 | ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute = internal.GetDatetime(int(obj.request.Category), data, &pos) 107 | 108 | //if index == 0 { 109 | // ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute = getDatetime(int(obj.request.Category), data, &pos) 110 | //} else { 111 | // ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute = getDatetimeNow(int(obj.request.Category), lasttime) 112 | //} 113 | ele.DateTime = fmt.Sprintf("%d-%02d-%02d %02d:%02d:00.000", ele.Year, ele.Month, ele.Day, ele.Hour, ele.Minute) 114 | 115 | price_open_diff := internal.DecodeVarint(data, &pos) 116 | price_close_diff := internal.DecodeVarint(data, &pos) 117 | 118 | price_high_diff := internal.DecodeVarint(data, &pos) 119 | price_low_diff := internal.DecodeVarint(data, &pos) 120 | 121 | var ivol uint32 122 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &ivol) 123 | ele.Vol = internal.IntToFloat64(int(ivol)) 124 | pos += 4 125 | 126 | var dbvol uint32 127 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &dbvol) 128 | ele.Amount = internal.IntToFloat64(int(dbvol)) 129 | pos += 4 130 | 131 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.UpCount) 132 | pos += 2 133 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.DownCount) 134 | pos += 2 135 | 136 | ele.Open = float64(price_open_diff+pre_diff_base) / 1000.0 137 | price_open_diff += pre_diff_base 138 | 139 | ele.Close = float64(price_open_diff+price_close_diff) / 1000.0 140 | ele.High = float64(price_open_diff+price_high_diff) / 1000.0 141 | ele.Low = float64(price_open_diff+price_low_diff) / 1000.0 142 | 143 | pre_diff_base = price_open_diff + price_close_diff 144 | 145 | obj.reply.List = append(obj.reply.List, ele) 146 | } 147 | return err 148 | } 149 | 150 | func (obj *IndexBarsPackage) Reply() interface{} { 151 | return obj.reply 152 | } 153 | -------------------------------------------------------------------------------- /quotes/api.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "errors" 5 | "runtime" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "gitee.com/quant1x/gox/logger" 12 | ) 13 | 14 | var ( 15 | ErrInvalidServerAddress = errors.New("invalid server address") 16 | ) 17 | 18 | // Server 主机信息 19 | type Server struct { 20 | Source string `json:"source"` 21 | Name string `json:"name"` 22 | Host string `json:"host"` 23 | Port int `json:"port"` 24 | CrossTime int64 `json:"crossTime"` 25 | } 26 | 27 | func (s Server) Addr() string { 28 | return strings.Join([]string{s.Host, strconv.Itoa(s.Port)}, ":") 29 | } 30 | 31 | func (s Server) String() string { 32 | //return fmt.Sprintf("%s[%s]: host=%s, port=%d", s.Source, s.Name, s.Host, s.Port) 33 | return s.Addr() 34 | } 35 | 36 | type Options struct { 37 | ConnectionTimeout time.Duration // 连接超时 38 | ReadTimeout time.Duration // 读超时 39 | WriteTimeout time.Duration // 写超时 40 | MaxRetryTimes int // 最大重试次数 41 | RetryDuration time.Duration // 重试时间 42 | releaseAddress func(server *Server) // 归还服务器地址回调函数 43 | } 44 | 45 | // StdApi 标准行情API接口 46 | type StdApi struct { 47 | connPool *ConnPool // 连接池 48 | opt *Options // 选项 49 | once sync.Once // 滑动窗口式Once 50 | servers []Server // 服务器组 51 | ch chan Server // 服务器地址channel 52 | } 53 | 54 | // NewStdApi 创建一个标准接口 55 | func NewStdApi() (*StdApi, error) { 56 | server := GetFastHost(HOST_HQ) 57 | return NewStdApiWithServers(server) 58 | } 59 | 60 | // NewStdApiWithServers 通过服务器组创建一个标准接口 61 | func NewStdApiWithServers(srvs []Server) (*StdApi, error) { 62 | opt := Options{ 63 | ConnectionTimeout: CONN_TIMEOUT * time.Second, 64 | } 65 | stdApi := StdApi{ 66 | servers: srvs, 67 | opt: &opt, 68 | } 69 | stdApi.ch = make(chan Server, POOL_MAX) 70 | //stdApi.once.SetOffsetTime(serverResetOffsetHours, serverResetOffsetMinutes) 71 | _factory := func() (any, error) { 72 | client := NewClient(stdApi.opt) 73 | server := stdApi.AcquireAddress() 74 | if server == nil { 75 | return nil, ErrInvalidServerAddress 76 | } 77 | err := client.Connect(server) 78 | if err != nil { 79 | stdApi.ReleaseAddress(server) 80 | return nil, err 81 | } 82 | stdApi.opt.releaseAddress = stdApi.ReleaseAddress 83 | err = stdApi.tdxHello1(client) 84 | if err != nil { 85 | _ = client.Close() 86 | return nil, err 87 | } 88 | err = stdApi.tdxHello2(client) 89 | if err != nil { 90 | _ = client.Close() 91 | return nil, err 92 | } 93 | return client, err 94 | } 95 | _close := func(v any) error { 96 | client := v.(*TcpClient) 97 | return client.Close() 98 | } 99 | _ping := func(v any) error { 100 | client := v.(*TcpClient) 101 | return stdApi.tdxPing(client) 102 | } 103 | maxCap := POOL_MAX 104 | bestIpCount := len(stdApi.servers) 105 | if bestIpCount == 0 { 106 | logger.Fatalf("no available hosts") 107 | } 108 | if maxCap > bestIpCount { 109 | maxCap = bestIpCount 110 | } 111 | 112 | maxIdle := maxCap 113 | 114 | halfCpuCount := runtime.NumCPU() / 2 115 | if maxIdle > halfCpuCount { 116 | maxIdle = halfCpuCount 117 | } 118 | 119 | cp, err := NewConnPool(maxCap, maxIdle, _factory, _close, _ping) 120 | if err != nil { 121 | return nil, err 122 | } 123 | stdApi.connPool = cp 124 | return &stdApi, nil 125 | } 126 | 127 | func (this *StdApi) Len() int { 128 | return len(this.servers) 129 | } 130 | 131 | func (this *StdApi) init() { 132 | for _, v := range this.servers { 133 | this.ch <- v 134 | } 135 | } 136 | 137 | // AcquireAddress 获取一个地址 138 | func (this *StdApi) AcquireAddress() *Server { 139 | this.once.Do(this.init) 140 | // 非阻塞获取 141 | //srv, ok := <-this.ch 142 | logger.Warnf("获取一个服务器地址...begin") 143 | // 阻塞获取一个地址 144 | server := <-this.ch 145 | logger.Warnf("获取一个服务器地址...end") 146 | if len(server.Host) == 0 || server.Port == 0 { 147 | logger.Warnf("获取一个服务器地址...failed: nil") 148 | return nil 149 | } 150 | logger.Warnf("获取一个服务器地址...server=%s", server) 151 | return &server 152 | } 153 | 154 | // ReleaseAddress 返还一个地址 155 | func (this *StdApi) ReleaseAddress(srv *Server) { 156 | logger.Warnf("返回一个服务器地址...") 157 | if srv == nil || len(srv.Host) == 0 || srv.Port == 0 { 158 | logger.Warnf("返回一个服务器地址...failed: nil") 159 | return 160 | } 161 | this.once.Do(this.init) 162 | logger.Warnf("返回一个服务器地址...server=%s, begin", *srv) 163 | // 阻塞返还一个地址 164 | this.ch <- *srv 165 | logger.Warnf("返回一个服务器地址...server=%s, end", *srv) 166 | } 167 | 168 | // NumOfServers 增加返回服务器IP数量 169 | func (this *StdApi) NumOfServers() int { 170 | return len(this.servers) 171 | } 172 | 173 | func (this *StdApi) GetMaxIdleCount() int { 174 | return this.connPool.GetMaxIdleCount() 175 | } 176 | 177 | // Close 关闭 178 | func (this *StdApi) Close() { 179 | this.connPool.CloseAll() 180 | } 181 | 182 | // 通过池关闭连接 183 | func (this *StdApi) poolClose(cli *TcpClient) error { 184 | return this.connPool.CloseConn(cli) 185 | } 186 | -------------------------------------------------------------------------------- /quotes/stock_company_info_content.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "strings" 8 | 9 | "gitee.com/quant1x/exchange" 10 | "gitee.com/quant1x/gotdx/internal" 11 | "gitee.com/quant1x/gotdx/proto" 12 | "gitee.com/quant1x/gox/encoding/binary/struc" 13 | "gitee.com/quant1x/gox/util/linkedhashmap" 14 | ) 15 | 16 | // CompanyInfoContentPackage 企业基本信息 17 | type CompanyInfoContentPackage struct { 18 | reqHeader *StdRequestHeader 19 | respHeader *StdResponseHeader 20 | request *CompanyInfoContentRequest 21 | reply *CompanyInfoContent 22 | contentHex string 23 | } 24 | 25 | type CompanyInfoContentRequest struct { 26 | Market uint16 // 市场代码 27 | Code [6]byte // 股票代码 28 | Unknown1 uint16 // 未知数据 29 | Filename [80]byte // 文件名 30 | Offset uint32 // 偏移量 31 | Length uint32 // 长度 32 | Unknown2 uint32 // 未知数据 33 | } 34 | 35 | // CompanyInfoContentReply 响应包结构, 36 | type CompanyInfoContentReply struct { 37 | Market uint16 `struc:"uint16,little"` // 市场代码 38 | Code string `struc:"[6]byte,little"` // 股票代码 39 | Unknown1 []byte `struc:"[2]byte,little"` // 未知 40 | Length uint16 `struc:"uint16,little"` // 词条总数 41 | Data []byte `struc:"sizefrom=Length"` 42 | } 43 | 44 | type CompanyInfoContent struct { 45 | Market exchange.MarketType `dataframe:"market"` // 市场代码 46 | Code string `dataframe:"code"` // 短码 47 | Name string `dataframe:"name"` // 名称 48 | Length uint32 `dataframe:"length"` // 长度 49 | Content string `dataframe:"content"` // 内容 50 | } 51 | 52 | func (this *CompanyInfoContent) Map(unit string) *linkedhashmap.Map { 53 | mapInfo := linkedhashmap.New() 54 | c := strings.ReplaceAll(this.Content, "-\\u003e", "->") 55 | //arr := strings.Split(c, "\\r\\n\\r\\n") 56 | arr := strings.Split(c, "\r\n\r\n") 57 | for i, block := range arr { 58 | block = strings.TrimSpace(block) 59 | //v = strings.ReplaceAll(v, " ", "") 60 | //v = strings.ReplaceAll(v, "│\\r\\n││", "") 61 | //v = strings.Trim(v, "│") 62 | //fmt.Println(i, block) 63 | if i > 0 && strings.Index(block, unit) >= 0 { 64 | arr := strings.Split(block, "\r\n") 65 | block = "" 66 | for _, v := range arr { 67 | if strings.Index(v, unit) >= 0 { 68 | continue 69 | } 70 | if strings.Index(v, "┌") >= 0 && strings.Index(v, "┐") >= 0 { 71 | continue 72 | } 73 | if strings.Index(v, "└") >= 0 && strings.Index(v, "┘") >= 0 { 74 | continue 75 | } 76 | if strings.Index(v, "├") >= 0 && strings.Index(v, "┤") >= 0 { 77 | continue 78 | } 79 | v = strings.TrimLeft(v, "│") 80 | v = strings.TrimRight(v, "│") 81 | v = strings.TrimSpace(v) 82 | v = strings.ReplaceAll(v, "│", "|") 83 | //v = strings.TrimLeft(v, "|") 84 | if v[0] == '|' { 85 | block += v[1:] 86 | } else { 87 | block += "|" + v 88 | } 89 | } 90 | list := strings.Split(block[1:], "|") 91 | 92 | for k := 0; k < len(list); k += 2 { 93 | key := strings.TrimSpace(list[k]) 94 | value := strings.TrimSpace(list[k+1]) 95 | mapInfo.Put(key, value) 96 | } 97 | //mapInfo.Each(func(key interface{}, value interface{}) { 98 | // fmt.Println(key, value) 99 | //}) 100 | 101 | break 102 | } 103 | } 104 | return mapInfo 105 | } 106 | 107 | func NewCompanyInfoContentPackage() *CompanyInfoContentPackage { 108 | pkg := new(CompanyInfoContentPackage) 109 | pkg.reqHeader = new(StdRequestHeader) 110 | pkg.respHeader = new(StdResponseHeader) 111 | //pkg.request = new(CompanyInfoContentRequest) 112 | //pkg.reply = new(CompanyInfoContent) 113 | 114 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 115 | //0c 116 | pkg.reqHeader.ZipFlag = proto.FlagNotZipped 117 | //1f 18 76 00 118 | pkg.reqHeader.SeqID = internal.SequenceId() 119 | //01 120 | pkg.reqHeader.PacketType = 0x01 121 | //0b 00 122 | //PkgLen1 uint16 123 | pkg.reqHeader.PkgLen1 = 0x0068 124 | //0b 00 125 | //PkgLen2 uint16 126 | pkg.reqHeader.PkgLen2 = 0x0068 127 | //10 00 128 | pkg.reqHeader.Method = proto.STD_MSG_COMPANY_CONTENT 129 | //pkg.contentHex = "0100" // 未解 130 | return pkg 131 | } 132 | 133 | func (obj *CompanyInfoContentPackage) SetParams(req *CompanyInfoContentRequest) { 134 | obj.request = req 135 | } 136 | 137 | func (obj *CompanyInfoContentPackage) Serialize() ([]byte, error) { 138 | buf := new(bytes.Buffer) 139 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 140 | b, err := hex.DecodeString(obj.contentHex) 141 | buf.Write(b) 142 | err = binary.Write(buf, binary.LittleEndian, obj.request) 143 | return buf.Bytes(), err 144 | } 145 | 146 | func (obj *CompanyInfoContentPackage) UnSerialize(header interface{}, data []byte) error { 147 | obj.respHeader = header.(*StdResponseHeader) 148 | 149 | var reply CompanyInfoContentReply 150 | buf := bytes.NewBuffer(data) 151 | err := struc.Unpack(buf, &reply) 152 | if err != nil { 153 | return err 154 | } 155 | response := CompanyInfoContent{ 156 | Market: exchange.MarketType(reply.Market), 157 | Code: reply.Code, 158 | Length: uint32(reply.Length), 159 | Content: internal.Utf8ToGbk(reply.Data), 160 | } 161 | obj.reply = &response 162 | return nil 163 | } 164 | 165 | func (obj *CompanyInfoContentPackage) Reply() interface{} { 166 | return obj.reply 167 | } 168 | -------------------------------------------------------------------------------- /securities/security_list.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "math" 5 | "os" 6 | "path/filepath" 7 | "slices" 8 | 9 | "gitee.com/quant1x/exchange" 10 | "gitee.com/quant1x/exchange/cache" 11 | "gitee.com/quant1x/gotdx" 12 | "gitee.com/quant1x/gotdx/quotes" 13 | "gitee.com/quant1x/gox/api" 14 | "gitee.com/quant1x/gox/coroutine" 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.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 | -------------------------------------------------------------------------------- /quotes/bestip_embed.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io" 7 | "os" 8 | "slices" 9 | "strings" 10 | "text/template" 11 | 12 | "gitee.com/quant1x/gotdx/internal" 13 | "gitee.com/quant1x/gox/api" 14 | "gitee.com/quant1x/gox/logger" 15 | "gopkg.in/ini.v1" 16 | ) 17 | 18 | var ( 19 | // ResourcesPath 资源路径 20 | ResourcesPath = "resources" 21 | ) 22 | 23 | //go:embed resources/* 24 | var resources embed.FS 25 | 26 | const ( 27 | sectionStandardServer = "HQHOST" 28 | defaultStandardPort = 7709 29 | keyHostNum = "HostNum" 30 | sectionExtensionServer = "DSHOST" 31 | defaultExtensionPort = 7727 32 | ) 33 | 34 | var ( 35 | ignoreStandardPortList = []int{80} // 标准行情需要忽略的端口号 36 | ignoreExtensionPortList = []int{} // 扩展行情需要忽略的端口号 37 | ) 38 | 39 | type tdxConfig struct { 40 | source string 41 | filename string 42 | } 43 | 44 | var ( 45 | tdxServerList = []tdxConfig{ 46 | tdxConfig{source: "通达信", filename: "tdx.cfg"}, 47 | tdxConfig{source: "中信证券", filename: "zhongxin.cfg"}, 48 | tdxConfig{source: "华泰证券", filename: "huatai.cfg"}, 49 | tdxConfig{source: "国泰君安", filename: "guotaijunan.cfg"}, 50 | } 51 | ) 52 | 53 | var ( 54 | templateAddress = `package quotes 55 | 56 | var ( 57 | // StandardServerList 标准行情服务器列表 58 | StandardServerList = []Server{ 59 | {{- range .Std}} 60 | {Source: "{{.Source}}", Name: "{{.Name}}", Host: "{{.Host}}", Port: {{.Port}}, CrossTime: 0}, 61 | {{- end}} 62 | } 63 | // ExtensionServerList 扩展行情服务器列表 64 | ExtensionServerList = []Server{ 65 | {{- range .Ext}} 66 | {Source: "{{.Source}}", Name: "{{.Name}}", Host: "{{.Host}}", Port: {{.Port}}, CrossTime: 0}, 67 | {{- end}} 68 | } 69 | ) 70 | 71 | ` 72 | ) 73 | 74 | func loadAllConfig() { 75 | var standardServers, extensionServers []Server 76 | for _, config := range tdxServerList { 77 | std, ext := loadTdxConfig(config) 78 | if len(std) > 0 { 79 | standardServers = append(standardServers, std...) 80 | } 81 | if len(ext) > 0 { 82 | extensionServers = append(extensionServers, ext...) 83 | } 84 | } 85 | fmt.Println("----------<" + sectionStandardServer + ">----------") 86 | for _, v := range standardServers { 87 | fmt.Printf(`{Source: "%s", Name: "%s", Host: "%s", Port: %d, CrossTime: 0},`+"\n", v.Source, v.Name, v.Host, v.Port) 88 | } 89 | fmt.Println("----------<" + sectionExtensionServer + ">----------") 90 | for _, v := range extensionServers { 91 | fmt.Printf(`{Source: "%s", Name: "%s", Host: "%s", Port: %d, CrossTime: 0},`+"\n", v.Source, v.Name, v.Host, v.Port) 92 | } 93 | tmpl, err := template.New("address").Parse(templateAddress) 94 | if err != nil { 95 | logger.Fatalf("解析服务器地址模版失败, error=%+v", err) 96 | } 97 | data := struct { 98 | Std []Server 99 | Ext []Server 100 | }{ 101 | Std: standardServers, 102 | Ext: extensionServers, 103 | } 104 | writer, err := os.Create("bestip_address.go") 105 | if err != nil { 106 | logger.Fatalf("创建bestip_address.go源文件失败, error=%+v", err) 107 | } 108 | err = tmpl.Execute(writer, data) 109 | if err != nil { 110 | logger.Fatalf("执行服务器地址模版失败, error=%+v", err) 111 | } 112 | } 113 | 114 | func loadTdxConfig(config tdxConfig) (std, ext []Server) { 115 | name := config.filename 116 | source := config.source 117 | fs, err := api.OpenEmbed(resources, ResourcesPath+"/"+name) 118 | if err != nil { 119 | logger.Fatalf("%+v", err) 120 | } 121 | data, err := io.ReadAll(fs) 122 | if err != nil { 123 | logger.Fatalf("%+v", err) 124 | } 125 | cfg, err := ini.Load(data) 126 | if err != nil { 127 | logger.Fatalf("%+v", err) 128 | } 129 | //fmt.Println("----------<" + sectionStandardServer + ">----------") 130 | section := cfg.Section(sectionStandardServer) 131 | if section == nil { 132 | return 133 | } 134 | v := section.Key(keyHostNum).Value() 135 | hostNum := int(api.ParseInt(v)) 136 | for i := 0; i < hostNum; i++ { 137 | hostName := section.Key(fmt.Sprintf("HostName%02d", i+1)).Value() 138 | hostName = internal.Utf8ToGbk(api.String2Bytes(hostName)) 139 | ipAddress := section.Key(fmt.Sprintf("IPAddress%02d", i+1)).Value() 140 | if isIPV6(ipAddress) { 141 | continue 142 | } 143 | tmpPort := section.Key(fmt.Sprintf("Port%02d", i+1)).Value() 144 | port := int(api.ParseInt(tmpPort)) 145 | if slices.Contains(ignoreStandardPortList, port) { 146 | continue 147 | } 148 | srv := Server{Source: source, Name: hostName, Host: ipAddress, Port: port} 149 | std = append(std, srv) 150 | } 151 | //fmt.Println("----------<" + sectionExtensionServer + ">----------") 152 | section = cfg.Section(sectionExtensionServer) 153 | if section == nil { 154 | return 155 | } 156 | v = section.Key(keyHostNum).Value() 157 | hostNum = int(api.ParseInt(v)) 158 | for i := 0; i < hostNum; i++ { 159 | hostName := section.Key(fmt.Sprintf("HostName%02d", i+1)).Value() 160 | hostName = internal.Utf8ToGbk(api.String2Bytes(hostName)) 161 | ipAddress := section.Key(fmt.Sprintf("IPAddress%02d", i+1)).Value() 162 | if isIPV6(ipAddress) { 163 | continue 164 | } 165 | tmpPort := section.Key(fmt.Sprintf("Port%02d", i+1)).Value() 166 | port := int(api.ParseInt(tmpPort)) 167 | if slices.Contains(ignoreExtensionPortList, port) { 168 | continue 169 | } 170 | srv := Server{Source: source, Name: hostName, Host: ipAddress, Port: port} 171 | ext = append(ext, srv) 172 | } 173 | return 174 | } 175 | 176 | func isIPV6(address string) bool { 177 | arr := strings.Split(address, ":") 178 | return len(arr) > 2 179 | } 180 | -------------------------------------------------------------------------------- /internal/time.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "time" 8 | 9 | "gitee.com/quant1x/gotdx/proto" 10 | "gitee.com/quant1x/gox/api" 11 | ) 12 | 13 | const ( 14 | __tm_h_width = 1000000 15 | __tm_m_width = 10000 16 | __tm_t_width = 1000 17 | ) 18 | 19 | func _format_time0(time_stamp string) string { 20 | // format time from reversed_bytes0 21 | // by using method from https://github.com/rainx/pytdx/issues/187 22 | length := len(time_stamp) 23 | t1 := api.ParseInt(time_stamp[:length-6]) 24 | tm := fmt.Sprintf("%02d:", t1) 25 | tmp := time_stamp[length-6 : length-4] 26 | n := api.ParseInt(tmp) 27 | if n < 60 { 28 | tm += fmt.Sprintf("%02s:", tmp) 29 | tmp = time_stamp[length-4:] 30 | f := api.ParseFloat(tmp) 31 | tm += fmt.Sprintf("%06.3f", (f*60.0)/10000.00) 32 | } else { 33 | tmp = time_stamp[length-6:] 34 | f := api.ParseFloat(tmp) 35 | tm += fmt.Sprintf("%02d:", int64(f*60.0)/1000000) 36 | n = int64(f) 37 | tm += fmt.Sprintf("%06.3f", float64((n*60)%1000000)*60/1000000.0) 38 | } 39 | return tm 40 | } 41 | 42 | func timeFromStr(time_stamp string) string { 43 | // format time from reversed_bytes0 44 | // by using method from https://github.com/rainx/pytdx/issues/187 45 | length := len(time_stamp) 46 | t1 := api.ParseInt(time_stamp[:length-6]) 47 | tm := fmt.Sprintf("%02d:", t1) 48 | tmp := time_stamp[length-6 : length-4] 49 | n := api.ParseInt(tmp) 50 | if n < 60 { 51 | tm += fmt.Sprintf("%02s:", tmp) 52 | tmp = time_stamp[length-4:] 53 | f := api.ParseFloat(tmp) 54 | tm += fmt.Sprintf("%06.3f", (f*60.0)/10000.00) 55 | } else { 56 | tmp = time_stamp[length-6:] 57 | f := api.ParseFloat(tmp) 58 | tm += fmt.Sprintf("%02d:", int64(f*60.0)/1000000) 59 | n = int64(f) 60 | tm += fmt.Sprintf("%06.3f", float64((n*60)%1000000)*60/1000000.0) 61 | } 62 | return tm 63 | } 64 | 65 | func TimeFromInt(stamp int) string { 66 | //123456789 67 | h := stamp / __tm_h_width 68 | tmp1 := stamp % __tm_h_width 69 | m1 := tmp1 / __tm_m_width 70 | tmp2 := tmp1 % __tm_m_width 71 | m := 0 72 | s := 0 73 | t := 0 74 | st := float64(0.00) 75 | if m1 < 60 { 76 | m = m1 77 | tmp3 := tmp2 * 60 78 | s = tmp3 / __tm_m_width 79 | t = tmp3 % __tm_m_width 80 | t /= 10 81 | st = float64(tmp3) / __tm_m_width 82 | } else { 83 | h += 1 84 | tmp3 := tmp1 85 | m = tmp3 / __tm_h_width 86 | tmp4 := (tmp3 % __tm_h_width) * 60 87 | s = tmp4 / __tm_h_width 88 | t = tmp4 % __tm_h_width 89 | t /= __tm_t_width 90 | st = float64(tmp4) / __tm_h_width 91 | } 92 | _ = s 93 | _ = t 94 | return fmt.Sprintf("%02d:%02d:%06.3f", h, m, st) 95 | } 96 | 97 | func GetDatetimeFromUint32(category int, zipday uint32, tminutes uint16) (year int, month int, day int, hour int, minute int) { 98 | hour = 15 99 | if category < 4 || category == 7 || category == 8 { 100 | year = int((zipday >> 11) + 2004) 101 | month = int((zipday % 2048) / 100) 102 | day = int((zipday % 2048) % 100) 103 | hour = int(tminutes / 60) 104 | minute = int(tminutes % 60) 105 | } else { 106 | year = int(zipday / 10000) 107 | month = int((zipday % 10000) / 100) 108 | day = int(zipday % 100) 109 | } 110 | return 111 | } 112 | 113 | func getDatetimeNow(category int, lasttime string) (year int, month int, day int, hour int, minute int) { 114 | utime, _ := time.Parse("2006-01-02 15:04:05", lasttime) 115 | switch category { 116 | case proto.KLINE_TYPE_5MIN: 117 | utime = utime.Add(time.Minute * 5) 118 | case proto.KLINE_TYPE_15MIN: 119 | utime = utime.Add(time.Minute * 15) 120 | case proto.KLINE_TYPE_30MIN: 121 | utime = utime.Add(time.Minute * 30) 122 | case proto.KLINE_TYPE_1HOUR: 123 | utime = utime.Add(time.Hour) 124 | case proto.KLINE_TYPE_DAILY: 125 | utime = utime.AddDate(0, 0, 1) 126 | case proto.KLINE_TYPE_WEEKLY: 127 | utime = utime.Add(time.Hour * 24 * 7) 128 | case proto.KLINE_TYPE_MONTHLY: 129 | utime = utime.AddDate(0, 1, 0) 130 | case proto.KLINE_TYPE_EXHQ_1MIN: 131 | utime = utime.Add(time.Minute) 132 | case proto.KLINE_TYPE_1MIN: 133 | utime = utime.Add(time.Minute) 134 | case proto.KLINE_TYPE_RI_K: 135 | utime = utime.AddDate(0, 0, 1) 136 | case proto.KLINE_TYPE_3MONTH: 137 | utime = utime.AddDate(0, 3, 0) 138 | case proto.KLINE_TYPE_YEARLY: 139 | utime = utime.AddDate(1, 0, 0) 140 | } 141 | 142 | if category < 4 || category == 7 || category == 8 { 143 | if (utime.Hour() >= 15 && utime.Minute() > 0) || (utime.Hour() > 15) { 144 | utime = utime.AddDate(0, 0, 1) 145 | utime = utime.Add(time.Minute * 30) 146 | hour = (utime.Hour() + 18) % 24 147 | } else { 148 | hour = utime.Hour() 149 | } 150 | minute = utime.Minute() 151 | } else { 152 | if utime.Unix() > time.Now().Unix() { 153 | utime = time.Now() 154 | } 155 | hour = utime.Hour() 156 | minute = utime.Minute() 157 | if utime.Hour() > 15 { 158 | hour = 15 159 | minute = 0 160 | } 161 | } 162 | year = utime.Year() 163 | month = int(utime.Month()) 164 | day = utime.Day() 165 | return 166 | } 167 | 168 | func GetTime(b []byte, pos *int) (h uint16, m uint16) { 169 | var sec uint16 170 | _ = binary.Read(bytes.NewBuffer(b[*pos:*pos+2]), binary.LittleEndian, &sec) 171 | h = sec / 60 172 | m = sec % 60 173 | *pos += 2 174 | return 175 | } 176 | 177 | func GetDatetime(category int, b []byte, pos *int) (year int, month int, day int, hour int, minute int) { 178 | hour = 15 179 | if category < 4 || category == 7 || category == 8 { 180 | var zipday, tminutes uint16 181 | _ = binary.Read(bytes.NewBuffer(b[*pos:*pos+2]), binary.LittleEndian, &zipday) 182 | *pos += 2 183 | _ = binary.Read(bytes.NewBuffer(b[*pos:*pos+2]), binary.LittleEndian, &tminutes) 184 | *pos += 2 185 | 186 | year = int((zipday >> 11) + 2004) 187 | month = int((zipday % 2048) / 100) 188 | day = int((zipday % 2048) % 100) 189 | hour = int(tminutes / 60) 190 | minute = int(tminutes % 60) 191 | } else { 192 | var zipday uint32 193 | _ = binary.Read(bytes.NewBuffer(b[*pos:*pos+4]), binary.LittleEndian, &zipday) 194 | *pos += 4 195 | year = int(zipday / 10000) 196 | month = int((zipday % 10000) / 100) 197 | day = int(zipday % 100) 198 | } 199 | return 200 | } 201 | -------------------------------------------------------------------------------- /quotes/stock_xdxr_info.go: -------------------------------------------------------------------------------- 1 | package quotes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | 9 | "gitee.com/quant1x/gotdx/internal" 10 | "gitee.com/quant1x/gotdx/proto" 11 | "gitee.com/quant1x/gox/encoding/binary/struc" 12 | ) 13 | 14 | var ( 15 | XDXR_CATEGORY_MAPPING = map[int]string{ 16 | 1: "除权除息", 17 | 2: "送配股上市", 18 | 3: "非流通股上市", 19 | 4: "未知股本变动", 20 | 5: "股本变化", 21 | 6: "增发新股", 22 | 7: "股份回购", 23 | 8: "增发新股上市", 24 | 9: "转配股上市", 25 | 10: "可转债上市", 26 | 11: "扩缩股", 27 | 12: "非流通股缩股", 28 | 13: "送认购权证", 29 | 14: "送认沽权证", 30 | } 31 | ) 32 | 33 | // XdxrInfoPackage 除权除息 34 | type XdxrInfoPackage struct { 35 | reqHeader *StdRequestHeader 36 | respHeader *StdResponseHeader 37 | request *XdxrInfoRequest 38 | response *XdxrInfoReply 39 | reply []XdxrInfo 40 | contentHex string 41 | } 42 | 43 | type XdxrInfoRequest struct { 44 | //Count uint16 // 总数 45 | Market uint8 // 市场代码 46 | Code [6]byte // 股票代码 47 | } 48 | 49 | // XdxrInfoReply 响应包结构 50 | type XdxrInfoReply struct { 51 | Unknown []byte `struc:"[9]byte,little"` // 未知 52 | Count uint16 `struc:"uint16,little,sizeof=List"` // 总数 53 | List []RawXdxrInfo `struc:"[29]byte, little"` // [29]byte和title中间必须要有一个空格 54 | } 55 | 56 | type RawXdxrInfo struct { 57 | Market int `struc:"uint8,little"` // 市场代码 58 | Code string `struc:"[6]byte,little"` // 股票代码 59 | Unknown int `struc:"uint8,little"` // 未知 60 | Date uint32 `struc:"uint32,little"` // 日期 61 | Category int `struc:"uint8,little"` // 类型 62 | Data []byte `struc:"[16]byte,little"` 63 | } 64 | 65 | type XdxrInfo struct { 66 | Date string // 日期 67 | Category int // 类型 68 | Name string // 类型名称 69 | FenHong float64 // 分红 70 | PeiGuJia float64 // 配股价 71 | SongZhuanGu float64 // 送转股 72 | PeiGu float64 // 配股 73 | SuoGu float64 // 缩股 74 | QianLiuTong float64 // 前流通 75 | HouLiuTong float64 // 后流通 76 | QianZongGuBen float64 // 前总股本 77 | HouZongGuBen float64 // 后总股本 78 | FenShu float64 // 份数 79 | XingQuanJia float64 // 行权价 80 | } 81 | 82 | // IsCapitalChange 是否股本变化 83 | func (x *XdxrInfo) IsCapitalChange() bool { 84 | switch x.Category { 85 | case 1, 11, 12, 13, 14: 86 | return false 87 | default: 88 | if x.HouLiuTong > 0 && x.HouZongGuBen > 0 { 89 | return true 90 | } 91 | } 92 | return false 93 | } 94 | 95 | // Adjust 返回复权回调函数 factor 96 | func (x *XdxrInfo) Adjust() func(p float64) float64 { 97 | songZhuangu := x.SongZhuanGu 98 | peiGu := x.PeiGu 99 | suoGu := x.SuoGu 100 | xdxrGuShu := (songZhuangu + peiGu - suoGu) / 10 101 | fenHong := x.FenHong 102 | peiGuJia := x.PeiGuJia 103 | xdxrFenHong := (peiGuJia*peiGu - fenHong) / 10 104 | 105 | factor := func(p float64) float64 { 106 | v := (p + xdxrFenHong) / (1 + xdxrGuShu) 107 | //return num.Decimal(v) 108 | return v 109 | } 110 | return factor 111 | } 112 | 113 | func NewXdxrInfoPackage() *XdxrInfoPackage { 114 | pkg := new(XdxrInfoPackage) 115 | pkg.reqHeader = new(StdRequestHeader) 116 | pkg.respHeader = new(StdResponseHeader) 117 | pkg.request = new(XdxrInfoRequest) 118 | pkg.response = new(XdxrInfoReply) 119 | 120 | //0c 1f 18 76 00 01 0b 00 0b 00 10 00 01 00 121 | //0c 122 | pkg.reqHeader.ZipFlag = proto.FlagNotZipped 123 | //1f 18 76 00 124 | pkg.reqHeader.SeqID = internal.SequenceId() 125 | //01 126 | pkg.reqHeader.PacketType = 0x01 127 | //0b 00 128 | //PkgLen1 uint16 129 | pkg.reqHeader.PkgLen1 = 0x000b 130 | //0b 00 131 | //PkgLen2 uint16 132 | pkg.reqHeader.PkgLen2 = 0x000b 133 | //10 00 134 | pkg.reqHeader.Method = proto.STD_MSG_XDXR_INFO 135 | pkg.contentHex = "0100" // 未解 136 | return pkg 137 | } 138 | 139 | func (obj *XdxrInfoPackage) SetParams(req *XdxrInfoRequest) { 140 | //req.Count = 1 141 | obj.request = req 142 | } 143 | 144 | func (obj *XdxrInfoPackage) Serialize() ([]byte, error) { 145 | buf := new(bytes.Buffer) 146 | err := binary.Write(buf, binary.LittleEndian, obj.reqHeader) 147 | b, err := hex.DecodeString(obj.contentHex) 148 | buf.Write(b) 149 | err = binary.Write(buf, binary.LittleEndian, obj.request) 150 | return buf.Bytes(), err 151 | } 152 | 153 | func (obj *XdxrInfoPackage) UnSerialize(header interface{}, data []byte) error { 154 | obj.respHeader = header.(*StdResponseHeader) 155 | // 构造流 156 | buf := bytes.NewBuffer(data) 157 | var reply XdxrInfoReply 158 | err := struc.Unpack(buf, &reply) 159 | if err != nil { 160 | return err 161 | } 162 | var list = []XdxrInfo{} 163 | for _, v := range reply.List { 164 | year, month, day, hour, minute := internal.GetDatetimeFromUint32(9, v.Date, 0) 165 | xdxr := XdxrInfo{ 166 | //Date string // 日期 167 | Date: fmt.Sprintf("%04d-%02d-%02d", year, month, day), 168 | //Category int // 类型 169 | Category: v.Category, 170 | //Name string // 类型名称 171 | Name: XDXR_CATEGORY_MAPPING[v.Category], 172 | //FenHong int // 分红 173 | //PeiGuJia int // 配股价 174 | //SongZhuanGu int // 送转股 175 | //PeiGu int // 配股 176 | //SuoGu int // 锁骨 177 | //QianLiuTong int // 盘前流通 178 | //HouLiuTong int // 盘后流通 179 | //QianZongGuBen int // 前总股本 180 | //HouZongGuBen int // 后总股本 181 | //FenShu int // 份数 182 | //XingGuanJia int // 行权价 183 | } 184 | switch xdxr.Category { 185 | case 1: 186 | var f float32 187 | pos := 0 188 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 189 | xdxr.FenHong = float64(f) 190 | pos += 4 191 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 192 | xdxr.PeiGuJia = float64(f) 193 | pos += 4 194 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 195 | xdxr.SongZhuanGu = float64(f) 196 | pos += 4 197 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 198 | xdxr.PeiGu = float64(f) 199 | case 11, 12: 200 | var f float32 201 | pos := 8 202 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 203 | xdxr.SuoGu = float64(f) 204 | case 13, 14: 205 | var f float32 206 | pos := 0 207 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 208 | xdxr.XingQuanJia = float64(f) 209 | pos = 8 210 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &f) 211 | xdxr.FenShu = float64(f) 212 | default: 213 | var i uint32 214 | pos := 0 215 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &i) 216 | xdxr.QianLiuTong = __get_v(i) 217 | pos += 4 218 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &i) 219 | xdxr.QianZongGuBen = __get_v(i) 220 | pos += 4 221 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &i) 222 | xdxr.HouLiuTong = __get_v(i) 223 | pos += 4 224 | _ = binary.Read(bytes.NewBuffer(v.Data[pos:pos+4]), binary.LittleEndian, &i) 225 | xdxr.HouZongGuBen = __get_v(i) 226 | } 227 | list = append(list, xdxr) 228 | _ = hour 229 | _ = minute 230 | } 231 | obj.reply = list 232 | return nil 233 | } 234 | 235 | func (obj *XdxrInfoPackage) Reply() interface{} { 236 | return obj.reply 237 | } 238 | 239 | func __get_v(v uint32) float64 { 240 | if v == 0 { 241 | return 0 242 | } 243 | return internal.IntToFloat64(int(v)) 244 | } 245 | -------------------------------------------------------------------------------- /securities/margin_trading.go: -------------------------------------------------------------------------------- 1 | package securities 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | urlpkg "net/url" 7 | "slices" 8 | "time" 9 | 10 | "gitee.com/quant1x/exchange" 11 | "gitee.com/quant1x/exchange/cache" 12 | "gitee.com/quant1x/gox/api" 13 | "gitee.com/quant1x/gox/coroutine" 14 | "gitee.com/quant1x/gox/http" 15 | ) 16 | 17 | const ( 18 | // 两融配置文件 19 | marginTradingFilename = "margin-trading.csv" 20 | // https://data.eastmoney.com/rzrq/detail/all.html 21 | ) 22 | 23 | type FinancingAndSecuritiesLendingTarget struct { 24 | Code string `name:"证券代码" dataframe:"code"` 25 | } 26 | 27 | var ( 28 | onceMarginTrading coroutine.RollingOnce 29 | cacheMarginTradingList []string 30 | mapMarginTrading = map[string]bool{} 31 | ) 32 | 33 | const ( 34 | // https://data.eastmoney.com/rzrq/detail/all.html 35 | // 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 36 | urlEastMoneyApiRZRQ = "https://datacenter-web.eastmoney.com/api/data/v1/get" 37 | rzrqPageSize = 500 38 | ) 39 | 40 | // SecurityMarginTrading 融资融券 41 | type SecurityMarginTrading struct { 42 | DATE string `name:"日期" json:"DATE"` 43 | MARKET string `name:"市场" json:"MARKET"` 44 | SCODE string `name:"代码" json:"SCODE"` 45 | SecName string `name:"证券名称" json:"SECNAME"` 46 | RZYE float64 `name:"融资余额(元)" json:"RZYE"` 47 | RQYL float64 `name:"融券余量(股)" json:"RQYL"` 48 | RZRQYE float64 `name:"融资融券余额(元)" json:"RZRQYE"` 49 | RQYE float64 `name:"融券余额(元)" json:"RQYE"` 50 | RQMCL float64 `name:"融券卖出量(股)" json:"RQMCL"` 51 | RZRQYECZ float64 `name:"融资融券余额差值(元)" json:"RZRQYECZ"` 52 | RZMRE float64 `name:"融资买入额(元)" json:"RZMRE"` 53 | SZ float64 `name:"SZ" json:"SZ"` 54 | RZYEZB float64 `name:"融资余额占流通市值比(%)" json:"RZYEZB"` 55 | RZMRE3D float64 `name:"3日融资买入额(元)" json:"RZMRE3D"` 56 | RZMRE5D float64 `name:"5日融资买入额(元)" json:"RZMRE5D"` 57 | RZMRE10D float64 `name:"10日融资买入额(元)" json:"RZMRE10D"` 58 | RZCHE float64 `name:"融资偿还额(元)" json:"RZCHE"` 59 | RZCHE3D float64 `name:"3日融资偿还额(元)" json:"RZCHE3D"` 60 | RZCHE5D float64 `name:"5日融资偿还额(元)" json:"RZCHE5D"` 61 | RZCHE10D float64 `name:"10日融资偿还额(元)" json:"RZCHE10D"` 62 | RZJME float64 `name:"融资净买额(元)" json:"RZJME"` 63 | RZJME3D float64 `name:"3日融资净买额(元)" json:"RZJME3D"` 64 | RZJME5D float64 `name:"5日融资净买额(元)" json:"RZJME5D"` 65 | RZJME10D float64 `name:"10日融资净买额(元)" json:"RZJME10D"` 66 | RQMCL3D float64 `name:"3日融券卖出量(股)" json:"RQMCL3D"` 67 | RQMCL5D float64 `name:"5日融券卖出量(股)" json:"RQMCL5D"` 68 | RQMCL10D float64 `name:"10日融券卖出量(股)" json:"RQMCL10D"` 69 | RQCHL float64 `name:"融券偿还量(股)" json:"RQCHL"` 70 | RQCHL3D float64 `name:"3日融券偿还量(股)" json:"RQCHL3D"` 71 | RQCHL5D float64 `name:"5日融券偿还量(股)" json:"RQCHL5D"` 72 | RQCHL10D float64 `name:"10日融券偿还量(股)" json:"RQCHL10D"` 73 | RQJMG float64 `name:"融券净卖出(股)" json:"RQJMG"` 74 | RQJMG3D float64 `name:"3日融券净卖出(股)" json:"RQJMG3D"` 75 | RQJMG5D float64 `name:"5日融券净卖出(股)" json:"RQJMG5D"` 76 | RQJMG10D float64 `name:"10日融券净卖出(股)" json:"RQJMG10D"` 77 | SPJ float64 `name:"收盘价" json:"SPJ"` 78 | ZDF float64 `name:"涨跌幅" json:"ZDF"` 79 | RChange3DCP float64 `name:"3日未识别" json:"RCHANGE3DCP"` 80 | RChange5DCP float64 `name:"5日未识别" json:"RCHANGE5DCP"` 81 | RChange10DCP float64 `name:"10日未识别" json:"RCHANGE10DCP"` 82 | KCB int `name:"科创板" json:"KCB"` 83 | TradeMarketCode string `name:"二级市场代码" json:"TRADE_MARKET_CODE"` 84 | TradeMarket string `name:"二级市场" json:"TRADE_MARKET"` 85 | FinBalanceGr float64 `json:"FIN_BALANCE_GR"` 86 | SecuCode string `name:"证券代码" json:"SECUCODE"` 87 | } 88 | 89 | type rawMarginTrading struct { 90 | Version string `json:"version"` 91 | Result struct { 92 | Pages int `json:"pages"` 93 | Data []SecurityMarginTrading `json:"data"` 94 | Count int `json:"count"` 95 | } `json:"result"` 96 | Success bool `json:"success"` 97 | Message string `json:"message"` 98 | Code int `json:"code"` 99 | } 100 | 101 | func rawMarginTradingList(date string, pageNumber int) ([]SecurityMarginTrading, int, error) { 102 | tradeDate := exchange.FixTradeDate(date) 103 | params := urlpkg.Values{ 104 | "reportName": {"RPTA_WEB_RZRQ_GGMX"}, 105 | "columns": {"ALL"}, 106 | "source": {"WEB"}, 107 | "sortColumns": {"scode"}, 108 | "sortTypes": {"1"}, 109 | "pageSize": {fmt.Sprintf("%d", rzrqPageSize)}, 110 | "pageNumber": {fmt.Sprintf("%d", pageNumber)}, 111 | "client": {"WEB"}, 112 | "filter": {fmt.Sprintf(`(DATE='%s')`, tradeDate)}, 113 | } 114 | 115 | url := urlEastMoneyApiRZRQ + "?" + params.Encode() 116 | data, err := http.Get(url) 117 | if err != nil { 118 | return nil, 0, err 119 | } 120 | var raw rawMarginTrading 121 | err = json.Unmarshal(data, &raw) 122 | if err != nil { 123 | return nil, 0, err 124 | } 125 | return raw.Result.Data, raw.Result.Pages, nil 126 | } 127 | 128 | func getMarginTradingDate() string { 129 | return exchange.GetFrontTradeDay() 130 | } 131 | 132 | // GetMarginTradingList 获取两融列表 133 | func GetMarginTradingList() []SecurityMarginTrading { 134 | date := getMarginTradingDate() 135 | var list []SecurityMarginTrading 136 | pages := 1 137 | for i := 0; i < pages; i++ { 138 | tmpList, tmpPages, err := rawMarginTradingList(date, i+1) 139 | if err != nil { 140 | break 141 | } 142 | list = append(list, tmpList...) 143 | if len(tmpList) < rzrqPageSize { 144 | break 145 | } 146 | if pages == 1 { 147 | pages = tmpPages 148 | } 149 | } 150 | return list 151 | } 152 | 153 | func lazyLoadMarginTrading() { 154 | target := cache.GetMetaPath() + "/" + marginTradingFilename 155 | // 1. 获取缓存文件状态 156 | var lastModified time.Time 157 | fs, err := api.GetFileStat(target) 158 | if err == nil { 159 | lastModified = fs.LastWriteTime 160 | } 161 | // 2. 临时两融列表 162 | var tempList []FinancingAndSecuritiesLendingTarget 163 | // 3. 比较缓存日期和最新的时间 164 | cacheLastDay := lastModified.Format(exchange.TradingDayDateFormat) 165 | if cacheLastDay < exchange.LastTradeDate() { 166 | // 过时, 下载 167 | list := GetMarginTradingList() 168 | for _, v := range list { 169 | securityCode := exchange.CorrectSecurityCode(v.SecuCode) 170 | tempList = append(tempList, FinancingAndSecuritiesLendingTarget{Code: securityCode}) 171 | } 172 | // 刷新本地缓存文件 173 | if len(tempList) > 0 { 174 | _ = api.SlicesToCsv(target, tempList) 175 | } 176 | } 177 | // 4. 如果文件不存在, 则从内置资源文件导出 178 | if len(tempList) == 0 && !api.FileExist(target) { 179 | filename := fmt.Sprintf("%s/%s", ResourcesPath, marginTradingFilename) 180 | _ = api.Export(resources, filename, target) 181 | } 182 | // 5. 如果没有更新, 则从缓存中加载 183 | if len(tempList) == 0 && api.FileExist(target) { 184 | _ = api.CsvToSlices(target, &tempList) 185 | } 186 | // 6. 准备加载两融标的代码列表到内存 187 | var codes []string 188 | for _, v := range tempList { 189 | code := v.Code 190 | securityCode := exchange.CorrectSecurityCode(code) 191 | codes = append(codes, securityCode) 192 | } 193 | if len(codes) > 0 { 194 | codes = api.SliceUnique(codes, func(a string, b string) int { 195 | if a < b { 196 | return -1 197 | } else if a > b { 198 | return 1 199 | } else { 200 | return 0 201 | } 202 | }) 203 | cacheMarginTradingList = slices.Clone(codes) 204 | clear(mapMarginTrading) 205 | for _, v := range cacheMarginTradingList { 206 | mapMarginTrading[v] = true 207 | } 208 | } 209 | } 210 | 211 | // MarginTradingList 获取两融标的列表 212 | func MarginTradingList() []string { 213 | onceMarginTrading.Do(lazyLoadMarginTrading) 214 | return cacheMarginTradingList 215 | } 216 | 217 | // IsMarginTradingTarget 是否两融标的 218 | func IsMarginTradingTarget(code string) bool { 219 | onceMarginTrading.Do(lazyLoadMarginTrading) 220 | securityCode := exchange.CorrectSecurityCode(code) 221 | _, ok := mapMarginTrading[securityCode] 222 | return ok 223 | } 224 | -------------------------------------------------------------------------------- /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 | // 其中 0 { 169 | //ele.ServerTime = timeFromStr(fmt.Sprintf("%d", ele.ReversedBytes0)) 170 | ele.ServerTime = internal.TimeFromInt(ele.ReversedBytes0) 171 | } else { 172 | ele.ServerTime = "0" 173 | // 如果出现这种情况, 可能是退市或者其实交易状态异常的数据, 摘牌的情况下, 证券代码是错的 174 | ele.Code = exchange.StockDelisting 175 | } 176 | 177 | ele.ReversedBytes1 = internal.DecodeVarint(data, &pos) 178 | 179 | ele.Vol = internal.DecodeVarint(data, &pos) 180 | ele.CurVol = internal.DecodeVarint(data, &pos) 181 | 182 | var amountraw uint32 183 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+4]), binary.LittleEndian, &amountraw) 184 | pos += 4 185 | ele.Amount = internal.IntToFloat64(int(amountraw)) 186 | 187 | ele.SVol = internal.DecodeVarint(data, &pos) 188 | ele.BVol = internal.DecodeVarint(data, &pos) 189 | 190 | ele.ReversedBytes2 = internal.DecodeVarint(data, &pos) 191 | ele.ReversedBytes3 = internal.DecodeVarint(data, &pos) 192 | //fmt.Printf("pos: %d\n", pos) 193 | //fmt.Println(hex.EncodeToString(data[:pos])) 194 | 195 | var bidLevels []V2Level 196 | var askLevels []V2Level 197 | //baNum := 5 198 | baNum := 1 199 | for i := 0; i < baNum; i++ { 200 | bidele := V2Level{Price: obj.getPrice(internal.DecodeVarint(data, &pos), price)} 201 | offerele := V2Level{Price: obj.getPrice(internal.DecodeVarint(data, &pos), price)} 202 | bidele.Vol = internal.DecodeVarint(data, &pos) 203 | offerele.Vol = internal.DecodeVarint(data, &pos) 204 | bidLevels = append(bidLevels, bidele) 205 | askLevels = append(askLevels, offerele) 206 | } 207 | ele.Bid1 = bidLevels[0].Price 208 | //ele.Bid2 = bidLevels[1].Price 209 | //ele.Bid3 = bidLevels[2].Price 210 | //ele.Bid4 = bidLevels[3].Price 211 | //ele.Bid5 = bidLevels[4].Price 212 | ele.Ask1 = askLevels[0].Price 213 | //ele.Ask2 = askLevels[1].Price 214 | //ele.Ask3 = askLevels[2].Price 215 | //ele.Ask4 = askLevels[3].Price 216 | //ele.Ask5 = askLevels[4].Price 217 | 218 | ele.BidVol1 = bidLevels[0].Vol 219 | //ele.BidVol2 = bidLevels[1].Vol 220 | //ele.BidVol3 = bidLevels[2].Vol 221 | //ele.BidVol4 = bidLevels[3].Vol 222 | //ele.BidVol5 = bidLevels[4].Vol 223 | 224 | ele.AskVol1 = askLevels[0].Vol 225 | //ele.AskVol2 = askLevels[1].Vol 226 | //ele.AskVol3 = askLevels[2].Vol 227 | //ele.AskVol4 = askLevels[3].Vol 228 | //ele.AskVol5 = askLevels[4].Vol 229 | //fmt.Printf("pos: %d\n", pos) 230 | //fmt.Println(hex.EncodeToString(data[:pos])) 231 | 232 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.ReversedBytes4) 233 | pos += 2 234 | //ele.ReversedBytes5 = getPrice(data, &pos) 235 | //ele.ReversedBytes6 = getPrice(data, &pos) 236 | //ele.ReversedBytes7 = getPrice(data, &pos) 237 | //ele.ReversedBytes8 = getPrice(data, &pos) 238 | 239 | var reversedbytes9 int16 240 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &reversedbytes9) 241 | pos += 2 242 | ele.Rate = float64(reversedbytes9) / 100.0 243 | 244 | // 保留 2个字节 245 | //_r1 := data[pos : pos+2] 246 | //_pos := 0 247 | //_price1 := getPrice(_r1, &_pos) 248 | pos += 2 249 | //pos += _pos 250 | 251 | // 保留 12x4字节 252 | _lenth := 12 * 4 253 | _r2 := data[pos : pos+_lenth] 254 | _pos2 := 0 255 | for { 256 | _p2 := obj.getPrice(internal.DecodeVarint(_r2, &_pos2), price) 257 | //_p2 := getPrice(_r2, &_pos2) 258 | if logger.IsDebug() { 259 | logger.Debug(_p2) 260 | } 261 | if _pos2 >= _lenth { 262 | break 263 | } 264 | } 265 | //_ = _price 266 | pos += _lenth 267 | 268 | _ = binary.Read(bytes.NewBuffer(data[pos:pos+2]), binary.LittleEndian, &ele.Active2) 269 | pos += 2 270 | 271 | obj.reply.List = append(obj.reply.List, ele) 272 | } 273 | return nil 274 | } 275 | 276 | func (obj *V2SecurityQuotesPackage) Reply() interface{} { 277 | return obj.reply 278 | } 279 | 280 | func (obj *V2SecurityQuotesPackage) getPrice(price int, diff int) float64 { 281 | return float64(price+diff) / 100.0 282 | } 283 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------