├── .gitignore ├── .idea ├── .gitignore ├── modules.xml ├── tdx-go.iml └── vcs.xml ├── LICENSE ├── README.md ├── cmd └── main.go ├── config ├── config.go └── stock_ip.json ├── core ├── client.go └── hub.go ├── go.mod ├── go.sum ├── proto ├── proto.go └── v1 │ ├── get_block_info.go │ ├── get_security_count.go │ ├── get_security_count_test.go │ ├── get_security_list.go │ ├── get_security_list_test.go │ ├── get_security_quotes.go │ ├── setup_cmd1.go │ ├── setup_cmd2.go │ └── setup_cmd3.go └── utils ├── compress.go ├── hex.go ├── hex_test.go ├── parse ├── parse.go └── strings.go └── sortable.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | build/ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 数据源本地存储已忽略文件 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # 基于编辑器的 HTTP 客户端请求 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/tdx-go.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tdx-go 2 | tdx行情go版本 3 | 参考pytdx,使用golang实现 4 | # 提示 5 | - 检测最优路线时需使用sudo权限 6 | # fork方法 7 | - cd $GOPATH/src/github.com/cyclegen 8 | 进入cyclegen工作目录下 9 | - git clone git@github.com:cyclegen/tdx-go.git 10 | clone主线仓库 11 | - cd $GOPATH/src/github.com/cyclegen/tdx-go 12 | 进入tdx-go项目 13 | - git remote add fork github.com/me/tdx-go.git 14 | 添加远程仓库 15 | - git push fork 16 | 推送到远程仓库 -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/cyclegen-community/tdx-go/config" 5 | "github.com/cyclegen-community/tdx-go/core" 6 | "github.com/cyclegen-community/tdx-go/proto" 7 | "github.com/cyclegen-community/tdx-go/proto/v1" 8 | "log" 9 | ) 10 | 11 | func init() { 12 | log.SetFlags(log.Lshortfile | log.Ldate) 13 | } 14 | func main() { 15 | quotesSrv := config.GetBestStockQuotesServer() 16 | //quotesSrvAddr := "106.120.74.86:7711" // quotesSrv.Addr() 17 | log.Println("正在连接到最优行情服务器: ", quotesSrv.Addr()) 18 | T(quotesSrv.IP, quotesSrv.Port) 19 | //T("106.120.74.86", 7709) 20 | } 21 | func T(ip string, port int) { 22 | cli := core.NewClient(ip, port) 23 | 24 | // CMD信令 1 25 | testProto(cli, func() (req proto.Marshaler, resp proto.Unmarshaler, err error) { 26 | req, resp, err = v1.NewSetupCmd1() 27 | return 28 | }) 29 | // CMD信令 2 30 | testProto(cli, func() (req proto.Marshaler, resp proto.Unmarshaler, err error) { 31 | req, resp, err = v1.NewSetupCmd2() 32 | return 33 | }) 34 | // CMD信令 3 35 | testProto(cli, func() (req proto.Marshaler, resp proto.Unmarshaler, err error) { 36 | req, resp, err = v1.NewSetupCmd3() 37 | return 38 | }) 39 | // 查询股票数量 40 | testProto(cli, func() (req proto.Marshaler, resp proto.Unmarshaler, err error) { 41 | req, resp, err = v1.NewGetSecurityCount(v1.MarketShangHai) 42 | return 43 | }) 44 | 45 | testProto(cli, func() (req proto.Marshaler, resp proto.Unmarshaler, err error) { 46 | req, resp, err = v1.NewGetSecurityList(v1.MarketShangHai, 255) 47 | return 48 | }) 49 | //testProto(cli, func() (req proto.Marshaler, resp proto.Unmarshaler, err error) { 50 | // req, resp, err = v1.NewGetSecurityQuotes() 51 | // return 52 | //}) 53 | } 54 | 55 | func testProto(cli *core.Client, factory proto.Factory) { 56 | req, resp, err := factory() 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | err = cli.Do(req, resp) 61 | if err != nil { 62 | log.Println(err) 63 | } 64 | log.Println(resp) 65 | } 66 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/cyclegen-community/tdx-go/utils" 6 | "github.com/sparrc/go-ping" 7 | "io/ioutil" 8 | "log" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | type Server struct { 17 | Name string `json:"name"` 18 | IP string `json:"ip"` 19 | Port int `json:"port"` 20 | } 21 | 22 | func (srv *Server) Addr() string { 23 | return strings.Join([]string{srv.IP, strconv.Itoa(srv.Port)}, ":") 24 | } 25 | 26 | const ( 27 | _StockQuotesServerConfigFile = "stock_ip.json" 28 | ) 29 | 30 | // StockQuotesServer 股票行情线路信息 31 | type StockQuotesServer []Server 32 | 33 | // GetStockQuotesServer 获取股票行情线路列表 34 | func GetStockQuotesServer() StockQuotesServer { 35 | var instance StockQuotesServer 36 | raw, err := ioutil.ReadFile("config/" + _StockQuotesServerConfigFile) 37 | if err != nil { 38 | log.Fatalf("读取"+_StockQuotesServerConfigFile+"失败, 错误详情: %v", err.Error()) 39 | } 40 | err = json.Unmarshal(raw, &instance) 41 | if err != nil { 42 | log.Fatalf("解析"+_StockQuotesServerConfigFile+"失败, 错误详情: %v", err.Error()) 43 | } 44 | return instance 45 | } 46 | 47 | // GetBestStockQuotesServer 获取最优股票行情线路 48 | func GetBestStockQuotesServer() Server { 49 | srvs := GetStockQuotesServer() 50 | results := sync.Map{} 51 | sortableSrvs := utils.SortableMapList{} 52 | wg := sync.WaitGroup{} 53 | for idx := range srvs { 54 | wg.Add(1) 55 | go func(id int) { 56 | defer wg.Done() 57 | if srvs[id].IP == "" { 58 | return 59 | } 60 | pinger, err := ping.NewPinger(srvs[id].IP) 61 | if err != nil { 62 | log.Println(err) 63 | return 64 | } 65 | pinger.SetPrivileged(true) 66 | pinger.Count = 5 67 | pinger.Timeout = time.Second 68 | pinger.Run() // blocks until finished 69 | 70 | stats := pinger.Statistics() // get send/receive/rtt stats 71 | 72 | var avgRtt int64 73 | // 丢包率不能高于50%,否则设置平均时延为负数 74 | if stats.PacketLoss > 0.5 { 75 | avgRtt = -1 76 | } else { 77 | avgRtt = stats.AvgRtt.Nanoseconds() 78 | } 79 | results.Store(srvs[id], avgRtt) 80 | }(idx) 81 | } 82 | wg.Wait() 83 | results.Range(func(key, value interface{}) bool { 84 | srv := key.(Server) 85 | avgRtt := value.(int64) 86 | //log.Printf("%v: %d ns", srv.Addr(), avgRtt) 87 | if avgRtt > 0 { 88 | sortableSrvs = append(sortableSrvs, utils.SortableMap{ 89 | srv, 90 | avgRtt, 91 | }) 92 | } 93 | return true 94 | }) 95 | 96 | sort.Sort(sortableSrvs) 97 | if len(sortableSrvs) > 0 { 98 | srv := sortableSrvs[0].Key.(Server) 99 | return srv 100 | } else { 101 | panic("所有服务器均无法连通!") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /config/stock_ip.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ip": "106.120.74.86", 4 | "port": 7711, 5 | "name": "北京行情主站1" 6 | }, 7 | { 8 | "ip": "113.105.73.88", 9 | "port": 7709, 10 | "name": "深圳行情主站" 11 | }, 12 | { 13 | "ip": "113.105.73.88", 14 | "port": 7711, 15 | "name": "深圳行情主站" 16 | }, 17 | { 18 | "ip": "114.80.80.222", 19 | "port": 7711, 20 | "name": "上海行情主站" 21 | }, 22 | { 23 | "ip": "117.184.140.156", 24 | "port": 7711, 25 | "name": "移动行情主站" 26 | }, 27 | { 28 | "ip": "119.147.171.206", 29 | "port": 443, 30 | "name": "广州行情主站" 31 | }, 32 | { 33 | "ip": "119.147.171.206", 34 | "port": 80, 35 | "name": "广州行情主站" 36 | }, 37 | { 38 | "ip": "218.108.50.178", 39 | "port": 7711, 40 | "name": "杭州行情主站" 41 | }, 42 | { 43 | "ip": "221.194.181.176", 44 | "port": 7711, 45 | "name": "北京行情主站2" 46 | }, 47 | { 48 | "ip": "106.120.74.86", 49 | "port": 7709 50 | }, 51 | { 52 | "ip": "112.95.140.74", 53 | "port": 7709 54 | }, 55 | { 56 | "ip": "112.95.140.92", 57 | "port": 7709 58 | }, 59 | { 60 | "ip": "112.95.140.93", 61 | "port": 7709 62 | }, 63 | { 64 | "ip": "113.05.73.88", 65 | "port": 7709 66 | }, 67 | { 68 | "ip": "114.67.61.70", 69 | "port": 7709 70 | }, 71 | { 72 | "ip": "114.80.149.19", 73 | "ort": 7709 74 | }, 75 | { 76 | "ip": "114.80.149.22", 77 | "port": 7709 78 | }, 79 | { 80 | "ip": "114.80.149.84", 81 | "port": 7709 82 | }, 83 | { 84 | "ip": "114.80.80.222", 85 | "port": 7709 86 | }, 87 | { 88 | "ip": "115.238.56.198", 89 | "port": 7709 90 | }, 91 | { 92 | "ip": "115.238.90.165", 93 | "port": 7709 94 | }, 95 | { 96 | "ip": "117.184.140.156", 97 | "port": 7709 98 | }, 99 | { 100 | "ip": "119.147.164.60", 101 | "port": 7709 102 | }, 103 | { 104 | "ip": "119.147.171.206", 105 | "port": 7709 106 | }, 107 | { 108 | "ip": "119.29.51.30", 109 | "port": 7709 110 | }, 111 | { 112 | "ip": "121.14.104.70", 113 | "port": 7709 114 | }, 115 | { 116 | "ip": "121.14.104.72", 117 | "port": 7709 118 | }, 119 | { 120 | "ip": "121.14.110.194", 121 | "port": 7709 122 | }, 123 | { 124 | "ip": "121.14.2.7", 125 | "port": 7709 126 | }, 127 | { 128 | "ip": "123.125.108.23", 129 | "port": 7709 130 | }, 131 | { 132 | "ip": "123.125.108.24", 133 | "port": 7709 134 | }, 135 | { 136 | "ip": "124.160.88.183", 137 | "port": 7709 138 | }, 139 | { 140 | "ip": "180.153.18.17", 141 | "port": 7709 142 | }, 143 | { 144 | "ip": "180.153.18.170", 145 | "port": 7709 146 | }, 147 | { 148 | "ip": "180.153.18.171", 149 | "port": 7709 150 | }, 151 | { 152 | "ip": "180.153.39.51", 153 | "port": 7709 154 | }, 155 | { 156 | "ip": "218.108.47.69", 157 | "port": 7709 158 | }, 159 | { 160 | "ip": "218.108.50.178", 161 | "port": 7709 162 | }, 163 | { 164 | "ip": "218.108.98.244", 165 | "port": 7709 166 | }, 167 | { 168 | "ip": "218.75.126.9", 169 | "port": 7709 170 | }, 171 | { 172 | "ip": "218.9.148.108", 173 | "port": 7709 174 | }, 175 | { 176 | "ip": "221.194.181.176", 177 | "port": 7709 178 | }, 179 | { 180 | "ip": "59.173.18.69", 181 | "port": 7709 182 | }, 183 | { 184 | "ip": "60.12.136.250", 185 | "port": 7709 186 | }, 187 | { 188 | "ip": "60.191.117.167", 189 | "port": 7709 190 | }, 191 | { 192 | "ip": "60.28.29.69", 193 | "port": 7709 194 | }, 195 | { 196 | "ip": "61.135.142.73", 197 | "port": 7709 198 | }, 199 | { 200 | "ip": "61.135.142.88", 201 | "port": 7709 202 | }, 203 | { 204 | "ip": "61.152.107.168", 205 | "port": 7721 206 | }, 207 | { 208 | "ip": "61.152.249.56", 209 | "port": 7709 210 | }, 211 | { 212 | "ip": "61.153.144.179", 213 | "port": 7709 214 | }, 215 | { 216 | "ip": "61.153.209.138", 217 | "port": 7709 218 | }, 219 | { 220 | "ip": "61.153.209.139", 221 | "port": 7709 222 | }, 223 | { 224 | "ip": "hq.cjis.cn", 225 | "port": 7709 226 | }, 227 | { 228 | "ip": "hq1.daton.com.cn", 229 | "port": 7709 230 | }, 231 | { 232 | "ip": "jstdx.gtjas.com", 233 | "port": 7709 234 | }, 235 | { 236 | "ip": "shtdx.gtjas.com", 237 | "port": 7709 238 | }, 239 | { 240 | "ip": "sztdx.gtjas.com", 241 | "port": 7709 242 | }, 243 | { 244 | "ip": "113.105.142.162", 245 | "port": 7721 246 | }, 247 | { 248 | "ip": "23.129.245.199", 249 | "port": 7721 250 | } 251 | ] -------------------------------------------------------------------------------- /core/client.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "github.com/cyclegen-community/tdx-go/proto" 6 | "github.com/cyclegen-community/tdx-go/utils" 7 | "log" 8 | "net" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type Client struct { 15 | conn net.Conn 16 | Host string 17 | Port int 18 | Timeout time.Duration 19 | MaxRetryTimes int 20 | RetryDuration time.Duration 21 | } 22 | 23 | // https://www.sohamkamani.com/golang/options-pattern/ 24 | // https://lingchao.xin/post/functional-options-pattern-in-go.html 25 | // NewBaseClient 创建BaseClient实例 26 | func NewClient(host string, port int) *Client { 27 | addr := strings.Join([]string{host, strconv.Itoa(port)}, ":") 28 | 29 | conn, err := net.Dial("tcp", addr) // net.DialTimeout() 30 | 31 | if err != nil { 32 | log.Fatalln(err) 33 | } 34 | return &Client{ 35 | conn: conn, 36 | Host: host, 37 | Port: port, 38 | MaxRetryTimes: 5, 39 | Timeout: time.Second, 40 | RetryDuration: time.Millisecond * 200, 41 | } 42 | } 43 | 44 | func (cli *Client) Do(request proto.Marshaler, response proto.Unmarshaler) error { 45 | // 序列化请求 46 | req, err := request.Marshal() 47 | if err != nil { 48 | return err 49 | } 50 | // 发送请求 51 | retryTimes := 0 52 | SEND: 53 | n, err := cli.conn.Write(req) 54 | // 重试 55 | if n < len(req) { 56 | retryTimes += 1 57 | if retryTimes <= cli.MaxRetryTimes { 58 | log.Printf("第%d次重试\n", retryTimes) 59 | goto SEND 60 | } else { 61 | return errors.New("数据未完整发送") 62 | } 63 | } 64 | if err != nil { 65 | return err 66 | } 67 | // 解析响应包头 68 | var header proto.PacketHeader 69 | // 读取包头 大小为16字节 70 | // 单次获取的字列流 71 | headerLength := 0x10 72 | headerBytes := make([]byte, headerLength) 73 | // 调用socket获取字节流并保存到data中 74 | headerBytes, err = cli.receive(headerLength) 75 | if err != nil { 76 | //log.Println(err) 77 | return err 78 | } 79 | err = header.Unmarshal(headerBytes) 80 | if err != nil { 81 | return err 82 | } 83 | // 根据获取响应体结构 84 | // 调用socket获取字节流并保存到data中 85 | bodyBytes, err := cli.receive(header.ZipSize) 86 | if err != nil { 87 | return err 88 | } 89 | // zlib解压缩 90 | if header.Compressed() { 91 | bodyBytes, err = utils.ZlibUnCompress(bodyBytes) 92 | } 93 | // 反序列化为响应体结构 94 | err = response.Unmarshal(bodyBytes) 95 | if err != nil { 96 | return err 97 | } 98 | return nil 99 | } 100 | func (cli *Client) receive(length int) (data []byte, err error) { 101 | var ( 102 | receivedSize int 103 | ) 104 | READ: 105 | tmp := make([]byte, length) 106 | // 调用socket获取字节流并保存到data中 107 | receivedSize, err = cli.conn.Read(tmp) 108 | // socket错误,可能为EOF 109 | if err != nil { 110 | return nil, err 111 | } 112 | // 数据添加到总输出,由于tmp申请内存时使用了length的长度, 113 | // 所以直接全部复制到data中会使得未完全传输的部分被填充为0导致数据获取不完整, 114 | // 故使用tmp[:receivedSize] 115 | data = append(data, tmp[:receivedSize]...) 116 | // 数据读满就可以返回了 117 | if len(data) == length { 118 | return 119 | } 120 | // 读取小于标准尺寸,说明到文件尾或者读取出现了问题没读满,可以返回了 121 | if receivedSize < length { 122 | goto READ 123 | } 124 | return 125 | } 126 | func (cli *Client) Close() error { 127 | return cli.conn.Close() 128 | } 129 | -------------------------------------------------------------------------------- /core/hub.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "sync" 4 | 5 | // todo: 链接池,无感切换 6 | type Hub struct { 7 | lock sync.Mutex 8 | Clients []Client 9 | } 10 | 11 | func (hub *Hub) Do() error { 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cyclegen-community/tdx-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/cyclegen/tdx-go v0.0.0-20200627104456-1906d8bccbce 7 | github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe 8 | github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c 9 | github.com/stretchr/testify v1.4.0 10 | golang.org/x/text v0.3.3 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cyclegen/tdx-go v0.0.0-20200627104456-1906d8bccbce h1:WEzzSNf+D76YmV+Gz5DaIYhWSVMPt8voOOG1H0Ujq5Y= 2 | github.com/cyclegen/tdx-go v0.0.0-20200627104456-1906d8bccbce/go.mod h1:WeGm1DsL474KJpGBPdDvJy1gyBEbgoe+3G/7xBnA0c8= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/libp2p/go-reuseport v0.0.1/go.mod h1:jn6RmB1ufnQwl0Q1f+YxAj8isJgDCQzaaxIFYDhcYEA= 7 | github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe h1:ewr1srjRCmcQogPQ/NCx6XCk6LGVmsVCc9Y3vvPZj+Y= 8 | github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= 9 | github.com/panjf2000/ants/v2 v2.4.0/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= 10 | github.com/panjf2000/gnet v1.2.3/go.mod h1:Vl8hElC5vYR1iW7Txb30rFd9X12CDktkLkLgpIHKAwo= 11 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 12 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/smallnest/goframe v1.0.0/go.mod h1:Dy8560GXrB6w5OJnVBU71dJtSyINdnqHHe6atDaZX00= 16 | github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c h1:gqEdF4VwBu3lTKGHS9rXE9x1/pEaSwCXRLOZRF6qtlw= 17 | github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 20 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 21 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 22 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= 25 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 26 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 27 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= 31 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 33 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 34 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 35 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 39 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 40 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 41 | -------------------------------------------------------------------------------- /proto/proto.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "bytes" 5 | "github.com/lunixbochs/struc" 6 | "log" 7 | ) 8 | 9 | type Factory func() (Marshaler, Unmarshaler, error) 10 | 11 | type Marshaler interface { 12 | Marshal() ([]byte, error) 13 | } 14 | 15 | type Unmarshaler interface { 16 | Unmarshal([]byte) error 17 | } 18 | 19 | // 行情服务器发送第一条指令的返回数据 20 | type PacketHeader struct { 21 | raw []byte 22 | Unknown1 uint `struc:"uint32,little";json:"unknown_1"` 23 | 24 | // B will be encoded/decoded as a 16-bit int (a "short") 25 | // but is stored as a native int in the struct 26 | Unknown2 uint `struc:"uint32,little";json:"unknown_2"` 27 | 28 | // the sizeof key links a buffer's size to any int field 29 | Unknown3 uint `struc:"uint32,little";json:"unknown_3"` 30 | ZipSize int `struc:"uint16,little";json:"zip_size"` 31 | 32 | // you can get freaky if you want 33 | UnzipSize int `struc:"uint16,little";json:"unzip_size"` 34 | } 35 | 36 | func (h *PacketHeader) Bytes() []byte { 37 | return h.raw 38 | } 39 | 40 | func (h *PacketHeader) Compressed() bool { 41 | return h.ZipSize != h.UnzipSize 42 | } 43 | 44 | func (h *PacketHeader) Size() int { 45 | return h.UnzipSize 46 | } 47 | func (h *PacketHeader) Unmarshal(data []byte) error { 48 | h.raw = data 49 | return DefaultUnmarshal(data, h) 50 | } 51 | 52 | // 基于struc包的反序列化方案 53 | func DefaultUnmarshal(data []byte, v interface{}) error { 54 | // 构造流 55 | buf := bytes.NewBuffer(data) 56 | // 使用struc解析到struct 57 | err := struc.Unpack(buf, v) 58 | if err != nil { 59 | log.Println(err) 60 | return err 61 | } 62 | return nil 63 | } 64 | 65 | // 基于struc包的序列化方案 66 | func DefaultMarshal(v interface{}) ([]byte, error) { 67 | // 构造流 68 | buf := bytes.Buffer{} 69 | // 使用struc解析到struct 70 | err := struc.Pack(&buf, v) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return buf.Bytes(), nil 75 | } 76 | -------------------------------------------------------------------------------- /proto/v1/get_block_info.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | //// 请求包结构 4 | //type SetupCmd3Request struct { 5 | // Cmd []byte `struc:"[42]byte";json:"cmd"` 6 | //} 7 | // 8 | //// 请求包序列化输出 9 | //func (req *SetupCmd3Request) Marshal() ([]byte, error) { 10 | // return DefaultMarshal(req) 11 | //} 12 | // 13 | //// 响应包结构 14 | //type SetupCmd3Response struct{ 15 | // Unknown []byte `json:"unknown"` 16 | //} 17 | // 18 | //func (resp *SetupCmd3Response) Unmarshal(data []byte) error { 19 | // resp.Unknown = data 20 | // return nil 21 | //} 22 | // 23 | // 24 | //// 创建SetupCmd3请求包 25 | //func NewSetupCmd3Request() (*SetupCmd3Request, error) { 26 | // request := &SetupCmd3Request{ 27 | // Cmd: utils.HexString2Bytes("0c 03 18 99 00 01 20 00 20 00 db 0f d5" + 28 | // "d0 c9 cc d6 a4 a8 af 00 00 00 8f c2 25" + 29 | // "40 13 00 00 d5 00 c9 cc bd f0 d7 ea 00" + 30 | // "00 00 02"), 31 | // } 32 | // return request, nil 33 | //} 34 | // 35 | //func NewSetupCmd3() (*SetupCmd3Request, *SetupCmd3Response, error) { 36 | // var response SetupCmd3Response 37 | // var request, err = NewSetupCmd3Request() 38 | // return request, &response, err 39 | //} 40 | -------------------------------------------------------------------------------- /proto/v1/get_security_count.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/cyclegen-community/tdx-go/proto" 5 | "github.com/cyclegen-community/tdx-go/utils" 6 | ) 7 | 8 | type Market int 9 | 10 | const ( 11 | MarketShenZhen Market = iota 12 | MarketShangHai = 1 13 | ) 14 | 15 | // 请求包结构 16 | type GetSecurityCountRequest struct { 17 | Unknown1 []byte `struc:"[12]byte"` 18 | Market Market `struc:"uint16,little";json:"market"` 19 | Unknown2 []byte `struc:"[4]byte"` 20 | } 21 | 22 | // 请求包序列化输出 23 | func (req *GetSecurityCountRequest) Marshal() ([]byte, error) { 24 | return proto.DefaultMarshal(req) 25 | } 26 | 27 | // 响应包结构 28 | type GetSecurityCountResponse struct { 29 | Count uint `struc:"uint16,little";json:"count"` 30 | } 31 | 32 | func (resp *GetSecurityCountResponse) Unmarshal(data []byte) error { 33 | return proto.DefaultUnmarshal(data, resp) 34 | } 35 | 36 | // todo: 检测market是否为合法值 37 | func NewGetSecurityCountRequest(market Market) (*GetSecurityCountRequest, error) { 38 | request := &GetSecurityCountRequest{ 39 | Unknown1: utils.HexString2Bytes("0c 0c 18 6c 00 01 08 00 08 00 4e 04"), 40 | Market: market, 41 | Unknown2: utils.HexString2Bytes("75 c7 33 01"), 42 | } 43 | return request, nil 44 | } 45 | 46 | func NewGetSecurityCount(market Market) (*GetSecurityCountRequest, *GetSecurityCountResponse, error) { 47 | var response GetSecurityCountResponse 48 | var request, err = NewGetSecurityCountRequest(market) 49 | return request, &response, err 50 | } 51 | -------------------------------------------------------------------------------- /proto/v1/get_security_count_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewSecurityCountRequest(t *testing.T) { 8 | t.Log(NewGetSecurityCount(MarketShenZhen)) 9 | t.Log(NewGetSecurityCount(MarketShangHai)) 10 | } 11 | -------------------------------------------------------------------------------- /proto/v1/get_security_list.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | // 获取股票列表 4 | import ( 5 | "github.com/cyclegen-community/tdx-go/proto" 6 | "github.com/cyclegen-community/tdx-go/utils" 7 | "github.com/cyclegen-community/tdx-go/utils/parse" 8 | "log" 9 | ) 10 | 11 | // 请求包结构 12 | type GetSecurityListRequest struct { 13 | // struc不允许slice解析,只允许包含长度的array,该长度可根据hex字符串计算 14 | Unknown1 []byte `struc:"[12]byte"` 15 | // pytdx中使用struct.Pack进行反序列化 16 | // 其中> (8 * 3) 9 | //hheax := ivol >> (8*3) // [4] 10 | hleax := (ivol >> (8 * 2)) & 0xff // [2] 11 | lheax := (ivol >> 8) & 0xff // [1] 12 | lleax := ivol & 0xff // [0] 13 | 14 | //dbl_1 := 1.0 15 | //dbl_2 := 2.0 16 | //dbl_128 := 128. 17 | 18 | dwEcx := logPoint*2 - 0x7f 19 | dwEdx := logPoint*2 - 0x86 20 | dwEsi := logPoint*2 - 0x8e 21 | dwEax := logPoint*2 - 0x96 22 | 23 | tmpEax := 0 24 | if dwEcx < 0 { 25 | tmpEax = -dwEcx 26 | } else { 27 | tmpEax = dwEcx 28 | } 29 | 30 | dblXmm6 := math.Pow(2.0, float64(tmpEax)) 31 | if dwEcx < 0 { 32 | dblXmm6 = 1.0 / dblXmm6 33 | } 34 | 35 | dblXmm4 := 0.0 36 | if hleax > 0x80 { 37 | tmpdblXmm3 := 0.0 38 | //tmpdblXmm1 := 0.0 39 | dwtmpeax := dwEdx + 1 40 | tmpdblXmm3 = math.Pow(2.0, float64(dwtmpeax)) 41 | dblXmm0 := math.Pow(2.0, float64(dwEdx)) * 128.0 42 | dblXmm0 += float64(hleax&0x7f) * tmpdblXmm3 43 | dblXmm4 = dblXmm0 44 | } else { 45 | dblXmm0 := 0.0 46 | if dwEdx >= 0 { 47 | dblXmm0 = math.Pow(2.0, float64(dwEdx)) * float64(hleax) 48 | } else { 49 | dblXmm0 = (1 / math.Pow(2.0, float64(dwEdx))) * float64(hleax) 50 | dblXmm4 = dblXmm0 51 | } 52 | } 53 | dblXmm3 := math.Pow(2.0, float64(dwEsi)) * float64(lheax) 54 | dblXmm1 := math.Pow(2.0, float64(dwEax)) * float64(lleax) 55 | if hleax&0x80 != 0 { 56 | dblXmm3 *= 2.0 57 | dblXmm1 *= 2.0 58 | } 59 | return dblXmm6 + dblXmm4 + dblXmm3 + dblXmm1 60 | } 61 | -------------------------------------------------------------------------------- /utils/parse/strings.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "bytes" 5 | "golang.org/x/text/encoding/simplifiedchinese" 6 | "golang.org/x/text/encoding/traditionalchinese" 7 | "golang.org/x/text/transform" 8 | "io/ioutil" 9 | ) 10 | 11 | //convert GBK to UTF-8 12 | func DecodeGBK(s []byte) ([]byte, error) { 13 | I := bytes.NewReader(s) 14 | O := transform.NewReader(I, simplifiedchinese.GBK.NewDecoder()) 15 | d, e := ioutil.ReadAll(O) 16 | if e != nil { 17 | return nil, e 18 | } 19 | return d, nil 20 | } 21 | 22 | //convert UTF-8 to GBK 23 | func EncodeGBK(s []byte) ([]byte, error) { 24 | I := bytes.NewReader(s) 25 | O := transform.NewReader(I, simplifiedchinese.GBK.NewEncoder()) 26 | d, e := ioutil.ReadAll(O) 27 | if e != nil { 28 | return nil, e 29 | } 30 | return d, nil 31 | } 32 | 33 | //convert BIG5 to UTF-8 34 | func DecodeBig5(s []byte) ([]byte, error) { 35 | I := bytes.NewReader(s) 36 | O := transform.NewReader(I, traditionalchinese.Big5.NewDecoder()) 37 | d, e := ioutil.ReadAll(O) 38 | if e != nil { 39 | return nil, e 40 | } 41 | return d, nil 42 | } 43 | 44 | //convert UTF-8 to BIG5 45 | func EncodeBig5(s []byte) ([]byte, error) { 46 | I := bytes.NewReader(s) 47 | O := transform.NewReader(I, traditionalchinese.Big5.NewEncoder()) 48 | d, e := ioutil.ReadAll(O) 49 | if e != nil { 50 | return nil, e 51 | } 52 | return d, nil 53 | } 54 | -------------------------------------------------------------------------------- /utils/sortable.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type SortableMap struct { 4 | Key interface{} 5 | Value int64 6 | } 7 | 8 | type SortableMapList []SortableMap 9 | 10 | func (p SortableMapList) Len() int { return len(p) } 11 | func (p SortableMapList) Less(i, j int) bool { return p[i].Value < p[j].Value } 12 | func (p SortableMapList) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 13 | --------------------------------------------------------------------------------