├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── conf └── settings.go ├── config.example.yml ├── db ├── client.go ├── enums.go └── models.go ├── engine ├── engine.go ├── engine_base.go └── engine_mock.go ├── examples ├── main.go ├── price_window.go ├── up_down_rate.go └── up_down_rate_test.go ├── exchange ├── base.go ├── binance │ ├── cancel_order.go │ ├── create_order.go │ ├── exchange.go │ ├── future │ │ ├── api.go │ │ ├── exchange.go │ │ └── exchange_test.go │ └── spot │ │ ├── api.go │ │ ├── exchange.go │ │ └── exchange_test.go ├── klines.go ├── mock │ ├── exchange.go │ └── klines.go └── models.go ├── go.mod ├── go.sum ├── images ├── backtest.png └── kline.png ├── server ├── main.go └── routes │ ├── fund.go │ ├── order.go │ └── strategy.go ├── strategy └── strategy_base.go └── utils └── utils.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 | .idea 18 | config.yml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/long2ice/trader/089aa6eaeff4971cbc2ed1cc2f0bc8456269cac3/CHANGELOG.md -------------------------------------------------------------------------------- /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 | # Trader 2 | 3 | ![kline](https://raw.githubusercontent.com/long2ice/trader/master/images/kline.png) 4 | 5 | ## Introduction 6 | 7 | `Trader` is a framework that automated cryptocurrency exchange with strategy. 8 | 9 | **Disclaimer: The profit and loss is yourself when use this framework!** 10 | 11 | ## Features 12 | 13 | - Current support spot/future of [binance](https://www.binance.com/), more exchange work in progress. 14 | - Easy write your own strategy. 15 | - Back test engine support to test your strategy. 16 | - Restful api server integration. 17 | 18 | ## Quick Start 19 | 20 | ### Write Strategy 21 | 22 | Just see [examples](https://github.com/long2ice/trader/tree/main/examples) 23 | 24 | #### Inherit `strategy.Base` 25 | 26 | ```go 27 | package strategy 28 | 29 | type UpDownRate struct { 30 | strategy.Base 31 | } 32 | ``` 33 | 34 | #### Implement `OnConnect` and `On1mKline`, or `OnTicker`. 35 | 36 | ```go 37 | package strategy 38 | 39 | func (s *UpDownRate) OnConnect() { 40 | // you can do something initialization here. 41 | } 42 | 43 | func (s *UpDownRate) On1mKline() { 44 | // here you can buy or sell order depending on 1m kline. 45 | } 46 | ``` 47 | 48 | #### Back test strategy 49 | 50 | The result will store in `orders` table in database. 51 | 52 | ```go 53 | package tests 54 | 55 | func TestUpDownRate(t *testing.T) { 56 | conf.InitConfig("config.yml") 57 | eng := (*engine.GetEngine(exchange.Mock, conf.BinanceApiKey, conf.BinanceApiSecret)).(*engine.Mock) 58 | BaseAsset := "ETH" 59 | QuoteAsset := "USDT" 60 | symbol := BaseAsset + QuoteAsset 61 | h, _ := time.ParseDuration("-48h") 62 | eng.Exchange.(*mock.Mock).StartTime = time.Now().Add(h) 63 | eng.Exchange.(*mock.Mock).EndTime = time.Now() 64 | eng.Exchange.(*mock.Mock).Symbol = symbol 65 | 66 | eng.RegisterStrategy(&UpDownRate{ 67 | KLineLimit: 10, 68 | Rate: decimal.NewFromInt(6).Div(decimal.NewFromInt(4)), 69 | Base: strategy.Base{ 70 | Streams: []string{strings.ToLower(symbol) + "@kline_1m"}, 71 | FundRatio: decimal.NewFromFloat(1), 72 | StopProfit: decimal.NewFromFloat(0.02), 73 | StopLoss: decimal.NewFromFloat(0.06), 74 | BaseAsset: BaseAsset, 75 | QuoteAsset: QuoteAsset, 76 | Exchange: eng.Exchange, 77 | }}, 78 | ) 79 | eng.Start(false) 80 | } 81 | ``` 82 | 83 | #### Run strategy 84 | 85 | After test strategy, and it's effective, that's the time to run it. 86 | 87 | ```go 88 | package main 89 | 90 | func main() { 91 | conf.InitConfig("config.yml") 92 | eng := (*engine.GetEngine(exchange.BinanceSpot, conf.BinanceApiKey, conf.BinanceApiSecret)).(*engine.Engine) 93 | eng.RegisterStrategy(&UpDownRate{ 94 | KLineLimit: 10, 95 | Rate: decimal.NewFromInt(6).Div(decimal.NewFromInt(4)), 96 | Base: strategy.Base{ 97 | FundRatio: decimal.NewFromFloat(1), 98 | StopProfit: decimal.NewFromFloat(0.02), 99 | StopLoss: decimal.NewFromFloat(0.06), 100 | BaseAsset: "ETH", 101 | QuoteAsset: "USDT", 102 | Exchange: eng.Exchange, 103 | Streams: []string{"ethusdt@kline_1m", "ethusdt@miniTicker"}, 104 | }}, 105 | ) 106 | eng.Start(true) 107 | } 108 | ``` 109 | 110 | ### Back test 111 | 112 | `Trader` has a back test engine, for that you can test your strategy. 113 | 114 | ![backtest](https://raw.githubusercontent.com/long2ice/trader/master/images/backtest.png?raw=true) 115 | 116 | ### Config 117 | 118 | Copy `config.example.yaml` to `config.yaml` then update it, and current use `MySQL`. 119 | 120 | You can get other custom config by `viper.GetString()` etc, see [viper](https://github.com/spf13/viper) framework for 121 | more details. 122 | 123 | ### Run trader 124 | 125 | Also see [examples](https://github.com/long2ice/trader/tree/main/examples). 126 | 127 | ```shell 128 | INFO[0001] Get account balances success balances="[{BTC 0.00000092 0} {ETH 0.0000053 0} {USDT 111.51295105 0} {BUSD 0.00914978 0}]" 129 | INFO[0001] Register strategy success strategy=UpDownRate 130 | INFO[0002] Subscribe account success 131 | INFO[0005] Subscribe market data success streams="[ethusdt@kline_1m ethusdt@miniTicker]" 132 | INFO[0005] Start trader success 133 | ``` 134 | 135 | You can earn `BTC` and `ETH` when sleep now! 136 | 137 | ## Restful api server 138 | 139 | ### GET /orders 140 | 141 | Get orders by strategy and symbol. 142 | 143 | ### GET /strategy 144 | 145 | Get strategy information by strategy and symbol. 146 | 147 | ### GET /fund 148 | 149 | Get fund information by strategy. 150 | 151 | ### POST /fund 152 | 153 | Add fund for strategy. 154 | 155 | ## License 156 | 157 | This project is licensed under the 158 | [Apache-2.0](https://github.com/long2ice/trader/blob/master/LICENSE) License. -------------------------------------------------------------------------------- /conf/settings.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var ( 9 | BinanceApiKey string 10 | BinanceApiSecret string 11 | ServerHost string 12 | ServerPort string 13 | Debug bool 14 | ) 15 | 16 | func InitConfig(configFile string) { 17 | viper.SetConfigFile(configFile) 18 | if err := viper.ReadInConfig(); err != nil { 19 | log.Error("Error reading config file, %s", err) 20 | } 21 | BinanceApiKey = viper.GetString("binance.api_key") 22 | BinanceApiSecret = viper.GetString("binance.api_secret") 23 | ServerHost = viper.GetString("server.host") 24 | ServerPort = viper.GetString("server.port") 25 | Debug = viper.GetBool("Debug") 26 | } 27 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | debug: true 2 | binance: 3 | api_key: xxx 4 | api_secret: xxx 5 | server: 6 | host: 0.0.0.0 7 | port: 8080 -------------------------------------------------------------------------------- /db/client.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | var Client *gorm.DB 9 | 10 | func Init(client *gorm.DB) { 11 | var err error 12 | Client = client 13 | err = Client.AutoMigrate(&Order{}, &KLine{}, &Fund{}) 14 | if err != nil { 15 | log.WithField("err", err).Error("AutoMigrate db fail") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /db/enums.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | type Side string 4 | 5 | const ( 6 | BUY Side = "BUY" 7 | SELL Side = "SELL" 8 | ) 9 | 10 | type PriceType string 11 | 12 | const ( 13 | LIMIT PriceType = "LIMIT" 14 | MARKET PriceType = "MARKET" 15 | ) 16 | -------------------------------------------------------------------------------- /db/models.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | "time" 6 | ) 7 | 8 | // Order 订单记录 9 | type Order struct { 10 | ID uint 11 | OrderId string 12 | Side Side 13 | Vol decimal.Decimal 14 | Price decimal.Decimal 15 | Amount decimal.Decimal 16 | Symbol string 17 | TimeInForce string 18 | Type PriceType 19 | Timestamp time.Time 20 | Strategy string 21 | CurrentFund decimal.Decimal 22 | Commission decimal.Decimal 23 | } 24 | 25 | // Fund 资金记录 26 | type Fund struct { 27 | ID uint 28 | TotalFund decimal.Decimal 29 | Strategy string `gorm:"uniqueindex;type:varchar(50);"` 30 | } 31 | 32 | // KLine 分钟K线 33 | type KLine struct { 34 | ID int64 35 | Symbol string `gorm:"index:idx_symbol_time"` 36 | OpenTime time.Time 37 | CloseTime time.Time `gorm:"index:idx_symbol_time"` 38 | Open decimal.Decimal 39 | Close decimal.Decimal 40 | High decimal.Decimal 41 | Low decimal.Decimal 42 | Vol decimal.Decimal 43 | Amount decimal.Decimal 44 | Num decimal.Decimal 45 | BuyVolume decimal.Decimal 46 | BuyAmount decimal.Decimal 47 | } 48 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | _ "github.com/long2ice/trader/exchange/binance/future" 5 | _ "github.com/long2ice/trader/exchange/binance/spot" 6 | "github.com/long2ice/trader/strategy" 7 | "os" 8 | "os/signal" 9 | ) 10 | 11 | type Engine struct { 12 | Base 13 | } 14 | 15 | func (e *Engine) Start(block bool) { 16 | e.SubscribeAccount() 17 | for _, s := range e.Strategies { 18 | //订阅行情 19 | s.OnConnect() 20 | err := e.SubscribeMarketData(s) 21 | if err != nil { 22 | e.GetLogger().WithField("err", err).Error("Subscribe market data fail") 23 | } 24 | } 25 | e.GetLogger().Info("Start engine success") 26 | if block { 27 | interrupt := make(chan os.Signal, 1) 28 | signal.Notify(interrupt, os.Interrupt, os.Kill) 29 | <-interrupt 30 | } 31 | } 32 | func (e *Engine) SubscribeMarketData(strategy strategy.IStrategy) error { 33 | streams := strategy.GetStreams() 34 | err := e.Exchange.SubscribeMarketData(streams, func(message map[string]interface{}) { 35 | if stream, ok := message["stream"]; ok { 36 | stream_ := stream.(string) 37 | callbacks := strategy.GetStreamCallback(stream_) 38 | for _, callback := range callbacks { 39 | callback(message["data"].(map[string]interface{})) 40 | } 41 | } 42 | }) 43 | if err != nil { 44 | e.GetLogger().WithField("err", err).WithField("streams", streams).Error("Failed to subscribe market data") 45 | } else { 46 | e.GetLogger().WithField("streams", streams).Info("Subscribe market data success") 47 | } 48 | return nil 49 | } 50 | func (e *Engine) SubscribeAccount() { 51 | err := e.Exchange.SubscribeAccount(func(message map[string]interface{}) { 52 | e.GetLogger().WithField("message", message).Info("Account data") 53 | eventType, _ := message["e"].(string) 54 | for _, s := range e.Strategies { 55 | switch eventType { 56 | case "outboundAccountPosition": //账户更新 57 | go s.OnAccount(message) 58 | case "executionReport": //订单更新 59 | go s.OnOrderUpdate(message) 60 | } 61 | } 62 | }) 63 | if err != nil { 64 | e.GetLogger().WithField("err", err).Error("Failed to subscribe account") 65 | } else { 66 | e.GetLogger().Info("Subscribe account success") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /engine/engine_base.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/long2ice/trader/db" 5 | "github.com/long2ice/trader/exchange" 6 | "github.com/long2ice/trader/strategy" 7 | "github.com/long2ice/trader/utils" 8 | log "github.com/sirupsen/logrus" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type IEngine interface { 13 | Start(block bool) 14 | SetDb(client *gorm.DB) 15 | RegisterStrategy(strategy strategy.IStrategy) 16 | SubscribeMarketData(strategy strategy.IStrategy) error 17 | SubscribeAccount() 18 | GetLogger() *log.Entry 19 | } 20 | 21 | type Base struct { 22 | IEngine 23 | ExchangeType exchange.Type 24 | Exchange exchange.IExchange 25 | Strategies []strategy.IStrategy 26 | apiKey string 27 | apiSecret string 28 | } 29 | 30 | func (e *Base) GetLogger() *log.Entry { 31 | return log.WithField("exchange", e.ExchangeType) 32 | } 33 | 34 | var engines = make(map[exchange.Type]*IEngine) 35 | 36 | func (e *Base) SetDb(client *gorm.DB) { 37 | db.Init(client) 38 | } 39 | func (e *Base) RegisterStrategy(s strategy.IStrategy) { 40 | e.Strategies = append(e.Strategies, s) 41 | e.GetLogger().WithField("symbol", s.GetSymbol()).WithField("strategy", utils.GetTypeName(s)).Info("Register strategy success") 42 | } 43 | 44 | func GetEngine(exchangeType exchange.Type, apiKey string, apiSecret string) *IEngine { 45 | if e, ok := engines[exchangeType]; ok { 46 | return e 47 | } 48 | var e IEngine 49 | 50 | ex, err := exchange.NewExchange(exchangeType, apiKey, apiSecret) 51 | if err != nil { 52 | log.WithField("err", err).Error("New exchange failed") 53 | } 54 | eb := Base{Exchange: ex, ExchangeType: exchangeType, apiKey: apiKey, apiSecret: apiSecret} 55 | if exchangeType == exchange.Mock { 56 | e = &Mock{Base: eb} 57 | } else { 58 | e = &Engine{Base: eb} 59 | } 60 | engines[exchangeType] = &e 61 | return &e 62 | } 63 | -------------------------------------------------------------------------------- /engine/engine_mock.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/long2ice/trader/db" 5 | _ "github.com/long2ice/trader/exchange/mock" 6 | "github.com/long2ice/trader/strategy" 7 | "github.com/long2ice/trader/utils" 8 | "strings" 9 | ) 10 | 11 | type Mock struct { 12 | Base 13 | } 14 | 15 | func (e *Mock) Start(block bool) { 16 | for _, s := range e.Strategies { 17 | db.Client.Where("strategy = ?", utils.GetTypeName(s)).Where("symbol = ?", s.GetSymbol()).Unscoped().Delete(&db.Order{}) 18 | s.OnConnect() 19 | err := e.SubscribeMarketData(s) 20 | if err != nil { 21 | e.GetLogger().WithField("err", err).Error("Fail to subscribe market data") 22 | } 23 | } 24 | e.GetLogger().Info("Mock finished") 25 | } 26 | func (e *Mock) SubscribeMarketData(strategy strategy.IStrategy) error { 27 | streams := strategy.GetStreams() 28 | err := e.Exchange.SubscribeMarketData(streams, func(message map[string]interface{}) { 29 | stream := message["stream"].(string) 30 | callbacks := strategy.GetStreamCallback(strings.ToLower(stream)) 31 | for _, callback := range callbacks { 32 | callback(message["data"].(map[string]interface{})) 33 | } 34 | }) 35 | if err != nil { 36 | e.GetLogger().WithField("err", err).WithField("streams", streams).Error("Failed to subscribe market data") 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/long2ice/trader/conf" 5 | "github.com/long2ice/trader/engine" 6 | "github.com/long2ice/trader/exchange" 7 | "github.com/long2ice/trader/server" 8 | "github.com/long2ice/trader/strategy" 9 | "github.com/shopspring/decimal" 10 | "gorm.io/driver/mysql" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func main() { 15 | conf.InitConfig("config.yml") 16 | eng := (*engine.GetEngine(exchange.BinanceSpot, conf.BinanceApiKey, conf.BinanceApiSecret)).(*engine.Engine) 17 | client, _ := gorm.Open(mysql.Open("root:123456@tcp(127.0.0.1:3306)/itrader?charset=utf8mb4&parseTime=True"), &gorm.Config{}) 18 | eng.SetDb(client) 19 | s := &UpDownRate{ 20 | KLineLimit: 10, 21 | Rate: decimal.NewFromInt(6).Div(decimal.NewFromInt(4)), 22 | Base: strategy.NewStrategy( 23 | "ETH", 24 | "USDT", 25 | eng.Exchange, 26 | []string{"ethusdt@kline_1m", "ethusdt@miniTicker"}, 27 | decimal.NewFromFloat(1), 28 | decimal.NewFromFloat(0.06), 29 | decimal.NewFromFloat(0.02)), 30 | } 31 | s.RegisterStreamCallback("ethusdt@kline_1m", s.On1mKLine) 32 | s.RegisterStreamCallback("ethusdt@miniTicker", s.OnTicker) 33 | 34 | eng.RegisterStrategy(s) 35 | eng.Start(false) 36 | server.Start() 37 | } 38 | -------------------------------------------------------------------------------- /examples/price_window.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "container/list" 5 | "github.com/long2ice/trader/exchange" 6 | "github.com/shopspring/decimal" 7 | "sync" 8 | ) 9 | 10 | var mutex sync.Mutex 11 | 12 | //价格窗口,存储近期价格以及快速得到最高价最低价 13 | type priceWindow struct { 14 | kLines *list.List 15 | } 16 | 17 | func newPriceWindow() *priceWindow { 18 | return &priceWindow{kLines: list.New()} 19 | } 20 | func (p *priceWindow) Prices() (decimal.Decimal, decimal.Decimal) { 21 | first := p.kLines.Front().Value.(exchange.KLine) 22 | minPrice := first.Low 23 | maxPrice := first.High 24 | for e := p.kLines.Front(); e != nil; e = e.Next() { 25 | kLine := e.Value.(exchange.KLine) 26 | if kLine.High.GreaterThan(maxPrice) { 27 | maxPrice = kLine.High 28 | } 29 | if kLine.Low.LessThan(minPrice) { 30 | minPrice = kLine.Low 31 | } 32 | } 33 | return minPrice, maxPrice 34 | } 35 | func (p *priceWindow) addKline(kLine exchange.KLine) { 36 | mutex.Lock() 37 | p.kLines.PushBack(kLine) 38 | p.kLines.Remove(p.kLines.Front()) 39 | mutex.Unlock() 40 | } 41 | func (p *priceWindow) addKLines(kLines []exchange.KLine) { 42 | for _, kline := range kLines { 43 | p.kLines.PushBack(kline) 44 | } 45 | } 46 | 47 | //获取最近K线升降次数 48 | func (p *priceWindow) getUpDown() (bool, decimal.Decimal, decimal.Decimal) { 49 | upTimes := decimal.Zero 50 | downTimes := decimal.Zero 51 | isDown := false 52 | for e := p.kLines.Front(); e != nil; e = e.Next() { 53 | kLine := e.Value.(exchange.KLine) 54 | if kLine.Open.GreaterThan(kLine.Close) { 55 | downTimes = downTimes.Add(decimal.NewFromInt(1)) 56 | } else { 57 | upTimes = upTimes.Add(decimal.NewFromInt(1)) 58 | } 59 | } 60 | if p.kLines.Back().Value.(exchange.KLine).Close.LessThan(p.kLines.Front().Value.(exchange.KLine).Close) { 61 | isDown = true 62 | } 63 | return isDown, upTimes, downTimes 64 | } 65 | -------------------------------------------------------------------------------- /examples/up_down_rate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "github.com/long2ice/trader/db" 6 | "github.com/long2ice/trader/strategy" 7 | "github.com/long2ice/trader/utils" 8 | "github.com/shopspring/decimal" 9 | "github.com/spf13/viper" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | var DEBUG = viper.GetBool("common.debug") 14 | 15 | /** 16 | 判断前若干次交易的升降比,当大于设定值的时候执行买和卖,注意:这只是个简单的策略,并不能盈利 17 | **/ 18 | type UpDownRate struct { 19 | strategy.Base 20 | Rate decimal.Decimal //升/降>Rate 卖,降/升>Rate买 21 | side db.Side //BUY or SELL 22 | KLineLimit int //存多少个K线 23 | lastBuyPrice decimal.Decimal //上次买入价 24 | priceWindow *priceWindow 25 | } 26 | 27 | func (s *UpDownRate) OnConnect() { 28 | var order db.Order 29 | result := db.Client.Where("symbol = ?", s.GetSymbol()).Where("strategy = ?", utils.GetTypeName(s)).Last(&order) 30 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 31 | s.side = db.SELL 32 | } else { 33 | if order.Side == db.BUY { 34 | s.lastBuyPrice = order.Price 35 | } 36 | s.side = order.Side 37 | } 38 | ex := s.Exchange 39 | //加载最近价格 40 | s.priceWindow = newPriceWindow() 41 | service := ex.NewKLineService() 42 | service.SetSymbol(s.GetSymbol()).SetInterval("1m").SetLimit(s.KLineLimit) 43 | kLines, err := service.Do() 44 | if err != nil { 45 | s.GetLogger().WithField("err", err).WithField("symbol", s.GetSymbol()).Error("Get latest kline error") 46 | } else { 47 | s.priceWindow.addKLines(kLines) 48 | } 49 | } 50 | func (s *UpDownRate) On1mKLine(data map[string]interface{}) { 51 | kLine := s.Exchange.ParseKLine(data) 52 | if !kLine.Finish { 53 | return 54 | } 55 | //更新priceWindow 56 | s.priceWindow.addKline(kLine) 57 | isDown, upTimes, downTimes := s.priceWindow.getUpDown() 58 | if upTimes.Equal(decimal.Zero) { 59 | return 60 | } 61 | var gains decimal.Decimal 62 | if s.lastBuyPrice.GreaterThan(decimal.Zero) { 63 | gains = kLine.Close.Sub(s.lastBuyPrice).Div(s.lastBuyPrice) 64 | } else { 65 | gains = decimal.Zero 66 | } 67 | isGains := gains.Neg().GreaterThan(s.StopLoss) 68 | //达到止盈或止损 69 | if (gains.GreaterThanOrEqual(s.StopProfit) || isGains) && s.side == db.BUY { 70 | //涨幅超过止盈或跌幅达到止损卖出 71 | s.GetLogger().WithField("symbol", s.GetSymbol()).WithField("涨跌幅", gains).WithField("交易量", kLine.Volume).WithField("当前最新价", s.LatestPrice).Info("达到止盈止损,卖出") 72 | //执行卖出 73 | order := db.Order{ 74 | Side: s.side, 75 | Symbol: s.GetSymbol(), 76 | Timestamp: kLine.CloseTime, 77 | Strategy: utils.GetTypeName(s), 78 | } 79 | price := kLine.Close 80 | var orderId string 81 | vol := s.Exchange.GetBalance(s.BaseAsset).Free.Truncate(5) 82 | if !DEBUG && vol.GreaterThan(decimal.Zero) { 83 | ret, err := s.Exchange.AddOrder(order) 84 | if err != nil { 85 | s.GetLogger().WithField("err", err).WithField("symbol", s.GetSymbol()).Error("创建卖出订单失败") 86 | return 87 | } 88 | vol, _ = decimal.NewFromString(ret["executedQty"].(string)) 89 | price, _ = decimal.NewFromString(ret["price"].(string)) 90 | orderId_, _ := ret["orderId"].(float64) 91 | orderId = utils.FloatToString(orderId_) 92 | s.Exchange.RefreshAccount() 93 | } 94 | s.side = db.SELL 95 | order.Price = price 96 | order.OrderId = orderId 97 | 98 | //数据库记录 99 | db.Client.Create(&order) 100 | } 101 | //满足策略并且降价幅度大于指定值,或者止损后 102 | if ((isDown && downTimes.Div(upTimes).GreaterThanOrEqual(s.Rate)) || isGains) && s.side == db.SELL { 103 | s.GetLogger().WithField("symbol", s.GetSymbol()).WithField("交易量", kLine.Volume).WithField("当前最新价", s.LatestPrice).Info("买入") 104 | price := kLine.Close 105 | var orderId string 106 | vol := decimal.Zero 107 | free := s.Exchange.GetBalance(s.QuoteAsset).Free.Truncate(8) 108 | if free.GreaterThanOrEqual(s.Fund.TotalFund) { 109 | free = free.Mul(s.FundRatio).Truncate(8) 110 | } 111 | s.side = db.BUY 112 | order := db.Order{ 113 | Side: s.side, 114 | Vol: vol, 115 | Price: price, 116 | OrderId: orderId, 117 | Symbol: s.GetSymbol(), 118 | Timestamp: kLine.CloseTime, 119 | Strategy: utils.GetTypeName(s), 120 | } 121 | //余额大于10美元执行买入 122 | if !DEBUG && free.GreaterThan(decimal.NewFromInt(10)) { 123 | ret, err := s.Exchange.AddOrder(order) 124 | if err != nil { 125 | s.GetLogger().WithField("symbol", s.GetSymbol()).WithField("err", err).Error("创建购买订单失败") 126 | return 127 | } 128 | vol, _ := decimal.NewFromString(ret["executedQty"].(string)) 129 | price = free.Div(vol) 130 | orderId_, _ := ret["orderId"].(float64) 131 | orderId = utils.FloatToString(orderId_) 132 | s.Exchange.RefreshAccount() 133 | } 134 | s.lastBuyPrice = price 135 | order.Price = price 136 | //数据库记录 137 | db.Client.Create(&order) 138 | } 139 | } 140 | func (s *UpDownRate) OnTicker(data map[string]interface{}) { 141 | ticker := s.Exchange.ParseTicker(data) 142 | s.LatestPrice = ticker.LatestPrice 143 | } 144 | -------------------------------------------------------------------------------- /examples/up_down_rate_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/long2ice/trader/conf" 5 | "github.com/long2ice/trader/engine" 6 | "github.com/long2ice/trader/exchange" 7 | "github.com/long2ice/trader/exchange/mock" 8 | "github.com/long2ice/trader/strategy" 9 | "github.com/shopspring/decimal" 10 | "gorm.io/driver/mysql" 11 | "gorm.io/gorm" 12 | "strings" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestUpDownRate(t *testing.T) { 18 | eng := (*engine.GetEngine(exchange.Mock, conf.BinanceApiKey, conf.BinanceApiSecret)).(*engine.Mock) 19 | client, _ := gorm.Open(mysql.Open("mysql://"), &gorm.Config{}) 20 | eng.Init("config.yml", client) 21 | BaseAsset := "ETH" 22 | QuoteAsset := "USDT" 23 | symbol := BaseAsset + QuoteAsset 24 | stream := strings.ToLower(symbol) + "@kline_1m" 25 | h, _ := time.ParseDuration("-48h") 26 | eng.Exchange.(*mock.Mock).StartTime = time.Now().Add(h) 27 | eng.Exchange.(*mock.Mock).EndTime = time.Now() 28 | eng.Exchange.(*mock.Mock).Symbol = symbol 29 | 30 | s := &UpDownRate{ 31 | KLineLimit: 10, 32 | Rate: decimal.NewFromInt(6).Div(decimal.NewFromInt(4)), 33 | Base: strategy.Base{ 34 | Streams: []string{}, 35 | FundRatio: decimal.NewFromFloat(1), 36 | StopProfit: decimal.NewFromFloat(0.02), 37 | StopLoss: decimal.NewFromFloat(0.06), 38 | BaseAsset: BaseAsset, 39 | QuoteAsset: QuoteAsset, 40 | Exchange: eng.Exchange, 41 | }} 42 | s.RegisterStreamCallback(stream, s.On1mKLine) 43 | eng.RegisterStrategy(s) 44 | eng.Start(false) 45 | } 46 | -------------------------------------------------------------------------------- /exchange/base.go: -------------------------------------------------------------------------------- 1 | package exchange 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "github.com/long2ice/trader/db" 10 | "github.com/shopspring/decimal" 11 | "gopkg.in/resty.v1" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | type Type string 18 | 19 | const ( 20 | BinanceSpot Type = "BinanceSpot" 21 | BinanceFuture Type = "BinanceFuture" 22 | Mock Type = "Mock" 23 | ) 24 | 25 | type IExchange interface { 26 | //订阅市场数据 27 | SubscribeMarketData(streams []string, callback func(map[string]interface{})) error 28 | //订阅订单更新 29 | SubscribeAccount(callback func(map[string]interface{})) error 30 | //近期K线 31 | NewKLineService() IKLineService 32 | //下单 33 | AddOrder(order db.Order) (map[string]interface{}, error) 34 | //取消订单 35 | CancelOrder(symbol string, orderId string) (map[string]interface{}, error) 36 | //刷新账户信息 37 | RefreshAccount() 38 | //获取账户余额 39 | GetBalance(asset string) Balance 40 | //获取账户所有余额 41 | GetBalances() []Balance 42 | //创建exchange 43 | NewExchange(apiKey string, apiSecret string) IExchange 44 | //ParseKLine 45 | ParseKLine(data map[string]interface{}) KLine 46 | //ParseTicker 47 | ParseTicker(data map[string]interface{}) Ticker 48 | } 49 | 50 | var exchanges = make(map[Type]IExchange) 51 | 52 | func RegisterExchange(exchange Type, iExchange IExchange) { 53 | exchanges[exchange] = iExchange 54 | } 55 | 56 | type BaseExchange struct { 57 | IExchange 58 | //余额信息 59 | Balances []Balance 60 | } 61 | 62 | func (exchange *BaseExchange) GetBalance(asset string) Balance { 63 | for _, b := range exchange.Balances { 64 | if b.Asset == asset { 65 | return b 66 | } 67 | } 68 | return Balance{Asset: asset, Free: decimal.NewFromInt(0), Locked: decimal.NewFromInt(0)} 69 | } 70 | 71 | func (exchange *BaseExchange) GetBalances() []Balance { 72 | return exchange.Balances 73 | } 74 | 75 | type IApi interface { 76 | CancelOrder(params map[string]interface{}) (map[string]interface{}, error) 77 | AddOrder(params map[string]interface{}) (map[string]interface{}, error) 78 | KLines(params map[string]interface{}) ([][]interface{}, error) 79 | CreateSpotListenKey() (string, bool) 80 | } 81 | 82 | type BaseApi struct { 83 | IApi 84 | ApiKey string 85 | ApiSecret string 86 | RestyClient *resty.Client 87 | } 88 | 89 | // 构建必要参数 90 | func (api *BaseApi) BuildCommonQuery(params map[string]interface{}, withSign bool) string { 91 | var joins []string 92 | for key, value := range params { 93 | switch value.(type) { 94 | case int, int64: 95 | joins = append(joins, fmt.Sprintf("%s=%d", key, value)) 96 | case string, interface{}, decimal.Decimal: 97 | joins = append(joins, fmt.Sprintf("%s=%s", key, value)) 98 | } 99 | } 100 | if withSign { 101 | timestamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) 102 | joins = append(joins, fmt.Sprintf("recvWindow=10000×tamp=%s", timestamp)) 103 | } 104 | query := strings.Join(joins, "&") 105 | 106 | if withSign { 107 | h := hmac.New(sha256.New, []byte(api.ApiSecret)) 108 | h.Write([]byte(query)) 109 | signature := hex.EncodeToString(h.Sum(nil)) 110 | query += "&signature=" + signature 111 | } 112 | return query 113 | } 114 | func NewExchange(exchange Type, apiKey string, apiSecret string) (IExchange, error) { 115 | if iExchange, ok := exchanges[exchange]; ok { 116 | e := iExchange.NewExchange(apiKey, apiSecret) 117 | return e, nil 118 | } else { 119 | return nil, errors.New("Unknown exchange:" + string(exchange)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /exchange/binance/cancel_order.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/long2ice/trader/exchange" 5 | ) 6 | 7 | //订单取消服务 8 | type ICancelOrderService interface { 9 | Collect() map[string]interface{} 10 | Do() (map[string]interface{}, error) 11 | } 12 | type CancelOrderService struct { 13 | Symbol string 14 | NewClientOrderId *string 15 | OrigClientOrderId *string 16 | OrderId *string 17 | Api exchange.IApi 18 | } 19 | 20 | func (service *CancelOrderService) Collect() map[string]interface{} { 21 | params := make(map[string]interface{}) 22 | params["symbol"] = service.Symbol 23 | if service.OrigClientOrderId != nil { 24 | params["origClientOrderId"] = *service.OrigClientOrderId 25 | } 26 | if service.OrderId != nil { 27 | params["orderId"] = *service.OrderId 28 | } 29 | if service.NewClientOrderId != nil { 30 | params["newClientOrderId"] = *service.NewClientOrderId 31 | } 32 | return params 33 | } 34 | 35 | func (service *CancelOrderService) Do() (map[string]interface{}, error) { 36 | ret, err := service.Api.CancelOrder(service.Collect()) 37 | if err != nil { 38 | return ret, err 39 | } 40 | return ret, nil 41 | } 42 | -------------------------------------------------------------------------------- /exchange/binance/create_order.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/long2ice/trader/db" 5 | "github.com/long2ice/trader/exchange" 6 | "github.com/shopspring/decimal" 7 | ) 8 | 9 | //订单创建服务 10 | type ICreateOrderService interface { 11 | Collect() map[string]interface{} 12 | Do() (map[string]interface{}, error) 13 | } 14 | 15 | type CreateOrderService struct { 16 | Symbol string 17 | Side db.Side 18 | Type db.PriceType 19 | TimeInForce *string 20 | Quantity *decimal.Decimal 21 | Price *decimal.Decimal 22 | QuoteOrderQty *decimal.Decimal 23 | NewClientOrderId *string 24 | StopPrice *decimal.Decimal 25 | NewOrderRespType *string 26 | Api exchange.IApi 27 | } 28 | 29 | func (service *CreateOrderService) Collect() map[string]interface{} { 30 | params := make(map[string]interface{}) 31 | params["symbol"] = service.Symbol 32 | params["side"] = service.Side 33 | params["type"] = service.Type 34 | if service.Type == db.LIMIT { 35 | params["timeInForce"] = *service.TimeInForce 36 | params["price"] = *service.Price 37 | params["quantity"] = *service.Quantity 38 | } else if service.Type == db.MARKET { 39 | if service.Quantity != nil { 40 | params["quantity"] = *service.Quantity 41 | } 42 | if service.QuoteOrderQty != nil { 43 | params["quoteOrderQty"] = *service.QuoteOrderQty 44 | } 45 | } 46 | 47 | if service.NewClientOrderId != nil { 48 | params["newClientOrderId"] = *service.NewClientOrderId 49 | } 50 | if service.StopPrice != nil { 51 | params["stopPrice"] = *service.StopPrice 52 | } 53 | if service.NewOrderRespType != nil { 54 | params["newOrderRespType"] = *service.NewOrderRespType 55 | } 56 | 57 | return params 58 | } 59 | func (service *CreateOrderService) Do() (map[string]interface{}, error) { 60 | params := service.Collect() 61 | ret, err := service.Api.AddOrder(params) 62 | if err != nil { 63 | return ret, err 64 | } 65 | return ret, nil 66 | } 67 | -------------------------------------------------------------------------------- /exchange/binance/exchange.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/long2ice/trader/exchange" 6 | "github.com/shopspring/decimal" 7 | log "github.com/sirupsen/logrus" 8 | "time" 9 | ) 10 | 11 | type Exchange struct { 12 | exchange.BaseExchange 13 | } 14 | 15 | func (e *Exchange) ParseTicker(data map[string]interface{}) exchange.Ticker { 16 | dbByte, _ := json.Marshal(data) 17 | var ticker exchange.Ticker 18 | err := json.Unmarshal(dbByte, &ticker) 19 | if err != nil { 20 | log.WithField("err", err).Error("ParseTicker failed") 21 | } 22 | return ticker 23 | } 24 | 25 | func (e *Exchange) ParseKLine(data map[string]interface{}) exchange.KLine { 26 | k, _ := data["k"].(map[string]interface{}) 27 | h, _ := k["h"] 28 | kh, _ := decimal.NewFromString(h.(string)) 29 | l, _ := k["l"] 30 | kl, _ := decimal.NewFromString(l.(string)) 31 | o, _ := k["o"] 32 | ko, _ := decimal.NewFromString(o.(string)) 33 | c, _ := k["c"] 34 | kc, _ := decimal.NewFromString(c.(string)) 35 | v, _ := k["v"] 36 | kv, _ := decimal.NewFromString(v.(string)) 37 | q, _ := k["q"] 38 | kq, _ := decimal.NewFromString(q.(string)) 39 | x, _ := k["x"] 40 | kx := x.(bool) 41 | t, _ := k["T"] 42 | kt := t.(float64) 43 | return exchange.KLine{ 44 | Open: ko, 45 | Close: kc, 46 | High: kh, 47 | Low: kl, 48 | Amount: kq, 49 | Volume: kv, 50 | Finish: kx, 51 | CloseTime: time.Unix(int64(kt/1000), 0), 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /exchange/binance/future/api.go: -------------------------------------------------------------------------------- 1 | package future 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/long2ice/trader/exchange" 7 | "github.com/shopspring/decimal" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const ( 12 | apiAddr = "https://fapi.binance.com" 13 | ) 14 | 15 | type Api struct { 16 | exchange.BaseApi 17 | } 18 | 19 | func (api *Api) CreateSpotListenKey() (string, bool) { 20 | url := apiAddr + "/fapi/v1/listenKey" 21 | var result map[string]interface{} 22 | var respError map[string]interface{} 23 | _, err := api.RestyClient.R().SetResult(&result).SetError(&respError).Post(url) 24 | if err != nil || respError != nil { 25 | log.WithField("respError", respError).WithField("err", err).Error("createSpotListenKey error") 26 | return "", false 27 | } else { 28 | listenKey := result["listenKey"] 29 | return listenKey.(string), true 30 | } 31 | } 32 | func (api *Api) AccountInfo() ([]exchange.Balance, error) { 33 | url := apiAddr + "/fapi/v2/balance?" 34 | query := api.BuildCommonQuery(map[string]interface{}{}, true) 35 | var respError map[string]interface{} 36 | var result []map[string]interface{} 37 | _, err := api.RestyClient.R().SetResult(&result).SetError(&respError).Get(url + query) 38 | if err != nil { 39 | log.WithField("err", err).Error("获取账号信息失败") 40 | return nil, err 41 | } else if respError != nil { 42 | log.WithField("respError", respError).Error("获取账号信息失败") 43 | return nil, errors.New(respError["msg"].(string)) 44 | } else { 45 | var balancesRet []exchange.Balance 46 | for _, balance := range result { 47 | asset, _ := balance["asset"] 48 | free, _ := balance["availableBalance"] 49 | free_, _ := decimal.NewFromString(free.(string)) 50 | all, _ := balance["balance"].(string) 51 | all_, _ := decimal.NewFromString(all) 52 | if all_.GreaterThan(decimal.Zero) { 53 | balancesRet = append(balancesRet, exchange.Balance{Asset: asset.(string), Free: free_, Locked: all_.Sub(free_)}) 54 | } 55 | } 56 | return balancesRet, nil 57 | } 58 | } 59 | func (api *Api) CancelOrder(params map[string]interface{}) (map[string]interface{}, error) { 60 | url := apiAddr + "/fapi/v1/order?" 61 | query := api.BuildCommonQuery(params, true) 62 | var respError map[string]interface{} 63 | resp, err := api.RestyClient.R().SetError(&respError).Delete(url + query) 64 | if err != nil { 65 | log.WithField("err", err).Error("撤单失败") 66 | return nil, err 67 | } else if respError != nil { 68 | log.WithField("respError", respError).Error("撤单失败") 69 | return nil, errors.New(respError["msg"].(string)) 70 | } else { 71 | var ret map[string]interface{} 72 | err := json.Unmarshal(resp.Body(), &ret) 73 | if err != nil { 74 | log.WithField("err", err).Error("解析撤单返回数据失败") 75 | return ret, err 76 | } 77 | log.WithField("撤单详情", ret).Info("撤单成功") 78 | return ret, nil 79 | } 80 | } 81 | func (api *Api) KLines(params map[string]interface{}) ([][]interface{}, error) { 82 | url := apiAddr + "/fapi/v1/klines?" 83 | var respError map[string]interface{} 84 | query := api.BuildCommonQuery(params, false) 85 | resp, err := api.RestyClient.R().SetError(&respError).Get(url + query) 86 | if err != nil { 87 | log.WithField("err", err).Error("获取KLine失败") 88 | return nil, err 89 | } else if respError != nil { 90 | log.WithField("respError", respError).Error("获取KLine失败") 91 | return nil, errors.New(respError["msg"].(string)) 92 | } else { 93 | var result [][]interface{} 94 | err := json.Unmarshal(resp.Body(), &result) 95 | if err != nil { 96 | log.WithField("err", err).Error("解析KLine数据失败") 97 | return result, err 98 | } 99 | return result, nil 100 | } 101 | } 102 | func (api *Api) AddOrder(params map[string]interface{}) (map[string]interface{}, error) { 103 | url := apiAddr + "/fapi/v1/order?" 104 | query := api.BuildCommonQuery(params, true) 105 | var respError map[string]interface{} 106 | resp, err := api.RestyClient.R().SetError(&respError).Post(url + query) 107 | if err != nil { 108 | log.WithField("err", err).Error("下单失败") 109 | return nil, err 110 | } else if respError != nil { 111 | log.WithField("respError", respError).Error("下单失败") 112 | return nil, errors.New(respError["msg"].(string)) 113 | } else { 114 | var ret map[string]interface{} 115 | err := json.Unmarshal(resp.Body(), &ret) 116 | if err != nil { 117 | log.WithField("err", err).Error("解析下单返回数据失败") 118 | return ret, err 119 | } 120 | log.WithField("订单详情", ret).Info("下单成功") 121 | return ret, nil 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /exchange/binance/future/exchange.go: -------------------------------------------------------------------------------- 1 | package future 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "github.com/gorilla/websocket" 7 | "github.com/long2ice/trader/db" 8 | "github.com/long2ice/trader/exchange" 9 | "github.com/long2ice/trader/exchange/binance" 10 | "github.com/shopspring/decimal" 11 | log "github.com/sirupsen/logrus" 12 | "gopkg.in/resty.v1" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | const ( 18 | //wsAddr = "wss://stream.goodusahost.cn" 19 | //wsAddr = "wss://stream.shyqxxy.com" 20 | wsAddr = "wss://fstream.binance.com" 21 | wsMarketAddr = wsAddr + "/stream" 22 | ) 23 | 24 | type Future struct { 25 | Api Api 26 | binance.Exchange 27 | //余额信息 28 | balances []exchange.Balance 29 | } 30 | 31 | type CancelOrderService struct { 32 | binance.CancelOrderService 33 | } 34 | type CreateOrderService struct { 35 | binance.CreateOrderService 36 | PositionSide string 37 | } 38 | 39 | func (service *CancelOrderService) Collect() map[string]interface{} { 40 | params := make(map[string]interface{}) 41 | params["symbol"] = service.Symbol 42 | if service.OrigClientOrderId != nil { 43 | params["origClientOrderId"] = *service.OrigClientOrderId 44 | } 45 | if service.OrderId != nil { 46 | params["orderId"] = *service.OrderId 47 | } 48 | return params 49 | } 50 | func (service *CreateOrderService) Collect() map[string]interface{} { 51 | params := make(map[string]interface{}) 52 | params["symbol"] = service.Symbol 53 | params["side"] = service.Side 54 | params["positionSide"] = service.PositionSide 55 | params["type"] = service.Type 56 | if service.Type == db.LIMIT { 57 | params["timeInForce"] = *service.TimeInForce 58 | params["price"] = *service.Price 59 | params["quantity"] = *service.Quantity 60 | } else if service.Type == db.MARKET { 61 | params["quantity"] = *service.Quantity 62 | } 63 | if service.NewClientOrderId != nil { 64 | params["newClientOrderId"] = *service.NewClientOrderId 65 | } 66 | if service.StopPrice != nil { 67 | params["stopPrice"] = *service.StopPrice 68 | } 69 | if service.NewOrderRespType != nil { 70 | params["newOrderRespType"] = *service.NewOrderRespType 71 | } 72 | return params 73 | } 74 | func init() { 75 | exchange.RegisterExchange(exchange.BinanceFuture, &Future{}) 76 | } 77 | 78 | func (future *Future) SubscribeMarketData(symbols []string, callback func(map[string]interface{})) error { 79 | addr := wsMarketAddr + "?streams=" + strings.Join(symbols, "/") 80 | wsMarketDataClient, _, err := websocket.DefaultDialer.Dial(addr, nil) 81 | if err != nil { 82 | log.WithField("err", err).WithField("symbols", symbols).Error("订阅行情失败") 83 | return err 84 | } 85 | go func() { 86 | for { 87 | var message map[string]interface{} 88 | err := wsMarketDataClient.ReadJSON(&message) 89 | if err != nil { 90 | log.WithField("err", err).Warning("解析行情消息错误,重新连接") 91 | wsMarketDataClient, _, err = websocket.DefaultDialer.Dial(wsMarketAddr, nil) 92 | if err != nil { 93 | log.WithField("err", err).Warning("重新连接失败,2秒后重试...") 94 | time.Sleep(time.Second * 2) 95 | } else { 96 | wsMarketDataClient, _, err = websocket.DefaultDialer.Dial(addr, nil) 97 | if err != nil { 98 | log.WithField("err", err).WithField("symbols", symbols).Error("重新连接失败") 99 | } else { 100 | log.Info("重新连接成功") 101 | } 102 | } 103 | continue 104 | } 105 | _, ok := message["error"] 106 | if ok { 107 | log.WithField("error", message["error"]).Error() 108 | continue 109 | } 110 | callback(message) 111 | } 112 | }() 113 | 114 | go func() { 115 | for range time.NewTicker(time.Second * 60 * 10).C { 116 | err := wsMarketDataClient.WriteControl(websocket.PongMessage, []byte{}, time.Now().Add(10*time.Second)) 117 | if err != nil { 118 | log.WithField("err", err).Error("行情pong失败") 119 | } 120 | } 121 | }() 122 | return nil 123 | } 124 | 125 | func (future *Future) SubscribeAccount(callback func(map[string]interface{})) error { 126 | listenKey, ok := future.Api.CreateSpotListenKey() 127 | if !ok { 128 | return errors.New("createSpotListenKey失败") 129 | } 130 | wsUrl := wsAddr + "/stream?streams=" + listenKey 131 | c, _, err := websocket.DefaultDialer.Dial(wsUrl, nil) 132 | if err != nil { 133 | log.WithField("err", err).WithField("wsUrl", wsUrl).Error("连接websocket失败") 134 | } 135 | go func() { 136 | for { 137 | var message map[string]interface{} 138 | err := c.ReadJSON(&message) 139 | if err != nil { 140 | log.WithField("err", err).Warning("解析账号消息错误,重新连接") 141 | c, _, err = websocket.DefaultDialer.Dial(wsUrl, nil) 142 | if err != nil { 143 | log.WithField("err", err).Warning("重新连接失败,2秒后重试...") 144 | time.Sleep(time.Second * 2) 145 | } else { 146 | log.Info("重新连接成功") 147 | } 148 | continue 149 | } 150 | log.WithField("message", message).Debug() 151 | callback(message) 152 | } 153 | }() 154 | //每30分钟延期SpotListenKey 155 | go func() { 156 | for range time.NewTicker(time.Second * 60 * 30).C { 157 | _, ok := future.Api.CreateSpotListenKey() 158 | if !ok { 159 | log.Error("延期SpotListenKey失败") 160 | } 161 | } 162 | }() 163 | //每10分钟ping 164 | go func() { 165 | for range time.NewTicker(time.Second * 60 * 10).C { 166 | err = c.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)) 167 | if err != nil { 168 | log.WithField("err", err).Error("账户信息ping失败") 169 | } 170 | } 171 | }() 172 | return nil 173 | } 174 | func (future *Future) AddOrder(order db.Order) (map[string]interface{}, error) { 175 | var positionSide string 176 | if order.Vol.GreaterThan(decimal.Zero) { 177 | positionSide = "LONG" 178 | } else { 179 | positionSide = "SHORT" 180 | } 181 | service := CreateOrderService{ 182 | binance.CreateOrderService{ 183 | Symbol: order.Symbol, 184 | Side: order.Side, 185 | Type: order.Type, 186 | Price: &order.Price, 187 | Quantity: &order.Vol, 188 | TimeInForce: &order.TimeInForce, 189 | Api: &future.Api, 190 | }, 191 | positionSide, 192 | } 193 | return service.Do() 194 | } 195 | 196 | func (future *Future) CancelOrder(symbol string, orderId string) (map[string]interface{}, error) { 197 | service := CancelOrderService{ 198 | binance.CancelOrderService{ 199 | Symbol: symbol, 200 | OrderId: &orderId, 201 | Api: &future.Api, 202 | }, 203 | } 204 | return service.Do() 205 | } 206 | func (future *Future) NewKLineService() exchange.IKLineService { 207 | var p exchange.IKLineService 208 | p = &exchange.KLineService{ 209 | Api: &future.Api, 210 | } 211 | return p 212 | } 213 | func (future *Future) RefreshAccount() { 214 | //初始化账号信息 215 | balances, err := future.Api.AccountInfo() 216 | if err != nil { 217 | log.WithField("err", err).Error("获取账号信息失败") 218 | } else { 219 | future.Balances = balances 220 | } 221 | } 222 | 223 | func (future *Future) NewExchange(apiKey string, apiSecret string) exchange.IExchange { 224 | b := &Future{ 225 | Api: Api{exchange.BaseApi{ 226 | ApiKey: apiKey, 227 | ApiSecret: apiSecret, 228 | RestyClient: resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).SetHeader("X-MBX-APIKEY", apiKey), 229 | }}, 230 | } 231 | var iExchange exchange.IExchange 232 | iExchange = b 233 | return iExchange 234 | } 235 | -------------------------------------------------------------------------------- /exchange/binance/future/exchange_test.go: -------------------------------------------------------------------------------- 1 | package future 2 | 3 | import ( 4 | "github.com/long2ice/trader/conf" 5 | "github.com/long2ice/trader/db" 6 | "github.com/long2ice/trader/exchange" 7 | "github.com/shopspring/decimal" 8 | log "github.com/sirupsen/logrus" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | conf.InitConfig("config.yml") 15 | code := m.Run() 16 | os.Exit(code) 17 | } 18 | func TestBinanceExchange_AddOrder(t *testing.T) { 19 | ex, err := exchange.NewExchange(exchange.BinanceFuture, conf.BinanceApiKey, conf.BinanceApiSecret) 20 | if err != nil { 21 | log.WithField("err", err).Error("创建交易所失败") 22 | } 23 | ret, err := ex.AddOrder(db.Order{ 24 | Side: "BUY", 25 | Vol: decimal.NewFromFloat(0.001), 26 | Price: decimal.NewFromInt(20000), 27 | Symbol: "BTCUSDT", 28 | Type: "LIMIT", 29 | }) 30 | log.WithField("account", ex.GetBalance("USDT")).WithField("ret", ret).Info() 31 | } 32 | -------------------------------------------------------------------------------- /exchange/binance/spot/api.go: -------------------------------------------------------------------------------- 1 | package spot 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/long2ice/trader/exchange" 7 | "github.com/shopspring/decimal" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type Api struct { 12 | exchange.BaseApi 13 | } 14 | 15 | const ( 16 | apiAddr = "https://api.binance.com" 17 | ) 18 | 19 | //创建或延期现货账户listenKey 20 | func (api *Api) CreateSpotListenKey() (string, bool) { 21 | url := apiAddr + "/api/v3/userDataStream" 22 | var result map[string]interface{} 23 | var respError map[string]interface{} 24 | _, err := api.RestyClient.R().SetResult(&result).SetError(&respError).Post(url) 25 | if err != nil || respError != nil { 26 | log.WithField("respError", respError).WithField("err", err).Error("createSpotListenKey error") 27 | return "", false 28 | } else { 29 | listenKey := result["listenKey"] 30 | return listenKey.(string), true 31 | } 32 | } 33 | 34 | func (api *Api) CancelOrder(params map[string]interface{}) (map[string]interface{}, error) { 35 | url := apiAddr + "/api/v3/order?" 36 | query := api.BuildCommonQuery(params, true) 37 | var respError map[string]interface{} 38 | resp, err := api.RestyClient.R().SetError(&respError).Delete(url + query) 39 | if err != nil { 40 | log.WithField("err", err).Error("撤单失败") 41 | return nil, err 42 | } else if respError != nil { 43 | log.WithField("respError", respError).Error("撤单失败") 44 | return nil, errors.New(respError["msg"].(string)) 45 | } else { 46 | var ret map[string]interface{} 47 | err := json.Unmarshal(resp.Body(), &ret) 48 | if err != nil { 49 | log.WithField("err", err).Error("解析撤单返回数据失败") 50 | return ret, err 51 | } 52 | log.WithField("撤单详情", ret).Info("撤单成功") 53 | return ret, nil 54 | } 55 | } 56 | func (api *Api) AccountInfo() ([]exchange.Balance, error) { 57 | url := apiAddr + "/api/v3/account?" 58 | query := api.BuildCommonQuery(map[string]interface{}{}, true) 59 | var respError map[string]interface{} 60 | var result map[string]interface{} 61 | _, err := api.RestyClient.R().SetResult(&result).SetError(&respError).Get(url + query) 62 | if err != nil { 63 | log.WithField("err", err).Error("获取账号信息失败") 64 | return nil, err 65 | } else if respError != nil { 66 | log.WithField("respError", respError).Error("获取账号信息失败") 67 | return nil, errors.New(respError["msg"].(string)) 68 | } else { 69 | var balancesRet []exchange.Balance 70 | balances, _ := result["balances"] 71 | for _, balance := range balances.([]interface{}) { 72 | balance := balance.(map[string]interface{}) 73 | asset, _ := balance["asset"] 74 | free, _ := balance["free"] 75 | free_, _ := decimal.NewFromString(free.(string)) 76 | locked, _ := balance["locked"] 77 | locked_, _ := decimal.NewFromString(locked.(string)) 78 | if free_.Add(locked_).GreaterThan(decimal.NewFromInt(0)) { 79 | balancesRet = append(balancesRet, exchange.Balance{Asset: asset.(string), Free: free_, Locked: locked_}) 80 | } 81 | } 82 | log.WithField("balances", balancesRet).Info("Get account balances success") 83 | return balancesRet, nil 84 | } 85 | } 86 | func (api *Api) AddOrder(params map[string]interface{}) (map[string]interface{}, error) { 87 | url := apiAddr + "/api/v3/order?" 88 | query := api.BuildCommonQuery(params, true) 89 | var respError map[string]interface{} 90 | resp, err := api.RestyClient.R().SetError(&respError).Post(url + query) 91 | if err != nil { 92 | log.WithField("err", err).Error("下单失败") 93 | return nil, err 94 | } else if respError != nil { 95 | log.WithField("respError", respError).Error("下单失败") 96 | return nil, errors.New(respError["msg"].(string)) 97 | } else { 98 | var ret map[string]interface{} 99 | err := json.Unmarshal(resp.Body(), &ret) 100 | if err != nil { 101 | log.WithField("err", err).Error("解析下单返回数据失败") 102 | return ret, err 103 | } 104 | log.WithField("订单详情", ret).Info("下单成功") 105 | return ret, nil 106 | } 107 | } 108 | func (api *Api) KLines(params map[string]interface{}) ([][]interface{}, error) { 109 | var respError map[string]interface{} 110 | query := api.BuildCommonQuery(params, false) 111 | url := apiAddr + "/api/v3/klines?" + query 112 | resp, err := api.RestyClient.R().SetError(&respError).Get(url) 113 | if err != nil { 114 | log.WithField("err", err).Error("获取KLine失败") 115 | return nil, err 116 | } else if respError != nil { 117 | log.WithField("respError", respError).Error("获取KLine失败") 118 | return nil, errors.New(respError["msg"].(string)) 119 | } else { 120 | var result [][]interface{} 121 | err := json.Unmarshal(resp.Body(), &result) 122 | if err != nil { 123 | log.WithField("err", err).Error("解析KLine数据失败") 124 | return result, err 125 | } 126 | return result, nil 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /exchange/binance/spot/exchange.go: -------------------------------------------------------------------------------- 1 | package spot 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "github.com/gorilla/websocket" 7 | "github.com/long2ice/trader/db" 8 | "github.com/long2ice/trader/exchange" 9 | "github.com/long2ice/trader/exchange/binance" 10 | log "github.com/sirupsen/logrus" 11 | "gopkg.in/resty.v1" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | //wsAddr = "wss://stream.goodusahost.cn" 18 | //wsAddr = "wss://stream.shyqxxy.com" 19 | wsAddr = "wss://stream.binance.com:9443" 20 | wsMarketAddr = wsAddr + "/stream" 21 | ) 22 | 23 | type Spot struct { 24 | binance.Exchange 25 | Api Api 26 | } 27 | 28 | func init() { 29 | exchange.RegisterExchange(exchange.BinanceSpot, &Spot{}) 30 | } 31 | func (s *Spot) AddOrder(order db.Order) (map[string]interface{}, error) { 32 | service := binance.CreateOrderService{ 33 | Symbol: order.Symbol, 34 | Side: order.Side, 35 | Type: order.Type, 36 | Api: &s.Api, 37 | } 38 | if !order.Price.IsZero() { 39 | service.Price = &order.Price 40 | } 41 | if !order.Vol.IsZero() { 42 | service.Quantity = &order.Vol 43 | } 44 | if !order.Amount.IsZero() { 45 | service.QuoteOrderQty = &order.Amount 46 | } 47 | return service.Do() 48 | } 49 | func (s *Spot) CancelOrder(symbol string, orderId string) (map[string]interface{}, error) { 50 | service := binance.CancelOrderService{ 51 | Symbol: symbol, 52 | OrderId: &orderId, 53 | Api: &s.Api, 54 | } 55 | return service.Do() 56 | } 57 | func (s *Spot) NewExchange(apiKey string, apiSecret string) exchange.IExchange { 58 | b := &Spot{ 59 | Api: Api{exchange.BaseApi{ 60 | ApiKey: apiKey, 61 | ApiSecret: apiSecret, 62 | RestyClient: resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).SetHeader("X-MBX-APIKEY", apiKey), 63 | }}, 64 | } 65 | var iExchange exchange.IExchange 66 | iExchange = b 67 | return iExchange 68 | } 69 | func (s *Spot) RefreshAccount() { 70 | //初始化账号信息 71 | balances, err := s.Api.AccountInfo() 72 | if err != nil { 73 | log.WithField("err", err).Error("获取账号信息失败") 74 | } else { 75 | s.Balances = balances 76 | } 77 | } 78 | func (s *Spot) NewKLineService() exchange.IKLineService { 79 | var p exchange.IKLineService 80 | p = &exchange.KLineService{ 81 | Api: &s.Api, 82 | } 83 | return p 84 | } 85 | 86 | //订阅账号更新 87 | func (s *Spot) SubscribeAccount(callback func(map[string]interface{})) error { 88 | listenKey, ok := s.Api.CreateSpotListenKey() 89 | if !ok { 90 | return errors.New("createSpotListenKey失败") 91 | } 92 | wsUrl := wsAddr + "/stream?streams=" + listenKey 93 | c, _, err := websocket.DefaultDialer.Dial(wsUrl, nil) 94 | if err != nil { 95 | log.WithField("err", err).WithField("wsUrl", wsUrl).Error("连接websocket失败") 96 | } 97 | go func() { 98 | for { 99 | var message map[string]interface{} 100 | err := c.ReadJSON(&message) 101 | if err != nil { 102 | log.WithField("err", err).Warning("解析账号消息错误,重新连接") 103 | c, _, err = websocket.DefaultDialer.Dial(wsUrl, nil) 104 | if err != nil { 105 | log.WithField("err", err).Warning("重新连接失败,2秒后重试...") 106 | time.Sleep(time.Second * 2) 107 | } else { 108 | log.Info("重新连接成功") 109 | } 110 | continue 111 | } 112 | log.WithField("message", message).Debug() 113 | callback(message) 114 | } 115 | }() 116 | //每30分钟延期SpotListenKey 117 | go func() { 118 | for range time.NewTicker(time.Second * 60 * 30).C { 119 | _, ok := s.Api.CreateSpotListenKey() 120 | if !ok { 121 | log.Error("延期SpotListenKey失败") 122 | } 123 | } 124 | }() 125 | //每10分钟ping 126 | go func() { 127 | for range time.NewTicker(time.Second * 60 * 10).C { 128 | err = c.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)) 129 | if err != nil { 130 | log.WithField("err", err).Error("账户信息ping失败") 131 | } 132 | } 133 | }() 134 | return nil 135 | } 136 | 137 | func (s *Spot) SubscribeMarketData(symbols []string, callback func(map[string]interface{})) error { 138 | addr := wsMarketAddr + "?streams=" + strings.Join(symbols, "/") 139 | wsMarketDataClient, _, err := websocket.DefaultDialer.Dial(addr, nil) 140 | if err != nil { 141 | log.WithField("err", err).WithField("symbols", symbols).Error("订阅行情失败") 142 | return err 143 | } 144 | go func() { 145 | for { 146 | var message map[string]interface{} 147 | err := wsMarketDataClient.ReadJSON(&message) 148 | if err != nil { 149 | log.WithField("err", err).Warning("解析行情消息错误,重新连接") 150 | wsMarketDataClient, _, err = websocket.DefaultDialer.Dial(wsMarketAddr, nil) 151 | if err != nil { 152 | log.WithField("err", err).Warning("重新连接失败,2秒后重试...") 153 | time.Sleep(time.Second * 2) 154 | } else { 155 | wsMarketDataClient, _, err = websocket.DefaultDialer.Dial(addr, nil) 156 | if err != nil { 157 | log.WithField("err", err).WithField("symbols", symbols).Error("重新连接失败") 158 | } else { 159 | log.Info("重新连接成功") 160 | } 161 | } 162 | continue 163 | } 164 | _, ok := message["error"] 165 | if ok { 166 | log.WithField("error", message["error"]).Error() 167 | continue 168 | } 169 | callback(message) 170 | } 171 | }() 172 | 173 | go func() { 174 | for range time.NewTicker(time.Second * 60 * 10).C { 175 | err := wsMarketDataClient.WriteControl(websocket.PongMessage, []byte{}, time.Now().Add(10*time.Second)) 176 | if err != nil { 177 | log.WithField("err", err).Error("行情pong失败") 178 | } 179 | } 180 | }() 181 | return nil 182 | } 183 | -------------------------------------------------------------------------------- /exchange/binance/spot/exchange_test.go: -------------------------------------------------------------------------------- 1 | package spot 2 | 3 | import ( 4 | "github.com/long2ice/trader/conf" 5 | "github.com/long2ice/trader/db" 6 | "github.com/long2ice/trader/exchange" 7 | "github.com/shopspring/decimal" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func TestMain(m *testing.M) { 15 | conf.InitConfig("config.yml") 16 | code := m.Run() 17 | os.Exit(code) 18 | } 19 | func TestBinanceExchange_RefreshAccount(t *testing.T) { 20 | Assert := assert.New(t) 21 | ex, err := exchange.NewExchange(exchange.BinanceSpot, conf.BinanceApiKey, conf.BinanceApiSecret) 22 | if err != nil { 23 | log.WithField("err", err).Error("创建变交易所失败") 24 | } 25 | ex.RefreshAccount() 26 | 27 | btc := ex.GetBalance("BTC") 28 | usdt := ex.GetBalance("USDT") 29 | 30 | log.WithField("btc", btc).WithField("usdt", usdt).Info() 31 | 32 | Assert.NotEqual(btc, decimal.NewFromInt(0)) 33 | Assert.NotEqual(usdt, decimal.NewFromInt(0)) 34 | 35 | } 36 | func TestBinanceExchange_AddOrder(t *testing.T) { 37 | ex, err := exchange.NewExchange(exchange.BinanceSpot, conf.BinanceApiKey, conf.BinanceApiSecret) 38 | if err != nil { 39 | log.WithField("err", err).Error("创建交易所失败") 40 | } 41 | ret, err := ex.AddOrder(db.Order{ 42 | Side: "BUY", 43 | Vol: decimal.NewFromFloat(0.001), 44 | Price: decimal.NewFromInt(20000), 45 | Symbol: "BTCUSDT", 46 | Type: "LIMIT", 47 | }) 48 | log.WithField("account", ex.GetBalance("USDT")).WithField("ret", ret).Info() 49 | } 50 | -------------------------------------------------------------------------------- /exchange/klines.go: -------------------------------------------------------------------------------- 1 | package exchange 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | log "github.com/sirupsen/logrus" 6 | "time" 7 | ) 8 | 9 | //KLine服务 10 | type IKLineService interface { 11 | SetSymbol(symbol string) IKLineService 12 | SetInterval(interval string) IKLineService 13 | SetStartTime(startTime int64) IKLineService 14 | SetEndTime(endTime int64) IKLineService 15 | SetLimit(limit int) IKLineService 16 | Collect() map[string]interface{} 17 | Do() ([]KLine, error) 18 | } 19 | type KLineService struct { 20 | Api IApi 21 | Symbol string 22 | Interval string 23 | StartTime *int64 24 | EndTime *int64 25 | Limit *int 26 | } 27 | 28 | func (s *KLineService) SetSymbol(symbol string) IKLineService { 29 | s.Symbol = symbol 30 | return s 31 | } 32 | func (s *KLineService) SetInterval(interval string) IKLineService { 33 | s.Interval = interval 34 | return s 35 | } 36 | func (s *KLineService) SetStartTime(startTime int64) IKLineService { 37 | s.StartTime = &startTime 38 | return s 39 | } 40 | func (s *KLineService) SetEndTime(endTime int64) IKLineService { 41 | s.EndTime = &endTime 42 | return s 43 | } 44 | func (s *KLineService) SetLimit(limit int) IKLineService { 45 | s.Limit = &limit 46 | return s 47 | } 48 | func (s *KLineService) Collect() map[string]interface{} { 49 | params := make(map[string]interface{}) 50 | params["symbol"] = s.Symbol 51 | params["interval"] = s.Interval 52 | if s.StartTime != nil { 53 | params["startTime"] = *s.StartTime 54 | } 55 | if s.EndTime != nil { 56 | params["endTime"] = *s.EndTime 57 | } 58 | if s.Limit != nil { 59 | params["limit"] = *s.Limit 60 | } 61 | return params 62 | } 63 | func (s *KLineService) Do() ([]KLine, error) { 64 | result := make([][]interface{}, 0) 65 | for *s.Limit > 1000 { 66 | do := 1000 67 | rest := *s.Limit - do 68 | s.Limit = &do 69 | ret, err := s.Api.KLines(s.Collect()) 70 | result = append(result, ret...) 71 | if err != nil { 72 | log.WithField("err", err).Error("Get KLines error") 73 | return nil, err 74 | } 75 | s.Limit = &rest 76 | startTime := int64(ret[len(ret)-1][6].(float64)) 77 | s.StartTime = &startTime 78 | } 79 | if *s.Limit > 0 { 80 | ret, err := s.Api.KLines(s.Collect()) 81 | result = append(result, ret...) 82 | if err != nil { 83 | log.WithField("err", err).Error("Get KLines error") 84 | return nil, err 85 | } 86 | } 87 | var kLines []KLine 88 | for _, item := range result { 89 | open, _ := decimal.NewFromString(item[1].(string)) 90 | high, _ := decimal.NewFromString(item[2].(string)) 91 | low, _ := decimal.NewFromString(item[3].(string)) 92 | close_, _ := decimal.NewFromString(item[4].(string)) 93 | volume, _ := decimal.NewFromString(item[5].(string)) 94 | closeTime, _ := item[6].(float64) 95 | amount, _ := decimal.NewFromString(item[7].(string)) 96 | kLines = append(kLines, KLine{ 97 | Open: open, 98 | Close: close_, 99 | High: high, 100 | Low: low, 101 | Amount: amount, 102 | Volume: volume, 103 | CloseTime: time.Unix(int64(closeTime/1000), 0), 104 | }) 105 | } 106 | return kLines, nil 107 | } 108 | -------------------------------------------------------------------------------- /exchange/mock/exchange.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "github.com/long2ice/trader/db" 5 | "github.com/long2ice/trader/exchange" 6 | "github.com/shopspring/decimal" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type Mock struct { 12 | exchange.BaseExchange 13 | StartTime time.Time 14 | EndTime time.Time 15 | Symbol string 16 | } 17 | 18 | func init() { 19 | exchange.RegisterExchange(exchange.Mock, &Mock{}) 20 | } 21 | func (mock *Mock) ParseTicker(data map[string]interface{}) exchange.Ticker { 22 | e_, _ := data["e"] 23 | E, _ := data["E"] 24 | s, _ := data["s"] 25 | o, _ := data["o"] 26 | h, _ := data["h"] 27 | l, _ := data["l"] 28 | c, _ := data["c"] 29 | v, _ := data["v"] 30 | q, _ := data["q"] 31 | tc, _ := decimal.NewFromString(c.(string)) 32 | tv, _ := decimal.NewFromString(v.(string)) 33 | tq, _ := decimal.NewFromString(q.(string)) 34 | to, _ := decimal.NewFromString(o.(string)) 35 | th, _ := decimal.NewFromString(h.(string)) 36 | tl, _ := decimal.NewFromString(l.(string)) 37 | return exchange.Ticker{ 38 | EventType: e_.(string), 39 | Time: E.(float64), 40 | Symbol: s.(string), 41 | LatestPrice: tc, 42 | First24PriceAft: to, 43 | High24Price: th, 44 | Low24Price: tl, 45 | Vol: tv, 46 | Amount: tq, 47 | } 48 | } 49 | 50 | func (mock *Mock) ParseKLine(data map[string]interface{}) exchange.KLine { 51 | h, _ := data["h"] 52 | h_, _ := h.(decimal.Decimal) 53 | l, _ := data["l"] 54 | l_, _ := l.(decimal.Decimal) 55 | o, _ := data["o"] 56 | o_, _ := o.(decimal.Decimal) 57 | c, _ := data["c"] 58 | c_, _ := c.(decimal.Decimal) 59 | v, _ := data["v"] 60 | v_, _ := v.(decimal.Decimal) 61 | q, _ := data["q"] 62 | q_, _ := q.(decimal.Decimal) 63 | t, _ := data["t"] 64 | return exchange.KLine{ 65 | Open: o_, 66 | Close: c_, 67 | High: h_, 68 | Low: l_, 69 | Amount: q_, 70 | Volume: v_, 71 | Finish: true, 72 | CloseTime: t.(time.Time), 73 | } 74 | } 75 | 76 | func (mock *Mock) SubscribeMarketData(streams []string, callback func(map[string]interface{})) error { 77 | var symbols []string 78 | for _, stream := range streams { 79 | symbols = append(symbols, strings.ToUpper(strings.Split(stream, "@")[0])) 80 | } 81 | var kLines []db.KLine 82 | db.Client.Where("close_time BETWEEN ? AND ?", mock.StartTime, mock.EndTime).Order("close_time").Where("symbol IN ?", symbols).Find(&kLines) 83 | for _, kline := range kLines { 84 | data := map[string]interface{}{ 85 | "h": kline.High, 86 | "l": kline.Low, 87 | "o": kline.Open, 88 | "c": kline.Close, 89 | "v": kline.Vol, 90 | "q": kline.Amount, 91 | "t": kline.CloseTime, 92 | } 93 | callback(map[string]interface{}{ 94 | "stream": strings.ToLower(kline.Symbol) + "@kline_1m", 95 | "data": data, 96 | }) 97 | } 98 | return nil 99 | } 100 | 101 | func (mock *Mock) NewExchange(apiKey string, apiSecret string) exchange.IExchange { 102 | return &Mock{} 103 | } 104 | func (mock *Mock) NewKLineService() exchange.IKLineService { 105 | var p exchange.IKLineService 106 | p = &KLineService{} 107 | p.SetStartTime(mock.StartTime.Unix()) 108 | return p 109 | } 110 | -------------------------------------------------------------------------------- /exchange/mock/klines.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "github.com/long2ice/trader/db" 5 | "github.com/long2ice/trader/exchange" 6 | "time" 7 | ) 8 | 9 | type KLineService struct { 10 | exchange.KLineService 11 | } 12 | 13 | func (service *KLineService) Do() ([]exchange.KLine, error) { 14 | var kLines []db.KLine 15 | startTime := time.Unix(int64(*service.StartTime), 0) 16 | db.Client.Where("symbol = ?", service.Symbol).Where("close_time > ?", startTime).Limit(*service.Limit).Order("close_time").Find(&kLines) 17 | var ret []exchange.KLine 18 | for _, line := range kLines { 19 | ret = append(ret, exchange.KLine{ 20 | Open: line.Open, 21 | Close: line.Close, 22 | High: line.High, 23 | Low: line.Low, 24 | Amount: line.Amount, 25 | Volume: line.Vol, 26 | Finish: true, 27 | CloseTime: line.CloseTime, 28 | }) 29 | } 30 | return ret, nil 31 | } 32 | -------------------------------------------------------------------------------- /exchange/models.go: -------------------------------------------------------------------------------- 1 | package exchange 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | "time" 6 | ) 7 | 8 | type Balance struct { 9 | Asset string 10 | Free decimal.Decimal 11 | Locked decimal.Decimal 12 | } 13 | type KLine struct { 14 | Open decimal.Decimal 15 | Close decimal.Decimal 16 | High decimal.Decimal 17 | Low decimal.Decimal 18 | Amount decimal.Decimal 19 | Volume decimal.Decimal 20 | Finish bool 21 | CloseTime time.Time 22 | } 23 | type Ticker struct { 24 | EventType string `json:"e"` // 事件类型 25 | Time float64 `json:"E"` // 事件时间 26 | Symbol string `json:"s"` // 交易对 27 | Change24Price decimal.Decimal `json:"p"` // 24小时价格变化 28 | Change24Percent decimal.Decimal `json:"P"` // 24小时价格变化(百分比) 29 | AvgPrice decimal.Decimal `json:"w"` // 平均价格 30 | First24PricePre decimal.Decimal `json:"x"` // 整整24小时之前,向前数的最后一次成交价格 31 | LatestPrice decimal.Decimal `json:"c"` // 最新成交价格 32 | LatestVol decimal.Decimal `json:"Q"` // 最新成交交易的成交量 33 | LatestHighPriceBuy decimal.Decimal `json:"b"` // 目前最高买单价 34 | LatestHighPriceBuyVol decimal.Decimal `json:"B"` // 目前最高买单价的挂单量 35 | LatestLowPriceSell decimal.Decimal `json:"a"` // 目前最低卖单价 36 | LatestLowPriceSellVol decimal.Decimal `json:"A"` // 目前最低卖单价的挂单量 37 | First24PriceAft decimal.Decimal `json:"o"` // 整整24小时前,向后数的第一次成交价格 38 | High24Price decimal.Decimal `json:"h"` // 24小时内最高成交价 39 | Low24Price decimal.Decimal `json:"l"` // 24小时内最低成交价 40 | Vol decimal.Decimal `json:"v"` // 24小时内成交量 41 | Amount decimal.Decimal `json:"q"` // 24小时内成交额 42 | StartTime float64 `json:"O"` // 统计开始时间 43 | EndTime float64 `json:"C"` // 统计结束时间 44 | FirstOrderId int `json:"F"` // 24小时内第一笔成交交易ID 45 | EndOrderId int `json:"L"` // 24小时内最后一笔成交交易ID 46 | OrderNum int `json:"n"` // 24小时内成交数 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/long2ice/trader 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/mitchellh/mapstructure v1.4.1 // indirect 9 | github.com/shopspring/decimal v1.2.0 10 | github.com/sirupsen/logrus v1.7.0 11 | github.com/spf13/viper v1.7.1 12 | github.com/stretchr/testify v1.7.0 13 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect 14 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect 15 | golang.org/x/text v0.3.3 // indirect 16 | gopkg.in/resty.v1 v1.12.0 17 | gorm.io/driver/mysql v1.0.3 18 | gorm.io/gorm v1.20.12 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 16 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 17 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 18 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 19 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 20 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 21 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 22 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 24 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 25 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 26 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 27 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 28 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 29 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 30 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 31 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 32 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 33 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 38 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 39 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 40 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 41 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 42 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 43 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 44 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 45 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 46 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 47 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 48 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 49 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 50 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 51 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 52 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 53 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 54 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 55 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 56 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 57 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 58 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 59 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 60 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 61 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 62 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 63 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 64 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 65 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 66 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 67 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 68 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 69 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 70 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 71 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 72 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 73 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 74 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 75 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 76 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 77 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 78 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 79 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 80 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 81 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 82 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 83 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 84 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 85 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 86 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 87 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 88 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 89 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 90 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 91 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 92 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 93 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 94 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 95 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 96 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 97 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 98 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 99 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 100 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 101 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 102 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 103 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 104 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 105 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 106 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 107 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 108 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 109 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 110 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 111 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 112 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 113 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 114 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 115 | github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= 116 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 117 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 118 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 119 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 120 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 121 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 122 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 123 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 124 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 125 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 126 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 127 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 128 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 129 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 130 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 131 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 132 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 133 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 134 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 135 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 136 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 137 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 138 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 139 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 140 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 141 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 142 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 143 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 144 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 145 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 146 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 147 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 148 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 149 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 150 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 151 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 152 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 153 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 154 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 155 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 156 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 157 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 158 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 159 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 160 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 161 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 162 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 163 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 164 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 165 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 166 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 167 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 168 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 169 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 170 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 171 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 172 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 173 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 174 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 175 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 176 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 177 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 178 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 179 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 180 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 181 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 182 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 183 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 184 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 185 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 186 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 187 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 188 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 189 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 190 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 191 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 192 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 193 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 194 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 195 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 196 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 197 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 198 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 199 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 200 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 201 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 202 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 203 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 204 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 205 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 206 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 207 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 208 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 209 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 210 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 211 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 212 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 213 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 214 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 215 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 216 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 217 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 218 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 219 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 220 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 221 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 222 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 223 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 224 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 225 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 226 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 227 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 228 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 229 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 230 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 231 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 232 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 233 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 234 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 235 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 236 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 237 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 238 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 239 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 240 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 241 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 242 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 243 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 244 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 245 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 246 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 247 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 248 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 249 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 250 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 251 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 252 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 253 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 254 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 255 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 256 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 257 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 258 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 259 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 260 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 261 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 262 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 263 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 264 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= 265 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 266 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 267 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 268 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 269 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 270 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 271 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 272 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 273 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 274 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 275 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 276 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 277 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 278 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 279 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 280 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 281 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 285 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 287 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 288 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 291 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 293 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 294 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 295 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 296 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 297 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 298 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 299 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 300 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 301 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 302 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 303 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 304 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 305 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 306 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 307 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 308 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 309 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 310 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 311 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 312 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 313 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 314 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 315 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 316 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 317 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 318 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 319 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 320 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 321 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 322 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 323 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 324 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 325 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 326 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 327 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 328 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 329 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 330 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 331 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 332 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 333 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 334 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 335 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 336 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 337 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 338 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 339 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 340 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 341 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 342 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 343 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 344 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 345 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 346 | gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= 347 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 348 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 349 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 350 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 351 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 352 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 353 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 354 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 355 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 356 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 357 | gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg= 358 | gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= 359 | gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 360 | gorm.io/gorm v1.20.12 h1:ebZ5KrSHzet+sqOCVdH9mTjW91L298nX3v5lVxAzSUY= 361 | gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 362 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 363 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 364 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 365 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 366 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 367 | -------------------------------------------------------------------------------- /images/backtest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/long2ice/trader/089aa6eaeff4971cbc2ed1cc2f0bc8456269cac3/images/backtest.png -------------------------------------------------------------------------------- /images/kline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/long2ice/trader/089aa6eaeff4971cbc2ed1cc2f0bc8456269cac3/images/kline.png -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/long2ice/trader/conf" 7 | "github.com/long2ice/trader/server/routes" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func Start() { 12 | if conf.Debug { 13 | gin.SetMode(gin.DebugMode) 14 | } else { 15 | gin.SetMode(gin.ReleaseMode) 16 | } 17 | r := gin.Default() 18 | r.GET("/orders", routes.GetOrders) 19 | r.GET("/strategy", routes.GetStrategy) 20 | r.GET("/strategies", routes.GetStrategies) 21 | r.GET("/fund", routes.GetFund) 22 | r.POST("/fund", routes.AddFund) 23 | 24 | err := r.Run(fmt.Sprintf("%s:%s", conf.ServerHost, conf.ServerPort)) 25 | if err != nil { 26 | log.WithField("err", err).Error("Start server failed") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/routes/fund.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/long2ice/trader/db" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | func GetFund(c *gin.Context) { 10 | strategy := c.Query("strategy") 11 | var fund db.Fund 12 | db.Client.Where("strategy = ?", strategy).Find(&fund) 13 | c.JSON(200, fund) 14 | } 15 | 16 | func AddFund(c *gin.Context) { 17 | fund := c.PostForm("fund") 18 | strategy := c.PostForm("strategy") 19 | db.Client.Model(&db.Fund{}).Where("strategy = ?", strategy).UpdateColumn("fund", gorm.Expr("fund + ?", fund)) 20 | c.JSON(200, fund) 21 | } 22 | -------------------------------------------------------------------------------- /server/routes/order.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/long2ice/trader/db" 6 | ) 7 | 8 | func GetOrders(c *gin.Context) { 9 | strategy := c.Query("strategy") 10 | symbol := c.Query("symbol") 11 | var orders []db.Order 12 | qs := db.Client 13 | if strategy != "" { 14 | qs = qs.Where("strategy = ?", strategy) 15 | } 16 | if symbol != "" { 17 | qs = qs.Where("symbol = ?", symbol) 18 | } 19 | qs.Find(&orders) 20 | c.JSON(200, orders) 21 | } 22 | -------------------------------------------------------------------------------- /server/routes/strategy.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/long2ice/trader/engine" 6 | "github.com/long2ice/trader/exchange" 7 | "github.com/long2ice/trader/strategy" 8 | "github.com/long2ice/trader/utils" 9 | ) 10 | 11 | func getStrategies(ex string) []strategy.IStrategy { 12 | eng := (*engine.GetEngine(exchange.Type(ex), "", "")).(*engine.Engine) 13 | return eng.Base.Strategies 14 | } 15 | func GetStrategy(c *gin.Context) { 16 | symbol := c.Query("symbol") 17 | strategy_ := c.Query("strategy") 18 | ex := c.Query("exchange") 19 | strategies := getStrategies(ex) 20 | data := make(map[string]interface{}) 21 | for _, s := range strategies { 22 | if s.GetSymbol() == symbol && utils.GetTypeName(s) == strategy_ { 23 | data["AvailableFunds"] = s.GetAvailableFunds() 24 | data["Streams"] = s.GetStreams() 25 | data["BaseAsset"] = s.GetBaseAsset() 26 | data["QuoteAsset"] = s.GetQuoteAsset() 27 | data["FundRatio"] = s.GetFundRatio() 28 | data["Fund"] = s.GetFund() 29 | data["StopLoss"] = s.GetStopLoss() 30 | data["StopProfit"] = s.GetStopProfit() 31 | data["LatestPrice"] = s.GetLatestPrice() 32 | c.JSON(200, data) 33 | break 34 | } 35 | } 36 | } 37 | 38 | func GetStrategies(c *gin.Context) { 39 | ex := c.Query("exchange") 40 | strategies := getStrategies(ex) 41 | var data []map[string]interface{} 42 | for _, s := range strategies { 43 | item := make(map[string]interface{}) 44 | item["AvailableFunds"] = s.GetAvailableFunds() 45 | item["Streams"] = s.GetStreams() 46 | item["BaseAsset"] = s.GetBaseAsset() 47 | item["QuoteAsset"] = s.GetQuoteAsset() 48 | item["FundRatio"] = s.GetFundRatio() 49 | item["Fund"] = s.GetFund() 50 | item["StopLoss"] = s.GetStopLoss() 51 | item["StopProfit"] = s.GetStopProfit() 52 | item["LatestPrice"] = s.GetLatestPrice() 53 | data = append(data, item) 54 | } 55 | c.JSON(200, data) 56 | } 57 | -------------------------------------------------------------------------------- /strategy/strategy_base.go: -------------------------------------------------------------------------------- 1 | package strategy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/long2ice/trader/db" 6 | "github.com/long2ice/trader/exchange" 7 | "github.com/long2ice/trader/utils" 8 | "github.com/shopspring/decimal" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type IStrategy interface { 13 | RegisterStreamCallback(stream string, callback func(map[string]interface{})) 14 | GetStreamCallback(stream string) []func(map[string]interface{}) 15 | //call when market connected 16 | OnConnect() 17 | OnAccount(message map[string]interface{}) 18 | OnOrderUpdate(message map[string]interface{}) 19 | GetStreams() []string 20 | GetSymbol() string 21 | GetBaseAsset() string 22 | GetQuoteAsset() string 23 | GetFundRatio() decimal.Decimal 24 | GetFund() decimal.Decimal 25 | GetStopLoss() decimal.Decimal 26 | GetStopProfit() decimal.Decimal 27 | GetLatestPrice() decimal.Decimal 28 | GetLogger() *log.Entry 29 | GetAvailableFunds() decimal.Decimal 30 | } 31 | 32 | type Base struct { 33 | IStrategy 34 | //资产币 35 | BaseAsset string 36 | //交易币,通常为USDT 37 | QuoteAsset string 38 | //交易所 39 | Exchange exchange.IExchange 40 | //需要订阅的行情 41 | Streams []string 42 | //使用资金比例 43 | FundRatio decimal.Decimal 44 | //资金 45 | Fund db.Fund 46 | //止损 47 | StopLoss decimal.Decimal 48 | //止盈 49 | StopProfit decimal.Decimal 50 | //当前最新价 51 | LatestPrice decimal.Decimal 52 | callback map[string][]func(map[string]interface{}) 53 | } 54 | 55 | func NewStrategy(baseAsset string, quoteAsset string, exchange exchange.IExchange, streams []string, fundRatio decimal.Decimal, stopLoss decimal.Decimal, stopProfit decimal.Decimal) Base { 56 | s := Base{ 57 | BaseAsset: baseAsset, 58 | QuoteAsset: quoteAsset, 59 | Exchange: exchange, 60 | Streams: streams, 61 | FundRatio: fundRatio, 62 | StopLoss: stopLoss, 63 | StopProfit: stopProfit, 64 | callback: make(map[string][]func(map[string]interface{})), 65 | } 66 | return s 67 | } 68 | 69 | // 获取交易对 70 | func (strategy *Base) GetSymbol() string { 71 | return fmt.Sprintf("%s%s", strategy.BaseAsset, strategy.QuoteAsset) 72 | } 73 | 74 | //获取行情streams 75 | func (strategy *Base) GetStreams() []string { 76 | return strategy.Streams 77 | } 78 | 79 | //获取可用资金 80 | func (strategy *Base) GetAvailableFunds() decimal.Decimal { 81 | return strategy.FundRatio.Mul(strategy.Fund.TotalFund) 82 | } 83 | 84 | //监听stream 85 | func (strategy *Base) RegisterStreamCallback(stream string, callback func(map[string]interface{})) { 86 | strategy.callback[stream] = append(strategy.callback[stream], callback) 87 | } 88 | 89 | //获取stream回调 90 | func (strategy *Base) GetStreamCallback(stream string) []func(map[string]interface{}) { 91 | return strategy.callback[stream] 92 | } 93 | 94 | //响应account 95 | func (strategy *Base) OnAccount(message map[string]interface{}) { 96 | go strategy.Exchange.RefreshAccount() 97 | } 98 | func (strategy *Base) GetLogger() *log.Entry { 99 | return log.WithField("strategy", utils.GetTypeName(strategy)) 100 | } 101 | func (strategy *Base) GetBaseAsset() string { 102 | return strategy.BaseAsset 103 | } 104 | func (strategy *Base) GetQuoteAsset() string { 105 | return strategy.QuoteAsset 106 | 107 | } 108 | func (strategy *Base) GetFundRatio() decimal.Decimal { 109 | return strategy.FundRatio 110 | 111 | } 112 | func (strategy *Base) GetFund() decimal.Decimal { 113 | return strategy.Fund.TotalFund 114 | 115 | } 116 | func (strategy *Base) GetStopLoss() decimal.Decimal { 117 | return strategy.StopLoss 118 | } 119 | func (strategy *Base) GetStopProfit() decimal.Decimal { 120 | return strategy.StopProfit 121 | } 122 | func (strategy *Base) GetLatestPrice() decimal.Decimal { 123 | return strategy.LatestPrice 124 | } 125 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | func Contains(s []string, e string) bool { 10 | for _, a := range s { 11 | if a == e { 12 | return true 13 | } 14 | } 15 | return false 16 | } 17 | func GetTypeName(v interface{}) string { 18 | valueOf := reflect.ValueOf(v) 19 | if valueOf.Type().Kind() == reflect.Ptr { 20 | return reflect.Indirect(valueOf).Type().Name() 21 | } else { 22 | return valueOf.Type().Name() 23 | } 24 | } 25 | func FloatToString(num float64) string { 26 | return strconv.FormatFloat(num, 'f', 0, 64) 27 | } 28 | func TsToTime(ts float64) time.Time { 29 | return time.Unix(int64(ts/1000), 0) 30 | } 31 | --------------------------------------------------------------------------------