├── .gitignore ├── LICENSE ├── api ├── market │ └── subscriber.go └── trade │ ├── trader.go │ └── trader_test.go ├── cache └── .gitignore ├── config ├── .gitignore └── trade.yaml.sample └── util ├── log.go ├── math.go └── yaml.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /api/market/subscriber.go: -------------------------------------------------------------------------------- 1 | package market 2 | 3 | import ( 4 | "bountyHunter/util" 5 | "fmt" 6 | "github.com/Sirupsen/logrus" 7 | "github.com/gorilla/websocket" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | type Configuration struct { 18 | IP string `yaml:"ip"` 19 | } 20 | 21 | type Quotation struct { 22 | Code string 23 | Name string 24 | PreClose float64 25 | Close float64 26 | Time time.Time 27 | Now time.Time 28 | Bids []OrderBook 29 | Asks []OrderBook 30 | } 31 | 32 | type QuotationStack struct { 33 | Length int 34 | quotations []*Quotation 35 | } 36 | 37 | type OrderBook struct { 38 | Price float64 39 | Amount int64 40 | } 41 | 42 | type Subscriber struct { 43 | Params string 44 | IP string 45 | token string 46 | cache map[string]*Quotation 47 | logger *logrus.Logger 48 | codeList []string 49 | quotationChanMap map[string]chan *Quotation 50 | strategyMap map[string][]string 51 | } 52 | 53 | type Api struct { 54 | Params string 55 | Cookie string 56 | IP string 57 | UA string 58 | token string 59 | cache map[string]*Quotation 60 | quotationChanMap map[string]chan *Quotation 61 | strategyMap map[string][]string 62 | } 63 | 64 | func New(configPath string) (subscriber *Subscriber) { 65 | config := &Configuration{} 66 | err := util.YamlFileDecode(configPath, config) 67 | if err != nil { 68 | panic(err) 69 | } 70 | subscriber = &Subscriber{} 71 | subscriber.codeList = []string{} 72 | subscriber.strategyMap = make(map[string][]string) 73 | subscriber.quotationChanMap = make(map[string]chan *Quotation) 74 | subscriber.IP = config.IP 75 | subscriber.logger = util.NewLogger("subscriber") 76 | return 77 | } 78 | 79 | func (sbr *Subscriber) Run() { 80 | sbr.logger.Info("running...") 81 | found := make(map[string]bool) 82 | uniqueCodeList := []string{} 83 | for _, code := range sbr.codeList { 84 | if !found[code] { 85 | found[code] = true 86 | uniqueCodeList = append(uniqueCodeList, code) 87 | } 88 | } 89 | sbr.codeList = uniqueCodeList 90 | 91 | for key, _ := range sbr.codeList { 92 | if sbr.codeList[key][0:2] == "15" || sbr.codeList[key][0:2] == "00" || sbr.codeList[key][0:2] == "30" { 93 | sbr.codeList[key] = "2cn_sz" + sbr.codeList[key] 94 | } 95 | if sbr.codeList[key][0:2] == "60" || sbr.codeList[key][0:2] == "50" { 96 | sbr.codeList[key] = "2cn_sh" + sbr.codeList[key] 97 | } 98 | if sbr.codeList[key][0:3] == "i39" { 99 | sbr.codeList[key] = "sz" + sbr.codeList[key][1:7] 100 | } 101 | if sbr.codeList[key][0:3] == "i00" { 102 | sbr.codeList[key] = "sh" + sbr.codeList[key][1:7] 103 | } 104 | } 105 | log.Println(sbr.codeList) 106 | 107 | start := 0 108 | end := 0 109 | length := len(sbr.codeList) 110 | for { 111 | end = start + 50 112 | if end >= length { 113 | end = length 114 | } 115 | params := strings.Join(sbr.codeList[start:end], ",") 116 | api := Api{ 117 | Params: params, 118 | IP: sbr.IP, 119 | quotationChanMap: sbr.quotationChanMap, 120 | strategyMap: sbr.strategyMap, 121 | } 122 | go api.Run() 123 | time.Sleep(time.Millisecond * 100) 124 | if end >= length { 125 | break 126 | } 127 | start = start + 50 128 | } 129 | } 130 | 131 | func (api *Api) Run() { 132 | api.refreshToken() 133 | go func() { 134 | for { 135 | time.Sleep(time.Minute * 1) 136 | api.refreshToken() 137 | } 138 | }() 139 | for { 140 | err := api.connect() 141 | if err != nil { 142 | log.Printf("connect failed: %s\n", err) 143 | } 144 | log.Println("closed") 145 | } 146 | } 147 | 148 | func (s *Subscriber) Subscribe(strategyName string, codeList []string) (quotationChan chan *Quotation) { 149 | for _, code := range codeList { 150 | s.strategyMap[code] = append(s.strategyMap[code], strategyName) 151 | } 152 | s.codeList = append(s.codeList, codeList...) 153 | quotationChan = make(chan *Quotation) 154 | s.quotationChanMap[strategyName] = quotationChan 155 | return 156 | } 157 | 158 | func (api *Api) connect() error { 159 | url := fmt.Sprintf("ws://ff.sinajs.cn/wskt?token=%s&list=%s", api.token, api.Params) 160 | c, _, err := websocket.DefaultDialer.Dial(url, nil) 161 | api.cache = make(map[string]*Quotation) 162 | if err != nil { 163 | return err 164 | } 165 | defer c.Close() 166 | for { 167 | _, message, err := c.ReadMessage() 168 | if err != nil { 169 | log.Printf("read message error: %s", err) 170 | return nil 171 | } 172 | raw := string(message) 173 | if strings.Contains(raw, "sys_auth=FAILED") { 174 | return fmt.Errorf("auth timeout") 175 | } 176 | rawLines := strings.SplitN(raw, "\n", -1) 177 | // @todo 如果有股票加入可能index的code会冲突 178 | for _, rawLine := range rawLines { 179 | if strings.Contains(rawLine, "sys_nxkey=") || strings.Contains(rawLine, "sys_time=") || len(rawLine) < 10 { 180 | continue 181 | } 182 | quo, err := api.parseQuotation(rawLine) 183 | if err == nil { 184 | strategyNameList := api.strategyMap[quo.Code] 185 | for _, strategyName := range strategyNameList { 186 | api.quotationChanMap[strategyName] <- quo 187 | } 188 | if quo.Close != 0 && quo.Bids[0].Price == quo.Asks[0].Price && quo.Bids[0].Amount == quo.Asks[0].Amount { 189 | strategyNameList := api.strategyMap["i"+quo.Code] 190 | for _, strategyName := range strategyNameList { 191 | api.quotationChanMap[strategyName] <- quo 192 | } 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | func (api *Api) parseQuotation(rawLine string) (*Quotation, error) { 200 | quo := &Quotation{} 201 | rawLines := strings.SplitN(rawLine, "=", 2) 202 | if len(rawLines) < 2 { 203 | return quo, fmt.Errorf("unexpected data %s", rawLine) 204 | } 205 | if rawLines[0][0:4] == "2cn_" { 206 | quo.Code = rawLines[0][6:12] 207 | rawLines = strings.Split(rawLines[1], ",") 208 | length := len(rawLines) 209 | if length < 40 { 210 | return quo, fmt.Errorf("unexpected data %s", rawLine) 211 | } 212 | quo.Name = rawLines[0] 213 | quo.Time, _ = time.Parse("2006-01-02T15:04:05.999999-07:00", fmt.Sprintf("%sT%s+08:00", rawLines[2], rawLines[1])) 214 | quo.Now = time.Now() 215 | quo.PreClose, _ = strconv.ParseFloat(rawLines[3], 64) 216 | quo.Close, _ = strconv.ParseFloat(rawLines[7], 64) 217 | 218 | rawLines = rawLines[length-40 : length] 219 | quo.Bids = make([]OrderBook, 10) 220 | quo.Asks = make([]OrderBook, 10) 221 | for index := 0; index < 10; index++ { 222 | quo.Bids[index].Price, _ = strconv.ParseFloat(rawLines[index], 64) 223 | quo.Bids[index].Amount, _ = strconv.ParseInt(rawLines[10+index], 64) 224 | quo.Asks[index].Price, _ = strconv.ParseFloat(rawLines[20+index], 64) 225 | quo.Asks[index].Amount, _ = strconv.ParseInt(rawLines[30+index], 64) 226 | } 227 | return quo, nil 228 | } else { 229 | if len(rawLines) < 2 || len(rawLines[1]) < 10 { 230 | return nil, fmt.Errorf("unable parse data") 231 | } 232 | quo.Code = rawLines[0][2:8] 233 | rawLines = strings.Split(rawLines[1], ",") 234 | quo.Name = rawLines[0] 235 | quo.Time, _ = time.Parse("2006-01-02T15:04:05.999999-07:00", fmt.Sprintf("%sT%s+08:00", rawLines[30], rawLines[31])) 236 | quo.Now = time.Now() 237 | quo.PreClose, _ = strconv.ParseFloat(rawLines[2], 64) 238 | quo.Close, _ = strconv.ParseFloat(rawLines[3], 64) 239 | quo.Bids = make([]OrderBook, 10) 240 | quo.Asks = make([]OrderBook, 10) 241 | return quo, nil 242 | } 243 | } 244 | 245 | func (api *Api) refreshToken() error { 246 | log.Println("refresh token") 247 | client := &http.Client{} 248 | if api.Params[0:2] == "sh" || api.Params[0:2] == "sz" { 249 | api.Params = "2cn_sh502014," + api.Params 250 | } 251 | url := fmt.Sprintf("http://current.sina.com.cn/auth/api/jsonp.php/getToken/AuthSign_Service.getSignCode?query=hq_pjb&ip=%s&_=0.9039749705698342&list=%s&retcode=0", api.IP, api.Params) 252 | req, _ := http.NewRequest("GET", url, nil) 253 | resp, err := client.Do(req) 254 | if err != nil { 255 | return err 256 | } 257 | defer resp.Body.Close() 258 | body, _ := ioutil.ReadAll(resp.Body) 259 | re := regexp.MustCompile(`result:"(.+?)"`) 260 | result := re.FindAllSubmatch(body, 1) 261 | if len(result) == 1 && len(result[0]) == 2 { 262 | api.token = string(result[0][1]) 263 | log.Printf("get token %s", api.token) 264 | return nil 265 | } else { 266 | return fmt.Errorf("can't match token") 267 | } 268 | } 269 | 270 | func (q *Quotation) GetDepthPrice(minDepth float64, side string) float64 { 271 | // 获取深度为depth的出价 也就是卖价 272 | depth := 0.0 273 | if side == "bid" || side == "sell" { 274 | for _, bid := range q.Bids { 275 | depth += bid.Amount 276 | if depth >= minDepth { 277 | return bid.Price 278 | } 279 | } 280 | return q.Bids[0].Price 281 | } else { 282 | // 买价 283 | for _, ask := range q.Asks { 284 | depth += ask.Amount 285 | if depth >= minDepth { 286 | return ask.Price 287 | } 288 | } 289 | return q.Asks[0].Price 290 | } 291 | } 292 | 293 | func (qs *QuotationStack) Push(q *Quotation) error { 294 | if qs.Length <= 0 { 295 | return fmt.Errorf("unexpected length %d", qs.Length) 296 | } 297 | qs.quotations = append(qs.quotations, q) 298 | curLen := len(qs.quotations) 299 | if curLen == qs.Length { 300 | return nil 301 | } 302 | if curLen > qs.Length { 303 | qs.quotations = qs.quotations[curLen-qs.Length : curLen] 304 | } 305 | return nil 306 | } 307 | 308 | func (qs *QuotationStack) All() ([]*Quotation, error) { 309 | if qs.Length != len(qs.quotations) { 310 | return []*Quotation{}, fmt.Errorf("stack required %d but is %d", qs.Length, len(qs.quotations)) 311 | } 312 | return qs.quotations, nil 313 | } 314 | -------------------------------------------------------------------------------- /api/trade/trader.go: -------------------------------------------------------------------------------- 1 | package trade 2 | 3 | import ( 4 | "bountyHunter/util" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/Sirupsen/logrus" 10 | "github.com/axgle/mahonia" 11 | "github.com/mreiferson/go-httpclient" 12 | "io/ioutil" 13 | "log" 14 | "net/http" 15 | "net/http/cookiejar" 16 | "os" 17 | "regexp" 18 | "strconv" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | type StockPosition struct { 24 | Code string `yaml:"-"` 25 | Name string `yaml:"-"` 26 | Amount int64 `yaml:"amount"` 27 | AvailableAmount int64 `yaml:"available_amount"` 28 | FrozenAmount int64 `yaml:"frozen_amount"` 29 | } 30 | 31 | type Balance struct { 32 | Balance float64 33 | AvailableBalance float64 34 | FrozenBalance float64 35 | } 36 | 37 | type Order struct { 38 | Code string 39 | Name string 40 | Amount int64 41 | Price float64 42 | Id int64 43 | Type string 44 | } 45 | 46 | type Account struct { 47 | client *http.Client 48 | logger *logrus.Logger 49 | Fee float64 `yaml:"fee"` 50 | Uid string `yaml:"uid"` 51 | Username string `yaml:"username"` 52 | Account1 string `yaml:"account1"` 53 | Account2 string `yaml:"account2"` 54 | Password1 string `yaml:"password1"` 55 | Password2 string `yaml:"password2"` 56 | Password3 string `yaml:"password3"` 57 | baseUrl string 58 | } 59 | 60 | type Data struct { 61 | No string `json:"entrust_no"` 62 | } 63 | 64 | type Result struct { 65 | ErrorCode string `json:"cssweb_code"` 66 | ErrorMessage string `json:"cssweb_msg"` 67 | Item []Data `json:"item"` 68 | } 69 | 70 | // 登录 71 | func (account *Account) Login() (err error) { 72 | cookieJar, _ := cookiejar.New(nil) 73 | account.logger = util.NewLogger("trader") 74 | transport := &httpclient.Transport{ 75 | ConnectTimeout: 3 * time.Second, 76 | RequestTimeout: 3 * time.Second, 77 | ResponseHeaderTimeout: 3 * time.Second, 78 | } 79 | defer transport.Close() 80 | account.client = &http.Client{ 81 | CheckRedirect: nil, 82 | Jar: cookieJar, 83 | Transport: transport, 84 | } 85 | account.logger.Info("begin login") 86 | account.baseUrl = "https://tradegw.htsc.com.cn/?" 87 | cacheByte, _ := ioutil.ReadFile(util.GetBasePath() + "/cache/" + account.Username + "Uid") 88 | cacheUid := string(cacheByte) 89 | if cacheUid != "" { 90 | account.logger.Info("read cache uid : " + cacheUid) 91 | account.Uid = cacheUid 92 | return 93 | } 94 | account.logger.Info("get verfiy code") 95 | loginUrl := "https://service.htsc.com.cn/service/login.jsp" 96 | req, _ := http.NewRequest("GET", loginUrl, nil) 97 | resp, _ := account.client.Do(req) 98 | req, _ = http.NewRequest("GET", "https://service.htsc.com.cn/service/pic/verifyCodeImage.jsp", nil) 99 | resp, _ = account.client.Do(req) 100 | defer resp.Body.Close() 101 | image, _ := ioutil.ReadAll(resp.Body) 102 | ioutil.WriteFile(util.GetBasePath()+"/cache/verify.jpg", image, 0644) 103 | var code string 104 | fmt.Println("input code:") 105 | fmt.Scanf("%s", &code) 106 | var raw = fmt.Sprintf("userType=jy&loginEvent=1&trdpwdEns=%s&macaddr=08-00-27-CE-7E-3E&hddInfo=VB0088e34c-9198b670+&lipInfo=10.0.2.15+&topath=null&accountType=1&userName=%s&servicePwd=%s&trdpwd=%s&vcode=", account.Password1, account.Username, account.Password2, account.Password1) 107 | account.logger.Infof("login post code : %s raw : %s", code, raw) 108 | req, _ = http.NewRequest("POST", "https://service.htsc.com.cn/service/loginAction.do?method=login", strings.NewReader(raw+code)) 109 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 110 | req.Header.Add("Refer", "https://service.htsc.com.cn/service/login.jsp?logout=yes") 111 | req.Header.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E)") 112 | os.Remove("./cache/verify.jpg") 113 | resp, _ = account.client.Do(req) 114 | defer resp.Body.Close() 115 | body, _ := ioutil.ReadAll(resp.Body) 116 | account.logger.Info("try to get uid") 117 | req, _ = http.NewRequest("GET", "https://service.htsc.com.cn/service/flashbusiness_new3.jsp?etfCode=", nil) 118 | resp, _ = account.client.Do(req) 119 | body, _ = ioutil.ReadAll(resp.Body) 120 | re := regexp.MustCompile(`var\ data\ =\ "(.+?)"`) 121 | result := re.FindAllStringSubmatch(string(body), 1) 122 | data, _ := base64.StdEncoding.DecodeString(result[0][1]) 123 | type User struct { 124 | Uid string `json:"uid"` 125 | } 126 | user := User{} 127 | json.Unmarshal([]byte(data), &user) 128 | account.Uid = user.Uid 129 | account.logger.Info("get uid success" + user.Uid) 130 | if user.Uid == "" { 131 | account.logger.Error("login error") 132 | return errors.New("login error") 133 | } 134 | ioutil.WriteFile("./cache/"+account.Username+"Uid", []byte(user.Uid), 0644) 135 | return 136 | } 137 | 138 | // 定时刷新使UID不过期 139 | func (account *Account) RefreshUid() { 140 | go func() { 141 | for { 142 | log.Println("use uid") 143 | _, err := account.Position() 144 | if err != nil { 145 | log.Println("uid maybe out of date: ", err) 146 | } 147 | time.Sleep(time.Second * 5) 148 | } 149 | }() 150 | select {} 151 | } 152 | 153 | // 异步挂单买 154 | func (account *Account) Buy(stock string, price float64, amount int64) (id int64, err error) { 155 | price = util.Round(price, 3) 156 | url := "uid=%s&cssweb_type=STOCK_BUY&version=1&custid=%s&op_branch_no=36&branch_no=36&op_entrust_way=7&op_station=IP$171.212.136.167;MAC$08-00-27-74-30-E4;HDD$VB95a57881-8897b350 &function_id=302&fund_account=%s&password=%s&identity_type=&exchange_type=%s&stock_account=%s&stock_code=%s&entrust_amount=%d&entrust_price=%.3f&entrust_prop=0&entrust_bs=1&ram=0.9656887338496745" 157 | if substr(stock, 0, 2) == "15" || substr(stock, 0, 2) == "00" || substr(stock, 0, 2) == "30" { 158 | url = fmt.Sprintf(url, account.Uid, account.Username, account.Username, account.Password3, "2", account.Account1, stock, amount, price) 159 | } else { 160 | url = fmt.Sprintf(url, account.Uid, account.Username, account.Username, account.Password3, "1", account.Account2, stock, amount, price) 161 | } 162 | account.logger.Infof("begin buy %s %f %d", stock, price, amount) 163 | url = account.baseUrl + account.base64encode(url) 164 | req, _ := http.NewRequest("GET", url, nil) 165 | resp, err := account.client.Do(req) 166 | if err != nil { 167 | return 168 | } 169 | body, _ := ioutil.ReadAll(resp.Body) 170 | defer resp.Body.Close() 171 | jsonString := account.base64decode(string(body)) 172 | result := Result{} 173 | account.logger.Infof("buy result %s", jsonString) 174 | json.Unmarshal([]byte(jsonString), &result) 175 | if result.ErrorMessage == "请重新登录" { 176 | account.logger.Error("buy token error") 177 | log.Println("token error") 178 | account.clearCache() 179 | } 180 | if result.ErrorCode != "success" { 181 | log.Printf("buy error %v", result) 182 | account.logger.Errorf("buy error %s", result) 183 | return 0, errors.New("buy error") 184 | } 185 | no, _ := strconv.ParseInt(result.Item[0].No, 64) 186 | account.logger.Infof("buy success op id : %d", no) 187 | return no, nil 188 | } 189 | 190 | // 异步挂单卖 191 | func (account *Account) Sell(stock string, price float64, amount int64) (id int64, err error) { 192 | price = util.Round(price, 3) 193 | // todo 194 | url := "uid=%s&cssweb_type=STOCK_SALE&version=1&custid=%s&op_branch_no=36&branch_no=36&op_entrust_way=7&op_station=IP$171.212.136.167;MAC$08-00-27-74-30-E4;HDD$VB95a57881-8897b350 &function_id=302&fund_account=%s&password=%s&identity_type=&exchange_type=%s&stock_account=%s&stock_code=%s&entrust_amount=%d&entrust_price=%.3f&entrust_prop=0&entrust_bs=2&ram=0.7360913073644042" 195 | if substr(stock, 0, 2) == "15" || substr(stock, 0, 2) == "00" { 196 | url = fmt.Sprintf(url, account.Uid, account.Username, account.Username, account.Password3, "2", account.Account1, stock, amount, price) 197 | } else { 198 | url = fmt.Sprintf(url, account.Uid, account.Username, account.Username, account.Password3, "1", account.Account2, stock, amount, price) 199 | } 200 | account.logger.Infof("begin sell %s %f %d", stock, price, amount) 201 | url = account.baseUrl + account.base64encode(url) 202 | req, _ := http.NewRequest("GET", url, nil) 203 | resp, err := account.client.Do(req) 204 | if err != nil { 205 | return 206 | } 207 | body, _ := ioutil.ReadAll(resp.Body) 208 | defer resp.Body.Close() 209 | jsonString := account.base64decode(string(body)) 210 | result := Result{} 211 | account.logger.Infof("sell result %s", jsonString) 212 | json.Unmarshal([]byte(jsonString), &result) 213 | if result.ErrorMessage == "请重新登录" { 214 | account.logger.Error("sell token error") 215 | log.Println("token error") 216 | account.clearCache() 217 | } 218 | if result.ErrorCode != "success" { 219 | log.Printf("sell error %v", result) 220 | account.logger.Errorf("sell error %s", result) 221 | return 0, errors.New("sell error") 222 | } 223 | no, _ := strconv.ParseInt(result.Item[0].No, 64) 224 | account.logger.Infof("sell success op id: %d", no) 225 | return no, nil 226 | } 227 | 228 | // 取消订单 229 | func (account *Account) Cancel(id int64) (err error) { 230 | url := "uid=%s&cssweb_type=STOCK_CANCEL&version=1&custid=%s&op_branch_no=36&branch_no=36&op_entrust_way=7&op_station=IP$171.212.136.167;MAC$08-00-27-74-30-E4;HDD$VB95a57881-8897b350 &function_id=304&fund_account=%s&password=%s&identity_type=&batch_flag=0&exchange_type=&entrust_no=%d&ram=0.544769384432584" 231 | url = fmt.Sprintf(url, account.Uid, account.Username, account.Username, account.Password3, id) 232 | account.logger.Infof("begin cancel %d", id) 233 | url = account.baseUrl + account.base64encode(url) 234 | req, _ := http.NewRequest("GET", url, nil) 235 | resp, _ := account.client.Do(req) 236 | body, _ := ioutil.ReadAll(resp.Body) 237 | jsonString := account.base64decode(string(body)) 238 | result := Result{} 239 | json.Unmarshal([]byte(jsonString), &result) 240 | if result.ErrorMessage == "请重新登录" { 241 | log.Println("token error") 242 | account.logger.Error("cancel token error") 243 | account.clearCache() 244 | err = errors.New("token error") 245 | return 246 | } 247 | if result.ErrorCode != "success" { 248 | log.Printf("cancel error %v", result) 249 | account.logger.Errorf("cancel error %s", result) 250 | return errors.New("cancel error") 251 | } 252 | no, _ := strconv.ParseInt(result.Item[0].No, 64) 253 | account.logger.Infof("cancel success op id %d", no) 254 | return 255 | } 256 | 257 | // 获取持仓 258 | func (account *Account) Position() (data []StockPosition, err error) { 259 | raw := fmt.Sprintf("uid=%s&cssweb_type=GET_STOCK_POSITION&version=1&custid=%s&op_branch_no=36&branch_no=36&op_entrust_way=7&op_station=IP$171.212.136.167;MAC$08-00-27-74-30-E4;HDD$VB95a57881-8897b350 &function_id=403&fund_account=%s&password=%s&identity_type=&exchange_type=&stock_account=&stock_code=&query_direction=&query_mode=0&request_num=100&position_str=&ram=0.39408391434699297", 260 | account.Uid, account.Username, account.Username, account.Password3) 261 | param := account.base64encode(raw) 262 | url := fmt.Sprintf("https://tradegw.htsc.com.cn/?%s", param) 263 | req, _ := http.NewRequest("GET", url, nil) 264 | resp, err := account.client.Do(req) 265 | if err != nil { 266 | account.logger.Errorln("get position err", err) 267 | return 268 | } 269 | defer resp.Body.Close() 270 | body, _ := ioutil.ReadAll(resp.Body) 271 | jsonString := account.base64decode(string(body)) 272 | type Item struct { 273 | Code string `json:"stock_code"` 274 | Name string `json:"stock_name"` 275 | Amount string `json:"current_amount"` 276 | AvailableAmount string `json:"enable_amount"` 277 | FrozenAmount string `json:"hand_flag"` 278 | } 279 | type Message struct { 280 | Code string `json:"cssweb_code"` 281 | ErrorMessage string `json:"cssweb_msg"` 282 | Items []Item `json:"item"` 283 | } 284 | message := Message{} 285 | json.Unmarshal([]byte(jsonString), &message) 286 | if message.ErrorMessage == "请重新登录" { 287 | log.Println("token error") 288 | account.logger.Error("position token error") 289 | account.clearCache() 290 | err = errors.New("token error") 291 | return 292 | } 293 | if len(message.Items) > 1 { 294 | message.Items = message.Items[:len(message.Items)-1] 295 | for _, item := range message.Items { 296 | stockPosition := StockPosition{} 297 | stockPosition.Name = item.Name 298 | stockPosition.Code = item.Code 299 | stockPosition.Amount, _ = strconv.ParseInt(item.Amount, 64) 300 | stockPosition.AvailableAmount, _ = strconv.ParseInt(item.AvailableAmount, 64) 301 | stockPosition.FrozenAmount = stockPosition.Amount - stockPosition.AvailableAmount 302 | data = append(data, stockPosition) 303 | } 304 | } 305 | return 306 | } 307 | 308 | // 获取账户资金 309 | func (account *Account) Balance() (data Balance, err error) { 310 | raw := fmt.Sprintf("uid=%s&cssweb_type=GET_FUNDS&version=1&custid=%s&op_branch_no=36&branch_no=36&op_entrust_way=7&op_station=IP$171.212.136.167;MAC$08-00-27-74-30-E4;HDD$VB95a57881-8897b350 &function_id=405&fund_account=%s&password=%s&identity_type=&money_type=&ram=0.5080185956321657", 311 | account.Uid, account.Username, account.Username, account.Password3) 312 | param := base64.StdEncoding.EncodeToString([]byte(raw)) 313 | url := fmt.Sprintf("https://tradegw.htsc.com.cn/?%s", param) 314 | req, _ := http.NewRequest("GET", url, nil) 315 | resp, _ := account.client.Do(req) 316 | defer resp.Body.Close() 317 | body, _ := ioutil.ReadAll(resp.Body) 318 | jsonString := account.base64decode(string(body)) 319 | type Item struct { 320 | Balance string `json:"current_balance"` 321 | AvailableBalance string `json:"enable_balance"` 322 | } 323 | type Message struct { 324 | Code string `json:"cssweb_code"` 325 | ErrorMessage string `json:"cssweb_msg"` 326 | Item []Item `json:"item"` 327 | } 328 | message := Message{} 329 | json.Unmarshal([]byte(jsonString), &message) 330 | if message.ErrorMessage == "请重新登录" { 331 | log.Println("token error") 332 | account.clearCache() 333 | err = errors.New("token error") 334 | return 335 | } 336 | data.Balance, _ = strconv.ParseFloat(message.Item[0].Balance, 64) 337 | data.AvailableBalance, _ = strconv.ParseFloat(message.Item[0].AvailableBalance, 64) 338 | data.FrozenBalance = data.Balance - data.AvailableBalance 339 | return 340 | } 341 | 342 | // 获取未交易成功列表 343 | func (account *Account) Pending() (data []Order, err error) { 344 | raw := fmt.Sprintf("uid=%s&cssweb_type=GET_CANCEL_LIST&version=1&custid=%s&op_branch_no=36&branch_no=36&op_entrust_way=7&op_station=IP$171.212.137.45;MAC$08-00-27-74-30-E4;HDD$VB95a57881-8897b350 &function_id=401&fund_account=%s&password=%s&identity_type=&exchange_type=&stock_account=&stock_code=&locate_entrust_no=&query_direction=&sort_direction=0&request_num=100&position_str=&ram=0.1524588279426098", 345 | account.Uid, account.Username, account.Username, account.Password3) 346 | param := base64.StdEncoding.EncodeToString([]byte(raw)) 347 | url := fmt.Sprintf("https://tradegw.htsc.com.cn/?%s", param) 348 | req, err := http.NewRequest("GET", url, nil) 349 | if err != nil { 350 | account.logger.Errorln("get pending err", err) 351 | return 352 | } 353 | resp, err := account.client.Do(req) 354 | if err != nil { 355 | account.logger.Errorln("get pending err", err) 356 | return 357 | } 358 | defer resp.Body.Close() 359 | body, _ := ioutil.ReadAll(resp.Body) 360 | jsonString := account.base64decode(string(body)) 361 | type Item struct { 362 | Code string `json:"stock_code"` 363 | Name string `json:"stock_name"` 364 | Amount string `json:"entrust_amount"` 365 | Price string `json:"entrust_price"` 366 | Id string `json:"entrust_no"` 367 | Type string `json:"entrust_bs"` 368 | } 369 | type Message struct { 370 | Code string `json:"cssweb_code"` 371 | ErrorMessage string `json:"cssweb_msg"` 372 | Items []Item `json:"item"` 373 | } 374 | message := Message{} 375 | json.Unmarshal([]byte(jsonString), &message) 376 | if message.ErrorMessage == "请重新登录" { 377 | log.Println("token error") 378 | account.clearCache() 379 | err = errors.New("token error") 380 | return 381 | } 382 | if len(message.Items) == 0 { 383 | return 384 | } 385 | message.Items = message.Items[:len(message.Items)-1] 386 | for _, item := range message.Items { 387 | order := Order{} 388 | order.Name = item.Name 389 | order.Code = item.Code 390 | order.Amount, _ = strconv.ParseInt(item.Amount, 64) 391 | order.Price, _ = strconv.ParseInt(item.Price, 64) 392 | order.Id, _ = strconv.ParseInt(item.Id, 64) 393 | if item.Type == "2" { 394 | order.Type = "sell" 395 | } else { 396 | order.Type = "buy" 397 | } 398 | data = append(data, order) 399 | } 400 | return 401 | } 402 | 403 | // // 同步买下单后自动检测是否交易成功 404 | // func (account *Account) BuySync(stock string, price float64, amount int64) (err error) { 405 | // return 406 | // var id int64 407 | // id, err = account.Buy(stock, price, amount) 408 | // if err != nil { 409 | // return 410 | // } 411 | // log.Println("buy sync order id is ", id) 412 | // isDeal := false 413 | // for !isDeal { 414 | // isDeal = true 415 | // log.Println("sync buy not deal wait...") 416 | // orderList, err := account.Pending() 417 | // for err != nil { 418 | // orderList, err = account.Pending() 419 | // } 420 | // for _, order := range orderList { 421 | // if order.Id == id { 422 | // isDeal = false 423 | // break 424 | // } 425 | // } 426 | // } 427 | // return 428 | // } 429 | 430 | // // 同步卖 431 | // func (account *Account) SellSync(stock string, price float64, amount int64) (err error) { 432 | // return 433 | // var id int64 434 | // id, err = account.Sell(stock, price, amount) 435 | // if err != nil { 436 | // return 437 | // } 438 | // log.Println("sell sync order id is ", id) 439 | 440 | // isDeal := false 441 | // for !isDeal { 442 | // isDeal = true 443 | // log.Println("sync sell not deal wait...") 444 | // orderList, err := account.Pending() 445 | // for err != nil { 446 | // orderList, err = account.Pending() 447 | // } 448 | // for _, order := range orderList { 449 | // if order.Id == id { 450 | // isDeal = false 451 | // break 452 | // } 453 | // } 454 | // } 455 | // return 456 | // } 457 | 458 | func (account *Account) clearCache() { 459 | os.Remove(util.GetBasePath() + "/cache/" + account.Username + "Uid") 460 | } 461 | 462 | func (account *Account) base64decode(str string) string { 463 | data, _ := base64.StdEncoding.DecodeString(str) 464 | str = fmt.Sprintf("%s", data) 465 | enc := mahonia.NewDecoder("gbk") 466 | gbk := enc.ConvertString(str) 467 | gbk = strings.Replace(gbk, "\n", "", -1) 468 | return gbk 469 | } 470 | 471 | func (account *Account) base64encode(str string) string { 472 | return base64.StdEncoding.EncodeToString([]byte(str)) 473 | } 474 | 475 | func substr(s string, pos, length int) string { 476 | runes := []rune(s) 477 | l := pos + length 478 | if l > len(runes) { 479 | l = len(runes) 480 | } 481 | return string(runes[pos:l]) 482 | } 483 | -------------------------------------------------------------------------------- /api/trade/trader_test.go: -------------------------------------------------------------------------------- 1 | package trade 2 | 3 | import ( 4 | "bountyHunter/util" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func newAccount() *Account { 10 | account := &Account{} 11 | err := util.YamlFileDecode(util.GetBasePath()+"/config/account.yaml", account) 12 | if err != nil { 13 | panic(err) 14 | } 15 | account.Login() 16 | return account 17 | } 18 | 19 | func Test_Login(t *testing.T) { 20 | a := newAccount() 21 | a.Login() 22 | } 23 | 24 | func Test_Postion(t *testing.T) { 25 | a := newAccount() 26 | log.Println(a.Position()) 27 | } 28 | 29 | func Test_Buy(t *testing.T) { 30 | a := newAccount() 31 | id, err := a.Buy("150260", 1.430, 100) 32 | log.Println(id, err) 33 | } 34 | -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | *.yaml -------------------------------------------------------------------------------- /config/trade.yaml.sample: -------------------------------------------------------------------------------- 1 | --- 2 | username: 666666666666 #华泰用户名 3 | account1: "6666666666" #深圳账户 (可在华泰APP内部查询) 4 | account2: "6666666666" #上海账户 5 | password1: "666666666666666666" #登录密码加密串(需要抓包) 6 | password2: 666666 #通信密码 7 | password3: "6666666666666666666" #交易密码加密串(需要抓包) 8 | fee: 0.0002 #交易佣金(并没有什么用) -------------------------------------------------------------------------------- /util/log.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/Sirupsen/logrus" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func NewLogger(name string) (logger *logrus.Logger) { 10 | logger = logrus.New() 11 | logOutput, err := os.OpenFile(GetBasePath()+"/log/"+name+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 12 | if err != nil { 13 | panic(err) 14 | } 15 | logger.Out = logOutput 16 | logger.Formatter = new(logrus.JSONFormatter) 17 | return logger 18 | } 19 | 20 | func GetBasePath() string { 21 | // @todo small hack 22 | dir, _ := os.Getwd() 23 | split := strings.Split(dir, "gotrade") 24 | return split[0] + "/gotrade" 25 | } 26 | -------------------------------------------------------------------------------- /util/math.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func Round(val float64, places int) float64 { 8 | var t float64 9 | f := math.Pow10(places) 10 | x := val * f 11 | if math.IsInf(x, 0) || math.IsNaN(x) { 12 | return val 13 | } 14 | if x >= 0.0 { 15 | t = math.Ceil(x) 16 | if (t - x) > 0.50000000001 { 17 | t -= 1.0 18 | } 19 | } else { 20 | t = math.Ceil(-x) 21 | if (t + x) > 0.50000000001 { 22 | t -= 1.0 23 | } 24 | t = -t 25 | } 26 | x = t / f 27 | 28 | if !math.IsInf(x, 0) { 29 | return x 30 | } 31 | 32 | return t 33 | } 34 | -------------------------------------------------------------------------------- /util/yaml.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "gopkg.in/yaml.v2" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | func YamlFileDecode(path string, out interface{}) (err error) { 10 | content, err := ioutil.ReadFile(path) 11 | if err != nil { 12 | return 13 | } 14 | err = yaml.Unmarshal(content, out) 15 | if err != nil { 16 | return 17 | } 18 | return 19 | } 20 | 21 | func YamlFileEncode(path string, in interface{}) (err error) { 22 | os.Remove(path) 23 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644) 24 | if err != nil { 25 | return 26 | } 27 | defer file.Close() 28 | out, err := yaml.Marshal(in) 29 | if err != nil { 30 | return 31 | } 32 | _, err = file.Write(out) 33 | if err != nil { 34 | return 35 | } 36 | return nil 37 | } 38 | --------------------------------------------------------------------------------