From bf3c65e4887c4537c7fe890d1fb52bdd1b37db58 Mon Sep 17 00:00:00 2001 From: zhangkun Date: Mon, 2 Dec 2024 14:03:35 +0800 Subject: [PATCH] first add --- .README.md.swp | Bin 0 -> 12288 bytes .gitignore | 1 + README.md | 24 + configs/basicConfig.json | 85 + core/candle.go | 450 ++++ core/config.go | 122 + core/const.go | 12 + core/core.go | 725 ++++++ core/ticker.go | 68 + go.mod | 23 + go.sum | 146 ++ jsonResp/balanceAndPositions.json | 123 + jsonResp/order.json | 56 + jsonResp/order2.json | 55 + jsonResp/position.json | 2248 +++++++++++++++++ main.go | 639 +++++ models/account.go | 44 + models/candle.go | 10 + models/globalCoin.go | 21 + models/instrument.go | 40 + models/ticker.go | 20 + okex/.gitignore | 1 + okex/LICENSE | 21 + okex/config/conf.go | 32 + okex/config/go.mod | 3 + okex/go.mod | 8 + okex/go.sum | 12 + okex/main.go | 237 ++ okex/readme.md | 283 +++ okex/rest/contants.go | 35 + okex/rest/go.mod | 3 + okex/rest/go.sum | 21 + okex/rest/rest.go | 331 +++ okex/rest/rest_test.go | 100 + okex/utils/go.mod | 8 + okex/utils/go.sum | 7 + okex/utils/utils.go | 102 + okex/utils/utils_test.go | 17 + okex/ws/go.mod | 8 + okex/ws/go.sum | 28 + okex/ws/utils.go | 75 + okex/ws/wImpl/BookData.go | 226 ++ okex/ws/wImpl/ErrData.go | 13 + okex/ws/wImpl/JRPCData.go | 50 + okex/ws/wImpl/ReqData.go | 47 + okex/ws/wImpl/contants.go | 241 ++ okex/ws/wImpl/contants_test.go | 28 + okex/ws/wInterface/IParam.go | 9 + okex/ws/wInterface/IReqData.go | 8 + okex/ws/wInterface/IRspData.go | 6 + okex/ws/ws_AddBookedDataHook_test.go | 147 ++ okex/ws/ws_cli.go | 725 ++++++ okex/ws/ws_contants.go | 18 + okex/ws/ws_jrpc.go | 157 ++ okex/ws/ws_jrpc_test.go | 186 ++ okex/ws/ws_middleware.go | 19 + okex/ws/ws_op.go | 532 ++++ okex/ws/ws_priv_channel.go | 40 + okex/ws/ws_priv_channel_Accout_test.go | 99 + okex/ws/ws_priv_channel_test.go | 247 ++ okex/ws/ws_pub_channel.go | 141 ++ okex/ws/ws_pub_channel_test.go | 669 +++++ okex/ws/ws_test.go | 386 +++ private/balance.go | 73 + private/order.go | 107 + submodules/okex/LICENSE | 21 + submodules/okex/config/conf.go | 32 + submodules/okex/config/go.mod | 3 + submodules/okex/go.mod | 8 + submodules/okex/go.sum | 12 + submodules/okex/main.go | 237 ++ submodules/okex/readme.md | 283 +++ submodules/okex/rest/contants.go | 35 + submodules/okex/rest/go.mod | 0 submodules/okex/rest/rest.go | 331 +++ submodules/okex/rest/rest_test.go | 100 + submodules/okex/utils/go.mod | 0 submodules/okex/utils/utils.go | 102 + submodules/okex/utils/utils_test.go | 17 + submodules/okex/ws/go.mod | 0 submodules/okex/ws/utils.go | 75 + submodules/okex/ws/wImpl/BookData.go | 226 ++ submodules/okex/ws/wImpl/ErrData.go | 13 + submodules/okex/ws/wImpl/JRPCData.go | 50 + submodules/okex/ws/wImpl/ReqData.go | 47 + submodules/okex/ws/wImpl/contants.go | 241 ++ submodules/okex/ws/wImpl/contants_test.go | 28 + submodules/okex/ws/wInterface/IParam.go | 9 + submodules/okex/ws/wInterface/IReqData.go | 8 + submodules/okex/ws/wInterface/IRspData.go | 6 + .../okex/ws/ws_AddBookedDataHook_test.go | 147 ++ submodules/okex/ws/ws_cli.go | 725 ++++++ submodules/okex/ws/ws_contants.go | 18 + submodules/okex/ws/ws_jrpc.go | 157 ++ submodules/okex/ws/ws_jrpc_test.go | 186 ++ submodules/okex/ws/ws_middleware.go | 19 + submodules/okex/ws/ws_op.go | 532 ++++ submodules/okex/ws/ws_priv_channel.go | 40 + .../okex/ws/ws_priv_channel_Accout_test.go | 99 + submodules/okex/ws/ws_priv_channel_test.go | 247 ++ submodules/okex/ws/ws_pub_channel.go | 141 ++ submodules/okex/ws/ws_pub_channel_test.go | 669 +++++ submodules/okex/ws/ws_test.go | 386 +++ utils/crpty.go | 14 + utils/maopao.go | 21 + utils/myStack.go | 51 + utils/rand.go | 51 + utils/sqrt.go | 11 + utils/tools.go | 116 + 109 files changed, 15932 insertions(+) create mode 100644 .README.md.swp create mode 100644 .gitignore create mode 100644 README.md create mode 100644 configs/basicConfig.json create mode 100644 core/candle.go create mode 100644 core/config.go create mode 100644 core/const.go create mode 100644 core/core.go create mode 100644 core/ticker.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 jsonResp/balanceAndPositions.json create mode 100644 jsonResp/order.json create mode 100644 jsonResp/order2.json create mode 100644 jsonResp/position.json create mode 100644 main.go create mode 100644 models/account.go create mode 100644 models/candle.go create mode 100644 models/globalCoin.go create mode 100644 models/instrument.go create mode 100644 models/ticker.go create mode 100644 okex/.gitignore create mode 100644 okex/LICENSE create mode 100644 okex/config/conf.go create mode 100644 okex/config/go.mod create mode 100644 okex/go.mod create mode 100644 okex/go.sum create mode 100644 okex/main.go create mode 100644 okex/readme.md create mode 100644 okex/rest/contants.go create mode 100644 okex/rest/go.mod create mode 100644 okex/rest/go.sum create mode 100644 okex/rest/rest.go create mode 100644 okex/rest/rest_test.go create mode 100644 okex/utils/go.mod create mode 100644 okex/utils/go.sum create mode 100644 okex/utils/utils.go create mode 100644 okex/utils/utils_test.go create mode 100644 okex/ws/go.mod create mode 100644 okex/ws/go.sum create mode 100644 okex/ws/utils.go create mode 100644 okex/ws/wImpl/BookData.go create mode 100644 okex/ws/wImpl/ErrData.go create mode 100644 okex/ws/wImpl/JRPCData.go create mode 100644 okex/ws/wImpl/ReqData.go create mode 100644 okex/ws/wImpl/contants.go create mode 100644 okex/ws/wImpl/contants_test.go create mode 100644 okex/ws/wInterface/IParam.go create mode 100644 okex/ws/wInterface/IReqData.go create mode 100644 okex/ws/wInterface/IRspData.go create mode 100644 okex/ws/ws_AddBookedDataHook_test.go create mode 100644 okex/ws/ws_cli.go create mode 100644 okex/ws/ws_contants.go create mode 100644 okex/ws/ws_jrpc.go create mode 100644 okex/ws/ws_jrpc_test.go create mode 100644 okex/ws/ws_middleware.go create mode 100644 okex/ws/ws_op.go create mode 100644 okex/ws/ws_priv_channel.go create mode 100644 okex/ws/ws_priv_channel_Accout_test.go create mode 100644 okex/ws/ws_priv_channel_test.go create mode 100644 okex/ws/ws_pub_channel.go create mode 100644 okex/ws/ws_pub_channel_test.go create mode 100644 okex/ws/ws_test.go create mode 100644 private/balance.go create mode 100644 private/order.go create mode 100644 submodules/okex/LICENSE create mode 100644 submodules/okex/config/conf.go create mode 100644 submodules/okex/config/go.mod create mode 100644 submodules/okex/go.mod create mode 100644 submodules/okex/go.sum create mode 100644 submodules/okex/main.go create mode 100644 submodules/okex/readme.md create mode 100644 submodules/okex/rest/contants.go create mode 100644 submodules/okex/rest/go.mod create mode 100644 submodules/okex/rest/rest.go create mode 100644 submodules/okex/rest/rest_test.go create mode 100644 submodules/okex/utils/go.mod create mode 100644 submodules/okex/utils/utils.go create mode 100644 submodules/okex/utils/utils_test.go create mode 100644 submodules/okex/ws/go.mod create mode 100644 submodules/okex/ws/utils.go create mode 100644 submodules/okex/ws/wImpl/BookData.go create mode 100644 submodules/okex/ws/wImpl/ErrData.go create mode 100644 submodules/okex/ws/wImpl/JRPCData.go create mode 100644 submodules/okex/ws/wImpl/ReqData.go create mode 100644 submodules/okex/ws/wImpl/contants.go create mode 100644 submodules/okex/ws/wImpl/contants_test.go create mode 100644 submodules/okex/ws/wInterface/IParam.go create mode 100644 submodules/okex/ws/wInterface/IReqData.go create mode 100644 submodules/okex/ws/wInterface/IRspData.go create mode 100644 submodules/okex/ws/ws_AddBookedDataHook_test.go create mode 100644 submodules/okex/ws/ws_cli.go create mode 100644 submodules/okex/ws/ws_contants.go create mode 100644 submodules/okex/ws/ws_jrpc.go create mode 100644 submodules/okex/ws/ws_jrpc_test.go create mode 100644 submodules/okex/ws/ws_middleware.go create mode 100644 submodules/okex/ws/ws_op.go create mode 100644 submodules/okex/ws/ws_priv_channel.go create mode 100644 submodules/okex/ws/ws_priv_channel_Accout_test.go create mode 100644 submodules/okex/ws/ws_priv_channel_test.go create mode 100644 submodules/okex/ws/ws_pub_channel.go create mode 100644 submodules/okex/ws/ws_pub_channel_test.go create mode 100644 submodules/okex/ws/ws_test.go create mode 100644 utils/crpty.go create mode 100644 utils/maopao.go create mode 100644 utils/myStack.go create mode 100644 utils/rand.go create mode 100644 utils/sqrt.go create mode 100644 utils/tools.go diff --git a/.README.md.swp b/.README.md.swp new file mode 100644 index 0000000000000000000000000000000000000000..db352f98444058d3e477da02937eef830db6169e GIT binary patch literal 12288 zcmeI&&1=*^7zf~~HxE_OiwA|#y}FrNDMRJB{ceC!PH$92Iv#tnxsPtHT13xB{d1sP5w~)yVWhXN9pfyb3 z*ha|vlNoR6nomxIn~BcpKxs3u_7Mxe#G|!q%;@>w3o_SMHuEG4^Hm;MjxU@7Q@o;M zHaR|aG&61$-G48J6)1oL>l7Fy2Zr|#nMtX=d+3hsQ|p|M01BW03ZMWApuh$cu;Ewxk3mAJ zTj1g0|No!w|DRh4`40I8`3iXrc?PLLZbGt<_bmE|)D?s926HGFLirx-`Ph zC9elH(qvT8m$LUa&|17BQls4 zo^>{sL9_ni%f(BCc#=jspha5*mM6P+J0{EY%xO9~b*ejDlrAmtz?G3HHa#WWD)EG( zvCctX`<&_!cD!?%PEX6oF>S;h9mS$#&P4#V=2B5A-Z#;VbuWe5_le_D%VK>cw(HWI mEnGdyA}JM%cqn7Dxn34{B|9<}3Tn>G= (len(ary)) { + done <- true + break + } + rand.Seed(time.Now().UnixNano()) + b := rand.Intn(2) + maxCandles = maxCandles * (i + b) * 2 + + if maxCandles < 3 { + maxCandles = 3 + } + if maxCandles > 30 { + maxCandles = 30 + } + mx := strconv.Itoa(maxCandles) + // fmt.Println("loop4 getCandlesWithRest, instId, period,limit,dura, t: ", instId, ary[i], mx, mdura) + go func(ii int) { + restQ := RestQueue{ + InstId: instId, + Bar: ary[ii], + Limit: mx, + Duration: mdura, + WithWs: true, + } + js, _ := json.Marshal(restQ) + core.RedisCli.LPush("restQueue", js) + }(i) + i++ + } + } + }(idx) + time.Sleep(dura - 10*time.Millisecond) + ticker.Stop() + // fmt.Println("loop4 Ticker stopped instId, dura: ", instId, dura, mdura) + done <- true + return nil +} + +// 当前的时间毫秒数 对于某个时间段,比如3分钟,10分钟,是否可以被整除, +func IsModOf(curInt int64, duration time.Duration) bool { + vol := int64(0) + if duration < 24*time.Hour { + // 小于1天 + vol = (curInt + 28800000) + } else if duration >= 24*time.Hour && duration < 48*time.Hour { + // 1天 + vol = curInt - 1633881600000 + } else if duration >= 48*time.Hour && duration < 72*time.Hour { + // 2天 + vol = curInt - 1633795200000 + } else if duration >= 72*time.Hour && duration < 120*time.Hour { + // 3天 + vol = curInt - 1633708800000 + } else if duration >= 120*time.Hour { + // 5天 + vol = curInt - 1633795200000 + } else { + // fmt.Println("noMatched:", curInt) + } + + mody := vol % duration.Milliseconds() + if mody == 0 { + return true + } + return false +} + +func (core *Core) SaveCandle(instId string, period string, rsp *rest.RESTAPIResult, dura time.Duration, withWs bool) { + js, err := simple.NewJson([]byte(rsp.Body)) + if err != nil { + fmt.Println("restTicker err: ", err, rsp.Body) + return + } + if len(rsp.Body) == 0 { + fmt.Println("rsp body is null") + return + } + itemList := js.Get("data").MustArray() + Daoxu(itemList) + + leng := len(itemList) + for _, v := range itemList { + candle := Candle{ + InstId: instId, + Period: period, + Data: v.([]interface{}), + From: "rest", + } + //保存rest得到的candle + saveCandle := os.Getenv("TUNAS_SAVECANDLE") + if saveCandle == "true" { + candle.SetToKey(core) + } + // 发布到allCandles|publish, 给外部订阅者用于setToKey,和给其他协程用于订阅ws + arys := []string{ALLCANDLES_PUBLISH} + if withWs { + arys = append(arys, ALLCANDLES_INNER_PUBLISH) + } + core.AddToGeneralCandleChnl(&candle, arys) + time.Sleep(dura / time.Duration(leng)) + } +} + +func Daoxu(arr []interface{}) { + var temp interface{} + length := len(arr) + for i := 0; i < length/2; i++ { + temp = arr[i] + arr[i] = arr[length-1-i] + arr[length-1-i] = temp + } +} + +func (cl *Candle) SetToKey(core *Core) ([]interface{}, error) { + data := cl.Data + tsi, err := strconv.ParseInt(data[0].(string), 10, 64) + tss := strconv.FormatInt(tsi, 10) + keyName := "candle" + cl.Period + "|" + cl.InstId + "|ts:" + tss + //过期时间:根号(当前candle的周期/1分钟)*10000 + + dt, err := json.Marshal(cl.Data) + exp := core.PeriodToMinutes(cl.Period) + // expf := float64(exp) * 60 + expf := utils.Sqrt(float64(exp)) * 100 + extt := time.Duration(expf) * time.Minute + curVolstr, _ := data[5].(string) + curVol, err := strconv.ParseFloat(curVolstr, 64) + if err != nil { + fmt.Println("err of convert ts:", err) + } + curVolCcystr, _ := data[6].(string) + curVolCcy, err := strconv.ParseFloat(curVolCcystr, 64) + curPrice := curVolCcy / curVol + if curPrice <= 0 { + fmt.Println("price有问题", curPrice, "dt: ", string(dt), "from:", cl.From) + err = errors.New("price有问题") + return cl.Data, err + } + redisCli := core.RedisCli + // tm := time.UnixMilli(tsi).Format("2006-01-02 15:04") + // fmt.Println("setToKey:", keyName, "ts: ", tm, "price: ", curPrice, "from:", cl.From) + redisCli.Set(keyName, dt, extt).Result() + core.SaveUniKey(cl.Period, keyName, extt, tsi) + return cl.Data, err +} + +func (mx *MaX) SetToKey() ([]interface{}, error) { + cstr := strconv.Itoa(mx.Count) + tss := strconv.FormatInt(mx.Ts, 10) + keyName := "ma" + cstr + "|candle" + mx.Period + "|" + mx.InstId + "|ts:" + tss + //过期时间:根号(当前candle的周期/1分钟)*10000 + dt := []interface{}{} + dt = append(dt, mx.Ts) + dt = append(dt, mx.Value) + dj, _ := json.Marshal(dt) + exp := mx.Core.PeriodToMinutes(mx.Period) + expf := utils.Sqrt(float64(exp)) * 100 + extt := time.Duration(expf) * time.Minute + // loc, _ := time.LoadLocation("Asia/Shanghai") + // tm := time.UnixMilli(mx.Ts).In(loc).Format("2006-01-02 15:04") + // fmt.Println("setToKey:", keyName, "ts:", tm, string(dj), "from: ", mx.From) + _, err := mx.Core.RedisCli.GetSet(keyName, dj).Result() + mx.Core.RedisCli.Expire(keyName, extt) + return dt, err +} + +func (core *Core) SaveUniKey(period string, keyName string, extt time.Duration, tsi int64) { + refName := keyName + "|refer" + refRes, _ := core.RedisCli.GetSet(refName, 1).Result() + core.RedisCli.Expire(refName, extt) + if len(refRes) == 0 { + core.SaveToSortSet(period, keyName, extt, tsi) + } +} + +// tsi: 上报时间timeStamp millinSecond +func (core *Core) SaveToSortSet(period string, keyName string, extt time.Duration, tsi int64) { + ary := strings.Split(keyName, "ts:") + setName := ary[0] + "sortedSet" + z := redis.Z{ + Score: float64(tsi), + Member: keyName, + } + rs, err := core.RedisCli.ZAdd(setName, z).Result() + if err != nil { + fmt.Println("err of ma7|ma30 add to redis:", err) + } else { + fmt.Println("sortedSet add to redis:", rs, keyName) + } +} + +func (cr *Core) PeriodToMinutes(period string) int64 { + ary := strings.Split(period, "") + beiStr := "1" + danwei := "" + if len(ary) == 3 { + beiStr = ary[0] + ary[1] + danwei = ary[2] + } else { + beiStr = ary[0] + danwei = ary[1] + } + cheng := 1 + bei, _ := strconv.Atoi(beiStr) + switch danwei { + case "m": + { + cheng = bei + break + } + case "H": + { + cheng = bei * 60 + break + } + case "D": + { + cheng = bei * 60 * 24 + break + } + case "W": + { + cheng = bei * 60 * 24 * 7 + break + } + case "M": + { + cheng = bei * 60 * 24 * 30 + break + } + case "Y": + { + cheng = bei * 60 * 24 * 365 + break + } + default: + { + fmt.Println("notmatch:", danwei) + } + } + return int64(cheng) +} + +// type ScanCmd struct { +// baseCmd +// +// page []string +// cursor uint64 +// +// process func(cmd Cmder) error +// } +func (core *Core) GetRangeKeyList(pattern string, from time.Time) ([]*simple.Json, error) { + // 比如,用来计算ma30或ma7,倒推多少时间范围, + redisCli := core.RedisCli + cursor := uint64(0) + n := 0 + allTs := []int64{} + var keys []string + for { + var err error + keys, cursor, _ = redisCli.Scan(cursor, pattern+"*", 2000).Result() + if err != nil { + panic(err) + } + n += len(keys) + if n == 0 { + break + } + } + + // keys, _ := redisCli.Keys(pattern + "*").Result() + for _, key := range keys { + keyAry := strings.Split(key, ":") + key = keyAry[1] + keyi64, _ := strconv.ParseInt(key, 10, 64) + allTs = append(allTs, keyi64) + } + nary := utils.RecursiveBubble(allTs, len(allTs)) + tt := from.UnixMilli() + ff := tt - tt%60000 + fi := int64(ff) + mary := []int64{} + for _, v := range nary { + if v < fi { + break + } + mary = append(mary, v) + } + res := []*simple.Json{} + for _, v := range mary { + // if k > 1 { + // break + // } + nv := pattern + strconv.FormatInt(v, 10) + str, err := redisCli.Get(nv).Result() + if err != nil { + fmt.Println("err of redis get key:", nv, err) + } + cur, err := simple.NewJson([]byte(str)) + if err != nil { + fmt.Println("err of create newJson:", str, err) + } + res = append(res, cur) + } + + return res, nil +} + +func (core *Core) InitStreamGroups() { + redisCli := core.RedisCli + streamNames := []string{ + "candles|stream", + "maXs|stream", + } + grpNames := []string{ + "sardine1", + "sardine2", + "sardine3", + } + for _, v := range streamNames { + for _, vv := range grpNames { + redisCli.XGroupCreate(v, vv, "0-0").Result() + } + } +} +func (cr *Core) AddToGeneralCandleChnl(candle *Candle, channels []string) { + //只让特定概率的事件发生 + // r := rand.New(rand.NewSource(time.Now().UnixNano())) + // val := r.Float64() + // rand.Seed(time.Now().UnixNano()) + // if from == "ws" && val > 0.90 { + // fmt.Println("drop ", from) + // return + // } + // fmt.Println("not drop ", from) + // TODO map + redisCli := cr.RedisCli + + ab, _ := json.Marshal(candle) + //特定的币只发送给特定的下游接收者 + // 这么设计不太好,业务逻辑和程序架构紧耦合了, + prename := cr.DispatchDownstreamNodes(candle.InstId) + + for _, v := range channels { + suffix := "" + env := os.Getenv("GO_ENV") + if env == "demoEnv" { + suffix = "-demoEnv" + } + vd := v + suffix + + // TODO FIXME cli2 + _, err := redisCli.Publish(vd, string(ab)).Result() + if len(prename) == 0 { + continue + } + nodeAdd := prename + "|" + v + suffix + // fmt.Println("nodeAdd: ", nodeAdd) + + // TODO FIXME cli2 + _, err = redisCli.Publish(nodeAdd, string(ab)).Result() + // fmt.Println("publish, channel,res,err:", nodeAdd, res, err, "candle:", string(ab)) + + if err != nil { + fmt.Println("err of ma7|ma30 add to redis2:", err, candle.From) + } + } + // TODO 下面这个先屏蔽,不订阅ws信息,否则系统压力会太大,等有更灵活的机制再说 + // redisCli.Publish("allCandlesInner|publish"+suffix, string(ab)).Result() +} diff --git a/core/config.go b/core/config.go new file mode 100644 index 0000000..62a7d4b --- /dev/null +++ b/core/config.go @@ -0,0 +1,122 @@ +package core + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + simple "github.com/bitly/go-simplejson" +) + +type MyConfig struct { + Env string `json:"env", string` + Config *simple.Json + CandleDimentions []string `json:"candleDimentions"` + RedisConf *RedisConfig `json:"redis"` + CredentialReadOnlyConf *CredentialConfig `json:"credential"` + CredentialMutableConf *CredentialConfig `json:"credential"` + ConnectConf *ConnectConfig `json:"connect"` + // ThreadsConf *ThreadsConfig `json:"threads"` +} + +type RedisConfig struct { + Url string `json:"url", string` + Password string `json:"password", string` + Index int `json:"index", string` +} + +type CredentialConfig struct { + SecretKey string `json:"secretKey", string` + BaseUrl string `json:"baseUrl", string` + OkAccessKey string `json:"okAccessKey", string` + OkAccessPassphrase string `json:"okAccessPassphrase", string` +} + +type ConnectConfig struct { + LoginSubUrl string `json:"loginSubUrl", string` + WsPrivateBaseUrl string `json:"wsPrivateBaseUrl", string` + WsPublicBaseUrl string `json:"wsPublicBaseUrl", string` + RestBaseUrl string `json:"restBaseUrl", string` +} +type ThreadsConfig struct { + MaxLenTickerStream int `json:"maxLenTickerStream", int` + MaxCandles int `json:"maxCandles", string` + AsyncChannels int `json:"asyncChannels", int` + MaxTickers int `json:"maxTickers", int` + RestPeriod int `json:"restPeriod", int` + WaitWs int `json:"waitWs", int` +} + +func (cfg MyConfig) Init() (MyConfig, error) { + env := os.Getenv("GO_ENV") + arystr := os.Getenv("TUNAS_CANDLESDIMENTIONS") + ary := strings.Split(arystr, "|") + cfg.CandleDimentions = ary + jsonStr, err := ioutil.ReadFile("/go/json/basicConfig.json") + if err != nil { + jsonStr, err = ioutil.ReadFile("configs/basicConfig.json") + if err != nil { + fmt.Println("err2:", err.Error()) + return cfg, err + } + cfg.Config, err = simple.NewJson([]byte(jsonStr)) + if err != nil { + fmt.Println("err2:", err.Error()) + return cfg, err + } + cfg.Env = env + } + cfg.Config = cfg.Config.Get(env) + + ru, err := cfg.Config.Get("redis").Get("url").String() + rp, _ := cfg.Config.Get("redis").Get("password").String() + ri, _ := cfg.Config.Get("redis").Get("index").Int() + redisConf := RedisConfig{ + Url: ru, + Password: rp, + Index: ri, + } + // fmt.Println("cfg: ", cfg) + cfg.RedisConf = &redisConf + ls, _ := cfg.Config.Get("connect").Get("loginSubUrl").String() + wsPub, _ := cfg.Config.Get("connect").Get("wsPrivateBaseUrl").String() + wsPri, _ := cfg.Config.Get("connect").Get("wsPublicBaseUrl").String() + restBu, _ := cfg.Config.Get("connect").Get("restBaseUrl").String() + connectConfig := ConnectConfig{ + LoginSubUrl: ls, + WsPublicBaseUrl: wsPub, + WsPrivateBaseUrl: wsPri, + RestBaseUrl: restBu, + } + cfg.ConnectConf = &connectConfig + return cfg, nil +} + +func (cfg *MyConfig) GetConfigJson(arr []string) *simple.Json { + env := os.Getenv("GO_ENV") + fmt.Println("env: ", env) + cfg.Env = env + + json, err := ioutil.ReadFile("/go/json/basicConfig.json") + + if err != nil { + json, err = ioutil.ReadFile("configs/basicConfig.json") + if err != nil { + log.Panic("read config error: ", err.Error()) + } + } + if err != nil { + fmt.Println("read file err: ", err) + } + rjson, err := simple.NewJson(json) + if err != nil { + fmt.Println("newJson err: ", err) + } + for _, s := range arr { + rjson = rjson.Get(s) + // fmt.Println(s, ": ", rjson) + } + return rjson +} diff --git a/core/const.go b/core/const.go new file mode 100644 index 0000000..5f6f475 --- /dev/null +++ b/core/const.go @@ -0,0 +1,12 @@ +package core + +const MAIN_ALLCOINS_PERIOD_MINUTES = 1 +const MAIN_ALLCOINS_ONCE_COUNTS = 3 +const MAIN_ALLCOINS_BAR_PERIOD = "3m" +const ALLCANDLES_PUBLISH = "allCandles|publish" +const ALLCANDLES_INNER_PUBLISH = "allCandlesiInner|publish" +const ORDER_PUBLISH = "private|order|publish" +const TICKERINFO_PUBLISH = "tickerInfo|publish" +const CCYPOSISTIONS_PUBLISH = "ccyPositions|publish" +const SUBACTIONS_PUBLISH = "subActions|publish" +const ORDER_RESP_PUBLISH = "private|actionResp|publish" diff --git a/core/core.go b/core/core.go new file mode 100644 index 0000000..6424d93 --- /dev/null +++ b/core/core.go @@ -0,0 +1,725 @@ +package core + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/rand" + "os" + "strconv" + "strings" + "sync" + "time" + "v5sdk_go/rest" + "v5sdk_go/ws" + "v5sdk_go/ws/wImpl" + + simple "github.com/bitly/go-simplejson" + "github.com/go-redis/redis" + "phyer.click/tunas/private" + "phyer.click/tunas/utils" +) + +type Core struct { + Env string + Cfg *MyConfig + RedisCli *redis.Client + RedisCli2 *redis.Client + Wg sync.WaitGroup + RestQueueChan chan *RestQueue + OrderChan chan *private.Order +} +type RestQueue struct { + InstId string + Bar string + After int64 + Before int64 + Limit string + Duration time.Duration + WithWs bool +} + +type SubAction struct { + ActionName string + ForAll bool + MetaInfo map[string]interface{} +} + +func (rst *RestQueue) Show(cr *Core) { + fmt.Println("restQueue:", rst.InstId, rst.Bar, rst.Limit) +} + +func (rst *RestQueue) Save(cr *Core) { + afterSec := "" + if rst.After > 0 { + afterSec = fmt.Sprint("&after=", rst.After) + } + beforeSec := "" + if rst.Before > 0 { + beforeSec = fmt.Sprint("&before=", rst.Before) + } + limitSec := "" + if len(rst.Limit) > 0 { + limitSec = fmt.Sprint("&limit=", rst.Limit) + } + link := "/api/v5/market/candles?instId=" + rst.InstId + "&bar=" + rst.Bar + limitSec + afterSec + beforeSec + + fmt.Println("restLink: ", link) + rsp, _ := cr.RestInvoke(link, rest.GET) + cr.SaveCandle(rst.InstId, rst.Bar, rsp, rst.Duration, rst.WithWs) +} + +func (cr *Core) ShowSysTime() { + rsp, _ := cr.RestInvoke("/api/v5/public/time", rest.GET) + fmt.Println("serverSystem time:", rsp) +} + +func (core *Core) Init() { + core.Env = os.Getenv("GO_ENV") + gitBranch := os.Getenv("gitBranchName") + commitID := os.Getenv("gitCommitID") + + fmt.Println("当前环境: ", core.Env) + fmt.Println("gitBranch: ", gitBranch) + fmt.Println("gitCommitID: ", commitID) + cfg := MyConfig{} + cfg, _ = cfg.Init() + core.Cfg = &cfg + cli, err := core.GetRedisCli() + core.RedisCli = cli + core.RestQueueChan = make(chan *RestQueue) + core.OrderChan = make(chan *private.Order) + if err != nil { + fmt.Println("init redis client err: ", err) + } +} + +func (core *Core) GetWsCli() (*ws.WsClient, error) { + url, err := core.Cfg.Config.Get("connect").Get("wsPublicBaseUrl").String() + if err != nil { + fmt.Println("err of json decode: ", err) + } + pubCli, err := ws.NewWsClient("wss://" + url) + pubCli.AddBookMsgHook(core.PubMsgDispatcher) + + if err != nil { + fmt.Println("err of create ublic ws cli:", err) + } + return pubCli, err +} + +func (core *Core) DispatchDownstreamNodes(originName string) string { + nodesStr := os.Getenv("TUNAS_DOWNSTREAM_NODES") + if len(nodesStr) == 0 { + return "" + } + nodes := strings.Split(nodesStr, "|") + count := len(nodes) + idx := utils.HashDispatch(originName, uint8(count)) + return nodes[idx] +} + +func (core *Core) Dispatch(channel string, ctype string, instId string, data interface{}) error { + // fmt.Println("start to SaveToRedis:", channel, ctype, instId, data) + b, err := json.Marshal(data) + js, err := simple.NewJson(b) + if err != nil { + fmt.Println("err of unMarshalJson1:", js) + } + + isUsdt := strings.Contains(instId, "-USDT") + instType, err := js.Get("instType").String() + if !isUsdt { + return err + } + + // fmt.Println("instId: ", instId) + redisCli := core.RedisCli + channelType := "" + if channel == "instruments" { + channelType = "instruments" + } else if strings.Contains(channel, "candle") { + channelType = "candle" + } + switch channelType { + case "instruments": + { + // fmt.Println("isInstrument:", instId) + if instType != "SPOT" { + return errors.New("instType is not SPOT") + } + _, err = redisCli.HSet("instruments|"+ctype+"|hash", instId, b).Result() + if err != nil { + fmt.Println("err of hset to redis:", err) + } + break + } + case "candle": + { + data := data.([]interface{}) + ary := strings.Split(channel, "candle") + // fmt.Println("dispatch candle:", ary[1], instId) + candle := Candle{ + InstId: instId, + Period: ary[1], + Data: data, + From: "ws", + } + core.WsSubscribe(&candle) + saveCandle := os.Getenv("TUNAS_SAVECANDLE") + if saveCandle == "true" { + candle.SetToKey(core) + } + // TODO mxLen要放到core.Cfg里 + arys := []string{ALLCANDLES_PUBLISH} + core.AddToGeneralCandleChnl(&candle, arys) + break + } + + default: + { + // data := data.([]interface{}) + // bj, _ := json.Marshal(data) + // fmt.Println("private data:", string(bj)) + return errors.New("channel type not catched") + } + } + return nil +} + +func (core *Core) PubMsgDispatcher(ts time.Time, data wImpl.MsgData) error { + instList := data.Data + for _, v := range instList { + core.Dispatch(data.Arg["channel"], data.Arg["instType"], data.Arg["instId"], v) + } + return nil +} + +func (core *Core) GetRedisCli() (*redis.Client, error) { + ru := core.Cfg.RedisConf.Url + rp := core.Cfg.RedisConf.Password + ri := core.Cfg.RedisConf.Index + re := os.Getenv("REDIS_URL") + if len(re) > 0 { + ru = re + } + client := redis.NewClient(&redis.Options{ + Addr: ru, + Password: rp, //默认空密码 + DB: ri, //使用默认数据库 + }) + pong, err := client.Ping().Result() + if pong == "PONG" && err == nil { + return client, err + } else { + fmt.Println("redis状态不可用:", ru, rp, ri, err) + } + + return client, nil +} + +func (core *Core) GetAllTickerInfo() (*rest.RESTAPIResult, error) { + // GET / 获取所有产品行情信息 + rsp, err := core.RestInvoke("/api/v5/market/tickers?instType=SPOT", rest.GET) + return rsp, err +} + +func (core *Core) GetBalances() (*rest.RESTAPIResult, error) { + // TODO 临时用了两个实现,restInvoke,复用原来的会有bug,不知道是谁的bug + rsp, err := core.RestInvoke2("/api/v5/account/balance", rest.GET, nil) + return rsp, err +} +func (core *Core) GetLivingOrderList() ([]*private.Order, error) { + // TODO 临时用了两个实现,restInvoke,复用原来的会有bug,不知道是谁的bug + params := make(map[string]interface{}) + data, err := core.RestInvoke2("/api/v5/trade/orders-pending", rest.GET, ¶ms) + odrsp := private.OrderResp{} + err = json.Unmarshal([]byte(data.Body), &odrsp) + str, _ := json.Marshal(odrsp) + fmt.Println("convert: err:", err, " body: ", data.Body, odrsp, " string:", string(str)) + list, err := odrsp.Convert() + fmt.Println("loopLivingOrder response data:", str) + fmt.Println(utils.GetFuncName(), " 当前数据是 ", data.V5Response.Code, " list len:", len(list)) + return list, err +} +func (core *Core) LoopInstrumentList() error { + for { + time.Sleep(3 * time.Second) + ctype := ws.SPOT + + redisCli := core.RedisCli + counts, err := redisCli.HLen("instruments|" + ctype + "|hash").Result() + if err != nil { + fmt.Println("err of hset to redis:", err) + } + if counts == 0 { + continue + } + break + } + return nil +} +func (core *Core) SubscribeTicker(op string) error { + mp := make(map[string]string) + + redisCli := core.RedisCli + ctype := ws.SPOT + mp, err := redisCli.HGetAll("instruments|" + ctype + "|hash").Result() + b, err := json.Marshal(mp) + js, err := simple.NewJson(b) + if err != nil { + fmt.Println("err of unMarshalJson3:", js) + } + // fmt.Println("ticker js: ", js) + instAry := js.MustMap() + for k, v := range instAry { + b = []byte(v.(string)) + _, err := simple.NewJson(b) + if err != nil { + fmt.Println("err of unMarshalJson4:", js) + } + time.Sleep(5 * time.Second) + go func(instId string, op string) { + + redisCli := core.RedisCli + _, err = redisCli.SAdd("tickers|"+op+"|set", instId).Result() + if err != nil { + fmt.Println("err of unMarshalJson5:", js) + } + }(k, op) + } + return nil +} + +func (core *Core) InnerSubscribeTicker(name string, op string, retry bool) error { + // 在这里 args1 初始化tickerList的列表 + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = name + arg["instType"] = ws.SPOT + args = append(args, arg) + + if retry { + go func(op string, args []map[string]string) { + core.retrySubscribe(op, args) + }(op, args) + } else { + go func(op string, args []map[string]string) { + core.OnceSubscribe(op, args) + }(op, args) + } + return nil +} + +func (core *Core) OnceSubscribe(op string, args []map[string]string) error { + wsCli, _ := core.GetWsCli() + res, _, err := wsCli.PubTickers(op, args) + // defer wsCli.Stop() + start := time.Now() + if err != nil { + fmt.Println("pubTickers err:", err) + } + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + return err +} + +func (core *Core) retrySubscribe(op string, args []map[string]string) error { + wsCli, _ := core.GetWsCli() + res, _, err := wsCli.PubTickers(op, args) + start := time.Now() + if err != nil { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + val := r.Int() / 20000000000000000 + fmt.Println("pubTickers err:", err, val, "秒后重试") + time.Sleep(time.Duration(val) * time.Second) + + return core.retrySubscribe(op, args) + } + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + return err +} + +func (core *Core) TickerScore(score float64, name string) error { + + redisCli := core.RedisCli + mb := redis.Z{ + Score: score, + Member: name, + } + _, err := redisCli.ZAdd("tradingRank", mb).Result() + if err != nil { + fmt.Println("zadd err:", err) + } + return err +} + +func (core *Core) RestInvoke(subUrl string, method string) (*rest.RESTAPIResult, error) { + restUrl, _ := core.Cfg.Config.Get("connect").Get("restBaseUrl").String() + //ep, method, uri string, param *map[string]interface{} + rest := rest.NewRESTAPI(restUrl, method, subUrl, nil) + key, _ := core.Cfg.Config.Get("credentialReadOnly").Get("okAccessKey").String() + secure, _ := core.Cfg.Config.Get("credentialReadOnly").Get("secretKey").String() + pass, _ := core.Cfg.Config.Get("credentialReadOnly").Get("okAccessPassphrase").String() + isDemo := false + if core.Env == "demoEnv" { + isDemo = true + } + rest.SetSimulate(isDemo).SetAPIKey(key, secure, pass) + response, err := rest.Run(context.Background()) + if err != nil { + fmt.Println("restInvoke1 err:", subUrl, err) + } + return response, err +} + +func (core *Core) RestInvoke2(subUrl string, method string, param *map[string]interface{}) (*rest.RESTAPIResult, error) { + key, err1 := core.Cfg.Config.Get("credentialReadOnly").Get("okAccessKey").String() + secret, err2 := core.Cfg.Config.Get("credentialReadOnly").Get("secretKey").String() + pass, err3 := core.Cfg.Config.Get("credentialReadOnly").Get("okAccessPassphrase").String() + userId, err4 := core.Cfg.Config.Get("connect").Get("userId").String() + restUrl, err5 := core.Cfg.Config.Get("connect").Get("restBaseUrl").String() + if err1 != nil || err2 != nil || err3 != nil || err4 != nil || err5 != nil { + fmt.Println(err1, err2, err3, err4, err5) + } else { + fmt.Println("key:", key, secret, pass, "userId:", userId, "restUrl: ", restUrl) + } + // reqParam := make(map[string]interface{}) + // if param != nil { + // reqParam = *param + // } + // rs := rest.NewRESTAPI(restUrl, method, subUrl, &reqParam) + isDemo := false + if core.Env == "demoEnv" { + isDemo = true + } + // rs.SetSimulate(isDemo).SetAPIKey(key, secret, pass).SetUserId(userId) + // response, err := rs.Run(context.Background()) + // if err != nil { + // fmt.Println("restInvoke2 err:", subUrl, err) + // } + apikey := rest.APIKeyInfo{ + ApiKey: key, + SecKey: secret, + PassPhrase: pass, + } + cli := rest.NewRESTClient(restUrl, &apikey, isDemo) + rsp, err := cli.Get(context.Background(), subUrl, param) + if err != nil { + return rsp, err + } + fmt.Println("response:", rsp, err) + return rsp, err +} + +func (core *Core) RestPost(subUrl string, param *map[string]interface{}) (*rest.RESTAPIResult, error) { + key, err1 := core.Cfg.Config.Get("credentialMutable").Get("okAccessKey").String() + secret, err2 := core.Cfg.Config.Get("credentialMutable").Get("secretKey").String() + pass, err3 := core.Cfg.Config.Get("credentialMutable").Get("okAccessPassphrase").String() + userId, err4 := core.Cfg.Config.Get("connect").Get("userId").String() + restUrl, err5 := core.Cfg.Config.Get("connect").Get("restBaseUrl").String() + if err1 != nil || err2 != nil || err3 != nil || err4 != nil || err5 != nil { + fmt.Println(err1, err2, err3, err4, err5) + } else { + fmt.Println("key:", key, secret, pass, "userId:", userId, "restUrl: ", restUrl) + } + // 请求的另一种方式 + apikey := rest.APIKeyInfo{ + ApiKey: key, + SecKey: secret, + PassPhrase: pass, + } + isDemo := false + if core.Env == "demoEnv" { + isDemo = true + } + cli := rest.NewRESTClient(restUrl, &apikey, isDemo) + rsp, err := cli.Post(context.Background(), subUrl, param) + if err != nil { + return rsp, err + } + return rsp, err +} + +// 我当前持有的币,每分钟刷新 +func (core *Core) GetMyFavorList() []string { + redisCli := core.RedisCli + opt := redis.ZRangeBy{ + Min: "10", + Max: "100000000000", + } + cary, _ := redisCli.ZRevRangeByScore("private|positions|sortedSet", opt).Result() + cl := []string{} + for _, v := range cary { + cry := strings.Split(v, "|")[0] + "-USDT" + cl = append(cl, cry) + } + return cary +} + +// 得到交易量排行榜,订阅其中前N名的各维度k线,并merge进来我已经购买的币列表,这个列表是动态更新的 +// 改了,不需要交易排行榜,我手动指定一个排行即可, tickersVol|sortedSet 改成 tickersList|sortedSet +func (core *Core) GetScoreList(count int) []string { + + redisCli := core.RedisCli + + curList, err := redisCli.ZRevRange("tickersList|sortedSet", 0, int64(count-1)).Result() + if err != nil { + fmt.Println("zrevrange err:", err) + } + return curList +} + +func (core *Core) SubscribeCandleWithDimention(op string, instIdList []string, dimension string) { + wsCli, err := core.GetWsCli() + if err != nil { + fmt.Println("ws client err", err) + } + err = wsCli.Start() + // 创建ws客户端 + if err != nil { + fmt.Println("ws client start err", err) + } + + var args []map[string]string + for _, vs := range instIdList { + arg := make(map[string]string) + arg["instId"] = vs + args = append(args, arg) + } + _, _, err = wsCli.PubKLine(op, wImpl.Period(dimension), args) + if err != nil { + fmt.Println("pubTickers err:", err) + } + +} + +// 订阅某币,先要和配置比对,是否允许订阅此币此周期, +func (core *Core) WsSubscribe(candle *Candle) error { + wsPeriods := []string{} + wsary := core.Cfg.Config.Get("wsDimentions").MustArray() + for _, v := range wsary { + wsPeriods = append(wsPeriods, v.(string)) + } + redisCli := core.RedisCli + period := candle.Period + instId := candle.InstId + from := candle.From + sname := instId + "|" + period + "ts|Subscribed|key" + + exists, _ := redisCli.Exists(sname).Result() + ttl, _ := redisCli.TTL(sname).Result() + inAry, _ := utils.In_Array(period, wsPeriods) + if !inAry { + estr := "subscribe 在配置中此period未被订阅: " + "," + period + // fmt.Println(estr) + err := errors.New(estr) + return err + } else { + fmt.Println("subscribe 已经订阅: ", period) + } + waitWs, _ := core.Cfg.Config.Get("threads").Get("waitWs").Int64() + willSub := false + if exists > 0 { + if ttl > 0 { + if from == "ws" { + redisCli.Expire(sname, time.Duration(waitWs)*time.Second).Result() + } + } else { + willSub = true + } + } else { + willSub = true + } + + if willSub { + // 需要订阅 + + instIdList := []string{} + instIdList = append(instIdList, instId) + + core.SubscribeCandleWithDimention(ws.OP_SUBSCRIBE, instIdList, period) + // 如果距离上次检查此candle此维度订阅状态已经过去超过2分钟还没有发现有ws消息上报,执行订阅 + dr := 1 * time.Duration(waitWs) * time.Second + redisCli.Set(sname, 1, dr).Result() + } else { + // fmt.Println("拒绝订阅candles:", keyName, "tm: ", tm, "otsi:", otsi) + } + + return nil +} + +func LoopBalances(cr *Core, mdura time.Duration) { + //协程:动态维护topScore + ticker := time.NewTicker(mdura) + for { + select { + case <-ticker.C: + //协程:循环执行rest请求candle + fmt.Println("loopBalance: receive ccyChannel start") + RestBalances(cr) + } + } +} +func LoopLivingOrders(cr *Core, mdura time.Duration) { + //协程:动态维护topScore + ticker := time.NewTicker(mdura) + for { + select { + case <-ticker.C: + //协程:循环执行rest请求candle + fmt.Println("loopLivingOrder: receive ccyChannel start") + RestLivingOrder(cr) + } + } +} + +func RestBalances(cr *Core) ([]*private.Ccy, error) { + // fmt.Println("restBalance loopBalance loop start") + ccyList := []*private.Ccy{} + rsp, err := cr.GetBalances() + if err != nil { + fmt.Println("loopBalance err00: ", err) + } + fmt.Println("loopBalance balance rsp: ", rsp) + if err != nil { + fmt.Println("loopBalance err01: ", err) + return ccyList, err + } + if len(rsp.Body) == 0 { + fmt.Println("loopBalance err03: rsp body is null") + return ccyList, err + } + js1, err := simple.NewJson([]byte(rsp.Body)) + if err != nil { + fmt.Println("loopBalance err1: ", err) + } + itemList := js1.Get("data").GetIndex(0).Get("details").MustArray() + // maxTickers是重点关注的topScore的coins的数量 + cli := cr.RedisCli + for _, v := range itemList { + js, _ := json.Marshal(v) + ccyResp := private.CcyResp{} + err := json.Unmarshal(js, &ccyResp) + if err != nil { + fmt.Println("loopBalance err2: ", err) + } + ccy, err := ccyResp.Convert() + ccyList = append(ccyList, ccy) + if err != nil { + fmt.Println("loopBalance err2: ", err) + } + z := redis.Z{ + Score: ccy.EqUsd, + Member: ccy.Ccy + "|position|key", + } + res, err := cli.ZAdd("private|positions|sortedSet", z).Result() + if err != nil { + fmt.Println("loopBalance err3: ", res, err) + } + res1, err := cli.Set(ccy.Ccy+"|position|key", js, 0).Result() + if err != nil { + fmt.Println("loopBalance err4: ", res1, err) + } + bjs, _ := json.Marshal(ccy) + tsi := time.Now().Unix() + tsii := tsi - tsi%600 + tss := strconv.FormatInt(tsii, 10) + cli.Set(CCYPOSISTIONS_PUBLISH+"|ts:"+tss, 1, 10*time.Minute).Result() + fmt.Println("ccy published: ", string(bjs)) + //TODO FIXME 50毫秒,每分钟上限是1200个订单,超过就无法遍历完成 + time.Sleep(50 * time.Millisecond) + suffix := "" + if cr.Env == "demoEnv" { + suffix = "-demoEnv" + } + // TODO FIXME cli2 + cli.Publish(CCYPOSISTIONS_PUBLISH+suffix, string(bjs)).Result() + } + return ccyList, nil +} + +func RestLivingOrder(cr *Core) ([]*private.Order, error) { + // fmt.Println("restOrder loopOrder loop start") + orderList := []*private.Order{} + list, err := cr.GetLivingOrderList() + if err != nil { + fmt.Println("loopLivingOrder err00: ", err) + } + fmt.Println("loopLivingOrder response:", list) + go func() { + for _, v := range list { + fmt.Println("order orderV:", v) + time.Sleep(30 * time.Millisecond) + cr.OrderChan <- v + } + }() + return orderList, nil +} + +func (cr *Core) ProcessOrder(od *private.Order) error { + // publish + go func() { + suffix := "" + if cr.Env == "demoEnv" { + suffix = "-demoEnv" + } + cn := ORDER_PUBLISH + suffix + bj, _ := json.Marshal(od) + + // TODO FIXME cli2 + res, _ := cr.RedisCli.Publish(cn, string(bj)).Result() + fmt.Println("order publish res: ", res, " content: ", string(bj)) + rsch := ORDER_RESP_PUBLISH + suffix + bj1, _ := json.Marshal(res) + + // TODO FIXME cli2 + res, _ = cr.RedisCli.Publish(rsch, string(bj1)).Result() + }() + return nil +} + +func (cr *Core) DispatchSubAction(action *SubAction) error { + go func() { + suffix := "" + if cr.Env == "demoEnv" { + suffix = "-demoEnv" + } + fmt.Println("action: ", action.ActionName, action.MetaInfo) + res, err := cr.RestPostWrapper("/api/v5/trade/"+action.ActionName, action.MetaInfo) + if err != nil { + fmt.Println(utils.GetFuncName(), " actionRes 1:", err) + } + rsch := ORDER_RESP_PUBLISH + suffix + bj1, _ := json.Marshal(res) + + // TODO FIXME cli2 + rs, _ := cr.RedisCli.Publish(rsch, string(bj1)).Result() + fmt.Println("action rs: ", rs) + }() + return nil +} + +func (cr *Core) RestPostWrapper(url string, param map[string]interface{}) (rest.Okexv5APIResponse, error) { + suffix := "" + if cr.Env == "demoEnv" { + suffix = "-demoEnv" + } + res, err := cr.RestPost(url, ¶m) + fmt.Println("actionRes 2:", res.V5Response.Msg, res.V5Response.Data, err) + bj, _ := json.Marshal(res) + + // TODO FIXME cli2 + cr.RedisCli.Publish(ORDER_RESP_PUBLISH+suffix, string(bj)) + return res.V5Response, nil +} diff --git a/core/ticker.go b/core/ticker.go new file mode 100644 index 0000000..b97f158 --- /dev/null +++ b/core/ticker.go @@ -0,0 +1,68 @@ +package core + +import ( + "fmt" + "reflect" + "strconv" +) + +type TickerInfo struct { + InstId string `json:"instId"` + Last float64 `json:"last"` + InstType string `json:"instType"` + VolCcy24h float64 `json:"volCcy24h"` + Ts int64 `json:"ts"` +} + +type TickerInfoResp struct { + InstId string `json:"instId"` + Last string `json:"last"` + InstType string `json:"instType"` + VolCcy24h string `json:"volCcy24h"` + Ts string `json:"ts"` +} + +func (tir *TickerInfoResp) Convert() TickerInfo { + ti := TickerInfo{ + InstId: tir.InstId, + InstType: tir.InstType, + Last: ToFloat64(tir.Last), + VolCcy24h: ToFloat64(tir.VolCcy24h), + Ts: ToInt64(tir.Ts), + } + return ti +} + +func ToString(val interface{}) string { + valstr := "" + if reflect.TypeOf(val).Name() == "string" { + valstr = val.(string) + } else if reflect.TypeOf(val).Name() == "float64" { + valstr = fmt.Sprintf("%f", val) + } else if reflect.TypeOf(val).Name() == "int64" { + valstr = strconv.FormatInt(val.(int64), 16) + } + return valstr +} + +func ToInt64(val interface{}) int64 { + vali := int64(0) + if reflect.TypeOf(val).Name() == "string" { + vali, _ = strconv.ParseInt(val.(string), 10, 64) + } else if reflect.TypeOf(val).Name() == "float64" { + vali = int64(val.(float64)) + } + return vali +} + +func ToFloat64(val interface{}) float64 { + valf := float64(0) + if reflect.TypeOf(val).Name() == "string" { + valf, _ = strconv.ParseFloat(val.(string), 64) + } else if reflect.TypeOf(val).Name() == "float64" { + valf = val.(float64) + } else if reflect.TypeOf(val).Name() == "int64" { + valf = float64(val.(int64)) + } + return valf +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e924f38 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module phyer.click/tunas + +replace ( + v5sdk_go/config => ./submodules/okex/config + v5sdk_go/rest => ./submodules/okex/rest + v5sdk_go/utils => ./submodules/okex/utils + v5sdk_go/ws => ./submodules/okex/ws +) + +go 1.14 + +require ( + github.com/bitly/go-simplejson v0.5.0 + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/go-redis/redis v6.15.9+incompatible + github.com/gorilla/websocket v1.5.1 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/onsi/gomega v1.16.0 // indirect + v5sdk_go/config v0.0.0-00010101000000-000000000000 // indirect + v5sdk_go/rest v0.0.0-00010101000000-000000000000 + v5sdk_go/utils v0.0.0-00010101000000-000000000000 // indirect + v5sdk_go/ws v0.0.0-00010101000000-000000000000 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4c76978 --- /dev/null +++ b/go.sum @@ -0,0 +1,146 @@ +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/jsonResp/balanceAndPositions.json b/jsonResp/balanceAndPositions.json new file mode 100644 index 0000000..055d9b2 --- /dev/null +++ b/jsonResp/balanceAndPositions.json @@ -0,0 +1,123 @@ +{ + "arg": { + "channel": "balance_and_position" + }, + "data": [{ + "balData": [{ + "cashBal": "0.000000009594", + "ccy": "BTC", + "uTime": "1631116773307" + }, { + "cashBal": "0.89910025", + "ccy": "ETH", + "uTime": "1631025341572" + }, { + "cashBal": "79586.878594366", + "ccy": "TOPC", + "uTime": "1629961501428" + }, { + "cashBal": "0.0000001816", + "ccy": "LUNA", + "uTime": "1630861153696" + }, { + "cashBal": "0.0000001186", + "ccy": "MASK", + "uTime": "1631028059063" + }, { + "cashBal": "0.0000149286384406", + "ccy": "USDT", + "uTime": "1631286518384" + }, { + "cashBal": "0.0000155492", + "ccy": "NU", + "uTime": "1630951420333" + }, { + "cashBal": "14.654412074", + "ccy": "ZEC", + "uTime": "1631025233669" + }, { + "cashBal": "81.537378431", + "ccy": "YGG", + "uTime": "1631117745754" + }, { + "cashBal": "0.0000000464", + "ccy": "PICKLE", + "uTime": "1630684891608" + }, { + "cashBal": "87198.5544112", + "ccy": "WXT", + "uTime": "1629907247424" + }, { + "cashBal": "0.0000316", + "ccy": "EC", + "uTime": "1630847969947" + }, { + "cashBal": "149.6670084176", + "ccy": "EFI", + "uTime": "1630401172180" + }, { + "cashBal": "0.3634", + "ccy": "BABYDOGE", + "uTime": "1629900682276" + }, { + "cashBal": "0.000030236", + "ccy": "AE", + "uTime": "1630688579353" + }, { + "cashBal": "0.4276632956", + "ccy": "YFII", + "uTime": "1631175274947" + }, { + "cashBal": "2.9723385262", + "ccy": "USDC", + "uTime": "1631116798568" + }, { + "cashBal": "0.0000006224", + "ccy": "DOGE", + "uTime": "1630868728486" + }, { + "cashBal": "0.037677167462", + "ccy": "USDK", + "uTime": "1629992951524" + }, { + "cashBal": "0.00000046", + "ccy": "AERGO", + "uTime": "1630855658607" + }, { + "cashBal": "0.0000331484", + "ccy": "REVV", + "uTime": "1630254401576" + }, { + "cashBal": "23.644622275", + "ccy": "SOL", + "uTime": "1631286518384" + }, { + "cashBal": "121422.162699056", + "ccy": "VIB", + "uTime": "1631027980427" + }, { + "cashBal": "0.0000004088", + "ccy": "XCH", + "uTime": "1630859983222" + }, { + "cashBal": "0.0000587688", + "ccy": "CVC", + "uTime": "1630861363361" + }, { + "cashBal": "0.0000368", + "ccy": "CNTM", + "uTime": "1630993584362" + }, { + "cashBal": "0.0000007918", + "ccy": "AVAX", + "uTime": "1631164302027" + }, { + "cashBal": "0.0000007332", + "ccy": "FIL", + "uTime": "1630859413545" + }], + "eventType": "snapshot", + "pTime": "1631593669419", + "posData": [] + }] +} diff --git a/jsonResp/order.json b/jsonResp/order.json new file mode 100644 index 0000000..51e1ff3 --- /dev/null +++ b/jsonResp/order.json @@ -0,0 +1,56 @@ +{ + "arg": { + "channel": "orders", + "instType": "FUTURES", + "instId": "BTC-USD-200329" + }, + "data": [{ + "instType": "FUTURES", + "instId": "BTC-USD-200329", + "ordId": "312269865356374016", + "clOrdId": "b1", + "tag": "", + "px": "999", + "sz": "333", + "notionalUsd": "", + "ordType": "limit", + "side": "buy", + "posSide": "long", + "tdMode": "cross", + "tgtCcy": "", + "fillSz": "0", + "fillPx": "long", + "tradeId": "0", + "accFillSz": "323", + "fillNotionalUsd": "", + "fillTime": "0", + "fillFee": "0.0001", + "fillFeeCcy": "BTC", + "execType": "T", + "source": "", + "state": "canceled", + "avgPx": "0", + "lever": "20", + "tpTriggerPx": "0", + "tpTriggerPxType": "last", + "tpOrdPx": "20", + "slTriggerPx": "0", + "slTriggerPxType": "last", + "slOrdPx": "20", + "feeCcy": "", + "fee": "", + "rebateCcy": "", + "rebate": "", + "tgtCcy":"", + "pnl": "", + "category": "", + "uTime": "1597026383085", + "cTime": "1597026383085", + "reqId": "", + "amendResult": "", + "code": "0", + "msg": "" + }] +} + + diff --git a/jsonResp/order2.json b/jsonResp/order2.json new file mode 100644 index 0000000..bd6c4a6 --- /dev/null +++ b/jsonResp/order2.json @@ -0,0 +1,55 @@ +{ + "arg": { + "channel": "orders", + "instType": "ANY", + "uid": "169408628405739520" + }, + "data": [{ + "accFillSz": "0", + "amendResult": "0", + "avgPx": "0", + "cTime": "1644507606496", + "category": "normal", + "ccy": "", + "clOrdId": "", + "code": "0", + "execType": "", + "fee": "0", + "feeCcy": "USDT", + "fillFee": "0", + "fillFeeCcy": "", + "fillNotionalUsd": "", + "fillPx": "", + "fillSz": "0", + "fillTime": "", + "instId": "ACA-USDT", + "instType": "SPOT", + "lever": "0", + "msg": "", + "notionalUsd": "1760.8526031423098", + "ordId": "412029997271109634", + "ordType": "limit", + "pnl": "0", + "posSide": "", + "px": "1.98", + "rebate": "0", + "rebateCcy": "ACA", + "reduceOnly": "false", + "reqId": "", + "side": "sell", + "slOrdPx": "", + "slTriggerPx": "", + "slTriggerPxType": "last", + "source": "", + "state": "live", + "sz": "888.644127", + "tag": "", + "tdMode": "cash", + "tgtCcy": "", + "tpOrdPx": "", + "tpTriggerPx": "", + "tpTriggerPxType": "last", + "tradeId": "", + "uTime": "1644539201208" + }] +} diff --git a/jsonResp/position.json b/jsonResp/position.json new file mode 100644 index 0000000..f870144 --- /dev/null +++ b/jsonResp/position.json @@ -0,0 +1,2248 @@ +{ + "code": "0", + "data": [{ + "adjEq": "", + "details": [{ + "availBal": "", + "availEq": "745.232431076", + "cashBal": "817.940015076", + "ccy": "IMX", + "crossLiab": "", + "disEq": "1394.1378586962883", + "eq": "817.940015076", + "eqUsd": "2788.2757173925766", + "frozenBal": "72.707584", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "72.707584", + "stgyEq": "0", + "twap": "0", + "uTime": "1641637064080", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "18992.5554751896", + "cashBal": "18992.5554751896", + "ccy": "LAT", + "crossLiab": "", + "disEq": "965.7714459133912", + "eq": "18992.5554751896", + "eqUsd": "1931.5428918267824", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640651110519", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000166815998455", + "cashBal": "8103.6412166816", + "ccy": "CNTM", + "crossLiab": "", + "disEq": "0", + "eq": "8103.6412166816", + "eqUsd": "1546.4685173442365", + "frozenBal": "8103.6412", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "8103.6412", + "stgyEq": "0", + "twap": "0", + "uTime": "1641665667010", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "8992.800019959", + "cashBal": "8992.800019959", + "ccy": "TRUE", + "crossLiab": "", + "disEq": "0", + "eq": "8992.800019959", + "eqUsd": "1444.4892046315604", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640870761451", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "12568.0396464128", + "cashBal": "12568.0396464128", + "ccy": "XUC", + "crossLiab": "", + "disEq": "0", + "eq": "12568.0396464128", + "eqUsd": "1253.2465684513234", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640538651722", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "1365.4612564", + "cashBal": "1365.4612564", + "ccy": "RIO", + "crossLiab": "", + "disEq": "0", + "eq": "1365.4612564", + "eqUsd": "852.0049881085255", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1641632702588", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "189143922.64", + "cashBal": "189143922.64", + "ccy": "SOS", + "crossLiab": "", + "disEq": "417.81892511175994", + "eq": "189143922.64", + "eqUsd": "835.6378502235199", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1641640233536", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000009152000011", + "cashBal": "24.5403359152", + "ccy": "DMD", + "crossLiab": "", + "disEq": "0", + "eq": "24.5403359152", + "eqUsd": "540.7046937518878", + "frozenBal": "24.540335", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "24.540335", + "stgyEq": "0", + "twap": "0", + "uTime": "1640589113624", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "35375.0847587288", + "cashBal": "35375.0847587288", + "ccy": "WSB", + "crossLiab": "", + "disEq": "0", + "eq": "35375.0847587288", + "eqUsd": "443.86475658152943", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1641631463730", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000024244440056", + "cashBal": "432.218705864444", + "ccy": "USDT", + "crossLiab": "", + "disEq": "432.2878608573823", + "eq": "432.218705864444", + "eqUsd": "432.2878608573823", + "frozenBal": "432.21870344", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "432.21870344", + "stgyEq": "0", + "twap": "0", + "uTime": "1641665832965", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000086", + "cashBal": "40.873651086", + "ccy": "ANT", + "crossLiab": "", + "disEq": "168.092890091175", + "eq": "40.873651086", + "eqUsd": "336.18578018235", + "frozenBal": "40.873651", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "40.873651", + "stgyEq": "0", + "twap": "0", + "uTime": "1641665832965", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0007175999999163", + "cashBal": "54718.3557176", + "ccy": "OMI", + "crossLiab": "", + "disEq": "0", + "eq": "54718.3557176", + "eqUsd": "284.28850332984047", + "frozenBal": "54718.355", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "54718.355", + "stgyEq": "0", + "twap": "0", + "uTime": "1640523353943", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "5414.4570668404", + "cashBal": "5414.4570668404", + "ccy": "CELT", + "crossLiab": "", + "disEq": "0", + "eq": "5414.4570668404", + "eqUsd": "273.2816606158903", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640862464311", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "38227.5083914", + "cashBal": "49227.5083914", + "ccy": "MEME", + "crossLiab": "", + "disEq": "0", + "eq": "49227.5083914", + "eqUsd": "215.37188008943096", + "frozenBal": "11000", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "11000", + "stgyEq": "0", + "twap": "0", + "uTime": "1641664241725", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "253.3038692352", + "cashBal": "253.3038692352", + "ccy": "TOWN", + "crossLiab": "", + "disEq": "0", + "eq": "253.3038692352", + "eqUsd": "142.53076645558212", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640871304338", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "5482.82193184", + "cashBal": "5482.82193184", + "ccy": "FAIR", + "crossLiab": "", + "disEq": "0", + "eq": "5482.82193184", + "eqUsd": "50.96154506678951", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640517575981", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000381383995318", + "cashBal": "1407.1645401384", + "ccy": "ETM", + "crossLiab": "", + "disEq": "0", + "eq": "1407.1645401384", + "eqUsd": "25.82534253193637", + "frozenBal": "1407.1645020000005", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "1407.1645020000005", + "stgyEq": "0", + "twap": "0", + "uTime": "1640412355040", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "1782.513277884", + "cashBal": "1782.513277884", + "ccy": "PROPS", + "crossLiab": "", + "disEq": "0", + "eq": "1782.513277884", + "eqUsd": "11.2315181257168", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640188255492", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "2.9723385262", + "cashBal": "2.9723385262", + "ccy": "USDC", + "crossLiab": "", + "disEq": "2.9726357600526203", + "eq": "2.9723385262", + "eqUsd": "2.9726357600526203", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1631116798606", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.037677167462", + "cashBal": "0.037677167462", + "ccy": "USDK", + "crossLiab": "", + "disEq": "0.0376733997452538", + "eq": "0.037677167462", + "eqUsd": "0.0376733997452538", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1629992951524", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000994", + "cashBal": "0.000000994", + "ccy": "ETH", + "crossLiab": "", + "disEq": "0.00309469972", + "eq": "0.000000994", + "eqUsd": "0.00309469972", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637636662368", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000001618", + "cashBal": "0.0000001618", + "ccy": "YFII", + "crossLiab": "", + "disEq": "0.000323548224", + "eq": "0.0000001618", + "eqUsd": "0.00040443528", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640506667488", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000008086", + "cashBal": "0.0000008086", + "ccy": "ZEC", + "crossLiab": "", + "disEq": "0.0000911065792", + "eq": "0.0000008086", + "eqUsd": "0.000113883224", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1636445414168", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000008314", + "cashBal": "0.0000008314", + "ccy": "XCH", + "crossLiab": "", + "disEq": "0.000037799601", + "eq": "0.0000008314", + "eqUsd": "0.000075599202", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1639486703941", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000001768", + "cashBal": "0.000000001768", + "ccy": "BTC", + "crossLiab": "", + "disEq": "0.0000737438104", + "eq": "0.000000001768", + "eqUsd": "0.0000737438104", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1636318277798", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000007912", + "cashBal": "0.0000007912", + "ccy": "LUNA", + "crossLiab": "", + "disEq": "0.00004807892952", + "eq": "0.0000007912", + "eqUsd": "0.0000534210328", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1636234728666", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000006782", + "cashBal": "0.0000006782", + "ccy": "AXS", + "crossLiab": "", + "disEq": "0.0000241649442", + "eq": "0.0000006782", + "eqUsd": "0.0000483298884", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637432639379", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000004902", + "cashBal": "0.0000004902", + "ccy": "AVAX", + "crossLiab": "", + "disEq": "0.00003719676816", + "eq": "0.0000004902", + "eqUsd": "0.0000413297424", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637114684625", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000008308", + "cashBal": "0.0000008308", + "ccy": "LPT", + "crossLiab": "", + "disEq": "0.00001464285", + "eq": "0.0000008308", + "eqUsd": "0.0000292857", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637880321082", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000009498", + "cashBal": "0.0000009498", + "ccy": "ENS", + "crossLiab": "", + "disEq": "0.0000137022897", + "eq": "0.0000009498", + "eqUsd": "0.0000274045794", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637590342730", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000008432", + "cashBal": "0.0000008432", + "ccy": "FIL", + "crossLiab": "", + "disEq": "0.00002128586728", + "eq": "0.0000008432", + "eqUsd": "0.0000250421968", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1638024258238", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000165", + "cashBal": "0.000000165", + "ccy": "SOL", + "crossLiab": "", + "disEq": "0.0000214242435", + "eq": "0.000000165", + "eqUsd": "0.000023804715", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1636414203894", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0005736", + "cashBal": "0.0005736", + "ccy": "BLOK", + "crossLiab": "", + "disEq": "0", + "eq": "0.0005736", + "eqUsd": "0.0000228250119284", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1639496461495", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000405", + "cashBal": "0.000000405", + "ccy": "OKT", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000405", + "eqUsd": "0.00001984095", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1636562034844", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000282", + "cashBal": "0.000000282", + "ccy": "CVX", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000282", + "eqUsd": "0.0000105444676626", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640276763065", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000155492", + "cashBal": "0.0000155492", + "ccy": "NU", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000155492", + "eqUsd": "0.0000096353440913", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1630951420333", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.3398", + "cashBal": "0.3398", + "ccy": "SHIB", + "crossLiab": "", + "disEq": "0.0000085446108", + "eq": "0.3398", + "eqUsd": "0.000009494012", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637110927595", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000799", + "cashBal": "0.000000799", + "ccy": "PICKLE", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000799", + "eqUsd": "0.0000069364990044", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1636125706854", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000446192", + "cashBal": "0.0000446192", + "ccy": "DOSE", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000446192", + "eqUsd": "0.0000057819041074", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640555574691", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000058036", + "cashBal": "0.000058036", + "ccy": "PEOPLE", + "crossLiab": "", + "disEq": "0.0000028481167", + "eq": "0.000058036", + "eqUsd": "0.0000056962334", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640352004749", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000001746", + "cashBal": "0.0000001746", + "ccy": "DOT", + "crossLiab": "", + "disEq": "0.00000380153088", + "eq": "0.0000001746", + "eqUsd": "0.0000042239232", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1636310025867", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000331484", + "cashBal": "0.0000331484", + "ccy": "REVV", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000331484", + "eqUsd": "0.0000041372093243", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1630254401576", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000030236", + "cashBal": "0.000030236", + "ccy": "AE", + "crossLiab": "", + "disEq": "0", + "eq": "0.000030236", + "eqUsd": "0.0000041339638544", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1630688579353", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000008876", + "cashBal": "0.0000008876", + "ccy": "ENJ", + "crossLiab": "", + "disEq": "0.00000101421614", + "eq": "0.0000008876", + "eqUsd": "0.00000202843228", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637346495577", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000006424", + "cashBal": "0.0000006424", + "ccy": "SIS", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000006424", + "eqUsd": "0.0000020136641235", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640479306899", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000003024", + "cashBal": "0.0000003024", + "ccy": "TRA", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000003024", + "eqUsd": "0.0000012473345485", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637442406369", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000009128", + "cashBal": "0.0000009128", + "ccy": "WNCG", + "crossLiab": "", + "disEq": "0.00000061175856", + "eq": "0.0000009128", + "eqUsd": "0.00000122351712", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637409967550", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000173", + "cashBal": "0.000000173", + "ccy": "DYDX", + "crossLiab": "", + "disEq": "0.0000005884595", + "eq": "0.000000173", + "eqUsd": "0.000001176919", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1634752334399", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000008088", + "cashBal": "0.0000008088", + "ccy": "DEVT", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000008088", + "eqUsd": "0.0000011646389573", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640349328023", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.902", + "cashBal": "0.902", + "ccy": "ELON", + "crossLiab": "", + "disEq": "0", + "eq": "0.902", + "eqUsd": "0.000001137422", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640276714670", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000001898", + "cashBal": "0.0000001898", + "ccy": "YGG", + "crossLiab": "", + "disEq": "0.00000044733962", + "eq": "0.0000001898", + "eqUsd": "0.00000089467924", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1634755847271", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000007308", + "cashBal": "0.0000007308", + "ccy": "ARK", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000007308", + "eqUsd": "0.0000007791808162", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637447163970", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000077176", + "cashBal": "0.000077176", + "ccy": "INT", + "crossLiab": "", + "disEq": "0", + "eq": "0.000077176", + "eqUsd": "0.0000006406696953", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640096874950", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000000364", + "cashBal": "0.0000000364", + "ccy": "MASK", + "crossLiab": "", + "disEq": "0.000000174083", + "eq": "0.0000000364", + "eqUsd": "0.000000348166", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1635314414594", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000937", + "cashBal": "0.000000937", + "ccy": "GALA", + "crossLiab": "", + "disEq": "0.000000148341155", + "eq": "0.000000937", + "eqUsd": "0.00000029668231", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640281475071", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000006424", + "cashBal": "0.0000006424", + "ccy": "OXT", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000006424", + "eqUsd": "0.000000200330365", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640524125149", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000076", + "cashBal": "0.000076", + "ccy": "RACA", + "crossLiab": "", + "disEq": "0", + "eq": "0.000076", + "eqUsd": "0.0000001958284003", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640348340218", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000704", + "cashBal": "0.000000704", + "ccy": "KONO", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000704", + "eqUsd": "0.0000001812096", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637374101149", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000298", + "cashBal": "0.000000298", + "ccy": "CQT", + "crossLiab": "", + "disEq": "0.000000077331", + "eq": "0.000000298", + "eqUsd": "0.000000154662", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1638186891681", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000004796", + "cashBal": "0.0000004796", + "ccy": "CVC", + "crossLiab": "", + "disEq": "0.0000000761998098", + "eq": "0.0000004796", + "eqUsd": "0.0000001523996195", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637408304557", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000316", + "cashBal": "0.0000316", + "ccy": "EC", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000316", + "eqUsd": "0.0000001494618042", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1630847969947", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000000964", + "cashBal": "0.0000000964", + "ccy": "AGLD", + "crossLiab": "", + "disEq": "0.00000007041538", + "eq": "0.0000000964", + "eqUsd": "0.00000014083076", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640089291757", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000282", + "cashBal": "0.000000282", + "ccy": "ELF", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000282", + "eqUsd": "0.0000001200192", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640542851129", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000004064", + "cashBal": "0.0000004064", + "ccy": "NAS", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000004064", + "eqUsd": "0.0000001182624", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640357053392", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000000212", + "cashBal": "0.0000000212", + "ccy": "OMG", + "crossLiab": "", + "disEq": "0.000000092213216", + "eq": "0.0000000212", + "eqUsd": "0.00000011526652", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1636268546963", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.00000046", + "cashBal": "0.00000046", + "ccy": "AERGO", + "crossLiab": "", + "disEq": "0", + "eq": "0.00000046", + "eqUsd": "0.0000001013071938", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1630855658607", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000009202", + "cashBal": "0.0000009202", + "ccy": "TRADE", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000009202", + "eqUsd": "0.0000000781366987", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640226003088", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000897", + "cashBal": "0.000000897", + "ccy": "CGS", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000897", + "eqUsd": "0.0000000670814331", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637338607055", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000004236", + "cashBal": "0.0000004236", + "ccy": "DOGE", + "crossLiab": "", + "disEq": "0.0000000578173334", + "eq": "0.0000004236", + "eqUsd": "0.0000000642414816", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637902382431", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000754", + "cashBal": "0.000000754", + "ccy": "APIX", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000754", + "eqUsd": "0.0000000554273128", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640321479655", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000938", + "cashBal": "0.000000938", + "ccy": "MITH", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000938", + "eqUsd": "0.000000046608694", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637399774773", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000000778", + "cashBal": "0.0000000778", + "ccy": "TAI", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000000778", + "eqUsd": "0.0000000349367412", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1639490904497", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000008614", + "cashBal": "0.0000008614", + "ccy": "ITC", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000008614", + "eqUsd": "0.0000000288618057", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637337468466", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000228", + "cashBal": "0.000000228", + "ccy": "VELO", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000228", + "eqUsd": "0.0000000256090088", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1638027233244", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000007534", + "cashBal": "0.0000007534", + "ccy": "VIB", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000007534", + "eqUsd": "0.0000000250173823", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1638470302386", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000000298", + "cashBal": "0.0000000298", + "ccy": "EFI", + "crossLiab": "", + "disEq": "0.00000001195427", + "eq": "0.0000000298", + "eqUsd": "0.00000002390854", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1639232871210", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.8858", + "cashBal": "0.8858", + "ccy": "POLYDOGE", + "crossLiab": "", + "disEq": "0", + "eq": "0.8858", + "eqUsd": "0.0000000215937054", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640212617230", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000000512", + "cashBal": "0.0000000512", + "ccy": "XLM", + "crossLiab": "", + "disEq": "0.0000000105488384", + "eq": "0.0000000512", + "eqUsd": "0.000000013186048", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637589689534", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000005416", + "cashBal": "0.0000005416", + "ccy": "PST", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000005416", + "eqUsd": "0.0000000104168427", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637424046861", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000005688", + "cashBal": "0.0000005688", + "ccy": "SPELL", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000005688", + "eqUsd": "0.0000000083269409", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1639008156176", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000001054", + "cashBal": "0.0000001054", + "ccy": "CELR", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000001054", + "eqUsd": "0.0000000069360578", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1639647647892", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000552", + "cashBal": "0.000000552", + "ccy": "ZYRO", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000552", + "eqUsd": "0.0000000050406168", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637993992781", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000007252", + "cashBal": "0.000007252", + "ccy": "INX", + "crossLiab": "", + "disEq": "0", + "eq": "0.000007252", + "eqUsd": "0.0000000046057108", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640709765322", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000394", + "cashBal": "0.000000394", + "ccy": "SOC", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000394", + "eqUsd": "0.0000000028136101", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1639105534974", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.983", + "cashBal": "0.983", + "ccy": "KISHU", + "crossLiab": "", + "disEq": "0.000000001080317", + "eq": "0.983", + "eqUsd": "0.000000002160634", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640283240914", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000008104", + "cashBal": "0.0000008104", + "ccy": "RNT", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000008104", + "eqUsd": "0.0000000016162446", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1639046039509", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000000488", + "cashBal": "0.0000000488", + "ccy": "TCT", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000000488", + "eqUsd": "0.0000000013749434", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637883712571", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.3634", + "cashBal": "0.3634", + "ccy": "BABYDOGE", + "crossLiab": "", + "disEq": "0.0000000005658138", + "eq": "0.3634", + "eqUsd": "0.0000000011316276", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1629900682276", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000000016", + "cashBal": "0.0000000016", + "ccy": "FSN", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000000016", + "eqUsd": "0.0000000009329772", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1640330508260", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000003292", + "cashBal": "0.0000003292", + "ccy": "BTT", + "crossLiab": "", + "disEq": "0.0000000004017392", + "eq": "0.0000003292", + "eqUsd": "0.0000000008034784", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1638964333554", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000002", + "cashBal": "0.0000002", + "ccy": "WXT", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000002", + "eqUsd": "0.0000000007447266", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1631982051921", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.0000004024", + "cashBal": "0.0000004024", + "ccy": "TOPC", + "crossLiab": "", + "disEq": "0", + "eq": "0.0000004024", + "eqUsd": "0.0000000005320632", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1632870511125", + "upl": "0", + "uplLiab": "" + }, { + "availBal": "", + "availEq": "0.000000018", + "cashBal": "0.000000018", + "ccy": "VSYS", + "crossLiab": "", + "disEq": "0", + "eq": "0.000000018", + "eqUsd": "0.00000000029286", + "frozenBal": "0", + "interest": "", + "isoEq": "0", + "isoLiab": "", + "isoUpl": "0", + "liab": "", + "maxLoan": "", + "mgnRatio": "", + "notionalLever": "0", + "ordFrozen": "0", + "stgyEq": "0", + "twap": "0", + "uTime": "1637412553497", + "upl": "0", + "uplLiab": "" + }], + "imr": "", + "isoEq": "0", + "mgnRatio": "", + "mmr": "", + "notionalUsd": "", + "ordFroz": "", + "totalEq": "13411.214484068489", + "uTime": "1641690180097" + }] + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..43cc815 --- /dev/null +++ b/main.go @@ -0,0 +1,639 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "math/rand" + "os" + "strings" + "time" + "v5sdk_go/rest" + "v5sdk_go/ws" + "v5sdk_go/ws/wImpl" + + simple "github.com/bitly/go-simplejson" + "github.com/go-redis/redis" + "phyer.click/tunas/core" + "phyer.click/tunas/private" + "phyer.click/tunas/utils" +) + +func init() { + // //fmt.Println("inited") +} + +// 通过rest接口,获取所有ticker信息,存入redis的stream和成交量排行榜 +func RestTicker(cr *core.Core, dura time.Duration) { + + rsp := rest.RESTAPIResult{} + js := simple.Json{} + itemList := []interface{}{} + fmt.Println("getAllTickerInfo err: ") + rsp1, err := cr.GetAllTickerInfo() + rsp = *rsp1 + js1, err := simple.NewJson([]byte(rsp.Body)) + js = *js1 + if err != nil { + fmt.Println("restTicker err: ", err) + return + } + if len(rsp.Body) == 0 { + fmt.Println("rsp body is null") + return + } + itemList = js.Get("data").MustArray() + // maxTickers是重点关注的topScore的coins的数量 + length, _ := cr.Cfg.Config.Get("threads").Get("maxTickers").Int() + fmt.Println("itemList length:", len(itemList)) + if len(itemList) < length { + return + } + // 关注多少个币,在这里设置, 只需要5个币 + allTicker := cr.GetScoreList(5) + redisCli := cr.RedisCli + // 全部币种列表,跟特定币种列表进行比对,匹配后push到redis + for _, v := range itemList { + tir := core.TickerInfoResp{} + bs, err := json.Marshal(v) + if err != nil { + fmt.Println("restTicker marshal err: ", err) + return + } + err = json.Unmarshal(bs, &tir) + if err != nil { + fmt.Println("restTicker unmarshal err: ", err) + return + } + ti := tir.Convert() + isUsdt := strings.Contains(ti.InstId, "-USDT") + if !isUsdt { + continue + } + if ti.InstType != "SPOT" { + continue + } + // 把单个ticker信息小时交易量存到交易量排行榜 + // fmt.Println("ticker item: ", item) + // 不需要排行榜了 + // _, err = redisCli.ZAdd("tickersVol|sortedSet", redis.Z{ti.VolCcy24h, ti.InstId}).Result() + // if err != nil { + // fmt.Println("restTicker redis err: ", err) + // } + + ab, _ := json.Marshal(ti) + suffix := "" + env := os.Getenv("GO_ENV") + if env == "demoEnv" { + suffix = "-demoEnv" + } + for _, v := range allTicker { + if v == ti.InstId { + redisCli.Publish(core.TICKERINFO_PUBLISH+suffix, string(ab)).Result() + } + } + } +} + +// 私有订阅: 订阅订单频道 +func wsPriv(core *core.Core) error { + cfg := core.Cfg + url, _ := cfg.Config.Get("connect").Get("wsPrivateBaseUrl").String() + priCli, err := ws.NewWsClient("wss://" + url) + err = priCli.Start() + if err != nil { + log.Println(err) + return err + } + priCli.SetDailTimeout(time.Second * 10) + defer func() { + priCli.Stop() + }() + var res bool + key, _ := core.Cfg.Config.Get("credentialReadOnly").Get("okAccessKey").String() + secure, _ := core.Cfg.Config.Get("credentialReadOnly").Get("secretKey").String() + pass, _ := core.Cfg.Config.Get("credentialReadOnly").Get("okAccessPassphrase").String() + // TODO 这里订阅收听订单频道结果,但是ws请求不可靠,这个所谓冗余机制存在,有比没有强,restPost函数 是保底方案 + priCli.AddBookMsgHook(func(ts time.Time, data wImpl.MsgData) error { + // 添加你的方法 + fmt.Println("这是自定义AddBookMsgHook") + bj, _ := json.Marshal(data) + fmt.Println("当前数据是", string(bj)) + resp := private.OrderResp{} + // TODO FIXME 这里默认所有的数据都是OrderResp类型,但是ws请求有其他类型的,这个地方将来肯定得改 + err := json.Unmarshal(bj, &resp) + if err != nil { + fmt.Println("Canvert MsgData to OrderResp err:", err) + return nil + } + fmt.Println("order resp: ", resp) + list, _ := resp.Convert() + fmt.Println("order list: ", list) + go func() { + for _, v := range list { + fmt.Println("order orderV:", v) + core.OrderChan <- v + } + }() + return nil + }) + + fmt.Println("key, secure, pass:", key, secure, pass) + res, _, err = priCli.Login(key, secure, pass) + + if res { + fmt.Println("私有订阅登录成功!") + } else { + fmt.Println("私有订阅登录失败!", err) + return err + } + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = ws.ANY + args = append(args, arg) + // 订阅订单频道动作执行 + res, msg, err := priCli.PrivBookOrder("subscribe", args) + bj, _ := json.Marshal(msg) + fmt.Println("PrivBookOrder res:", res, " msg:", string(bj), " err:", err) + for { + fmt.Println(utils.GetFuncName(), url+" is in subscribing") + time.Sleep(10 * time.Minute) + priCli.Stop() + wsPriv(core) + } + return err +} + +// 统一受理发起rest请求的请求 +func LoopSaveCandle(cr *core.Core) { + for { + ary, err := cr.RedisCli.BRPop(0, "restQueue").Result() + if err != nil { + fmt.Println("brpop err:", err) + continue + } + restQ := core.RestQueue{} + json.Unmarshal([]byte(ary[1]), &restQ) + // fmt.Println("before: ", restQ.InstId) + // before: USDT|position|key + ary1 := strings.Split(restQ.InstId, "|") + if ary1[0] == "USDT" { + // "USDT-USDT" 这个没有意义,忽略 + continue + } + if len(ary1) > 1 && ary1[1] == "position" { + restQ.InstId = ary1[0] + "-USDT" + } + // fmt.Println("after: ", restQ.InstId) + // after: restQueue-USDT + go func() { + restQ.Show(cr) + restQ.Save(cr) + }() + } +} + +// 这个函数被废弃了, 根据现有逻辑,redisCli.Scan(cursor, "*"+pattern+"*", 2000) 得不到任何内容 +func LoopRestQ(cr *core.Core) { + redisCli := cr.RedisCli + cursor := uint64(0) + n := 0 + // allTs := []int64{} + rstr := []string{} + pattern := "restQueue" + fmt.Println("LoopRestQ start") + for { + var err error + rstr, cursor, err = redisCli.Scan(cursor, "*"+pattern+"*", 2000).Result() + if err != nil { + panic(err) + } + n += len(rstr) + if n == 0 { + break + } + if n > 200000 { + break + } + if n > 0 { + fmt.Println("LoopRestQ rstr: ", rstr) + } + } + // fmt.Println("LoopRestQ rstr: ", rstr) + fmt.Println("LoopRestQ rstr len: ", len(rstr)) + for _, keyN := range rstr { + val, err := redisCli.Get(keyN).Result() + fmt.Println("LoopRestQ val:", val) + restQ := core.RestQueue{} + if err != nil || len(val) == 0 { + continue + } + err = json.Unmarshal([]byte(val), &restQ) + if err != nil { + fmt.Println("RestQueue Unmarshal err: ", err) + } + // res, err := redisCli.LPush("restQueue", val).Result() + fmt.Println("restQueue will LPush: ", val) + if err != nil { + redisCli.Del(keyN) + } + } + time.Sleep(10 * time.Second) + LoopRestQ(cr) +} + +func LoopSubscribe(cr *core.Core) { + redisCli := cr.RedisCli + suffix := "" + if cr.Env == "demoEnv" { + suffix = "-demoEnv" + } + pubsub := redisCli.Subscribe(core.ALLCANDLES_INNER_PUBLISH + suffix) + _, err := pubsub.Receive() + if err != nil { + log.Fatal(err) + } + // 用管道来接收消息 + ch := pubsub.Channel() + // 处理消息 + + for msg := range ch { + cd := core.Candle{} + json.Unmarshal([]byte(msg.Payload), &cd) + fmt.Println("msg.PayLoad:", msg.Payload, "candle:", cd) + go func() { + cr.WsSubscribe(&cd) + }() + } +} + +// 订阅并执行sardine端传来的订单相关的动作 +func LoopSubscribeSubAction(cr *core.Core) { + redisCli := cr.RedisCli + suffix := "" + if cr.Env == "demoEnv" { + suffix = "-demoEnv" + } + // TODO FIXME cli2 + prisub := redisCli.Subscribe(core.SUBACTIONS_PUBLISH + suffix) + _, err := prisub.Receive() + if err != nil { + log.Fatal(err) + } + // 用管道来接收消息 + ch := prisub.Channel() + // 处理消息 + + for msg := range ch { + action := core.SubAction{} + json.Unmarshal([]byte(msg.Payload), &action) + fmt.Println("actionMsg.PayLoad:", msg.Payload, "action:", action) + go func() { + cr.DispatchSubAction(&action) + }() + } +} + +// period: 每个循环开始的时间点,单位:秒 +// delay:延时多少秒后去取此值, 单位:秒 +// mdura:多少个分钟之内,遍历完获取到的goins列表, 单位:秒 +// onceCount:每次获取这个coin几个当前周期的candle数据 +// range: 随机的范围,从0开始到range个周期,作为查询的after值,也就是随机n个周期,去取之前的记录,对于2D,5D等数据,可以用来补全数据, range值越大,随机散点的范围越大, 越失焦 + +func LoopAllCoinsList(period int64, delay int64, mdura int, barPeriod string, onceCount int, rge int) { + cr := core.Core{} + cr.Init() + allScoreChan := make(chan []string) + // fmt.Println("allCoins1") + per1 := 1 * time.Minute + ticker := time.NewTicker(per1) + go func() { + for { + tsi := time.Now().Unix() + // fmt.Println("tsi, period, delay, tsi%(period): ", tsi, period, delay, tsi%(period)) + if tsi%(period) != delay { + time.Sleep(1 * time.Second) + continue + } + select { + case <-ticker.C: + go func() { + // -1 是获取全部coin列表 + list := cr.GetScoreList(5) + // fmt.Println("allCoins3", list) + allScoreChan <- list + }() + } + } + }() + for { + allScore, _ := <-allScoreChan + // fmt.Println("allCoins allScore", allScore) + if len(allScore) == 0 { + continue + } + + utils.TickerWrapper(time.Duration(mdura)*time.Second, allScore, func(i int, ary []string) error { + nw := time.Now() + rand.Seed(nw.UnixNano()) + ct := rand.Intn(rge) + minutes := cr.PeriodToMinutes(barPeriod) + tmi := nw.UnixMilli() + tmi = tmi - tmi%60000 + tmi = tmi - (int64(ct) * minutes * 60000) + fmt.Println("instId: ", ary[i]) + restQ := core.RestQueue{ + InstId: ary[i], + Bar: barPeriod, + + WithWs: false, + After: tmi, + } + js, err := json.Marshal(restQ) + // fmt.Println("allCoins lpush js:", string(js)) + cr.RedisCli.LPush("restQueue", js) + return err + }) + } +} + +func LoopCheckMyFavorList(core *core.Core) { + maxTickers, _ := core.Cfg.Config.Get("threads").Get("maxTickers").Int() + myFavorChan := make(chan []string) + //协程:动态维护topScore + go func(mx int) { + myFavor := []string{} + for { + myFavor = core.GetMyFavorList() + if len(myFavor) < mx { + fmt.Println("topScore 长度不符合预期") + break + } else { + fmt.Println("topScore 长度符合预期") + } + myFavorChan <- myFavor + time.Sleep(12 * time.Minute) + } + }(maxTickers) + //协程:循环执行rest请求candle + + for { + myFavor, _ := <-myFavorChan + go func() { + loop2(core, myFavor, maxTickers) + }() + } +} + +func loop2(core *core.Core, topScore []string, maxTickers int) { + restPeriod, _ := core.Cfg.Config.Get("threads").Get("restPeriod").Int() + maxCandles, _ := core.Cfg.Config.Get("threads").Get("maxCandles").Int() + if len(topScore) < maxTickers { + return + } + // fmt.Println("loop1 ", 12*time.Minute, " topScore2: ", topScore) + // fmt.Println("topScore: ", topScore, len(topScore), mx, ok) + //每隔Period1,重新发起一次rest请求的大循环。里面还有60秒一次的小循环. 作为ws请求的备份(保底)机制,实效性差了一点,但是稳定性高于ws + dura := time.Duration(restPeriod) * time.Second + + mdura := dura/time.Duration(len(topScore)) - 20*time.Millisecond + ticker := time.NewTicker(mdura) + done := make(chan bool) + idx := 0 + go func(i int) { + for { + select { + case <-ticker.C: + if i >= 4 { //: 12分钟 / 3分钟 = 4 + done <- true + break + } + for { + //内层循环3分钟一圈,3分钟内遍历完topScore限定的candle列表, 12分钟能够跑4圈 + if i >= 4 { //: 12分钟 / 3分钟 = 4 + done <- true + break + } + go func() { + // fmt.Println("loop2 :", "dura:", dura, "i:", i) + // core.InVokeCandle(topScore, per1, maxCandles) + mdura := dura / time.Duration(len(topScore)+1) + for k, v := range topScore { + go func(k int, v string) { + time.Sleep(mdura*time.Duration(k) - 10*time.Millisecond) + core.GetCandlesWithRest(v, k, mdura, maxCandles) + }(k, v) + } + i++ + }() + time.Sleep(dura) + continue + } + } + } + }(idx) + time.Sleep(dura - 100*time.Millisecond) + done <- true + ticker.Stop() +} + +// 订阅公共频道 +func wsPub(core *core.Core) { + wsCli, _ := core.GetWsCli() + err := wsCli.Start() + // 创建ws客户端 + if err != nil { + //fmt.Println("ws client err", err) + return + } + + // 设置连接超时 + + wsCli.SetDailTimeout(time.Second * 20) + + defer wsCli.Stop() + // 订阅产品频道 + // 在这里初始化instrument列表 + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = ws.SPOT + //arg["instType"] = OPTION + args = append(args, arg) + + // start := time.Now() + //订阅 + + //设置订阅事件的event handler + wsCli.AddBookMsgHook(core.PubMsgDispatcher) + res, _, err := wsCli.PubInstruemnts(ws.OP_SUBSCRIBE, args) + //fmt.Println("args:", args) + if res { + // usedTime := time.Since(start) + //fmt.Println("订阅成功!", usedTime.String()) + } else { + //fmt.Println("订阅失败!", err) + } + core.LoopInstrumentList() + + // start = time.Now() + // res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) + // if res { + // usedTime := time.Since(start) + // //fmt.Println("取消订阅成功!", usedTime.String()) + // } else { + // //fmt.Println("取消订阅失败!", err) + // } +} + +// 处理 ws 订单请求相关订阅回调 +func subscribeOrder(cr *core.Core) { + fmt.Println("subscribeOrder order:") + for { + order := <-cr.OrderChan + go func() { + bj, _ := json.Marshal(order) + fmt.Println("process order:", string(bj)) + cr.ProcessOrder(order) + }() + } +} +func main() { + cr := core.Core{} + cr.Init() + cr.ShowSysTime() + // 从rest接口获取的ticker记录种的交量计入排行榜,指定周期刷新一次 + go func() { + per1 := 1 * time.Minute + RestTicker(&cr, per1) + limiter := time.Tick(per1) + for { + <-limiter + go func() { + RestTicker(&cr, per1) + }() + } + }() + //全员1分钟candle & maX + // period: 每个循环开始的时间点,单位:秒 + // delay:延时多少秒后去取此值 + // mdura:多少秒之内,遍历完获取到的goins列表 + // onceCount:每次获取这个coin几个当前周期的candle数据 + // 全员3m + // go func() { + // fmt.Println("LoopAllCoinsList1") + // LoopAllCoinsList(180, 0, 180, "3m", 5, 6) + // }() + + // 全员15m candle + // go func() { + // fmt.Println("LoopAllCoinsList2") + // LoopAllCoinsList(380, 90, 380, "15m", 4, 7) + // }() + // 全员30m candle + // go func() { + // fmt.Println("LoopAllCoinsList2") + // LoopAllCoinsList(510, 90, 500, "30m", 5, 8) + // }() + // 全员1H candle + // go func() { + // fmt.Println("LoopAllCoinsList2") + // LoopAllCoinsList(770, 0, 760, "1H", 9, 12) + // }() + // 全员2H candle + go func() { + fmt.Println("LoopAllCoinsList2") + LoopAllCoinsList(820, 0, 820, "2H", 12, 15) + }() + // 全员4小时candle + // go func() { + // fmt.Println("LoopAllCoinsList1") + // LoopAllCoinsList(1280, 150, 1280, "4H", 15, 19) + // }() + // 全员6小时candle + //go func() { + // fmt.Println("LoopAllCoinsList1") + // LoopAllCoinsList(1440, 180, 1440, "6H", 17, 21) + //}() + // 全员12小时candle + go func() { + fmt.Println("LoopAllCoinsList1") + LoopAllCoinsList(1680, 180, 1680, "12H", 19, 23) + }() + // 全员1Day candle & maX + //go func() { + // fmt.Println("LoopAllCoinsList1") + // LoopAllCoinsList(1920, 4, 1920, "1D", 25, 30) + // }() + // 全员2Day candle & maX + go func() { + fmt.Println("LoopAllCoinsList1") + LoopAllCoinsList(3840, 220, 3840, "2D", 26, 31) + }() + // 全员5Day candle & maX + go func() { + fmt.Println("LoopAllCoinsList1") + LoopAllCoinsList(6400, 4, 6400, "5D", 28, 35) + }() + // 循环检查tickersVol|sortedSet,并执行订阅candles + go func() { + LoopCheckMyFavorList(&cr) + }() + + go func() { + LoopSaveCandle(&cr) + }() + go func() { + LoopSubscribe(&cr) + }() + // 订阅下游sardine发过来的要执行的动作 + go func() { + LoopSubscribeSubAction(&cr) + }() + // 废弃 + // go func() { + // LoopRestQ(&cr) + // }() + + //----------- + //私有部分 + go func() { + core.LoopLivingOrders(&cr, 1*time.Minute) + }() + go func() { + core.LoopBalances(&cr, 1*time.Minute) + }() + + // 公共订阅 + // wsPub(&cr) + + // 停止私有订阅 + + // go func() { + // //正常情况下 wsPrive不会主动退出,如果不慎退出了,自动重新运行 + // for { + // wsPriv(&cr) + // } + //}() + + go func() { + subscribeOrder(&cr) + }() + // gcl := map[string]models.GlobalCoin{} + // msgr := cr.Messager{} + // msgr.Init() + // msgr.Login() + // msgr.Alive() + // msgr.GlobalSubscribe() // 订阅全局该订阅的公共和私有内容 + // msgr.Dispatcher(gcl) + // msgr.Pop() + + // //fmt.Println("listenning ... ") + // 这个地方为了让main不退出, 将来可以改成一个http的listener + // ip := "0.0.0.0:6066" + // if err := http.ListenAndServe(ip, nil); err != nil { + // fmt.Printf("start pprof failed on %s\n", ip) + // } + // 永久阻塞 + select {} +} diff --git a/models/account.go b/models/account.go new file mode 100644 index 0000000..836392a --- /dev/null +++ b/models/account.go @@ -0,0 +1,44 @@ +package models + +type Account struct { + UTime int64 `json:"uTime"` //币种余额信息的更新时间 +} + +// { +// "uTime": "1597026383085", +// "totalEq": "41624.32", +// "isoEq": "3624.32", +// "adjEq": "41624.32", +// "ordFroz": "0", +// "imr": "4162.33", +// "mmr": "4", +// "notionalUsd": "", +// "mgnRatio": "41624.32", +// "details": [ +// { +// "availBal": "", +// "availEq": "1", +// "ccy": "BTC", +// "cashBal": "1", +// "uTime": "1617279471503", +// "disEq": "50559.01", +// "eq": "1", +// "eqUsd": "45078.3790756226851775", +// "frozenBal": "0", +// "interest": "0", +// "isoEq": "0", +// "liab": "0", +// "maxLoan": "", +// "mgnRatio": "", +// "notionalLever": "0.0022195262185864", +// "ordFrozen": "0", +// "upl": "0", +// "uplLiab": "0", +// "crossLiab": "0", +// "isoLiab": "0", +// "coinUsdPrice": "60000", +// "stgyEq":"0", +// "isoUpl":"" +// } +// ] +// } diff --git a/models/candle.go b/models/candle.go new file mode 100644 index 0000000..600900b --- /dev/null +++ b/models/candle.go @@ -0,0 +1,10 @@ +package models + +type Candle struct { + InstId string `json:"instId"` //开始时间 + Ts int64 `json:"ts"` // 最高价格 + O float64 `json:"o"` //最低价格 + C string `json:"c"` //收盘价格 + Vol string `json:"vol"` // 交易量,以张为单位 + VolCcy string `json:"volCcy"` // 交易量,以币为单位 +} diff --git a/models/globalCoin.go b/models/globalCoin.go new file mode 100644 index 0000000..da126c2 --- /dev/null +++ b/models/globalCoin.go @@ -0,0 +1,21 @@ +package models + +import ( + "phyer.click/tunas/utils" +) + +type GlobalCoin struct { + CoinName string `json:"coinName"` + Instrument *Instrument `json:"instrument"` + Ticker *Ticker `json:"ticker"` + CandleMapList map[string](map[string]utils.MyStack) `json:"candleMapList"` // map["BTC"]["oneMintue"] +} + +func (coin *GlobalCoin) GetInstrument() *Instrument { + return coin.Instrument +} +func (coin *GlobalCoin) SetInstrument(instr *Instrument) { + coin.Instrument = instr +} + +// gcl := map[string]GlobalGoin{} diff --git a/models/instrument.go b/models/instrument.go new file mode 100644 index 0000000..ea27215 --- /dev/null +++ b/models/instrument.go @@ -0,0 +1,40 @@ +package models + +type Instrument struct { + Alias string `json:"alias" mapstructure:"alias"` //合约日期别名, 目前只关注: “this_week” + BaseCcy string `json:"baseCcy" mapstructure:"baseCcy"` //交易货币币种,如 BTC-USDT 中BTC + Category int32 `json:"category" mapstructure:"category"` // 手续费档位,每个交易产品属于哪个档位手续费,币币交易, 第1类和第3类:卖0.080% 买0.100%,第2类:0.060% 0.060% + CtVal float64 `json:"ctVal" mapstructure:"ctVal"` //合约面值 + CtValCcy string `json:"ctValCcy" mapstructure:"ctValCcy"` //合约面值计价币种 + InstId string `json:"instId" mapstructure:"instId"` // 产品ID + InstType string `json:"instType" mapstructure:"instType"` // 产品类型 只关注SPOT + LotSz float64 `json:"lotSz" mapstructure:"lotSz"` //下单数量精度 + MinSz int32 `json:"minSz" mapstructure:"minSz"` //最小下单数量 + QuoteCcy string `json:"quoteCcy" mapstructure:"quoteCcy"` // 计价货币币种,如 BTC-USDT 中 USDT ,仅适用于币币 + State string `json:"state" mapstructure:"state"` //产品状态:live, suspend,expired,preopen + TickSz float64 `json:"tickSz" mapstructure:"tickSz"` //下单价格精度,如0.00001 +} + +// { +// alias: , +// baseCcy: AE, +// category: 2, +// ctMult: , +// ctType: , +// ctVal: , +// ctValCcy: , +// expTime: , +// instId: AE-BTC, +// instType: SPOT, +// lever: , +// listTime: , +// lotSz: 0.00000001, +// minSz: 10, +// optType: , +// quoteCcy: BTC, +// settleCcy: , +// state: live, +// stk: , +// tickSz: 0.00000001, +// uly: +// } diff --git a/models/ticker.go b/models/ticker.go new file mode 100644 index 0000000..5931835 --- /dev/null +++ b/models/ticker.go @@ -0,0 +1,20 @@ +package models + +type Ticker struct { + InstType string `json:"instType,omitempty"` //SWAP, + InstId string `json:"instId,omitempty"` //LTC-USD-SWAP, + Last float64 `json:"last,omitempty"` //9999.99, + LastSz float64 `json:"lastSz,omitempty"` //0.1, + AskPx float64 `json:"askPx,omitempty"` //9999.99, + AskSz int32 `json:"askSz,omitempty"` //11, + bidPx float64 `json:"bidPx,omitempty"` //8888.88, + BidSz int32 `json:"bidSz,omitempty"` //5, + Open24h int32 `json:"open24h,omitempty"` //9000, + High24h int32 `json:"high24h,omitempty"` //10000, + Low24h float64 `json:"low24h,omitempty"` //8888.88, + VolCcy24h int32 `json:"volCcy24h,omitempty"` //2222, + Vol24h int32 `json:"vol24h,omitempty"` //2222, + SodUtc0 int32 `json:"sodUtc0,omitempty"` //2222, + SodUtc8 int32 `json:"sodUtc8,omitempty"` //2222, + Ts int64 `json:"ts,omitempty"` //1597026383085 +} diff --git a/okex/.gitignore b/okex/.gitignore new file mode 100644 index 0000000..49f2b9c --- /dev/null +++ b/okex/.gitignore @@ -0,0 +1 @@ +ws/vendor/ diff --git a/okex/LICENSE b/okex/LICENSE new file mode 100644 index 0000000..35781f8 --- /dev/null +++ b/okex/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/okex/config/conf.go b/okex/config/conf.go new file mode 100644 index 0000000..c1c20b5 --- /dev/null +++ b/okex/config/conf.go @@ -0,0 +1,32 @@ +package config + +import "fmt" + +type Env struct { + RestEndpoint string `yaml:"RestEndpoint"` + WsEndpoint string `yaml:"WsEndpoint"` + IsSimulation bool `yaml:"IsSimulation"` +} + +type ApiInfo struct { + ApiKey string `yaml:"ApiKey"` + SecretKey string `yaml:"SecretKey"` + Passphrase string `yaml:"Passphrase"` +} + +type MetaData struct { + Description string `yaml:"Description"` +} + +type Config struct { + MetaData `yaml:"MetaData"` + Env `yaml:"Env"` + ApiInfo `yaml:"ApiInfo"` +} + +func (s *ApiInfo) String() string { + res := "ApiInfo{" + res += fmt.Sprintf("ApiKey:%v,SecretKey:%v,Passphrase:%v", s.ApiKey, s.SecretKey, s.Passphrase) + res += "}" + return res +} diff --git a/okex/config/go.mod b/okex/config/go.mod new file mode 100644 index 0000000..bf70c70 --- /dev/null +++ b/okex/config/go.mod @@ -0,0 +1,3 @@ +module v5sdk_go/config + +go 1.14 diff --git a/okex/go.mod b/okex/go.mod new file mode 100644 index 0000000..cd83b72 --- /dev/null +++ b/okex/go.mod @@ -0,0 +1,8 @@ +module v5sdk_go + +go 1.15 + +require ( + github.com/gorilla/websocket v1.4.2 + github.com/stretchr/testify v1.7.0 +) diff --git a/okex/go.sum b/okex/go.sum new file mode 100644 index 0000000..8eb6d1d --- /dev/null +++ b/okex/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/okex/main.go b/okex/main.go new file mode 100644 index 0000000..5781aa8 --- /dev/null +++ b/okex/main.go @@ -0,0 +1,237 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + . "v5sdk_go/rest" + . "v5sdk_go/ws" +) + +/* + rest API请求 + 更多示例请查看 rest/rest_test.go +*/ +func REST() { + // 设置您的APIKey + apikey := APIKeyInfo{ + ApiKey: "eca767d4-fda5-4a1b-bb28-49ae18093307", + SecKey: "8CA3628A556ED137977DB298D37BC7F3", + PassPhrase: "Op3Druaron", + } + + // 第三个参数代表是否为模拟环境,更多信息查看接口说明 + cli := NewRESTClient("https://www.okex.win", &apikey, false) + rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) + if err != nil { + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", rsp.Code) + fmt.Println("\t总耗时: ", rsp.TotalUsedTime) + fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) + fmt.Println("\t返回消息: ", rsp.Body) + fmt.Println("\terrCode: ", rsp.V5Response.Code) + fmt.Println("\terrMsg: ", rsp.V5Response.Msg) + fmt.Println("\tdata: ", rsp.V5Response.Data) + +} + +// 订阅私有频道 +func wsPriv() { + ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" + + // 填写您自己的APIKey信息 + apikey := "xxxx" + secretKey := "xxxxx" + passphrase := "xxxxx" + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + defer r.Stop() + var res bool + + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + fmt.Println("登录成功!") + } else { + fmt.Println("登录失败!", err) + return + } + + // 订阅账户频道 + var args []map[string]string + arg := make(map[string]string) + arg["ccy"] = "BTC" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!耗时:", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + + time.Sleep(100 * time.Second) + start = time.Now() + res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + } + +} + +// 订阅公共频道 +func wsPub() { + ep := "wss://wsaws.okex.com:8443/ws/v5/public?brokerId=9999" + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + defer r.Stop() + // 订阅产品频道 + // 在这里初始化instrument列表 + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = FUTURES + //arg["instType"] = OPTION + args = append(args, arg) + + start := time.Now() + //订阅 + res, _, err := r.PubInstruemnts(OP_SUBSCRIBE, args) + fmt.Println("args:", args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + + // 在这里 args1 初始化tickerList的列表 + var args1 []map[string]string + arg1 := make(map[string]string) + arg1["instId"] = "ETH-USDT" + //arg["instType"] = OPTION + args1 = append(args1, arg1) + //------------------------------------------------------ + start1 := time.Now() + res, _, err = r.PubTickers(OP_SUBSCRIBE, args1) + fmt.Println("args:", args) + if res { + usedTime := time.Since(start1) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + time.Sleep(300 * time.Second) + // + // start = time.Now() + // res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) + // if res { + // usedTime := time.Since(start) + // fmt.Println("取消订阅成功!", usedTime.String()) + // } else { + // fmt.Println("取消订阅失败!", err) + // } +} + +// websocket交易 +func wsJrpc() { + ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" + + // 填写您自己的APIKey信息 + apikey := "xxxx" + secretKey := "xxxxx" + passphrase := "xxxxx" + + var res bool + var req_id string + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + + defer r.Stop() + + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + fmt.Println("登录成功!") + } else { + fmt.Println("登录失败!", err) + return + } + + start := time.Now() + param := map[string]interface{}{} + param["instId"] = "BTC-USDT" + param["tdMode"] = "cash" + param["side"] = "buy" + param["ordType"] = "market" + param["sz"] = "200" + req_id = "00001" + + res, _, err = r.PlaceOrder(req_id, param) + if res { + usedTime := time.Since(start) + fmt.Println("下单成功!", usedTime.String()) + } else { + usedTime := time.Since(start) + fmt.Println("下单失败!", usedTime.String(), err) + } +} + +func main() { + // 公共订阅 + wsPub() + + // 私有订阅 + // wsPriv() + + // websocket交易 + // wsJrpc() + + // rest请求 + // REST() +} diff --git a/okex/readme.md b/okex/readme.md new file mode 100644 index 0000000..9ff3e39 --- /dev/null +++ b/okex/readme.md @@ -0,0 +1,283 @@ +# 简介 +OKEX go版本的v5sdk,仅供学习交流使用。 +(文档持续完善中) +# 项目说明 + +## REST调用 +``` go + // 设置您的APIKey + apikey := APIKeyInfo{ + ApiKey: "xxxx", + SecKey: "xxxx", + PassPhrase: "xxxx", + } + + // 第三个参数代表是否为模拟环境,更多信息查看接口说明 + cli := NewRESTClient("https://www.okex.win", &apikey, true) + rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) + if err != nil { + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", rsp.Code) + fmt.Println("\t总耗时: ", rsp.TotalUsedTime) + fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) + fmt.Println("\t返回消息: ", rsp.Body) + fmt.Println("\terrCode: ", rsp.V5Response.Code) + fmt.Println("\terrMsg: ", rsp.V5Response.Msg) + fmt.Println("\tdata: ", rsp.V5Response.Data) + ``` +更多示例请查看rest/rest_test.go + +## websocket订阅 + +### 私有频道 +```go + ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" + + // 填写您自己的APIKey信息 + apikey := "xxxx" + secretKey := "xxxxx" + passphrase := "xxxxx" + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + defer r.Stop() + + var res bool + // 私有频道需要登录 + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + fmt.Println("登录成功!") + } else { + fmt.Println("登录失败!", err) + return + } + + + var args []map[string]string + arg := make(map[string]string) + arg["ccy"] = "BTC" + args = append(args, arg) + + start := time.Now() + // 订阅账户频道 + res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!耗时:", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + + time.Sleep(100 * time.Second) + start = time.Now() + // 取消订阅账户频道 + res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + } +``` +更多示例请查看ws/ws_priv_channel_test.go + +### 公有频道 +```go + ep := "wss://ws.okex.com:8443/ws/v5/public?brokerId=9999" + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + + defer r.Stop() + + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = FUTURES + //arg["instType"] = OPTION + args = append(args, arg) + + start := time.Now() + + // 订阅产品频道 + res, _, err := r.PubInstruemnts(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + + time.Sleep(30 * time.Second) + + start = time.Now() + + // 取消订阅产品频道 + res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + } +``` +更多示例请查看ws/ws_pub_channel_test.go + +## websocket交易 +```go + ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" + + // 填写您自己的APIKey信息 + apikey := "xxxx" + secretKey := "xxxxx" + passphrase := "xxxxx" + + var res bool + var req_id string + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + + defer r.Stop() + + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + fmt.Println("登录成功!") + } else { + fmt.Println("登录失败!", err) + return + } + + start := time.Now() + param := map[string]interface{}{} + param["instId"] = "BTC-USDT" + param["tdMode"] = "cash" + param["side"] = "buy" + param["ordType"] = "market" + param["sz"] = "200" + req_id = "00001" + + // 单个下单 + res, _, err = r.PlaceOrder(req_id, param) + if res { + usedTime := time.Since(start) + fmt.Println("下单成功!", usedTime.String()) + } else { + usedTime := time.Since(start) + fmt.Println("下单失败!", usedTime.String(), err) + } + +``` +更多示例请查看ws/ws_jrpc_test.go + +## wesocket推送 +websocket推送数据分为两种类型数据:`普通推送数据`和`深度类型数据`。 + +```go +ws/wImpl/BookData.go + +// 普通推送 +type MsgData struct { + Arg map[string]string `json:"arg"` + Data []interface{} `json:"data"` +} + +// 深度数据 +type DepthData struct { + Arg map[string]string `json:"arg"` + Action string `json:"action"` + Data []DepthDetail `json:"data"` +} +``` +如果需要对推送数据做处理用户可以自定义回调函数: +1. 全局消息处理的回调函数 +该回调函数会处理所有从服务端接受到的数据。 +```go +/* + 添加全局消息处理的回调函数 +*/ +func (a *WsClient) AddMessageHook(fn ReceivedDataCallback) error { + a.onMessageHook = fn + return nil +} +``` +使用方法参见 ws/ws_test.go中测试用例TestAddMessageHook。 + +2. 订阅消息处理回调函数 +可以处理所有非深度类型的数据,包括 订阅/取消订阅,普通推送数据。 +```go +/* + 添加订阅消息处理的回调函数 +*/ +func (a *WsClient) AddBookMsgHook(fn ReceivedMsgDataCallback) error { + a.onBookMsgHook = fn + return nil +} +``` +使用方法参见 ws/ws_test.go中测试用例TestAddBookedDataHook。 + + +3. 深度消息处理的回调函数 +这里需要说明的是,Wsclient提供了深度数据管理和自动checksum的功能,用户如果需要关闭此功能,只需要调用EnableAutoDepthMgr方法。 +```go +/* + 添加深度消息处理的回调函数 +*/ +func (a *WsClient) AddDepthHook(fn ReceivedDepthDataCallback) error { + a.onDepthHook = fn + return nil +} +``` +使用方法参见 ws/ws_pub_channel_test.go中测试用例TestOrderBooks。 + +4. 错误消息类型回调函数 +```go +func (a *WsClient) AddErrMsgHook(fn ReceivedDataCallback) error { + a.OnErrorHook = fn + return nil +} +``` + +# 联系方式 +邮箱:caron_co@163.com +微信:caron_co diff --git a/okex/rest/contants.go b/okex/rest/contants.go new file mode 100644 index 0000000..4497aea --- /dev/null +++ b/okex/rest/contants.go @@ -0,0 +1,35 @@ +package rest + +const ( + + /* + http headers + */ + OK_ACCESS_KEY = "OK-ACCESS-KEY" + OK_ACCESS_SIGN = "OK-ACCESS-SIGN" + OK_ACCESS_TIMESTAMP = "OK-ACCESS-TIMESTAMP" + OK_ACCESS_PASSPHRASE = "OK-ACCESS-PASSPHRASE" + X_SIMULATE_TRADING = "x-simulated-trading" + + CONTENT_TYPE = "Content-Type" + ACCEPT = "Accept" + COOKIE = "Cookie" + LOCALE = "locale=" + + APPLICATION_JSON = "application/json" + APPLICATION_JSON_UTF8 = "application/json; charset=UTF-8" + + /* + i18n: internationalization + */ + ENGLISH = "en_US" + SIMPLIFIED_CHINESE = "zh_CN" + //zh_TW || zh_HK + TRADITIONAL_CHINESE = "zh_HK" + + GET = "GET" + POST = "POST" +) + + + diff --git a/okex/rest/go.mod b/okex/rest/go.mod new file mode 100644 index 0000000..f2adc9f --- /dev/null +++ b/okex/rest/go.mod @@ -0,0 +1,3 @@ +module v5sdk_go/rest + +go 1.14 diff --git a/okex/rest/go.sum b/okex/rest/go.sum new file mode 100644 index 0000000..5d9b33a --- /dev/null +++ b/okex/rest/go.sum @@ -0,0 +1,21 @@ +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/okex/rest/rest.go b/okex/rest/rest.go new file mode 100644 index 0000000..d764125 --- /dev/null +++ b/okex/rest/rest.go @@ -0,0 +1,331 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + . "v5sdk_go/utils" +) + +type RESTAPI struct { + EndPoint string `json:"endPoint"` + // GET/POST + Method string `json:"method"` + Uri string `json:"uri"` + Param map[string]interface{} `json:"param"` + Timeout time.Duration + ApiKeyInfo *APIKeyInfo + isSimulate bool +} + +type APIKeyInfo struct { + ApiKey string + PassPhrase string + SecKey string + UserId string +} + +type RESTAPIResult struct { + Url string `json:"url"` + Param string `json:"param"` + Header string `json:"header"` + Code int `json:"code"` + // 原始返回信息 + Body string `json:"body"` + // okexV5返回的数据 + V5Response Okexv5APIResponse `json:"v5Response"` + ReqUsedTime time.Duration `json:"reqUsedTime"` + TotalUsedTime time.Duration `json:"totalUsedTime"` +} + +type Okexv5APIResponse struct { + Code string `json:"code"` + Msg string `json:"msg"` + Data []map[string]interface{} `json:"data"` +} + +/* + endPoint:请求地址 + apiKey + isSimulate: 是否为模拟环境 +*/ +func NewRESTClient(endPoint string, apiKey *APIKeyInfo, isSimulate bool) *RESTAPI { + + res := &RESTAPI{ + EndPoint: endPoint, + ApiKeyInfo: apiKey, + isSimulate: isSimulate, + Timeout: 5 * time.Second, + } + return res +} + +func NewRESTAPI(ep, method, uri string, param *map[string]interface{}) *RESTAPI { + //TODO:参数校验 + reqParam := make(map[string]interface{}) + + if param != nil { + reqParam = *param + } + res := &RESTAPI{ + EndPoint: ep, + Method: method, + Uri: uri, + Param: reqParam, + Timeout: 150 * time.Second, + } + return res +} + +func (this *RESTAPI) SetSimulate(b bool) *RESTAPI { + this.isSimulate = b + return this +} + +func (this *RESTAPI) SetAPIKey(apiKey, secKey, passPhrase string) *RESTAPI { + if this.ApiKeyInfo == nil { + this.ApiKeyInfo = &APIKeyInfo{ + ApiKey: apiKey, + PassPhrase: passPhrase, + SecKey: secKey, + } + } else { + this.ApiKeyInfo.ApiKey = apiKey + this.ApiKeyInfo.PassPhrase = passPhrase + this.ApiKeyInfo.SecKey = secKey + } + return this +} + +func (this *RESTAPI) SetUserId(userId string) *RESTAPI { + if this.ApiKeyInfo == nil { + fmt.Println("ApiKey为空") + return this + } + + this.ApiKeyInfo.UserId = userId + return this +} + +func (this *RESTAPI) SetTimeOut(timeout time.Duration) *RESTAPI { + this.Timeout = timeout + return this +} + +// GET请求 +func (this *RESTAPI) Get(ctx context.Context, uri string, param *map[string]interface{}) (res *RESTAPIResult, err error) { + this.Method = GET + this.Uri = uri + + reqParam := make(map[string]interface{}) + + if param != nil { + reqParam = *param + } + this.Param = reqParam + return this.Run(ctx) +} + +// POST请求 +func (this *RESTAPI) Post(ctx context.Context, uri string, param *map[string]interface{}) (res *RESTAPIResult, err error) { + this.Method = POST + this.Uri = uri + + reqParam := make(map[string]interface{}) + + if param != nil { + reqParam = *param + } + this.Param = reqParam + + return this.Run(ctx) +} + +func (this *RESTAPI) Run(ctx context.Context) (res *RESTAPIResult, err error) { + + if this.ApiKeyInfo == nil { + err = errors.New("APIKey不可为空") + return + } + + procStart := time.Now() + + defer func() { + if res != nil { + res.TotalUsedTime = time.Since(procStart) + } + }() + + client := &http.Client{ + Timeout: this.Timeout, + } + + uri, body, err := this.GenReqInfo() + if err != nil { + return + } + + url := this.EndPoint + uri + bodyBuf := new(bytes.Buffer) + bodyBuf.ReadFrom(strings.NewReader(body)) + + req, err := http.NewRequest(this.Method, url, bodyBuf) + if err != nil { + return + } + + res = &RESTAPIResult{ + Url: url, + Param: body, + } + + // Sign and set request headers + timestamp := IsoTime() + preHash := PreHashString(timestamp, this.Method, uri, body) + //log.Println("preHash:", preHash) + sign, err := HmacSha256Base64Signer(preHash, this.ApiKeyInfo.SecKey) + if err != nil { + return + } + //log.Println("sign:", sign) + headStr := this.SetHeaders(req, timestamp, sign) + res.Header = headStr + + this.PrintRequest(req, body, preHash) + resp, err := client.Do(req) + if err != nil { + fmt.Println("请求失败!", err) + return + } + defer resp.Body.Close() + + res.ReqUsedTime = time.Since(procStart) + + resBuff, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("获取请求结果失败!", err) + return + } + + res.Body = string(resBuff) + res.Code = resp.StatusCode + + // 解析结果 + var v5rsp Okexv5APIResponse + err = json.Unmarshal(resBuff, &v5rsp) + if err != nil { + fmt.Println("解析v5返回失败!", err) + return + } + + res.V5Response = v5rsp + + return +} + +/* + 生成请求对应的参数 +*/ +func (this *RESTAPI) GenReqInfo() (uri string, body string, err error) { + uri = this.Uri + + switch this.Method { + case GET: + getParam := []string{} + + if len(this.Param) == 0 { + return + } + + for k, v := range this.Param { + getParam = append(getParam, fmt.Sprintf("%v=%v", k, v)) + } + uri = uri + "?" + strings.Join(getParam, "&") + + case POST: + + var rawBody []byte + rawBody, err = json.Marshal(this.Param) + if err != nil { + return + } + body = string(rawBody) + default: + err = errors.New("request type unknown!") + return + } + + return +} + +/* + Set http request headers: + Accept: application/json + Content-Type: application/json; charset=UTF-8 (default) + Cookie: locale=en_US (English) + OK-ACCESS-KEY: (Your setting) + OK-ACCESS-SIGN: (Use your setting, auto sign and add) + OK-ACCESS-TIMESTAMP: (Auto add) + OK-ACCESS-PASSPHRASE: Your setting +*/ +func (this *RESTAPI) SetHeaders(request *http.Request, timestamp string, sign string) (header string) { + + request.Header.Add(ACCEPT, APPLICATION_JSON) + header += ACCEPT + ":" + APPLICATION_JSON + "\n" + + request.Header.Add(CONTENT_TYPE, APPLICATION_JSON_UTF8) + header += CONTENT_TYPE + ":" + APPLICATION_JSON_UTF8 + "\n" + + request.Header.Add(COOKIE, LOCALE+ENGLISH) + header += COOKIE + ":" + LOCALE + ENGLISH + "\n" + + request.Header.Add(OK_ACCESS_KEY, this.ApiKeyInfo.ApiKey) + header += OK_ACCESS_KEY + ":" + this.ApiKeyInfo.ApiKey + "\n" + + request.Header.Add(OK_ACCESS_SIGN, sign) + header += OK_ACCESS_SIGN + ":" + sign + "\n" + + request.Header.Add(OK_ACCESS_TIMESTAMP, timestamp) + header += OK_ACCESS_TIMESTAMP + ":" + timestamp + "\n" + + request.Header.Add(OK_ACCESS_PASSPHRASE, this.ApiKeyInfo.PassPhrase) + header += OK_ACCESS_PASSPHRASE + ":" + this.ApiKeyInfo.PassPhrase + "\n" + + //模拟盘交易标记 + if this.isSimulate { + request.Header.Add(X_SIMULATE_TRADING, "1") + header += X_SIMULATE_TRADING + ":1" + "\n" + } + return +} + +/* + 打印header信息 +*/ +func (this *RESTAPI) PrintRequest(request *http.Request, body string, preHash string) { + if this.ApiKeyInfo.SecKey != "" { + fmt.Println(" Secret-Key: " + this.ApiKeyInfo.SecKey) + } + fmt.Println(" Request(" + IsoTime() + "):") + fmt.Println("\tUrl: " + request.URL.String()) + fmt.Println("\tMethod: " + strings.ToUpper(request.Method)) + if len(request.Header) > 0 { + fmt.Println("\tHeaders: ") + for k, v := range request.Header { + if strings.Contains(k, "Ok-") { + k = strings.ToUpper(k) + } + fmt.Println("\t\t" + k + ": " + v[0]) + } + } + fmt.Println("\tBody: " + body) + if preHash != "" { + fmt.Println(" PreHash: " + preHash) + } +} diff --git a/okex/rest/rest_test.go b/okex/rest/rest_test.go new file mode 100644 index 0000000..cc6c753 --- /dev/null +++ b/okex/rest/rest_test.go @@ -0,0 +1,100 @@ +package rest + +import ( + "context" + "fmt" + "testing" +) + +/* + GET请求 +*/ +func TestRESTAPIGet(t *testing.T) { + + rest := NewRESTAPI("https://www.okex.win", GET, "/api/v5/account/balance", nil) + rest.SetSimulate(true).SetAPIKey("xxxx", "xxxx", "xxxx") + rest.SetUserId("xxxxx") + response, err := rest.Run(context.Background()) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", response.Code) + fmt.Println("\t总耗时: ", response.TotalUsedTime) + fmt.Println("\t请求耗时: ", response.ReqUsedTime) + fmt.Println("\t返回消息: ", response.Body) + fmt.Println("\terrCode: ", response.V5Response.Code) + fmt.Println("\terrMsg: ", response.V5Response.Msg) + fmt.Println("\tdata: ", response.V5Response.Data) + + // 请求的另一种方式 + apikey := APIKeyInfo{ + ApiKey: "xxxxx", + SecKey: "xxxxx", + PassPhrase: "xxx", + } + + cli := NewRESTClient("https://www.okex.win", &apikey, true) + rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) + if err != nil { + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", rsp.Code) + fmt.Println("\t总耗时: ", rsp.TotalUsedTime) + fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) + fmt.Println("\t返回消息: ", rsp.Body) + fmt.Println("\terrCode: ", rsp.V5Response.Code) + fmt.Println("\terrMsg: ", rsp.V5Response.Msg) + fmt.Println("\tdata: ", rsp.V5Response.Data) +} + +/* + POST请求 +*/ +func TestRESTAPIPost(t *testing.T) { + param := make(map[string]interface{}) + param["greeksType"] = "PA" + + rest := NewRESTAPI("https://www.okex.win", POST, "/api/v5/account/set-greeks", ¶m) + rest.SetSimulate(true).SetAPIKey("xxxx", "xxxx", "xxxx") + response, err := rest.Run(context.Background()) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", response.Code) + fmt.Println("\t总耗时: ", response.TotalUsedTime) + fmt.Println("\t请求耗时: ", response.ReqUsedTime) + fmt.Println("\t返回消息: ", response.Body) + fmt.Println("\terrCode: ", response.V5Response.Code) + fmt.Println("\terrMsg: ", response.V5Response.Msg) + fmt.Println("\tdata: ", response.V5Response.Data) + + // 请求的另一种方式 + apikey := APIKeyInfo{ + ApiKey: "xxxx", + SecKey: "xxxxx", + PassPhrase: "xxxx", + } + + cli := NewRESTClient("https://www.okex.win", &apikey, true) + rsp, err := cli.Post(context.Background(), "/api/v5/account/set-greeks", ¶m) + if err != nil { + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", rsp.Code) + fmt.Println("\t总耗时: ", rsp.TotalUsedTime) + fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) + fmt.Println("\t返回消息: ", rsp.Body) + fmt.Println("\terrCode: ", rsp.V5Response.Code) + fmt.Println("\terrMsg: ", rsp.V5Response.Msg) + fmt.Println("\tdata: ", rsp.V5Response.Data) +} diff --git a/okex/utils/go.mod b/okex/utils/go.mod new file mode 100644 index 0000000..240946f --- /dev/null +++ b/okex/utils/go.mod @@ -0,0 +1,8 @@ +module v5sdk_go/utils + +go 1.15 + +require ( + github.com/gorilla/websocket v1.4.2 + github.com/stretchr/testify v1.7.0 +) diff --git a/okex/utils/go.sum b/okex/utils/go.sum new file mode 100644 index 0000000..5b6508a --- /dev/null +++ b/okex/utils/go.sum @@ -0,0 +1,7 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/okex/utils/utils.go b/okex/utils/utils.go new file mode 100644 index 0000000..830186d --- /dev/null +++ b/okex/utils/utils.go @@ -0,0 +1,102 @@ +package utils + +import ( + "bytes" + "compress/flate" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "io/ioutil" + "log" + "strconv" + "strings" + "time" + //"net/http" +) + +/* + Get a epoch time + eg: 1521221737 +*/ +func EpochTime() string { + millisecond := time.Now().UnixNano() / 1000000 + epoch := strconv.Itoa(int(millisecond)) + epochBytes := []byte(epoch) + epoch = string(epochBytes[:10]) + return epoch +} + +/* + signing a message + using: hmac sha256 + base64 + eg: + message = Pre_hash function comment + secretKey = E65791902180E9EF4510DB6A77F6EBAE + + return signed string = TO6uwdqz+31SIPkd4I+9NiZGmVH74dXi+Fd5X0EzzSQ= +*/ +func HmacSha256Base64Signer(message string, secretKey string) (string, error) { + mac := hmac.New(sha256.New, []byte(secretKey)) + _, err := mac.Write([]byte(message)) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil +} + +/* + the pre hash string + eg: + timestamp = 2018-03-08T10:59:25.789Z + method = POST + request_path = /orders?before=2&limit=30 + body = {"product_id":"BTC-USD-0309","order_id":"377454671037440"} + + return pre hash string = 2018-03-08T10:59:25.789ZPOST/orders?before=2&limit=30{"product_id":"BTC-USD-0309","order_id":"377454671037440"} +*/ +func PreHashString(timestamp string, method string, requestPath string, body string) string { + return timestamp + strings.ToUpper(method) + requestPath + body +} + +/* + struct convert json string +*/ +func Struct2JsonString(raw interface{}) (jsonString string, err error) { + //fmt.Println("转化json,", raw) + data, err := json.Marshal(raw) + if err != nil { + log.Println("convert json failed!", err) + return "", err + } + //log.Println(string(data)) + return string(data), nil +} + +// 解压缩消息 +func GzipDecode(in []byte) ([]byte, error) { + reader := flate.NewReader(bytes.NewReader(in)) + defer reader.Close() + + return ioutil.ReadAll(reader) +} + + +/* + Get a iso time + eg: 2018-03-16T18:02:48.284Z +*/ +func IsoTime() string { + utcTime := time.Now().UTC() + iso := utcTime.String() + isoBytes := []byte(iso) + iso = string(isoBytes[:10]) + "T" + string(isoBytes[11:23]) + "Z" + return iso +} + + + + + + + diff --git a/okex/utils/utils_test.go b/okex/utils/utils_test.go new file mode 100644 index 0000000..09bcf5b --- /dev/null +++ b/okex/utils/utils_test.go @@ -0,0 +1,17 @@ +package utils + +import ( + "fmt" + "testing" +) + +func TestHmacSha256Base64Signer(t *testing.T) { + raw := `2021-04-06T03:33:21.681ZPOST/api/v5/trade/order{"instId":"ETH-USDT-SWAP","ordType":"limit","px":"2300","side":"sell","sz":"1","tdMode":"cross"}` + key := "1A9E86759F2D2AA16E389FD3B7F8273E" + res, err := HmacSha256Base64Signer(raw, key) + if err != nil { + t.Fatal(err) + } + fmt.Println(res) + t.Log(res) +} diff --git a/okex/ws/go.mod b/okex/ws/go.mod new file mode 100644 index 0000000..4ed1f0a --- /dev/null +++ b/okex/ws/go.mod @@ -0,0 +1,8 @@ +module v5sdk_go/ws + +go 1.15 + +require ( + github.com/gorilla/websocket v1.4.2 + github.com/stretchr/testify v1.7.0 +) diff --git a/okex/ws/go.sum b/okex/ws/go.sum new file mode 100644 index 0000000..49362ff --- /dev/null +++ b/okex/ws/go.sum @@ -0,0 +1,28 @@ +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/okex/ws/utils.go b/okex/ws/utils.go new file mode 100644 index 0000000..3c75066 --- /dev/null +++ b/okex/ws/utils.go @@ -0,0 +1,75 @@ +package ws + +import ( + "errors" + "log" + "runtime/debug" + . "v5sdk_go/ws/wImpl" + . "v5sdk_go/ws/wInterface" +) + +// 判断返回结果成功失败 +func checkResult(wsReq WSReqData, wsRsps []*Msg) (res bool, err error) { + defer func() { + a := recover() + if a != nil { + log.Printf("Receive End. Recover msg: %+v", a) + debug.PrintStack() + } + return + }() + + res = false + if len(wsRsps) == 0 { + return + } + + for _, v := range wsRsps { + switch v.Info.(type) { + case ErrData: + return + } + if wsReq.GetType() != v.Info.(WSRspData).MsgType() { + err = errors.New("消息类型不一致") + return + } + } + + //检查所有频道是否都更新成功 + if wsReq.GetType() == MSG_NORMAL { + req, ok := wsReq.(ReqData) + if !ok { + log.Println("类型转化失败", req) + err = errors.New("类型转化失败") + return + } + + for idx, _ := range req.Args { + ok := false + i_req := req.Args[idx] + //fmt.Println("检查",i_req) + for i, _ := range wsRsps { + info, _ := wsRsps[i].Info.(RspData) + //fmt.Println("<<",info) + if info.Event == req.Op && info.Arg["channel"] == i_req["channel"] && info.Arg["instType"] == i_req["instType"] { + ok = true + continue + } + } + if !ok { + err = errors.New("未得到所有的期望的返回结果") + return + } + } + } else { + for i, _ := range wsRsps { + info, _ := wsRsps[i].Info.(JRPCRsp) + if info.Code != "0" { + return + } + } + } + + res = true + return +} diff --git a/okex/ws/wImpl/BookData.go b/okex/ws/wImpl/BookData.go new file mode 100644 index 0000000..8867c8d --- /dev/null +++ b/okex/ws/wImpl/BookData.go @@ -0,0 +1,226 @@ +/* + 订阅频道后收到的推送数据 +*/ + +package wImpl + +import ( + "bytes" + "errors" + "fmt" + "hash/crc32" + "log" + "strconv" + "strings" +) + +// 普通推送 +type MsgData struct { + Arg map[string]string `json:"arg"` + Data []interface{} `json:"data"` +} + +// 深度数据 +type DepthData struct { + Arg map[string]string `json:"arg"` + Action string `json:"action"` + Data []DepthDetail `json:"data"` +} + +type DepthDetail struct { + Asks [][]string `json:"asks"` + Bids [][]string `json:"bids"` + Ts string `json:"ts"` + Checksum int32 `json:"checksum"` +} + +/* + 深度数据校验 +*/ +func (this *DepthData) CheckSum(snap *DepthDetail) (pDepData *DepthDetail, err error) { + + if len(this.Data) != 1 { + err = errors.New("深度数据错误!") + return + } + + if this.Action == DEPTH_SNAPSHOT { + _, cs := CalCrc32(this.Data[0].Asks, this.Data[0].Bids) + + if cs != this.Data[0].Checksum { + err = errors.New("校验失败!") + return + } + pDepData = &this.Data[0] + log.Println("snapshot校验成功", this.Data[0].Checksum) + + } + + if this.Action == DEPTH_UPDATE { + if snap == nil { + err = errors.New("深度快照数据不可为空!") + return + } + + pDepData, err = MergDepthData(*snap, this.Data[0], this.Data[0].Checksum) + if err != nil { + return + } + log.Println("update校验成功", this.Data[0].Checksum) + } + + return +} + +func CalCrc32(askDepths [][]string, bidDepths [][]string) (bytes.Buffer, int32) { + + crc32BaseBuffer := bytes.Buffer{} + crcAskDepth, crcBidDepth := 25, 25 + + if len(askDepths) < 25 { + crcAskDepth = len(askDepths) + } + if len(bidDepths) < 25 { + crcBidDepth = len(bidDepths) + } + if crcAskDepth == crcBidDepth { + for i := 0; i < crcAskDepth; i++ { + if crc32BaseBuffer.Len() > 0 { + crc32BaseBuffer.WriteString(":") + } + crc32BaseBuffer.WriteString( + fmt.Sprintf("%v:%v:%v:%v", + (bidDepths)[i][0], (bidDepths)[i][1], + (askDepths)[i][0], (askDepths)[i][1])) + } + } else { + + var crcArr []string + for i, j := 0, 0; i < crcBidDepth || j < crcAskDepth; { + + if i < crcBidDepth { + crcArr = append(crcArr, fmt.Sprintf("%v:%v", (bidDepths)[i][0], (bidDepths)[i][1])) + i++ + } + + if j < crcAskDepth { + crcArr = append(crcArr, fmt.Sprintf("%v:%v", (askDepths)[j][0], (askDepths)[j][1])) + j++ + } + } + + crc32BaseBuffer.WriteString(strings.Join(crcArr, ":")) + } + + expectCrc32 := int32(crc32.ChecksumIEEE(crc32BaseBuffer.Bytes())) + + return crc32BaseBuffer, expectCrc32 +} + +/* + 深度合并的内部方法 + 返回结果: + res:合并后的深度 + index: 最新的 ask1/bids1 的索引 +*/ +func mergeDepth(oldDepths [][]string, newDepths [][]string, method string) (res [][]string, err error) { + + oldIdx, newIdx := 0, 0 + + for oldIdx < len(oldDepths) && newIdx < len(newDepths) { + + oldItem := oldDepths[oldIdx] + newItem := newDepths[newIdx] + var oldPrice, newPrice float64 + oldPrice, err = strconv.ParseFloat(oldItem[0], 10) + if err != nil { + return + } + newPrice, err = strconv.ParseFloat(newItem[0], 10) + if err != nil { + return + } + + if oldPrice == newPrice { + if newItem[1] != "0" { + res = append(res, newItem) + } + + oldIdx++ + newIdx++ + } else { + switch method { + // 降序 + case "bids": + if oldPrice < newPrice { + res = append(res, newItem) + newIdx++ + } else { + + res = append(res, oldItem) + oldIdx++ + } + // 升序 + case "asks": + if oldPrice > newPrice { + res = append(res, newItem) + newIdx++ + } else { + + res = append(res, oldItem) + oldIdx++ + } + } + } + + } + + if oldIdx < len(oldDepths) { + res = append(res, oldDepths[oldIdx:]...) + } + + if newIdx < len(newDepths) { + res = append(res, newDepths[newIdx:]...) + } + + return +} + +/* + 深度合并,并校验 +*/ +func MergDepthData(snap DepthDetail, update DepthDetail, expChecksum int32) (res *DepthDetail, err error) { + + newAskDepths, err1 := mergeDepth(snap.Asks, update.Asks, "asks") + if err1 != nil { + return + } + + // log.Println("old Ask - ", snap.Asks) + // log.Println("update Ask - ", update.Asks) + // log.Println("new Ask - ", newAskDepths) + newBidDepths, err2 := mergeDepth(snap.Bids, update.Bids, "bids") + if err2 != nil { + return + } + // log.Println("old Bids - ", snap.Bids) + // log.Println("update Bids - ", update.Bids) + // log.Println("new Bids - ", newBidDepths) + + cBuf, checksum := CalCrc32(newAskDepths, newBidDepths) + if checksum != expChecksum { + err = errors.New("校验失败!") + log.Println("buffer:", cBuf.String()) + log.Fatal(checksum, expChecksum) + return + } + + res = &DepthDetail{ + Asks: newAskDepths, + Bids: newBidDepths, + Ts: update.Ts, + Checksum: update.Checksum, + } + + return +} diff --git a/okex/ws/wImpl/ErrData.go b/okex/ws/wImpl/ErrData.go new file mode 100644 index 0000000..020164b --- /dev/null +++ b/okex/ws/wImpl/ErrData.go @@ -0,0 +1,13 @@ +/* + 错误数据 +*/ +package wImpl + +// 服务端请求错误返回消息格式 +type ErrData struct { + Event string `json:"event"` + Code string `json:"code"` + Msg string `json:"msg"` +} + + diff --git a/okex/ws/wImpl/JRPCData.go b/okex/ws/wImpl/JRPCData.go new file mode 100644 index 0000000..cfd3223 --- /dev/null +++ b/okex/ws/wImpl/JRPCData.go @@ -0,0 +1,50 @@ +/* + JRPC请求/响应数据 +*/ +package wImpl + +import ( + "encoding/json" + . "v5sdk_go/utils" +) + +// jrpc请求结构体 +type JRPCReq struct { + Id string `json:"id"` + Op string `json:"op"` + Args []map[string]interface{} `json:"args"` +} + +func (r JRPCReq) GetType() int { + return MSG_JRPC +} + +func (r JRPCReq) ToString() string { + data, err := Struct2JsonString(r) + if err != nil { + return "" + } + return data +} + +func (r JRPCReq) Len() int { + return 1 +} + +// jrpc响应结构体 +type JRPCRsp struct { + Id string `json:"id"` + Op string `json:"op"` + Data []map[string]interface{} `json:"data"` + Code string `json:"code"` + Msg string `json:"msg"` +} + +func (r JRPCRsp) MsgType() int { + return MSG_JRPC +} + +func (r JRPCRsp) String() string { + raw, _ := json.Marshal(r) + return string(raw) +} diff --git a/okex/ws/wImpl/ReqData.go b/okex/ws/wImpl/ReqData.go new file mode 100644 index 0000000..df7afa1 --- /dev/null +++ b/okex/ws/wImpl/ReqData.go @@ -0,0 +1,47 @@ +/* + 普通订阅请求和响应的数据格式 +*/ + +package wImpl + +import ( + "encoding/json" + . "v5sdk_go/utils" +) + +// 客户端请求消息格式 +type ReqData struct { + Op string `json:"op"` + Args []map[string]string `json:"args"` +} + +func (r ReqData) GetType() int { + return MSG_NORMAL +} + +func (r ReqData) ToString() string { + data, err := Struct2JsonString(r) + if err != nil { + return "" + } + return data +} + +func (r ReqData) Len() int { + return len(r.Args) +} + +// 服务端请求响应消息格式 +type RspData struct { + Event string `json:"event"` + Arg map[string]string `json:"arg"` +} + +func (r RspData) MsgType() int { + return MSG_NORMAL +} + +func (r RspData) String() string { + raw, _ := json.Marshal(r) + return string(raw) +} diff --git a/okex/ws/wImpl/contants.go b/okex/ws/wImpl/contants.go new file mode 100644 index 0000000..c095fae --- /dev/null +++ b/okex/ws/wImpl/contants.go @@ -0,0 +1,241 @@ +package wImpl + +import ( + "regexp" +) + +/* + + */ + +const ( + MSG_NORMAL = iota + MSG_JRPC +) + +//事件 +type Event int + +/* + EventID +*/ +const ( + EVENT_UNKNOWN Event = iota + EVENT_ERROR + EVENT_PING + EVENT_LOGIN + + //订阅公共频道 + EVENT_BOOK_INSTRUMENTS + EVENT_STATUS + EVENT_BOOK_TICKERS + EVENT_BOOK_OPEN_INTEREST + EVENT_BOOK_KLINE + EVENT_BOOK_TRADE + EVENT_BOOK_ESTIMATE_PRICE + EVENT_BOOK_MARK_PRICE + EVENT_BOOK_MARK_PRICE_CANDLE_CHART + EVENT_BOOK_LIMIT_PRICE + EVENT_BOOK_ORDER_BOOK + EVENT_BOOK_ORDER_BOOK5 + EVENT_BOOK_ORDER_BOOK_TBT + EVENT_BOOK_ORDER_BOOK50_TBT + EVENT_BOOK_OPTION_SUMMARY + EVENT_BOOK_FUND_RATE + EVENT_BOOK_KLINE_INDEX + EVENT_BOOK_INDEX_TICKERS + + //订阅私有频道 + EVENT_BOOK_ACCOUNT + EVENT_BOOK_POSTION + EVENT_BOOK_ORDER + EVENT_BOOK_ALG_ORDER + EVENT_BOOK_B_AND_P + + // JRPC + EVENT_PLACE_ORDER + EVENT_PLACE_BATCH_ORDERS + EVENT_CANCEL_ORDER + EVENT_CANCEL_BATCH_ORDERS + EVENT_AMEND_ORDER + EVENT_AMEND_BATCH_ORDERS + + //订阅返回数据 + EVENT_BOOKED_DATA + EVENT_DEPTH_DATA +) + +/* + EventID,事件名称,channel + 注: 带有周期参数的频道 如 行情频道 ,需要将channel写为 正则表达模式方便 类型匹配,如 "^candle*" +*/ +var EVENT_TABLE = [][]interface{}{ + // 未知的消息 + {EVENT_UNKNOWN, "未知", ""}, + // 错误的消息 + {EVENT_ERROR, "错误", ""}, + // Ping + {EVENT_PING, "ping", ""}, + // 登陆 + {EVENT_LOGIN, "登录", ""}, + + /* + 订阅公共频道 + */ + + {EVENT_BOOK_INSTRUMENTS, "产品", "instruments"}, + {EVENT_STATUS, "status", "status"}, + {EVENT_BOOK_TICKERS, "行情", "tickers"}, + {EVENT_BOOK_OPEN_INTEREST, "持仓总量", "open-interest"}, + {EVENT_BOOK_KLINE, "K线", "candle"}, + {EVENT_BOOK_TRADE, "交易", "trades"}, + {EVENT_BOOK_ESTIMATE_PRICE, "预估交割/行权价格", "estimated-price"}, + {EVENT_BOOK_MARK_PRICE, "标记价格", "mark-price"}, + {EVENT_BOOK_MARK_PRICE_CANDLE_CHART, "标记价格K线", "mark-price-candle"}, + {EVENT_BOOK_LIMIT_PRICE, "限价", "price-limit"}, + {EVENT_BOOK_ORDER_BOOK, "400档深度", "books"}, + {EVENT_BOOK_ORDER_BOOK5, "5档深度", "books5"}, + {EVENT_BOOK_ORDER_BOOK_TBT, "tbt深度", "books-l2-tbt"}, + {EVENT_BOOK_ORDER_BOOK50_TBT, "tbt50深度", "books50-l2-tbt"}, + {EVENT_BOOK_OPTION_SUMMARY, "期权定价", "opt-summary"}, + {EVENT_BOOK_FUND_RATE, "资金费率", "funding-rate"}, + {EVENT_BOOK_KLINE_INDEX, "指数K线", "index-candle"}, + {EVENT_BOOK_INDEX_TICKERS, "指数行情", "index-tickers"}, + + /* + 订阅私有频道 + */ + {EVENT_BOOK_ACCOUNT, "账户", "account"}, + {EVENT_BOOK_POSTION, "持仓", "positions"}, + {EVENT_BOOK_ORDER, "订单", "orders"}, + {EVENT_BOOK_ALG_ORDER, "策略委托订单", "orders-algo"}, + {EVENT_BOOK_B_AND_P, "账户余额和持仓", "balance_and_position"}, + + /* + JRPC + */ + {EVENT_PLACE_ORDER, "下单", "order"}, + {EVENT_PLACE_BATCH_ORDERS, "批量下单", "batch-orders"}, + {EVENT_CANCEL_ORDER, "撤单", "cancel-order"}, + {EVENT_CANCEL_BATCH_ORDERS, "批量撤单", "batch-cancel-orders"}, + {EVENT_AMEND_ORDER, "改单", "amend-order"}, + {EVENT_AMEND_BATCH_ORDERS, "批量改单", "batch-amend-orders"}, + + /* + 订阅返回数据 + 注意:推送数据channle统一为"" + */ + {EVENT_BOOKED_DATA, "普通推送", ""}, + {EVENT_DEPTH_DATA, "深度推送", ""}, +} + +/* + 获取事件名称 +*/ +func (e Event) String() string { + for _, v := range EVENT_TABLE { + eventId := v[0].(Event) + if e == eventId { + return v[1].(string) + } + } + + return "" +} + +/* + 通过事件获取对应的channel信息 + 对于频道名称有时间周期的 通过参数 pd 传入,拼接后返回完整channel信息 +*/ +func (e Event) GetChannel(pd Period) string { + + channel := "" + + for _, v := range EVENT_TABLE { + eventId := v[0].(Event) + if e == eventId { + channel = v[2].(string) + break + } + } + + if channel == "" { + return "" + } + + return channel + string(pd) +} + +/* + 通过channel信息匹配获取事件类型 +*/ +func GetEventId(raw string) Event { + evt := EVENT_UNKNOWN + + for _, v := range EVENT_TABLE { + channel := v[2].(string) + if raw == channel { + evt = v[0].(Event) + break + } + + regexp := regexp.MustCompile(`^(.*)([1-9][0-9]?[\w])$`) + //regexp := regexp.MustCompile(`^http://www.flysnow.org/([\d]{4})/([\d]{2})/([\d]{2})/([\w-]+).html$`) + + substr := regexp.FindStringSubmatch(raw) + //fmt.Println(substr) + if len(substr) >= 2 { + if substr[1] == channel { + evt = v[0].(Event) + break + } + } + } + + return evt +} + +// 时间维度 +type Period string + +const ( + // 年 + PERIOD_1YEAR Period = "1Y" + + // 月 + PERIOD_6Mon Period = "6M" + PERIOD_3Mon Period = "3M" + PERIOD_1Mon Period = "1M" + + // 周 + PERIOD_1WEEK Period = "1W" + + // 天 + PERIOD_5DAY Period = "5D" + PERIOD_3DAY Period = "3D" + PERIOD_2DAY Period = "2D" + PERIOD_1DAY Period = "1D" + + // 小时 + PERIOD_12HOUR Period = "12H" + PERIOD_6HOUR Period = "6H" + PERIOD_4HOUR Period = "4H" + PERIOD_2HOUR Period = "2H" + PERIOD_1HOUR Period = "1H" + + // 分钟 + PERIOD_30MIN Period = "30m" + PERIOD_15MIN Period = "15m" + PERIOD_5MIN Period = "5m" + PERIOD_3MIN Period = "3m" + PERIOD_1MIN Period = "1m" + + // 缺省 + PERIOD_NONE Period = "" +) + +// 深度枚举 +const ( + DEPTH_SNAPSHOT = "snapshot" + DEPTH_UPDATE = "update" +) diff --git a/okex/ws/wImpl/contants_test.go b/okex/ws/wImpl/contants_test.go new file mode 100644 index 0000000..b4186ba --- /dev/null +++ b/okex/ws/wImpl/contants_test.go @@ -0,0 +1,28 @@ +package wImpl + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetEventId(t *testing.T) { + + id1 := GetEventId("index-candle30m") + + assert.True(t, id1 == EVENT_BOOK_KLINE_INDEX) + + id2 := GetEventId("candle1Y") + + assert.True(t, id2 == EVENT_BOOK_KLINE) + + id3 := GetEventId("orders-algo") + assert.True(t, id3 == EVENT_BOOK_ALG_ORDER) + + id4 := GetEventId("balance_and_position") + assert.True(t, id4 == EVENT_BOOK_B_AND_P) + + id5 := GetEventId("index-candle1m") + assert.True(t, id5 == EVENT_BOOK_KLINE_INDEX) + +} diff --git a/okex/ws/wInterface/IParam.go b/okex/ws/wInterface/IParam.go new file mode 100644 index 0000000..72f10e1 --- /dev/null +++ b/okex/ws/wInterface/IParam.go @@ -0,0 +1,9 @@ +package wInterface + +import . "v5sdk_go/ws/wImpl" + +// 请求数据 +type WSParam interface { + EventType() Event + ToMap() *map[string]string +} diff --git a/okex/ws/wInterface/IReqData.go b/okex/ws/wInterface/IReqData.go new file mode 100644 index 0000000..3bd299e --- /dev/null +++ b/okex/ws/wInterface/IReqData.go @@ -0,0 +1,8 @@ +package wInterface + +// 请求数据 +type WSReqData interface { + GetType() int + Len() int + ToString() string +} diff --git a/okex/ws/wInterface/IRspData.go b/okex/ws/wInterface/IRspData.go new file mode 100644 index 0000000..5282d71 --- /dev/null +++ b/okex/ws/wInterface/IRspData.go @@ -0,0 +1,6 @@ +package wInterface + +// 返回数据 +type WSRspData interface { + MsgType() int +} diff --git a/okex/ws/ws_AddBookedDataHook_test.go b/okex/ws/ws_AddBookedDataHook_test.go new file mode 100644 index 0000000..70ff328 --- /dev/null +++ b/okex/ws/ws_AddBookedDataHook_test.go @@ -0,0 +1,147 @@ +package ws + +// HOW TO RUN +// go test ws_cli.go ws_op.go ws_contants.go utils.go ws_pub_channel.go ws_pub_channel_test.go ws_priv_channel.go ws_priv_channel_test.go ws_jrpc.go ws_jrpc_test.go ws_test_AddBookedDataHook.go -v +import ( + "fmt" + "log" + "testing" + "time" + . "v5sdk_go/ws/wImpl" +) + +const ( + TRADE_ACCOUNT = iota + ISOLATE_ACCOUNT + CROSS_ACCOUNT + CROSS_ACCOUNT_B +) + +func init() { + log.SetFlags(log.LstdFlags | log.Llongfile) + +} + +func prework() *WsClient { + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err, ep) + } + return r +} +func prework_pri(t int) *WsClient { + // 模拟环境 + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + var apikey, passphrase, secretKey string + // 把账号密码写这里 + switch t { + case TRADE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case ISOLATE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT_B: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + } + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err) + } + + var res bool + start := time.Now() + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + usedTime := time.Since(start) + fmt.Println("登录成功!", usedTime.String()) + } else { + log.Fatal("登录失败!", err) + } + fmt.Println(apikey, secretKey, passphrase) + return r +} + +func TestAddBookedDataHook(t *testing.T) { + var r *WsClient + + /*订阅私有频道*/ + { + r = prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + r.AddBookMsgHook(func(ts time.Time, data MsgData) error { + // 添加你的方法 + fmt.Println("这是自定义AddBookMsgHook") + fmt.Println("当前数据是", data) + return nil + }) + + param := map[string]string{} + param["channel"] = "account" + param["ccy"] = "BTC" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(100 * time.Second) + } + + //订阅公共频道 + { + r = prework() + var res bool + var err error + + // r.AddBookMsgHook(func(ts time.Time, data MsgData) error { + // 添加你的方法 + // fmt.Println("这是公共自定义AddBookMsgHook") + // fmt.Println("当前数据是", data) + // return nil + // }) + + param := map[string]string{} + param["channel"] = "instruments" + param["instType"] = "FUTURES" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + select {} + } + +} diff --git a/okex/ws/ws_cli.go b/okex/ws/ws_cli.go new file mode 100644 index 0000000..f06fd86 --- /dev/null +++ b/okex/ws/ws_cli.go @@ -0,0 +1,725 @@ +package ws + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "regexp" + "runtime/debug" + "sync" + "time" + . "v5sdk_go/config" + . "v5sdk_go/utils" + . "v5sdk_go/ws/wImpl" + + "github.com/gorilla/websocket" +) + +// 全局回调函数 +type ReceivedDataCallback func(*Msg) error + +// 普通订阅推送数据回调函数 +type ReceivedMsgDataCallback func(time.Time, MsgData) error + +// 深度订阅推送数据回调函数 +type ReceivedDepthDataCallback func(time.Time, DepthData) error + +// websocket +type WsClient struct { + WsEndPoint string + WsApi *ApiInfo + conn *websocket.Conn + sendCh chan string //发消息队列 + resCh chan *Msg //收消息队列 + + errCh chan *Msg + regCh map[Event]chan *Msg //请求响应队列 + + quitCh chan struct{} + lock sync.RWMutex + + onMessageHook ReceivedDataCallback //全局消息回调函数 + onBookMsgHook ReceivedMsgDataCallback //普通订阅消息回调函数 + onDepthHook ReceivedDepthDataCallback //深度订阅消息回调函数 + OnErrorHook ReceivedDataCallback //错误处理回调函数 + + // 记录深度信息 + DepthDataList map[string]DepthDetail + autoDepthMgr bool // 深度数据管理(checksum等) + DepthDataLock sync.RWMutex + + isStarted bool //防止重复启动和关闭 + dailTimeout time.Duration +} + +/* + 服务端响应详细信息 + Timestamp: 接受到消息的时间 + Info: 接受到的消息字符串 +*/ +type Msg struct { + Timestamp time.Time `json:"timestamp"` + Info interface{} `json:"info"` +} + +func (this *Msg) Print() { + fmt.Println("【消息时间】", this.Timestamp.Format("2006-01-02 15:04:05.000")) + str, _ := json.Marshal(this.Info) + fmt.Println("【消息内容】", string(str)) +} + +/* + 订阅结果封装后的消息结构体 +*/ +type ProcessDetail struct { + EndPoint string `json:"endPoint"` + ReqInfo string `json:"ReqInfo"` //订阅请求 + SendTime time.Time `json:"sendTime"` //发送订阅请求的时间 + RecvTime time.Time `json:"recvTime"` //接受到订阅结果的时间 + UsedTime time.Duration `json:"UsedTime"` //耗时 + Data []*Msg `json:"data"` //订阅结果数据 +} + +func (p *ProcessDetail) String() string { + data, _ := json.Marshal(p) + return string(data) +} + +// 创建ws对象 +func NewWsClient(ep string) (r *WsClient, err error) { + if ep == "" { + err = errors.New("websocket endpoint cannot be null") + return + } + + r = &WsClient{ + WsEndPoint: ep, + sendCh: make(chan string), + resCh: make(chan *Msg), + errCh: make(chan *Msg), + regCh: make(map[Event]chan *Msg), + //cbs: make(map[Event]ReceivedDataCallback), + quitCh: make(chan struct{}), + DepthDataList: make(map[string]DepthDetail), + dailTimeout: time.Second * 10, + // 自动深度校验默认开启 + autoDepthMgr: true, + } + + return +} + +/* + 新增记录深度信息 +*/ +func (a *WsClient) addDepthDataList(key string, dd DepthDetail) error { + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + a.DepthDataList[key] = dd + return nil +} + +/* + 更新记录深度信息(如果没有记录不会更新成功) +*/ +func (a *WsClient) updateDepthDataList(key string, dd DepthDetail) error { + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + if _, ok := a.DepthDataList[key]; !ok { + return errors.New("更新失败!未发现记录" + key) + } + + a.DepthDataList[key] = dd + return nil +} + +/* + 删除记录深度信息 +*/ +func (a *WsClient) deleteDepthDataList(key string) error { + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + delete(a.DepthDataList, key) + return nil +} + +/* + 设置是否自动深度管理,开启 true,关闭 false +*/ +func (a *WsClient) EnableAutoDepthMgr(b bool) error { + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + + if len(a.DepthDataList) != 0 { + err := errors.New("当前有深度数据处于订阅中") + return err + } + + a.autoDepthMgr = b + return nil +} + +/* + 获取当前的深度快照信息(合并后的) +*/ +func (a *WsClient) GetSnapshotByChannel(data DepthData) (snapshot *DepthDetail, err error) { + key, err := json.Marshal(data.Arg) + if err != nil { + return + } + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + val, ok := a.DepthDataList[string(key)] + if !ok { + return + } + snapshot = new(DepthDetail) + raw, err := json.Marshal(val) + if err != nil { + return + } + err = json.Unmarshal(raw, &snapshot) + if err != nil { + return + } + return +} + +// 设置dial超时时间 +func (a *WsClient) SetDailTimeout(tm time.Duration) { + a.dailTimeout = tm +} + +// 非阻塞启动 +func (a *WsClient) Start() error { + a.lock.RLock() + if a.isStarted { + a.lock.RUnlock() + fmt.Println("ws已经启动") + return nil + } else { + a.lock.RUnlock() + a.lock.Lock() + defer a.lock.Unlock() + // 增加超时处理 + done := make(chan struct{}) + ctx, cancel := context.WithTimeout(context.Background(), a.dailTimeout) + defer cancel() + go func() { + defer func() { + close(done) + }() + var c *websocket.Conn + fmt.Println("a.WsEndPoint: ", a.WsEndPoint) + c, _, err := websocket.DefaultDialer.Dial(a.WsEndPoint, nil) + if err != nil { + err = errors.New("dial error:" + err.Error()) + return + } + a.conn = c + + }() + select { + case <-ctx.Done(): + err := errors.New("连接超时退出!") + return err + case <-done: + + } + //TODO 自定义的推送消息回调回来试试放在这里 + go a.receive() + go a.work() + a.isStarted = true + log.Println("客户端已启动!", a.WsEndPoint) + return nil + } +} + +// 客户端退出消息channel +func (a *WsClient) IsQuit() <-chan struct{} { + return a.quitCh +} + +func (a *WsClient) work() { + defer func() { + a.Stop() + err := recover() + if err != nil { + log.Printf("work End. Recover msg: %+v", a) + debug.PrintStack() + } + + }() + + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: // 保持心跳 + // go a.Ping(1000) + go func() { + _, _, err := a.Ping(1000) + if err != nil { + fmt.Println("心跳检测失败!", err) + a.Stop() + return + } + + }() + + case <-a.quitCh: // 保持心跳 + return + case data, ok := <-a.resCh: //接收到服务端发来的消息 + if !ok { + return + } + //log.Println("接收到来自resCh的消息:", data) + if a.onMessageHook != nil { + err := a.onMessageHook(data) + if err != nil { + log.Println("执行onMessageHook函数错误!", err) + } + } + case errMsg, ok := <-a.errCh: //错误处理 + if !ok { + return + } + if a.OnErrorHook != nil { + err := a.OnErrorHook(errMsg) + if err != nil { + log.Println("执行OnErrorHook函数错误!", err) + } + } + case req, ok := <-a.sendCh: //从发送队列中取出数据发送到服务端 + if !ok { + return + } + //log.Println("接收到来自req的消息:", req) + err := a.conn.WriteMessage(websocket.TextMessage, []byte(req)) + if err != nil { + log.Printf("发送请求失败: %s\n", err) + return + } + log.Printf("[发送请求] %v\n", req) + } + } + +} + +/* + 处理接受到的消息 +*/ +func (a *WsClient) receive() { + defer func() { + a.Stop() + err := recover() + if err != nil { + log.Printf("Receive End. Recover msg: %+v", a) + debug.PrintStack() + } + + }() + + for { + messageType, message, err := a.conn.ReadMessage() + if err != nil { + if a.isStarted { + log.Println("receive message error!" + err.Error()) + } + + break + } + + txtMsg := message + switch messageType { + case websocket.TextMessage: + case websocket.BinaryMessage: + txtMsg, err = GzipDecode(message) + if err != nil { + log.Println("解压失败!") + continue + } + } + + log.Println("[收到消息]", string(txtMsg)) + + //发送结果到默认消息处理通道 + + timestamp := time.Now() + msg := &Msg{Timestamp: timestamp, Info: string(txtMsg)} + + a.resCh <- msg + + evt, data, err := a.parseMessage(txtMsg) + if err != nil { + log.Println("解析消息失败!", err) + continue + } + + //log.Println("解析消息成功!消息类型 =", evt) + + a.lock.RLock() + ch, ok := a.regCh[evt] + a.lock.RUnlock() + if !ok { + //只有推送消息才会主动创建通道和消费队列 + if evt == EVENT_BOOKED_DATA || evt == EVENT_DEPTH_DATA { + //log.Println("channel不存在!event:", evt) + //a.lock.RUnlock() + a.lock.Lock() + a.regCh[evt] = make(chan *Msg) + ch = a.regCh[evt] + a.lock.Unlock() + + //log.Println("创建", evt, "通道") + + // 创建消费队列 + go func(evt Event) { + //log.Println("创建goroutine evt:", evt) + + for msg := range a.regCh[evt] { + //log.Println(msg) + // msg.Print() + switch evt { + // 处理普通推送数据 + case EVENT_BOOKED_DATA: + fn := a.onBookMsgHook + if fn != nil { + err = fn(msg.Timestamp, msg.Info.(MsgData)) + if err != nil { + log.Println("订阅数据回调函数执行失败!", err) + } + //log.Println("函数执行成功!", err) + } + // 处理深度推送数据 + case EVENT_DEPTH_DATA: + fn := a.onDepthHook + + depData := msg.Info.(DepthData) + + // 开启深度数据管理功能的,会合并深度数据 + if a.autoDepthMgr { + a.MergeDepth(depData) + } + + // 运行用户定义回调函数 + if fn != nil { + err = fn(msg.Timestamp, msg.Info.(DepthData)) + if err != nil { + log.Println("深度回调函数执行失败!", err) + } + + } + } + + } + //log.Println("退出goroutine evt:", evt) + }(evt) + + //continue + } else { + //log.Println("程序异常!通道已关闭", evt) + continue + } + + } + + //log.Println(evt,"事件已注册",ch) + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Millisecond*1000) + defer cancel() + select { + /* + 丢弃消息容易引发数据处理处理错误 + */ + // case <-ctx.Done(): + // log.Println("等待超时,消息丢弃 - ", data) + case ch <- &Msg{Timestamp: timestamp, Info: data}: + } + } +} + +/* + 开启了深度数据管理功能后,系统会自动合并深度信息 +*/ +func (a *WsClient) MergeDepth(depData DepthData) (err error) { + if !a.autoDepthMgr { + return + } + + key, err := json.Marshal(depData.Arg) + if err != nil { + err = errors.New("数据错误") + return + } + + // books5 不需要做checksum + if depData.Arg["channel"] == "books5" { + a.addDepthDataList(string(key), depData.Data[0]) + return + } + + if depData.Action == "snapshot" { + + _, err = depData.CheckSum(nil) + if err != nil { + log.Println("校验失败", err) + return + } + + a.addDepthDataList(string(key), depData.Data[0]) + + } else { + + var newSnapshot *DepthDetail + a.DepthDataLock.RLock() + oldSnapshot, ok := a.DepthDataList[string(key)] + if !ok { + log.Println("深度数据错误,全量数据未发现!") + err = errors.New("数据错误") + return + } + a.DepthDataLock.RUnlock() + newSnapshot, err = depData.CheckSum(&oldSnapshot) + if err != nil { + log.Println("深度校验失败", err) + err = errors.New("校验失败") + return + } + + a.updateDepthDataList(string(key), *newSnapshot) + } + return +} + +/* + 通过ErrorCode判断事件类型 +*/ +func GetInfoFromErrCode(data ErrData) Event { + switch data.Code { + case "60001": + return EVENT_LOGIN + case "60002": + return EVENT_LOGIN + case "60003": + return EVENT_LOGIN + case "60004": + return EVENT_LOGIN + case "60005": + return EVENT_LOGIN + case "60006": + return EVENT_LOGIN + case "60007": + return EVENT_LOGIN + case "60008": + return EVENT_LOGIN + case "60009": + return EVENT_LOGIN + case "60010": + return EVENT_LOGIN + case "60011": + return EVENT_LOGIN + } + + return EVENT_UNKNOWN +} + +/* + 从error返回中解析出对应的channel + error信息样例 + {"event":"error","msg":"channel:index-tickers,instId:BTC-USDT1 doesn't exist","code":"60018"} +*/ +func GetInfoFromErrMsg(raw string) (channel string) { + reg := regexp.MustCompile(`channel:(.*?),`) + if reg == nil { + fmt.Println("MustCompile err") + return + } + //提取关键信息 + result := reg.FindAllStringSubmatch(raw, -1) + for _, text := range result { + channel = text[1] + } + return +} + +/* + 解析消息类型 +*/ +func (a *WsClient) parseMessage(raw []byte) (evt Event, data interface{}, err error) { + evt = EVENT_UNKNOWN + //log.Println("解析消息") + //log.Println("消息内容:", string(raw)) + if string(raw) == "pong" { + evt = EVENT_PING + data = raw + return + } + //log.Println(0, evt) + var rspData = RspData{} + err = json.Unmarshal(raw, &rspData) + if err == nil { + op := rspData.Event + if op == OP_SUBSCRIBE || op == OP_UNSUBSCRIBE { + channel := rspData.Arg["channel"] + evt = GetEventId(channel) + data = rspData + return + } + } + + //log.Println("ErrData") + var errData = ErrData{} + err = json.Unmarshal(raw, &errData) + if err == nil { + op := errData.Event + switch op { + case OP_LOGIN: + evt = EVENT_LOGIN + data = errData + //log.Println(3, evt) + return + case OP_ERROR: + data = errData + // TODO:细化报错对应的事件判断 + + //尝试从msg字段中解析对应的事件类型 + evt = GetInfoFromErrCode(errData) + if evt != EVENT_UNKNOWN { + return + } + evt = GetEventId(GetInfoFromErrMsg(errData.Msg)) + if evt == EVENT_UNKNOWN { + evt = EVENT_ERROR + return + } + return + } + //log.Println(5, evt) + } + + //log.Println("JRPCRsp") + var jRPCRsp = JRPCRsp{} + err = json.Unmarshal(raw, &jRPCRsp) + if err == nil { + data = jRPCRsp + evt = GetEventId(jRPCRsp.Op) + if evt != EVENT_UNKNOWN { + return + } + } + + var depthData = DepthData{} + err = json.Unmarshal(raw, &depthData) + if err == nil { + evt = EVENT_DEPTH_DATA + data = depthData + //log.Println("-->>EVENT_DEPTH_DATA", evt) + //log.Println(evt, data) + //log.Println(6) + switch depthData.Arg["channel"] { + case "books": + return + case "books-l2-tbt": + return + case "books50-l2-tbt": + return + case "books5": + return + default: + + } + } + + //log.Println("MsgData") + var msgData = MsgData{} + err = json.Unmarshal(raw, &msgData) + if err == nil { + evt = EVENT_BOOKED_DATA + data = msgData + //log.Println("-->>EVENT_BOOK_DATA", evt) + //log.Println(evt, data) + //log.Println(6) + return + } + + evt = EVENT_UNKNOWN + err = errors.New("message unknown") + return +} + +func (a *WsClient) Stop() error { + + a.lock.Lock() + defer a.lock.Unlock() + if !a.isStarted { + return nil + } + + a.isStarted = false + + if a.conn != nil { + a.conn.Close() + } + close(a.errCh) + close(a.sendCh) + close(a.resCh) + close(a.quitCh) + + for _, ch := range a.regCh { + close(ch) + } + + log.Println("ws客户端退出!") + return nil +} + +/* + 添加全局消息处理的回调函数 +*/ +func (a *WsClient) AddMessageHook(fn ReceivedDataCallback) error { + a.onMessageHook = fn + return nil +} + +/* + 添加订阅消息处理的回调函数 +*/ +func (a *WsClient) AddBookMsgHook(fn ReceivedMsgDataCallback) error { + a.onBookMsgHook = fn + return nil +} + +/* + 添加深度消息处理的回调函数 + 例如: + cli.AddDepthHook(func(ts time.Time, data DepthData) error { return nil }) +*/ +func (a *WsClient) AddDepthHook(fn ReceivedDepthDataCallback) error { + a.onDepthHook = fn + return nil +} + +/* + 添加错误类型消息处理的回调函数 +*/ +func (a *WsClient) AddErrMsgHook(fn ReceivedDataCallback) error { + a.OnErrorHook = fn + return nil +} + +/* + 判断连接是否存活 +*/ +func (a *WsClient) IsAlive() bool { + res := false + if a.conn == nil { + return res + } + res, _, _ = a.Ping(500) + return res +} diff --git a/okex/ws/ws_contants.go b/okex/ws/ws_contants.go new file mode 100644 index 0000000..69a5dfe --- /dev/null +++ b/okex/ws/ws_contants.go @@ -0,0 +1,18 @@ +package ws + +//操作符 +const ( + OP_LOGIN = "login" + OP_ERROR = "error" + OP_SUBSCRIBE = "subscribe" + OP_UNSUBSCRIBE = "unsubscribe" +) + +// instrument Type +const ( + SPOT = "SPOT" + SWAP = "SWAP" + FUTURES = "FUTURES" + OPTION = "OPTION" + ANY = "ANY" +) diff --git a/okex/ws/ws_jrpc.go b/okex/ws/ws_jrpc.go new file mode 100644 index 0000000..b27480e --- /dev/null +++ b/okex/ws/ws_jrpc.go @@ -0,0 +1,157 @@ +package ws + +import ( + "context" + "log" + "time" + . "v5sdk_go/ws/wImpl" +) + +/* + websocket交易 通用请求 + 参数说明: + evtId:封装的事件类型 + id: 请求ID + op: 请求参数op + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) jrpcReq(evtId Event, op string, id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + req := &JRPCReq{ + Id: id, + Op: op, + Args: params, + } + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + defer cancel() + ctx = context.WithValue(ctx, "detail", detail) + + msg, err := a.process(ctx, evtId, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +/* + 单个下单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) PlaceOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + op := "order" + evtId := EVENT_PLACE_ORDER + + var args []map[string]interface{} + args = append(args, param) + + return a.jrpcReq(evtId, op, id, args, timeOut...) + +} + +/* + 批量下单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) BatchPlaceOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "batch-orders" + evtId := EVENT_PLACE_BATCH_ORDERS + return a.jrpcReq(evtId, op, id, params, timeOut...) + +} + +/* + 单个撤单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) CancelOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "cancel-order" + evtId := EVENT_CANCEL_ORDER + + var args []map[string]interface{} + args = append(args, param) + + return a.jrpcReq(evtId, op, id, args, timeOut...) + +} + +/* + 批量撤单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) BatchCancelOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "batch-cancel-orders" + evtId := EVENT_CANCEL_BATCH_ORDERS + return a.jrpcReq(evtId, op, id, params, timeOut...) + +} + +/* + 单个改单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) AmendOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "amend-order" + evtId := EVENT_AMEND_ORDER + + var args []map[string]interface{} + args = append(args, param) + + return a.jrpcReq(evtId, op, id, args, timeOut...) + +} + +/* + 批量改单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) BatchAmendOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "batch-amend-orders" + evtId := EVENT_AMEND_BATCH_ORDERS + return a.jrpcReq(evtId, op, id, params, timeOut...) + +} diff --git a/okex/ws/ws_jrpc_test.go b/okex/ws/ws_jrpc_test.go new file mode 100644 index 0000000..502e7e1 --- /dev/null +++ b/okex/ws/ws_jrpc_test.go @@ -0,0 +1,186 @@ +package ws + +import ( + "fmt" + "testing" + "time" + . "v5sdk_go/ws/wImpl" +) + +func PrintDetail(d *ProcessDetail) { + fmt.Println("[详细信息]") + fmt.Println("请求地址:", d.EndPoint) + fmt.Println("请求内容:", d.ReqInfo) + fmt.Println("发送时间:", d.SendTime.Format("2006-01-02 15:04:05.000")) + fmt.Println("响应时间:", d.RecvTime.Format("2006-01-02 15:04:05.000")) + fmt.Println("耗时:", d.UsedTime.String()) + fmt.Printf("接受到 %v 条消息:\n", len(d.Data)) + for _, v := range d.Data { + fmt.Printf("[%v] %v\n", v.Timestamp.Format("2006-01-02 15:04:05.000"), v.Info) + } +} + +func (r *WsClient) makeOrder(instId string, tdMode string, side string, ordType string, px string, sz string) (orderId string, err error) { + + var res bool + var data *ProcessDetail + + param := map[string]interface{}{} + param["instId"] = instId + param["tdMode"] = tdMode + param["side"] = side + param["ordType"] = ordType + if px != "" { + param["px"] = px + } + param["sz"] = sz + + res, data, err = r.PlaceOrder("0011", param) + if err != nil { + return + } + if res && len(data.Data) == 1 { + rsp := data.Data[0].Info.(JRPCRsp) + if len(rsp.Data) == 1 { + val, ok := rsp.Data[0]["ordId"] + if !ok { + return + } + orderId = val.(string) + return + } + } + + return +} + +/* + 单个下单 +*/ +// func TestPlaceOrder(t *testing.T) { +// r := prework_pri(CROSS_ACCOUNT) +// r := prework_pri(TRADE_ACCOUNT) +// var res bool +// var err error +// var data *ProcessDetail +// +// start := time.Now() +// param := map[string]interface{}{} +// param["instId"] = "BTC-USDT" +// param["tdMode"] = "cash" +// param["side"] = "buy" +// param["ordType"] = "market" +// param["px"] = "1" +// param["sz"] = "200" +// +// res, data, err = r.PlaceOrder("0011", param) +// if res { +// usedTime := time.Since(start) +// fmt.Println("下单成功!", usedTime.String()) +// PrintDetail(data) +// } else { +// usedTime := time.Since(start) +// fmt.Println("下单失败!", usedTime.String(), err) +// } +// +// } + +/* + 批量下单 +*/ +// func TestPlaceBatchOrder(t *testing.T) { +// r := prework_pri(CROSS_ACCOUNT) +// var res bool +// var err error +// var data *ProcessDetail +// +// start := time.Now() +// var params []map[string]interface{} +// param := map[string]interface{}{} +// param["instId"] = "BTC-USDT" +// param["tdMode"] = "cash" +// param["side"] = "sell" +// param["ordType"] = "market" +// param["sz"] = "0.001" +// params = append(params, param) +// param = map[string]interface{}{} +// param["instId"] = "BTC-USDT" +// param["tdMode"] = "cash" +// param["side"] = "buy" +// param["ordType"] = "market" +// param["sz"] = "100" +// params = append(params, param) +// res, data, err = r.BatchPlaceOrders("001", params) +// usedTime := time.Since(start) +// if err != nil { +// fmt.Println("下单失败!", err, usedTime.String()) +// t.Fail() +// } +// if res { +// fmt.Println("下单成功!", usedTime.String()) +// PrintDetail(data) +// } else { +// +// fmt.Println("下单失败!", usedTime.String()) +// t.Fail() +// } +// +// } + +/* + 撤销订单 +*/ +// func TestCancelOrder(t *testing.T) { +// r := prework_pri(CROSS_ACCOUNT) +// +// 用户自定义limit限价价格 +// ordId, _ := r.makeOrder("BTC-USDT", "cash", "sell", "limit", "57000", "0.01") +// if ordId == "" { +// t.Fatal() +// } +// +// t.Log("生成挂单:orderId=", ordId) +// +// param := map[string]interface{}{} +// param["instId"] = "BTC-USDT" +// param["ordId"] = ordId +// start := time.Now() +// res, _, _ := r.CancelOrder("1", param) +// if res { +// usedTime := time.Since(start) +// fmt.Println("撤单成功!", usedTime.String()) +// } else { +// t.Fatal("撤单失败!") +// } +// } + +/* + 修改订单 +*/ +func TestAmendlOrder(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + + // 用户自定义limit限价价格 + ordId, _ := r.makeOrder("BTC-USDT", "cash", "sell", "limit", "57000", "0.01") + if ordId == "" { + t.Fatal() + } + + t.Log("生成挂单:orderId=", ordId) + + param := map[string]interface{}{} + param["instId"] = "BTC-USDT" + param["ordId"] = ordId + // 调整修改订单的参数 + //param["newSz"] = "0.02" + param["newPx"] = "57001" + + start := time.Now() + res, _, _ := r.AmendOrder("1", param) + if res { + usedTime := time.Since(start) + fmt.Println("修改订单成功!", usedTime.String()) + } else { + t.Fatal("修改订单失败!") + } +} diff --git a/okex/ws/ws_middleware.go b/okex/ws/ws_middleware.go new file mode 100644 index 0000000..800ea02 --- /dev/null +++ b/okex/ws/ws_middleware.go @@ -0,0 +1,19 @@ +package ws + +import "fmt" + +type ReqFunc func(...interface{}) (res bool, msg *Msg, err error) +type Decorator func(ReqFunc) ReqFunc + +func handler(h ReqFunc, decors ...Decorator) ReqFunc { + for i := range decors { + d := decors[len(decors)-1-i] + h = d(h) + } + return h +} + +func preprocess() (res bool, msg *Msg, err error) { + fmt.Println("preprocess") + return +} diff --git a/okex/ws/ws_op.go b/okex/ws/ws_op.go new file mode 100644 index 0000000..f3522f4 --- /dev/null +++ b/okex/ws/ws_op.go @@ -0,0 +1,532 @@ +package ws + +import ( + "context" + "errors" + "log" + "sync" + "time" + . "v5sdk_go/config" + "v5sdk_go/rest" + . "v5sdk_go/utils" + . "v5sdk_go/ws/wImpl" + . "v5sdk_go/ws/wInterface" +) + +/* + Ping服务端保持心跳。 + timeOut:超时时间(毫秒),如果不填默认为5000ms +*/ +func (a *WsClient) Ping(timeOut ...int) (res bool, detail *ProcessDetail, err error) { + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + res = true + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + msg, err := a.process(ctx, EVENT_PING, nil) + if err != nil { + res = false + log.Println("处理请求失败!", err) + return + } + detail.Data = msg + + if len(msg) == 0 { + res = false + return + } + + str := string(msg[0].Info.([]byte)) + if str != "pong" { + res = false + return + } + + return +} + +/* + 登录私有频道 +*/ +func (a *WsClient) Login(apiKey, secKey, passPhrase string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + if apiKey == "" { + err = errors.New("ApiKey cannot be null") + return + } + + if secKey == "" { + err = errors.New("SecretKey cannot be null") + return + } + + if passPhrase == "" { + err = errors.New("Passphrase cannot be null") + return + } + + a.WsApi = &ApiInfo{ + ApiKey: apiKey, + SecretKey: secKey, + Passphrase: passPhrase, + } + + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + res = true + + timestamp := EpochTime() + + preHash := PreHashString(timestamp, rest.GET, "/users/self/verify", "") + //fmt.Println("preHash:", preHash) + var sign string + if sign, err = HmacSha256Base64Signer(preHash, secKey); err != nil { + log.Println("处理签名失败!", err) + return + } + + args := map[string]string{} + args["apiKey"] = apiKey + args["passphrase"] = passPhrase + args["timestamp"] = timestamp + args["sign"] = sign + req := &ReqData{ + Op: OP_LOGIN, + Args: []map[string]string{args}, + } + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + + msg, err := a.process(ctx, EVENT_LOGIN, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + + if len(msg) == 0 { + res = false + return + } + + info, _ := msg[0].Info.(ErrData) + + if info.Code == "0" && info.Event == OP_LOGIN { + log.Println("登录成功!") + } else { + log.Println("登录失败!") + res = false + return + } + + return +} + +/* + 等待结果响应 +*/ +func (a *WsClient) waitForResult(e Event, timeOut int) (data interface{}, err error) { + + if _, ok := a.regCh[e]; !ok { + a.lock.Lock() + a.regCh[e] = make(chan *Msg) + a.lock.Unlock() + //log.Println("注册", e, "事件成功") + } + + a.lock.RLock() + defer a.lock.RUnlock() + ch := a.regCh[e] + //log.Println(e, "等待响应!") + select { + case <-time.After(time.Duration(timeOut) * time.Millisecond): + log.Println(e, "超时未响应!") + err = errors.New(e.String() + "超时未响应!") + return + case data = <-ch: + //log.Println(data) + } + + return +} + +/* + 发送消息到服务端 +*/ +func (a *WsClient) Send(ctx context.Context, op WSReqData) (err error) { + select { + case <-ctx.Done(): + log.Println("发生失败退出!") + err = errors.New("发送超时退出!") + case a.sendCh <- op.ToString(): + } + + return +} + +func (a *WsClient) process(ctx context.Context, e Event, op WSReqData) (data []*Msg, err error) { + defer func() { + _ = recover() + }() + + var detail *ProcessDetail + if val := ctx.Value("detail"); val != nil { + detail = val.(*ProcessDetail) + } else { + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + } + defer func() { + //fmt.Println("处理完成,", e.String()) + detail.UsedTime = detail.RecvTime.Sub(detail.SendTime) + }() + + //查看事件是否被注册 + if _, ok := a.regCh[e]; !ok { + a.lock.Lock() + a.regCh[e] = make(chan *Msg) + a.lock.Unlock() + //log.Println("注册", e, "事件成功") + } else { + //log.Println("事件", e, "已注册!") + err = errors.New("事件" + e.String() + "尚未处理完毕") + return + } + + //预期请求响应的条数 + expectCnt := 1 + if op != nil { + expectCnt = op.Len() + } + recvCnt := 0 + + //等待完成通知 + wg := sync.WaitGroup{} + wg.Add(1) + // 这里要先定义go routine func(){} 是为了在里面订阅channel的内容, 我们知道一个队列要想往里塞东西,必先给他安排一个订阅它的协程 + go func(ctx context.Context) { + defer func() { + a.lock.Lock() + delete(a.regCh, e) + //log.Println("事件已注销!",e) + a.lock.Unlock() + wg.Done() + }() + + a.lock.RLock() + ch := a.regCh[e] //请求响应队列 + a.lock.RUnlock() + + //log.Println(e, "等待响应!") + done := false + ok := true + for { + var item *Msg + select { + case <-ctx.Done(): + log.Println(e, "超时未响应!") + err = errors.New(e.String() + "超时未响应!") + return + case item, ok = <-ch: + if !ok { + return + } + detail.RecvTime = time.Now() + //log.Println(e, "接受到数据", item) + // 这里只是把推送的数据显示出来,并没有做更近一步的处理,后续可以二次开发,在这个位置上进行处理 + data = append(data, item) + recvCnt++ + //log.Println(data) + if recvCnt == expectCnt { + done = true + break + } + } + if done { + break + } + } + if ok { + close(ch) + } + + }(ctx) + + // + switch e { + case EVENT_PING: + msg := "ping" + detail.ReqInfo = msg + a.sendCh <- msg + detail.SendTime = time.Now() + default: + detail.ReqInfo = op.ToString() + //这个时候ctx中已经提供了meta信息,用于发送ws请求 + err = a.Send(ctx, op) + if err != nil { + log.Println("发送[", e, "]消息失败!", err) + return + } + detail.SendTime = time.Now() + } + + wg.Wait() + return +} + +/* + 根据args请求参数判断请求类型 + 如:{"channel": "account","ccy": "BTC"} 类型为 EVENT_BOOK_ACCOUNT +*/ +func GetEventByParam(param map[string]string) (evtId Event) { + evtId = EVENT_UNKNOWN + channel, ok := param["channel"] + if !ok { + return + } + + evtId = GetEventId(channel) + return +} + +/* + 订阅频道。 + req:请求json字符串 +*/ +func (a *WsClient) Subscribe(param map[string]string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + evtid := GetEventByParam(param) + if evtid == EVENT_UNKNOWN { + err = errors.New("非法的请求参数!") + return + } + + var args []map[string]string + args = append(args, param) + + req := ReqData{ + Op: OP_SUBSCRIBE, + Args: args, + } + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + + msg, err := a.process(ctx, evtid, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + + //检查所有频道是否都更新成功 + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +/* + 取消订阅频道。 + req:请求json字符串 +*/ +func (a *WsClient) UnSubscribe(param map[string]string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + evtid := GetEventByParam(param) + if evtid == EVENT_UNKNOWN { + err = errors.New("非法的请求参数!") + return + } + + var args []map[string]string + args = append(args, param) + + req := ReqData{ + Op: OP_UNSUBSCRIBE, + Args: args, + } + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + msg, err := a.process(ctx, evtid, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + //检查所有频道是否都更新成功 + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +/* + jrpc请求 +*/ +func (a *WsClient) Jrpc(id, op string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + evtid := GetEventId(op) + if evtid == EVENT_UNKNOWN { + err = errors.New("非法的请求参数!") + return + } + + req := JRPCReq{ + Id: id, + Op: op, + Args: params, + } + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + msg, err := a.process(ctx, evtid, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + + //检查所有频道是否都更新成功 + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +func (a *WsClient) PubChannel(evtId Event, op string, params []map[string]string, pd Period, timeOut ...int) (res bool, msg []*Msg, err error) { + + // 参数校验 + pa, err := checkParams(evtId, params, pd) + if err != nil { + return + } + + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + req := ReqData{ + Op: op, + Args: pa, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + msg, err = a.process(ctx, evtId, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + + //检查所有频道是否都更新成功 + + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +// 参数校验 +func checkParams(evtId Event, params []map[string]string, pd Period) (res []map[string]string, err error) { + + channel := evtId.GetChannel(pd) + if channel == "" { + err = errors.New("参数校验失败!未知的类型:" + evtId.String()) + return + } + log.Println(channel) + if params == nil { + tmp := make(map[string]string) + tmp["channel"] = channel + res = append(res, tmp) + } else { + //log.Println(params) + for _, param := range params { + + tmp := make(map[string]string) + for k, v := range param { + tmp[k] = v + } + + val, ok := tmp["channel"] + if !ok { + tmp["channel"] = channel + } else { + if val != channel { + err = errors.New("参数校验失败!channel应为" + channel + val) + return + } + } + + res = append(res, tmp) + } + } + + return +} diff --git a/okex/ws/ws_priv_channel.go b/okex/ws/ws_priv_channel.go new file mode 100644 index 0000000..650d5ec --- /dev/null +++ b/okex/ws/ws_priv_channel.go @@ -0,0 +1,40 @@ +package ws + +import ( + . "v5sdk_go/ws/wImpl" +) + +/* + 订阅账户频道 +*/ +func (a *WsClient) PrivAccout(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_ACCOUNT, op, params, PERIOD_NONE, timeOut...) +} + +/* + 订阅持仓频道 +*/ +func (a *WsClient) PrivPostion(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_POSTION, op, params, PERIOD_NONE, timeOut...) +} + +/* + 订阅订单频道 +*/ +func (a *WsClient) PrivBookOrder(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_ORDER, op, params, PERIOD_NONE, timeOut...) +} + +/* + 订阅策略委托订单频道 +*/ +func (a *WsClient) PrivBookAlgoOrder(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_ALG_ORDER, op, params, PERIOD_NONE, timeOut...) +} + +/* + 订阅账户余额和持仓频道 +*/ +func (a *WsClient) PrivBalAndPos(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_B_AND_P, op, params, PERIOD_NONE, timeOut...) +} diff --git a/okex/ws/ws_priv_channel_Accout_test.go b/okex/ws/ws_priv_channel_Accout_test.go new file mode 100644 index 0000000..5cac15b --- /dev/null +++ b/okex/ws/ws_priv_channel_Accout_test.go @@ -0,0 +1,99 @@ +package ws + +// HOW TO RUN +// go test ws_cli.go ws_op.go ws_contants.go utils.go ws_priv_channel.go ws_priv_channel_Accout_test.go -v + +import ( + "fmt" + "log" + "testing" + "time" +) + +const ( + TRADE_ACCOUNT = iota + ISOLATE_ACCOUNT + CROSS_ACCOUNT + CROSS_ACCOUNT_B +) + +func prework_pri(t int) *WsClient { + // 模拟环境 + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + var apikey, passphrase, secretKey string + // 把账号密码写这里 + switch t { + case TRADE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case ISOLATE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT_B: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + } + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err) + } + + var res bool + start := time.Now() + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + usedTime := time.Since(start) + fmt.Println("登录成功!", usedTime.String()) + } else { + log.Fatal("登录失败!", err) + } + fmt.Println(apikey, secretKey, passphrase) + return r +} + +// 账户频道 测试 +func TestAccout(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + var args []map[string]string + arg := make(map[string]string) + arg["ccy"] = "BTC" + args = append(args, arg) + fmt.Println("args: ", args) + start := time.Now() + res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅所有成功!", usedTime.String()) + } else { + fmt.Println("订阅所有成功!", err) + t.Fatal("订阅所有成功!", err) + } + + time.Sleep(100 * time.Second) + start = time.Now() + // res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) + // if res { + // usedTime := time.Since(start) + // fmt.Println("取消订阅所有成功!", usedTime.String()) + // } else { + // fmt.Println("取消订阅所有失败!", err) + // t.Fatal("取消订阅所有失败!", err) + // } + +} diff --git a/okex/ws/ws_priv_channel_test.go b/okex/ws/ws_priv_channel_test.go new file mode 100644 index 0000000..99cd382 --- /dev/null +++ b/okex/ws/ws_priv_channel_test.go @@ -0,0 +1,247 @@ +package ws + +import ( + "fmt" + "log" + "testing" + "time" +) + +const ( + TRADE_ACCOUNT = iota + ISOLATE_ACCOUNT + CROSS_ACCOUNT + CROSS_ACCOUNT_B +) + +func prework_pri(t int) *WsClient { + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + var apikey, passphrase, secretKey string + // 把账号密码写这里 + switch t { + case TRADE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case ISOLATE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT_B: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + } + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err) + } + + var res bool + //start := time.Now() + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + //usedTime := time.Since(start) + //fmt.Println("登录成功!",usedTime.String()) + } else { + log.Fatal("登录失败!", err) + } + fmt.Println(apikey, secretKey, passphrase) + return r +} + +// 账户频道 测试 +func TestAccout(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + var args []map[string]string + arg := make(map[string]string) + //arg["ccy"] = "BTC" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅所有成功!", usedTime.String()) + } else { + fmt.Println("订阅所有成功!", err) + t.Fatal("订阅所有成功!", err) + } + + time.Sleep(100 * time.Second) + start = time.Now() + res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅所有成功!", usedTime.String()) + } else { + fmt.Println("取消订阅所有失败!", err) + t.Fatal("取消订阅所有失败!", err) + } + +} + +// 持仓频道 测试 +func TestPositon(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = FUTURES + arg["uly"] = "BTC-USD" + //arg["instId"] = "BTC-USD-210319" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivPostion(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60000 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PrivPostion(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 订单频道 测试 +func TestBookOrder(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + arg["instType"] = "ANY" + //arg["instType"] = "SWAP" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivBookOrder(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(6000 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PrivBookOrder(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 策略委托订单频道 测试 +func TestAlgoOrder(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = "SPOT" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivBookAlgoOrder(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PrivBookAlgoOrder(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 账户余额和持仓频道 测试 +func TestPrivBalAndPos(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivBalAndPos(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(600 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PrivBalAndPos(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} diff --git a/okex/ws/ws_pub_channel.go b/okex/ws/ws_pub_channel.go new file mode 100644 index 0000000..bad023f --- /dev/null +++ b/okex/ws/ws_pub_channel.go @@ -0,0 +1,141 @@ +package ws + +import ( + "errors" + . "v5sdk_go/ws/wImpl" +) + +/* + 产品频道 +*/ +func (a *WsClient) PubInstruemnts(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_INSTRUMENTS, op, params, PERIOD_NONE, timeOut...) +} + +// 获取系统维护的状态,当系统维护状态改变时推送 +func (a *WsClient) PubStatus(op string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_STATUS, op, nil, PERIOD_NONE, timeOut...) +} + +/* + 行情频道 +*/ +func (a *WsClient) PubTickers(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_TICKERS, op, params, PERIOD_NONE, timeOut...) +} + +/* + 持仓总量频道 +*/ +func (a *WsClient) PubOpenInsterest(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_OPEN_INTEREST, op, params, PERIOD_NONE, timeOut...) +} + +/* + K线频道 +*/ +func (a *WsClient) PubKLine(op string, period Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_KLINE, op, params, period, timeOut...) +} + +/* + 交易频道 +*/ +func (a *WsClient) PubTrade(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_TRADE, op, params, PERIOD_NONE, timeOut...) +} + +/* + 预估交割/行权价格频道 +*/ +func (a *WsClient) PubEstDePrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_ESTIMATE_PRICE, op, params, PERIOD_NONE, timeOut...) + +} + +/* + 标记价格频道 +*/ +func (a *WsClient) PubMarkPrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_MARK_PRICE, op, params, PERIOD_NONE, timeOut...) +} + +/* + 标记价格K线频道 +*/ +func (a *WsClient) PubMarkPriceCandle(op string, pd Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_MARK_PRICE_CANDLE_CHART, op, params, pd, timeOut...) +} + +/* + 限价频道 +*/ +func (a *WsClient) PubLimitPrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_LIMIT_PRICE, op, params, PERIOD_NONE, timeOut...) +} + +/* + 深度频道 +*/ +func (a *WsClient) PubOrderBooks(op string, channel string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + switch channel { + // 400档快照 + case "books": + return a.PubChannel(EVENT_BOOK_ORDER_BOOK, op, params, PERIOD_NONE, timeOut...) + // 5档快照 + case "books5": + return a.PubChannel(EVENT_BOOK_ORDER_BOOK5, op, params, PERIOD_NONE, timeOut...) + // 400 tbt + case "books-l2-tbt": + return a.PubChannel(EVENT_BOOK_ORDER_BOOK_TBT, op, params, PERIOD_NONE, timeOut...) + // 50 tbt + case "books50-l2-tbt": + return a.PubChannel(EVENT_BOOK_ORDER_BOOK50_TBT, op, params, PERIOD_NONE, timeOut...) + + default: + err = errors.New("未知的channel") + return + } + +} + +/* + 期权定价频道 +*/ +func (a *WsClient) PubOptionSummary(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_OPTION_SUMMARY, op, params, PERIOD_NONE, timeOut...) +} + +/* + 资金费率频道 +*/ +func (a *WsClient) PubFundRate(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_FUND_RATE, op, params, PERIOD_NONE, timeOut...) +} + +/* + 指数K线频道 +*/ +func (a *WsClient) PubKLineIndex(op string, pd Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_KLINE_INDEX, op, params, pd, timeOut...) +} + +/* + 指数行情频道 +*/ +func (a *WsClient) PubIndexTickers(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_INDEX_TICKERS, op, params, PERIOD_NONE, timeOut...) +} diff --git a/okex/ws/ws_pub_channel_test.go b/okex/ws/ws_pub_channel_test.go new file mode 100644 index 0000000..ac96eef --- /dev/null +++ b/okex/ws/ws_pub_channel_test.go @@ -0,0 +1,669 @@ +package ws + +import ( + "encoding/json" + "fmt" + "log" + "strings" + "testing" + "time" + . "v5sdk_go/ws/wImpl" +) + +func prework() *WsClient { + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err, ep) + } + return r +} + +// 产品频道测试 +func TestInstruemnts(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = SPOT + //arg["instType"] = OPTION + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubInstruemnts(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(3 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// status频道测试 +func TestStatus(t *testing.T) { + r := prework() + var err error + var res bool + + start := time.Now() + res, _, err = r.PubStatus(OP_SUBSCRIBE) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(10000 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubStatus(OP_UNSUBSCRIBE) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 行情频道测试 +func TestTickers(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubTickers(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(600 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubTickers(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 持仓总量频道 测试 +func TestOpenInsterest(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "LTC-USD-SWAP" + + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubOpenInsterest(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubOpenInsterest(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// K线频道测试 +func TestKLine(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + // 1分钟K + period := PERIOD_1MIN + + start := time.Now() + res, _, err = r.PubKLine(OP_SUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubKLine(OP_UNSUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 交易频道测试 +func TestTrade(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubTrade(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubTrade(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 预估交割/行权价格频道 测试 +func TestEstDePrice(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = FUTURES + arg["uly"] = "BTC-USD" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubEstDePrice(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubEstDePrice(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 标记价格频道 测试 +func TestMarkPrice(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubMarkPrice(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubMarkPrice(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 标记价格K线频道 测试s +func TestMarkPriceCandle(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + period := PERIOD_1MIN + + start := time.Now() + res, _, err = r.PubMarkPriceCandle(OP_SUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubMarkPriceCandle(OP_UNSUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 限价频道 测试 +func TestLimitPrice(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubLimitPrice(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubLimitPrice(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 深度频道 测试 +func TestOrderBooks(t *testing.T) { + r := prework() + var err error + var res bool + + /* + 设置关闭深度数据管理 + */ + // err = r.EnableAutoDepthMgr(false) + // if err != nil { + // fmt.Println("关闭自动校验失败!") + // } + + end := make(chan struct{}) + + r.AddDepthHook(func(ts time.Time, data DepthData) error { + // 对于深度类型数据处理的用户可以自定义 + + // 检测深度数据是否正常 + key, _ := json.Marshal(data.Arg) + fmt.Println("个数:", len(data.Data[0].Asks)) + checksum := data.Data[0].Checksum + fmt.Println("[自定义方法] ", string(key), ", checksum = ", checksum) + + for _, ask := range data.Data[0].Asks { + + arr := strings.Split(ask[0], ".") + //fmt.Println(arr) + if len(arr) > 1 && len(arr[1]) > 2 { + fmt.Println("ask数据异常,", checksum, "ask:", ask) + t.Fatal() + end <- struct{}{} + return nil + } else { + fmt.Println("bid数据正常,", checksum, "ask:", ask) + } + + } + + for _, bid := range data.Data[0].Bids { + + arr := strings.Split(bid[0], ".") + //fmt.Println(arr) + if len(arr) > 1 && len(arr[1]) > 2 { + fmt.Println("bid数据异常,", checksum, "bid:", bid) + t.Fatal() + end <- struct{}{} + return nil + } else { + fmt.Println("ask数据正常,", checksum, "bid:", bid) + } + + } + + // // 查看当前合并后的全量深度数据 + // snapshot, err := r.GetSnapshotByChannel(data) + // if err != nil { + // t.Fatal("深度数据不存在!") + // } + // // 展示ask/bid 前5档数据 + // fmt.Println(" Ask 5 档数据 >> ") + // for _, v := range snapshot.Asks[:5] { + // fmt.Println(" price:", v[0], " amount:", v[1]) + // } + // fmt.Println(" Bid 5 档数据 >> ") + // for _, v := range snapshot.Bids[:5] { + // fmt.Println(" price:", v[0], " amount:", v[1]) + // } + return nil + }) + + // 可选类型:books books5 books-l2-tbt + channel := "books50-l2-tbt" + + instIds := []string{"BTC-USDT"} + for _, instId := range instIds { + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = instId + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubOrderBooks(OP_SUBSCRIBE, channel, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + } + } + + select { + case <-end: + + } + //等待推送 + for _, instId := range instIds { + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = instId + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubOrderBooks(OP_UNSUBSCRIBE, channel, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + } + +} + +// 期权定价频道 测试 +func TestOptionSummary(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["uly"] = "BTC-USD" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubOptionSummary(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubOptionSummary(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 资金费率 测试 +func TestFundRate(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USD-SWAP" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubFundRate(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(600 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubFundRate(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 指数K线频道 测试 +func TestKLineIndex(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + + arg["instId"] = "BTC-USDT" + args = append(args, arg) + period := PERIOD_1MIN + + start := time.Now() + res, _, err = r.PubKLineIndex(OP_SUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubKLineIndex(OP_UNSUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 指数行情频道 测试 +func TestIndexMarket(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubIndexTickers(OP_SUBSCRIBE, args) + if err != nil { + fmt.Println("订阅失败!", err) + } + usedTime := time.Since(start) + if res { + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", usedTime.String()) + //return + } + + time.Sleep(600 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubIndexTickers(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} diff --git a/okex/ws/ws_test.go b/okex/ws/ws_test.go new file mode 100644 index 0000000..8ac7e7e --- /dev/null +++ b/okex/ws/ws_test.go @@ -0,0 +1,386 @@ +package ws + +import ( + "fmt" + "log" + "testing" + "time" + . "v5sdk_go/ws/wImpl" + + "github.com/stretchr/testify/assert" +) + +func init() { + log.SetFlags(log.LstdFlags | log.Llongfile) + +} + +func TestPing(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + + res, _, _ := r.Ping() + assert.True(t, res, true) +} + +func TestWsClient_SubscribeAndUnSubscribe(t *testing.T) { + r := prework() + var err error + var res bool + + param := map[string]string{} + param["channel"] = "opt-summary" + param["uly"] = "BTC-USD" + + start := time.Now() + res, _, err = r.Subscribe(param) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.UnSubscribe(param) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +func TestWsClient_SubscribeAndUnSubscribe_priv(t *testing.T) { + r := prework_pri(ISOLATE_ACCOUNT) + var err error + var res bool + + var params []map[string]string + params = append(params, map[string]string{"channel": "orders", "instType": SPOT, "instId": "BTC-USDT"}) + //一个失败的订阅用例 + params = append(params, map[string]string{"channel": "positions", "instType": "any"}) + + for _, v := range params { + start := time.Now() + var data *ProcessDetail + res, data, err = r.Subscribe(v) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + PrintDetail(data) + } else { + fmt.Println("订阅失败!", err) + //return + } + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.UnSubscribe(v) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + } + + } + +} + +func TestWsClient_Jrpc(t *testing.T) { + //r := prework_pri(ISOLATE_ACCOUNT) + r := prework_pri(CROSS_ACCOUNT) + var res bool + var err error + var data *ProcessDetail + + start := time.Now() + var args []map[string]interface{} + + param := map[string]interface{}{} + param["instId"] = "BTC-USDT" + param["clOrdId"] = "SIM0dcopy16069997808063455" + param["tdMode"] = "cross" + param["side"] = "sell" + param["ordType"] = "limit" + param["px"] = "19333.3" + param["sz"] = "0.18605445" + + param1 := map[string]interface{}{} + param1["instId"] = "BTC-USDT" + param1["clOrdId"] = "SIM0dcopy16069997808063456" + param1["tdMode"] = "cross" + param1["side"] = "sell" + param1["ordType"] = "limit" + param1["px"] = "19334.2" + param1["sz"] = "0.03508913" + + param2 := map[string]interface{}{} + param2["instId"] = "BTC-USDT" + param2["clOrdId"] = "SIM0dcopy16069997808063457" + param2["tdMode"] = "cross" + param2["side"] = "sell" + param2["ordType"] = "limit" + param2["px"] = "19334.8" + param2["sz"] = "0.03658186" + + param3 := map[string]interface{}{} + param3["instId"] = "BTC-USDT" + param3["clOrdId"] = "SIM0dcopy16069997808063458" + param3["tdMode"] = "cross" + param3["side"] = "sell" + param3["ordType"] = "limit" + param3["px"] = "19334.9" + param3["sz"] = "0.5" + + param4 := map[string]interface{}{} + param4["instId"] = "BTC-USDT" + param4["clOrdId"] = "SIM0dcopy16069997808063459" + param4["tdMode"] = "cross" + param4["side"] = "sell" + param4["ordType"] = "limit" + param4["px"] = "19335.2" + param4["sz"] = "0.3" + + param5 := map[string]interface{}{} + param5["instId"] = "BTC-USDT" + param5["clOrdId"] = "SIM0dcopy16069997808063460" + param5["tdMode"] = "cross" + param5["side"] = "sell" + param5["ordType"] = "limit" + param5["px"] = "19335.9" + param5["sz"] = "0.051" + + param6 := map[string]interface{}{} + param6["instId"] = "BTC-USDT" + param6["clOrdId"] = "SIM0dcopy16069997808063461" + param6["tdMode"] = "cross" + param6["side"] = "sell" + param6["ordType"] = "limit" + param6["px"] = "19336.4" + param6["sz"] = "1" + + param7 := map[string]interface{}{} + param7["instId"] = "BTC-USDT" + param7["clOrdId"] = "SIM0dcopy16069997808063462" + param7["tdMode"] = "cross" + param7["side"] = "sell" + param7["ordType"] = "limit" + param7["px"] = "19336.8" + param7["sz"] = "0.475" + + param8 := map[string]interface{}{} + param8["instId"] = "BTC-USDT" + param8["clOrdId"] = "SIM0dcopy16069997808063463" + param8["tdMode"] = "cross" + param8["side"] = "sell" + param8["ordType"] = "limit" + param8["px"] = "19337.3" + param8["sz"] = "0.21299357" + + param9 := map[string]interface{}{} + param9["instId"] = "BTC-USDT" + param9["clOrdId"] = "SIM0dcopy16069997808063464" + param9["tdMode"] = "cross" + param9["side"] = "sell" + param9["ordType"] = "limit" + param9["px"] = "19337.5" + param9["sz"] = "0.5" + + args = append(args, param) + args = append(args, param1) + args = append(args, param2) + args = append(args, param3) + args = append(args, param4) + args = append(args, param5) + args = append(args, param6) + args = append(args, param7) + args = append(args, param8) + args = append(args, param9) + + res, data, err = r.Jrpc("okexv5wsapi001", "order", args) + if res { + usedTime := time.Since(start) + fmt.Println("下单成功!", usedTime.String()) + PrintDetail(data) + } else { + usedTime := time.Since(start) + fmt.Println("下单失败!", usedTime.String(), err) + } +} + +/* + 测试 添加全局消息回调函数 +*/ +func TestAddMessageHook(t *testing.T) { + + r := prework_pri(CROSS_ACCOUNT) + + r.AddMessageHook(func(msg *Msg) error { + // 添加你的方法 + fmt.Println("这是全局消息自定义MessageHook") + fmt.Println("当前数据是", msg) + return nil + }) + + select {} +} + +/* + 普通推送数据回调函数 +*/ +func TestAddBookedDataHook(t *testing.T) { + var r *WsClient + + /*订阅私有频道*/ + { + r = prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + r.AddBookMsgHook(func(ts time.Time, data MsgData) error { + // 添加你的方法 + fmt.Println("这是私有自定义AddBookMsgHook") + fmt.Println("当前数据是", data) + return nil + }) + + param := map[string]string{} + param["channel"] = "account" + param["ccy"] = "BTC" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(100 * time.Second) + } + + //订阅公共频道 + { + r = prework() + var res bool + var err error + + r.AddBookMsgHook(func(ts time.Time, data MsgData) error { + // 添加你的方法 + fmt.Println("这是公共自定义AddBookMsgHook") + fmt.Println("当前数据是", data) + return nil + }) + + param := map[string]string{} + param["channel"] = "instruments" + param["instType"] = "FUTURES" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + select {} + } + +} + +func TestGetInfoFromErrMsg(t *testing.T) { + a := assert.New(t) + buf := ` +"channel:index-tickers,instId:BTC-USDT1 doesn't exist" + ` + ch := GetInfoFromErrMsg(buf) + //t.Log(ch) + a.Equal("index-tickers", ch) + + //assert.True(t,ch == "index-tickers") +} + +/* + + */ +func TestParseMessage(t *testing.T) { + r := prework() + var evt Event + msg := `{"event":"error","msg":"Contract does not exist.","code":"51001"}` + + evt, _, _ = r.parseMessage([]byte(msg)) + assert.True(t, EVENT_ERROR == evt) + + msg = `{"event":"error","msg":"channel:positions,ccy:BTC doesn't exist","code":"60018"}` + evt, _, _ = r.parseMessage([]byte(msg)) + assert.True(t, EVENT_BOOK_POSTION == evt) +} + +/* + 原始方式 深度订阅 测试 +*/ +func TestSubscribeTBT(t *testing.T) { + r := prework() + var res bool + var err error + + // 添加你的方法 + r.AddDepthHook(func(ts time.Time, data DepthData) error { + //fmt.Println("这是自定义AddBookMsgHook") + fmt.Println("当前数据是:", data) + return nil + }) + + param := map[string]string{} + param["channel"] = "books-l2-tbt" + //param["channel"] = "books" + param["instId"] = "BTC-USD-SWAP" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) +} + +/* + + */ +func TestSubscribeBalAndPos(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + param := map[string]string{} + + // 产品信息 + param["channel"] = "balance_and_position" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) +} diff --git a/private/balance.go b/private/balance.go new file mode 100644 index 0000000..764f002 --- /dev/null +++ b/private/balance.go @@ -0,0 +1,73 @@ +package private + +import ( + "strconv" +) + +// {"availEq":0,"cashBal":2397.6,"ccy":"FODL","disEq":0,"eq":2397.6,"eqUsd":340.3419189984,"frozenBal":2397.6,"ordFrozen":2397.6,"uTime":1649902953671} +// {"availEq":0,"cashBal":12987,"ccy":"ANW","disEq":0,"eq":12987,"eqUsd":358.555901184,"frozenBal":12987,"ordFrozen":12987,"uTime":1649596232202} +// {"availEq":0,"cashBal":6995.8,"ccy":"ABT","disEq":0,"eq":6995.8,"eqUsd":1082.5316450676,"frozenBal":6995.8,"ordFrozen":6995.8,"uTime":1648835098048} + +// "availBal string ", +// "crossLiab string`json:""` +// "interest string`json:""` +// "isoEq string`json:""` +// "isoLiab string`json:""` +// "isoUpl":"0", +// "liab string`json:""` +// "maxLoan string 1453.92289531493594", +// "mgnRatio string ", +// "notionalLever string ", +// "ordFrozen string`json:""` +// "twap string`json:""` +// "upl string 0.570822125136023", +// "uplLiab string`json:""` +// "stgyEq":"0" + +type CcyResp struct { + AvailEq string `json:"availEq"` + CashBal string `json:"cashBal"` + Ccy string `json:"ccy"` + DisEq string `json:"disEq"` + Eq string `json:"eq"` + EqUsd string `json:"eqUsd"` + FrozenBal string `json:"frozenBal"` + OrdFrozen string `json:"ordFrozen"` + UTime string `json:"uTime"` + AvailBal string `json:"availBal"` +} + +type Ccy struct { + AvailEq float64 `json:"availEq"` + CashBal float64 `json:"cashBal"` + Ccy string `json:"ccy"` + DisEq float64 `json:"disEq"` + Eq float64 `json:"eq":wa` + EqUsd float64 `json:"eqUsd"` + FrozenBal float64 `json:"frozenBal"` + OrdFrozen float64 `json:"ordFrozen"` + UTime int64 `json:"uTime"` + AvailBal float64 `json:"availBal"` +} +type BalanceResp struct { +} +type Balance struct { +} + +func (ccyResp *CcyResp) Convert() (*Ccy, error) { + ccy := Ccy{} + ccy.Ccy = ccyResp.Ccy + ccy.AvailEq, _ = strconv.ParseFloat(ccyResp.AvailEq, 64) + ccy.CashBal, _ = strconv.ParseFloat(ccyResp.CashBal, 64) + ccy.DisEq, _ = strconv.ParseFloat(ccyResp.DisEq, 64) + ccy.Eq, _ = strconv.ParseFloat(ccyResp.Eq, 64) + ccy.EqUsd, _ = strconv.ParseFloat(ccyResp.EqUsd, 64) + ccy.FrozenBal, _ = strconv.ParseFloat(ccyResp.FrozenBal, 64) + ccy.OrdFrozen, _ = strconv.ParseFloat(ccyResp.OrdFrozen, 64) + ccy.UTime, _ = strconv.ParseInt(ccyResp.UTime, 10, 64) + + return &ccy, nil +} +func (balanceResp *BalanceResp) Convert() (*Balance, error) { + return nil, nil +} diff --git a/private/order.go b/private/order.go new file mode 100644 index 0000000..3ad3eda --- /dev/null +++ b/private/order.go @@ -0,0 +1,107 @@ +package private + +import ( + "phyer.click/tunas/utils" +) + +type OrderResp struct { + Arg ArgResp `json:"arg"` + Data []*OrderDataResp `json:"data"` +} + +type ArgResp struct { + Channel string `json:"channel"` + InstType string `json:"instType"` + InstId string `json:"instId"` +} +type OrderDataResp struct { + InstType string `json:"instType"` + InstId string `json:"instId"` + OrdId string `json:"ordId"` + ClOrdId string `json:"clOrdId"` + Tag string `json:"tag"` + Px string `json:"Px"` + Sz string `json:"sz"` + NotionalUsd string `json:"notionalUsd"` + OrdType string `json:"ordType"` + Side string `json:"side"` + PosSide string `json:"posSide"` + TdMode string `json:"tdMode"` + TgtCcy string `json:"tgtCcy"` + FillSz string `json:"fillSz"` + FillPx string `json:"fillPx"` + TradeId string `json:"tradeId"` + AccFillSz string `json:"accFillSz"` + FillNotionalUsd string `json:"FillNotionalUsd"` + FillTime string `json:"fillTime"` + FillFee string `json:"fillFee"` + FillFeeCcy string `json:"fillFeeCcy"` + ExecType string `json:"execType"` + Source string `json:"source"` + State string `json:"state"` + AvgPx string `json:"avgPx"` + Lever string `json:"lever"` + TpTriggerPxstring string `json:"tpTriggerPxstring"` + TpTriggerPxType string `json:"tpTriggerPxType"` + TpOrdPx string `json:"tpOrdPx"` + SlTriggerPx string `json:"slTriggerPx"` + SlTriggerPxType string `json:"slTriggerPxType"` + SlOrdPx string `json:"slOrdPx"` + FeeCcy string `json:"feeCcy"` + Fee string `json:"fee"` + RebateCcy string `json:"rebateCcy"` + Rebate string `json:"rebate"` + TgtCcystring string `json:"tgtCcystring"` + Pnl string `json:"pnl"` + Category string `json:"category"` + UTime string `json:"uTime"` + CTime string `json:"cTime"` + ReqId string `json:"reqId"` + AmendResult string `json:"amendResult"` + Code string `json:"code"` + Msg string `json:"msg"` +} + +type Order struct { + CTime int64 `json:"cTime,number"` // 订单创建时间 + UTime int64 `json:"uTime,number"` // 订单状态更新时间 + InstId string `json:"instId"` + OrdId string `json:"ordId"` + ClOrdId string `json:"clOrdId"` // 自定义订单ID + Px float64 `json:"px,number"` // 委托价格 + Side string `json:"side"` //交易方向 sell, buy + Sz float64 `json:"sz,number"` // 委托总量 + AccFillSz float64 `json:"accFillSz,number"` //累计成交数量 + AvgPx float64 `json:"avgPx,number"` //成交均价 + State string `json:"state"` //订单状态 + TgtCcy string `json:"tgtCcy"` // 限定了size的单位,两个选项:base_ccy: 交易货币 ;quote_ccy:计价货币(美元) + // canceled:撤单成功 + // live:等待成交 + // partially_filled:部分成交 + // filled:完全成交 +} + +func (orderResp *OrderResp) Convert() ([]*Order, error) { + orderList := []*Order{} + for _, v := range orderResp.Data { + // fmt.Println("v.Sz:", v.Sz, reflect.TypeOf(v.Sz).Name()) + curOrder := Order{} + curOrder.CTime = utils.ToInt64(v.CTime) + curOrder.UTime = utils.ToInt64(v.UTime) + curOrder.InstId = v.InstId + curOrder.OrdId = v.OrdId + curOrder.ClOrdId = v.ClOrdId + curOrder.Side = v.Side + curOrder.Px = utils.ToFloat64(v.Px) + curOrder.Sz = utils.ToFloat64(v.Sz) + curOrder.AccFillSz = utils.ToFloat64(v.AccFillSz) + curOrder.AvgPx = utils.ToFloat64(v.AvgPx) + curOrder.State = v.State + orderList = append(orderList, &curOrder) + } + return orderList, nil +} + +// func (order *Order) Process(cr *core.Core) error { +// return nil +// } diff --git a/submodules/okex/LICENSE b/submodules/okex/LICENSE new file mode 100644 index 0000000..35781f8 --- /dev/null +++ b/submodules/okex/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/submodules/okex/config/conf.go b/submodules/okex/config/conf.go new file mode 100644 index 0000000..c1c20b5 --- /dev/null +++ b/submodules/okex/config/conf.go @@ -0,0 +1,32 @@ +package config + +import "fmt" + +type Env struct { + RestEndpoint string `yaml:"RestEndpoint"` + WsEndpoint string `yaml:"WsEndpoint"` + IsSimulation bool `yaml:"IsSimulation"` +} + +type ApiInfo struct { + ApiKey string `yaml:"ApiKey"` + SecretKey string `yaml:"SecretKey"` + Passphrase string `yaml:"Passphrase"` +} + +type MetaData struct { + Description string `yaml:"Description"` +} + +type Config struct { + MetaData `yaml:"MetaData"` + Env `yaml:"Env"` + ApiInfo `yaml:"ApiInfo"` +} + +func (s *ApiInfo) String() string { + res := "ApiInfo{" + res += fmt.Sprintf("ApiKey:%v,SecretKey:%v,Passphrase:%v", s.ApiKey, s.SecretKey, s.Passphrase) + res += "}" + return res +} diff --git a/submodules/okex/config/go.mod b/submodules/okex/config/go.mod new file mode 100644 index 0000000..bf70c70 --- /dev/null +++ b/submodules/okex/config/go.mod @@ -0,0 +1,3 @@ +module v5sdk_go/config + +go 1.14 diff --git a/submodules/okex/go.mod b/submodules/okex/go.mod new file mode 100644 index 0000000..cd83b72 --- /dev/null +++ b/submodules/okex/go.mod @@ -0,0 +1,8 @@ +module v5sdk_go + +go 1.15 + +require ( + github.com/gorilla/websocket v1.4.2 + github.com/stretchr/testify v1.7.0 +) diff --git a/submodules/okex/go.sum b/submodules/okex/go.sum new file mode 100644 index 0000000..8eb6d1d --- /dev/null +++ b/submodules/okex/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/submodules/okex/main.go b/submodules/okex/main.go new file mode 100644 index 0000000..5781aa8 --- /dev/null +++ b/submodules/okex/main.go @@ -0,0 +1,237 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + . "v5sdk_go/rest" + . "v5sdk_go/ws" +) + +/* + rest API请求 + 更多示例请查看 rest/rest_test.go +*/ +func REST() { + // 设置您的APIKey + apikey := APIKeyInfo{ + ApiKey: "eca767d4-fda5-4a1b-bb28-49ae18093307", + SecKey: "8CA3628A556ED137977DB298D37BC7F3", + PassPhrase: "Op3Druaron", + } + + // 第三个参数代表是否为模拟环境,更多信息查看接口说明 + cli := NewRESTClient("https://www.okex.win", &apikey, false) + rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) + if err != nil { + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", rsp.Code) + fmt.Println("\t总耗时: ", rsp.TotalUsedTime) + fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) + fmt.Println("\t返回消息: ", rsp.Body) + fmt.Println("\terrCode: ", rsp.V5Response.Code) + fmt.Println("\terrMsg: ", rsp.V5Response.Msg) + fmt.Println("\tdata: ", rsp.V5Response.Data) + +} + +// 订阅私有频道 +func wsPriv() { + ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" + + // 填写您自己的APIKey信息 + apikey := "xxxx" + secretKey := "xxxxx" + passphrase := "xxxxx" + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + defer r.Stop() + var res bool + + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + fmt.Println("登录成功!") + } else { + fmt.Println("登录失败!", err) + return + } + + // 订阅账户频道 + var args []map[string]string + arg := make(map[string]string) + arg["ccy"] = "BTC" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!耗时:", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + + time.Sleep(100 * time.Second) + start = time.Now() + res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + } + +} + +// 订阅公共频道 +func wsPub() { + ep := "wss://wsaws.okex.com:8443/ws/v5/public?brokerId=9999" + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + defer r.Stop() + // 订阅产品频道 + // 在这里初始化instrument列表 + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = FUTURES + //arg["instType"] = OPTION + args = append(args, arg) + + start := time.Now() + //订阅 + res, _, err := r.PubInstruemnts(OP_SUBSCRIBE, args) + fmt.Println("args:", args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + + // 在这里 args1 初始化tickerList的列表 + var args1 []map[string]string + arg1 := make(map[string]string) + arg1["instId"] = "ETH-USDT" + //arg["instType"] = OPTION + args1 = append(args1, arg1) + //------------------------------------------------------ + start1 := time.Now() + res, _, err = r.PubTickers(OP_SUBSCRIBE, args1) + fmt.Println("args:", args) + if res { + usedTime := time.Since(start1) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + time.Sleep(300 * time.Second) + // + // start = time.Now() + // res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) + // if res { + // usedTime := time.Since(start) + // fmt.Println("取消订阅成功!", usedTime.String()) + // } else { + // fmt.Println("取消订阅失败!", err) + // } +} + +// websocket交易 +func wsJrpc() { + ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" + + // 填写您自己的APIKey信息 + apikey := "xxxx" + secretKey := "xxxxx" + passphrase := "xxxxx" + + var res bool + var req_id string + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + + defer r.Stop() + + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + fmt.Println("登录成功!") + } else { + fmt.Println("登录失败!", err) + return + } + + start := time.Now() + param := map[string]interface{}{} + param["instId"] = "BTC-USDT" + param["tdMode"] = "cash" + param["side"] = "buy" + param["ordType"] = "market" + param["sz"] = "200" + req_id = "00001" + + res, _, err = r.PlaceOrder(req_id, param) + if res { + usedTime := time.Since(start) + fmt.Println("下单成功!", usedTime.String()) + } else { + usedTime := time.Since(start) + fmt.Println("下单失败!", usedTime.String(), err) + } +} + +func main() { + // 公共订阅 + wsPub() + + // 私有订阅 + // wsPriv() + + // websocket交易 + // wsJrpc() + + // rest请求 + // REST() +} diff --git a/submodules/okex/readme.md b/submodules/okex/readme.md new file mode 100644 index 0000000..9ff3e39 --- /dev/null +++ b/submodules/okex/readme.md @@ -0,0 +1,283 @@ +# 简介 +OKEX go版本的v5sdk,仅供学习交流使用。 +(文档持续完善中) +# 项目说明 + +## REST调用 +``` go + // 设置您的APIKey + apikey := APIKeyInfo{ + ApiKey: "xxxx", + SecKey: "xxxx", + PassPhrase: "xxxx", + } + + // 第三个参数代表是否为模拟环境,更多信息查看接口说明 + cli := NewRESTClient("https://www.okex.win", &apikey, true) + rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) + if err != nil { + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", rsp.Code) + fmt.Println("\t总耗时: ", rsp.TotalUsedTime) + fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) + fmt.Println("\t返回消息: ", rsp.Body) + fmt.Println("\terrCode: ", rsp.V5Response.Code) + fmt.Println("\terrMsg: ", rsp.V5Response.Msg) + fmt.Println("\tdata: ", rsp.V5Response.Data) + ``` +更多示例请查看rest/rest_test.go + +## websocket订阅 + +### 私有频道 +```go + ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" + + // 填写您自己的APIKey信息 + apikey := "xxxx" + secretKey := "xxxxx" + passphrase := "xxxxx" + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + defer r.Stop() + + var res bool + // 私有频道需要登录 + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + fmt.Println("登录成功!") + } else { + fmt.Println("登录失败!", err) + return + } + + + var args []map[string]string + arg := make(map[string]string) + arg["ccy"] = "BTC" + args = append(args, arg) + + start := time.Now() + // 订阅账户频道 + res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!耗时:", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + + time.Sleep(100 * time.Second) + start = time.Now() + // 取消订阅账户频道 + res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + } +``` +更多示例请查看ws/ws_priv_channel_test.go + +### 公有频道 +```go + ep := "wss://ws.okex.com:8443/ws/v5/public?brokerId=9999" + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + + defer r.Stop() + + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = FUTURES + //arg["instType"] = OPTION + args = append(args, arg) + + start := time.Now() + + // 订阅产品频道 + res, _, err := r.PubInstruemnts(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + } + + time.Sleep(30 * time.Second) + + start = time.Now() + + // 取消订阅产品频道 + res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + } +``` +更多示例请查看ws/ws_pub_channel_test.go + +## websocket交易 +```go + ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" + + // 填写您自己的APIKey信息 + apikey := "xxxx" + secretKey := "xxxxx" + passphrase := "xxxxx" + + var res bool + var req_id string + + // 创建ws客户端 + r, err := NewWsClient(ep) + if err != nil { + log.Println(err) + return + } + + // 设置连接超时 + r.SetDailTimeout(time.Second * 2) + err = r.Start() + if err != nil { + log.Println(err) + return + } + + defer r.Stop() + + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + fmt.Println("登录成功!") + } else { + fmt.Println("登录失败!", err) + return + } + + start := time.Now() + param := map[string]interface{}{} + param["instId"] = "BTC-USDT" + param["tdMode"] = "cash" + param["side"] = "buy" + param["ordType"] = "market" + param["sz"] = "200" + req_id = "00001" + + // 单个下单 + res, _, err = r.PlaceOrder(req_id, param) + if res { + usedTime := time.Since(start) + fmt.Println("下单成功!", usedTime.String()) + } else { + usedTime := time.Since(start) + fmt.Println("下单失败!", usedTime.String(), err) + } + +``` +更多示例请查看ws/ws_jrpc_test.go + +## wesocket推送 +websocket推送数据分为两种类型数据:`普通推送数据`和`深度类型数据`。 + +```go +ws/wImpl/BookData.go + +// 普通推送 +type MsgData struct { + Arg map[string]string `json:"arg"` + Data []interface{} `json:"data"` +} + +// 深度数据 +type DepthData struct { + Arg map[string]string `json:"arg"` + Action string `json:"action"` + Data []DepthDetail `json:"data"` +} +``` +如果需要对推送数据做处理用户可以自定义回调函数: +1. 全局消息处理的回调函数 +该回调函数会处理所有从服务端接受到的数据。 +```go +/* + 添加全局消息处理的回调函数 +*/ +func (a *WsClient) AddMessageHook(fn ReceivedDataCallback) error { + a.onMessageHook = fn + return nil +} +``` +使用方法参见 ws/ws_test.go中测试用例TestAddMessageHook。 + +2. 订阅消息处理回调函数 +可以处理所有非深度类型的数据,包括 订阅/取消订阅,普通推送数据。 +```go +/* + 添加订阅消息处理的回调函数 +*/ +func (a *WsClient) AddBookMsgHook(fn ReceivedMsgDataCallback) error { + a.onBookMsgHook = fn + return nil +} +``` +使用方法参见 ws/ws_test.go中测试用例TestAddBookedDataHook。 + + +3. 深度消息处理的回调函数 +这里需要说明的是,Wsclient提供了深度数据管理和自动checksum的功能,用户如果需要关闭此功能,只需要调用EnableAutoDepthMgr方法。 +```go +/* + 添加深度消息处理的回调函数 +*/ +func (a *WsClient) AddDepthHook(fn ReceivedDepthDataCallback) error { + a.onDepthHook = fn + return nil +} +``` +使用方法参见 ws/ws_pub_channel_test.go中测试用例TestOrderBooks。 + +4. 错误消息类型回调函数 +```go +func (a *WsClient) AddErrMsgHook(fn ReceivedDataCallback) error { + a.OnErrorHook = fn + return nil +} +``` + +# 联系方式 +邮箱:caron_co@163.com +微信:caron_co diff --git a/submodules/okex/rest/contants.go b/submodules/okex/rest/contants.go new file mode 100644 index 0000000..4497aea --- /dev/null +++ b/submodules/okex/rest/contants.go @@ -0,0 +1,35 @@ +package rest + +const ( + + /* + http headers + */ + OK_ACCESS_KEY = "OK-ACCESS-KEY" + OK_ACCESS_SIGN = "OK-ACCESS-SIGN" + OK_ACCESS_TIMESTAMP = "OK-ACCESS-TIMESTAMP" + OK_ACCESS_PASSPHRASE = "OK-ACCESS-PASSPHRASE" + X_SIMULATE_TRADING = "x-simulated-trading" + + CONTENT_TYPE = "Content-Type" + ACCEPT = "Accept" + COOKIE = "Cookie" + LOCALE = "locale=" + + APPLICATION_JSON = "application/json" + APPLICATION_JSON_UTF8 = "application/json; charset=UTF-8" + + /* + i18n: internationalization + */ + ENGLISH = "en_US" + SIMPLIFIED_CHINESE = "zh_CN" + //zh_TW || zh_HK + TRADITIONAL_CHINESE = "zh_HK" + + GET = "GET" + POST = "POST" +) + + + diff --git a/submodules/okex/rest/go.mod b/submodules/okex/rest/go.mod new file mode 100644 index 0000000..e69de29 diff --git a/submodules/okex/rest/rest.go b/submodules/okex/rest/rest.go new file mode 100644 index 0000000..d764125 --- /dev/null +++ b/submodules/okex/rest/rest.go @@ -0,0 +1,331 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + . "v5sdk_go/utils" +) + +type RESTAPI struct { + EndPoint string `json:"endPoint"` + // GET/POST + Method string `json:"method"` + Uri string `json:"uri"` + Param map[string]interface{} `json:"param"` + Timeout time.Duration + ApiKeyInfo *APIKeyInfo + isSimulate bool +} + +type APIKeyInfo struct { + ApiKey string + PassPhrase string + SecKey string + UserId string +} + +type RESTAPIResult struct { + Url string `json:"url"` + Param string `json:"param"` + Header string `json:"header"` + Code int `json:"code"` + // 原始返回信息 + Body string `json:"body"` + // okexV5返回的数据 + V5Response Okexv5APIResponse `json:"v5Response"` + ReqUsedTime time.Duration `json:"reqUsedTime"` + TotalUsedTime time.Duration `json:"totalUsedTime"` +} + +type Okexv5APIResponse struct { + Code string `json:"code"` + Msg string `json:"msg"` + Data []map[string]interface{} `json:"data"` +} + +/* + endPoint:请求地址 + apiKey + isSimulate: 是否为模拟环境 +*/ +func NewRESTClient(endPoint string, apiKey *APIKeyInfo, isSimulate bool) *RESTAPI { + + res := &RESTAPI{ + EndPoint: endPoint, + ApiKeyInfo: apiKey, + isSimulate: isSimulate, + Timeout: 5 * time.Second, + } + return res +} + +func NewRESTAPI(ep, method, uri string, param *map[string]interface{}) *RESTAPI { + //TODO:参数校验 + reqParam := make(map[string]interface{}) + + if param != nil { + reqParam = *param + } + res := &RESTAPI{ + EndPoint: ep, + Method: method, + Uri: uri, + Param: reqParam, + Timeout: 150 * time.Second, + } + return res +} + +func (this *RESTAPI) SetSimulate(b bool) *RESTAPI { + this.isSimulate = b + return this +} + +func (this *RESTAPI) SetAPIKey(apiKey, secKey, passPhrase string) *RESTAPI { + if this.ApiKeyInfo == nil { + this.ApiKeyInfo = &APIKeyInfo{ + ApiKey: apiKey, + PassPhrase: passPhrase, + SecKey: secKey, + } + } else { + this.ApiKeyInfo.ApiKey = apiKey + this.ApiKeyInfo.PassPhrase = passPhrase + this.ApiKeyInfo.SecKey = secKey + } + return this +} + +func (this *RESTAPI) SetUserId(userId string) *RESTAPI { + if this.ApiKeyInfo == nil { + fmt.Println("ApiKey为空") + return this + } + + this.ApiKeyInfo.UserId = userId + return this +} + +func (this *RESTAPI) SetTimeOut(timeout time.Duration) *RESTAPI { + this.Timeout = timeout + return this +} + +// GET请求 +func (this *RESTAPI) Get(ctx context.Context, uri string, param *map[string]interface{}) (res *RESTAPIResult, err error) { + this.Method = GET + this.Uri = uri + + reqParam := make(map[string]interface{}) + + if param != nil { + reqParam = *param + } + this.Param = reqParam + return this.Run(ctx) +} + +// POST请求 +func (this *RESTAPI) Post(ctx context.Context, uri string, param *map[string]interface{}) (res *RESTAPIResult, err error) { + this.Method = POST + this.Uri = uri + + reqParam := make(map[string]interface{}) + + if param != nil { + reqParam = *param + } + this.Param = reqParam + + return this.Run(ctx) +} + +func (this *RESTAPI) Run(ctx context.Context) (res *RESTAPIResult, err error) { + + if this.ApiKeyInfo == nil { + err = errors.New("APIKey不可为空") + return + } + + procStart := time.Now() + + defer func() { + if res != nil { + res.TotalUsedTime = time.Since(procStart) + } + }() + + client := &http.Client{ + Timeout: this.Timeout, + } + + uri, body, err := this.GenReqInfo() + if err != nil { + return + } + + url := this.EndPoint + uri + bodyBuf := new(bytes.Buffer) + bodyBuf.ReadFrom(strings.NewReader(body)) + + req, err := http.NewRequest(this.Method, url, bodyBuf) + if err != nil { + return + } + + res = &RESTAPIResult{ + Url: url, + Param: body, + } + + // Sign and set request headers + timestamp := IsoTime() + preHash := PreHashString(timestamp, this.Method, uri, body) + //log.Println("preHash:", preHash) + sign, err := HmacSha256Base64Signer(preHash, this.ApiKeyInfo.SecKey) + if err != nil { + return + } + //log.Println("sign:", sign) + headStr := this.SetHeaders(req, timestamp, sign) + res.Header = headStr + + this.PrintRequest(req, body, preHash) + resp, err := client.Do(req) + if err != nil { + fmt.Println("请求失败!", err) + return + } + defer resp.Body.Close() + + res.ReqUsedTime = time.Since(procStart) + + resBuff, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("获取请求结果失败!", err) + return + } + + res.Body = string(resBuff) + res.Code = resp.StatusCode + + // 解析结果 + var v5rsp Okexv5APIResponse + err = json.Unmarshal(resBuff, &v5rsp) + if err != nil { + fmt.Println("解析v5返回失败!", err) + return + } + + res.V5Response = v5rsp + + return +} + +/* + 生成请求对应的参数 +*/ +func (this *RESTAPI) GenReqInfo() (uri string, body string, err error) { + uri = this.Uri + + switch this.Method { + case GET: + getParam := []string{} + + if len(this.Param) == 0 { + return + } + + for k, v := range this.Param { + getParam = append(getParam, fmt.Sprintf("%v=%v", k, v)) + } + uri = uri + "?" + strings.Join(getParam, "&") + + case POST: + + var rawBody []byte + rawBody, err = json.Marshal(this.Param) + if err != nil { + return + } + body = string(rawBody) + default: + err = errors.New("request type unknown!") + return + } + + return +} + +/* + Set http request headers: + Accept: application/json + Content-Type: application/json; charset=UTF-8 (default) + Cookie: locale=en_US (English) + OK-ACCESS-KEY: (Your setting) + OK-ACCESS-SIGN: (Use your setting, auto sign and add) + OK-ACCESS-TIMESTAMP: (Auto add) + OK-ACCESS-PASSPHRASE: Your setting +*/ +func (this *RESTAPI) SetHeaders(request *http.Request, timestamp string, sign string) (header string) { + + request.Header.Add(ACCEPT, APPLICATION_JSON) + header += ACCEPT + ":" + APPLICATION_JSON + "\n" + + request.Header.Add(CONTENT_TYPE, APPLICATION_JSON_UTF8) + header += CONTENT_TYPE + ":" + APPLICATION_JSON_UTF8 + "\n" + + request.Header.Add(COOKIE, LOCALE+ENGLISH) + header += COOKIE + ":" + LOCALE + ENGLISH + "\n" + + request.Header.Add(OK_ACCESS_KEY, this.ApiKeyInfo.ApiKey) + header += OK_ACCESS_KEY + ":" + this.ApiKeyInfo.ApiKey + "\n" + + request.Header.Add(OK_ACCESS_SIGN, sign) + header += OK_ACCESS_SIGN + ":" + sign + "\n" + + request.Header.Add(OK_ACCESS_TIMESTAMP, timestamp) + header += OK_ACCESS_TIMESTAMP + ":" + timestamp + "\n" + + request.Header.Add(OK_ACCESS_PASSPHRASE, this.ApiKeyInfo.PassPhrase) + header += OK_ACCESS_PASSPHRASE + ":" + this.ApiKeyInfo.PassPhrase + "\n" + + //模拟盘交易标记 + if this.isSimulate { + request.Header.Add(X_SIMULATE_TRADING, "1") + header += X_SIMULATE_TRADING + ":1" + "\n" + } + return +} + +/* + 打印header信息 +*/ +func (this *RESTAPI) PrintRequest(request *http.Request, body string, preHash string) { + if this.ApiKeyInfo.SecKey != "" { + fmt.Println(" Secret-Key: " + this.ApiKeyInfo.SecKey) + } + fmt.Println(" Request(" + IsoTime() + "):") + fmt.Println("\tUrl: " + request.URL.String()) + fmt.Println("\tMethod: " + strings.ToUpper(request.Method)) + if len(request.Header) > 0 { + fmt.Println("\tHeaders: ") + for k, v := range request.Header { + if strings.Contains(k, "Ok-") { + k = strings.ToUpper(k) + } + fmt.Println("\t\t" + k + ": " + v[0]) + } + } + fmt.Println("\tBody: " + body) + if preHash != "" { + fmt.Println(" PreHash: " + preHash) + } +} diff --git a/submodules/okex/rest/rest_test.go b/submodules/okex/rest/rest_test.go new file mode 100644 index 0000000..cc6c753 --- /dev/null +++ b/submodules/okex/rest/rest_test.go @@ -0,0 +1,100 @@ +package rest + +import ( + "context" + "fmt" + "testing" +) + +/* + GET请求 +*/ +func TestRESTAPIGet(t *testing.T) { + + rest := NewRESTAPI("https://www.okex.win", GET, "/api/v5/account/balance", nil) + rest.SetSimulate(true).SetAPIKey("xxxx", "xxxx", "xxxx") + rest.SetUserId("xxxxx") + response, err := rest.Run(context.Background()) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", response.Code) + fmt.Println("\t总耗时: ", response.TotalUsedTime) + fmt.Println("\t请求耗时: ", response.ReqUsedTime) + fmt.Println("\t返回消息: ", response.Body) + fmt.Println("\terrCode: ", response.V5Response.Code) + fmt.Println("\terrMsg: ", response.V5Response.Msg) + fmt.Println("\tdata: ", response.V5Response.Data) + + // 请求的另一种方式 + apikey := APIKeyInfo{ + ApiKey: "xxxxx", + SecKey: "xxxxx", + PassPhrase: "xxx", + } + + cli := NewRESTClient("https://www.okex.win", &apikey, true) + rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) + if err != nil { + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", rsp.Code) + fmt.Println("\t总耗时: ", rsp.TotalUsedTime) + fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) + fmt.Println("\t返回消息: ", rsp.Body) + fmt.Println("\terrCode: ", rsp.V5Response.Code) + fmt.Println("\terrMsg: ", rsp.V5Response.Msg) + fmt.Println("\tdata: ", rsp.V5Response.Data) +} + +/* + POST请求 +*/ +func TestRESTAPIPost(t *testing.T) { + param := make(map[string]interface{}) + param["greeksType"] = "PA" + + rest := NewRESTAPI("https://www.okex.win", POST, "/api/v5/account/set-greeks", ¶m) + rest.SetSimulate(true).SetAPIKey("xxxx", "xxxx", "xxxx") + response, err := rest.Run(context.Background()) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", response.Code) + fmt.Println("\t总耗时: ", response.TotalUsedTime) + fmt.Println("\t请求耗时: ", response.ReqUsedTime) + fmt.Println("\t返回消息: ", response.Body) + fmt.Println("\terrCode: ", response.V5Response.Code) + fmt.Println("\terrMsg: ", response.V5Response.Msg) + fmt.Println("\tdata: ", response.V5Response.Data) + + // 请求的另一种方式 + apikey := APIKeyInfo{ + ApiKey: "xxxx", + SecKey: "xxxxx", + PassPhrase: "xxxx", + } + + cli := NewRESTClient("https://www.okex.win", &apikey, true) + rsp, err := cli.Post(context.Background(), "/api/v5/account/set-greeks", ¶m) + if err != nil { + return + } + + fmt.Println("Response:") + fmt.Println("\thttp code: ", rsp.Code) + fmt.Println("\t总耗时: ", rsp.TotalUsedTime) + fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) + fmt.Println("\t返回消息: ", rsp.Body) + fmt.Println("\terrCode: ", rsp.V5Response.Code) + fmt.Println("\terrMsg: ", rsp.V5Response.Msg) + fmt.Println("\tdata: ", rsp.V5Response.Data) +} diff --git a/submodules/okex/utils/go.mod b/submodules/okex/utils/go.mod new file mode 100644 index 0000000..e69de29 diff --git a/submodules/okex/utils/utils.go b/submodules/okex/utils/utils.go new file mode 100644 index 0000000..830186d --- /dev/null +++ b/submodules/okex/utils/utils.go @@ -0,0 +1,102 @@ +package utils + +import ( + "bytes" + "compress/flate" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "io/ioutil" + "log" + "strconv" + "strings" + "time" + //"net/http" +) + +/* + Get a epoch time + eg: 1521221737 +*/ +func EpochTime() string { + millisecond := time.Now().UnixNano() / 1000000 + epoch := strconv.Itoa(int(millisecond)) + epochBytes := []byte(epoch) + epoch = string(epochBytes[:10]) + return epoch +} + +/* + signing a message + using: hmac sha256 + base64 + eg: + message = Pre_hash function comment + secretKey = E65791902180E9EF4510DB6A77F6EBAE + + return signed string = TO6uwdqz+31SIPkd4I+9NiZGmVH74dXi+Fd5X0EzzSQ= +*/ +func HmacSha256Base64Signer(message string, secretKey string) (string, error) { + mac := hmac.New(sha256.New, []byte(secretKey)) + _, err := mac.Write([]byte(message)) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil +} + +/* + the pre hash string + eg: + timestamp = 2018-03-08T10:59:25.789Z + method = POST + request_path = /orders?before=2&limit=30 + body = {"product_id":"BTC-USD-0309","order_id":"377454671037440"} + + return pre hash string = 2018-03-08T10:59:25.789ZPOST/orders?before=2&limit=30{"product_id":"BTC-USD-0309","order_id":"377454671037440"} +*/ +func PreHashString(timestamp string, method string, requestPath string, body string) string { + return timestamp + strings.ToUpper(method) + requestPath + body +} + +/* + struct convert json string +*/ +func Struct2JsonString(raw interface{}) (jsonString string, err error) { + //fmt.Println("转化json,", raw) + data, err := json.Marshal(raw) + if err != nil { + log.Println("convert json failed!", err) + return "", err + } + //log.Println(string(data)) + return string(data), nil +} + +// 解压缩消息 +func GzipDecode(in []byte) ([]byte, error) { + reader := flate.NewReader(bytes.NewReader(in)) + defer reader.Close() + + return ioutil.ReadAll(reader) +} + + +/* + Get a iso time + eg: 2018-03-16T18:02:48.284Z +*/ +func IsoTime() string { + utcTime := time.Now().UTC() + iso := utcTime.String() + isoBytes := []byte(iso) + iso = string(isoBytes[:10]) + "T" + string(isoBytes[11:23]) + "Z" + return iso +} + + + + + + + diff --git a/submodules/okex/utils/utils_test.go b/submodules/okex/utils/utils_test.go new file mode 100644 index 0000000..09bcf5b --- /dev/null +++ b/submodules/okex/utils/utils_test.go @@ -0,0 +1,17 @@ +package utils + +import ( + "fmt" + "testing" +) + +func TestHmacSha256Base64Signer(t *testing.T) { + raw := `2021-04-06T03:33:21.681ZPOST/api/v5/trade/order{"instId":"ETH-USDT-SWAP","ordType":"limit","px":"2300","side":"sell","sz":"1","tdMode":"cross"}` + key := "1A9E86759F2D2AA16E389FD3B7F8273E" + res, err := HmacSha256Base64Signer(raw, key) + if err != nil { + t.Fatal(err) + } + fmt.Println(res) + t.Log(res) +} diff --git a/submodules/okex/ws/go.mod b/submodules/okex/ws/go.mod new file mode 100644 index 0000000..e69de29 diff --git a/submodules/okex/ws/utils.go b/submodules/okex/ws/utils.go new file mode 100644 index 0000000..3c75066 --- /dev/null +++ b/submodules/okex/ws/utils.go @@ -0,0 +1,75 @@ +package ws + +import ( + "errors" + "log" + "runtime/debug" + . "v5sdk_go/ws/wImpl" + . "v5sdk_go/ws/wInterface" +) + +// 判断返回结果成功失败 +func checkResult(wsReq WSReqData, wsRsps []*Msg) (res bool, err error) { + defer func() { + a := recover() + if a != nil { + log.Printf("Receive End. Recover msg: %+v", a) + debug.PrintStack() + } + return + }() + + res = false + if len(wsRsps) == 0 { + return + } + + for _, v := range wsRsps { + switch v.Info.(type) { + case ErrData: + return + } + if wsReq.GetType() != v.Info.(WSRspData).MsgType() { + err = errors.New("消息类型不一致") + return + } + } + + //检查所有频道是否都更新成功 + if wsReq.GetType() == MSG_NORMAL { + req, ok := wsReq.(ReqData) + if !ok { + log.Println("类型转化失败", req) + err = errors.New("类型转化失败") + return + } + + for idx, _ := range req.Args { + ok := false + i_req := req.Args[idx] + //fmt.Println("检查",i_req) + for i, _ := range wsRsps { + info, _ := wsRsps[i].Info.(RspData) + //fmt.Println("<<",info) + if info.Event == req.Op && info.Arg["channel"] == i_req["channel"] && info.Arg["instType"] == i_req["instType"] { + ok = true + continue + } + } + if !ok { + err = errors.New("未得到所有的期望的返回结果") + return + } + } + } else { + for i, _ := range wsRsps { + info, _ := wsRsps[i].Info.(JRPCRsp) + if info.Code != "0" { + return + } + } + } + + res = true + return +} diff --git a/submodules/okex/ws/wImpl/BookData.go b/submodules/okex/ws/wImpl/BookData.go new file mode 100644 index 0000000..8867c8d --- /dev/null +++ b/submodules/okex/ws/wImpl/BookData.go @@ -0,0 +1,226 @@ +/* + 订阅频道后收到的推送数据 +*/ + +package wImpl + +import ( + "bytes" + "errors" + "fmt" + "hash/crc32" + "log" + "strconv" + "strings" +) + +// 普通推送 +type MsgData struct { + Arg map[string]string `json:"arg"` + Data []interface{} `json:"data"` +} + +// 深度数据 +type DepthData struct { + Arg map[string]string `json:"arg"` + Action string `json:"action"` + Data []DepthDetail `json:"data"` +} + +type DepthDetail struct { + Asks [][]string `json:"asks"` + Bids [][]string `json:"bids"` + Ts string `json:"ts"` + Checksum int32 `json:"checksum"` +} + +/* + 深度数据校验 +*/ +func (this *DepthData) CheckSum(snap *DepthDetail) (pDepData *DepthDetail, err error) { + + if len(this.Data) != 1 { + err = errors.New("深度数据错误!") + return + } + + if this.Action == DEPTH_SNAPSHOT { + _, cs := CalCrc32(this.Data[0].Asks, this.Data[0].Bids) + + if cs != this.Data[0].Checksum { + err = errors.New("校验失败!") + return + } + pDepData = &this.Data[0] + log.Println("snapshot校验成功", this.Data[0].Checksum) + + } + + if this.Action == DEPTH_UPDATE { + if snap == nil { + err = errors.New("深度快照数据不可为空!") + return + } + + pDepData, err = MergDepthData(*snap, this.Data[0], this.Data[0].Checksum) + if err != nil { + return + } + log.Println("update校验成功", this.Data[0].Checksum) + } + + return +} + +func CalCrc32(askDepths [][]string, bidDepths [][]string) (bytes.Buffer, int32) { + + crc32BaseBuffer := bytes.Buffer{} + crcAskDepth, crcBidDepth := 25, 25 + + if len(askDepths) < 25 { + crcAskDepth = len(askDepths) + } + if len(bidDepths) < 25 { + crcBidDepth = len(bidDepths) + } + if crcAskDepth == crcBidDepth { + for i := 0; i < crcAskDepth; i++ { + if crc32BaseBuffer.Len() > 0 { + crc32BaseBuffer.WriteString(":") + } + crc32BaseBuffer.WriteString( + fmt.Sprintf("%v:%v:%v:%v", + (bidDepths)[i][0], (bidDepths)[i][1], + (askDepths)[i][0], (askDepths)[i][1])) + } + } else { + + var crcArr []string + for i, j := 0, 0; i < crcBidDepth || j < crcAskDepth; { + + if i < crcBidDepth { + crcArr = append(crcArr, fmt.Sprintf("%v:%v", (bidDepths)[i][0], (bidDepths)[i][1])) + i++ + } + + if j < crcAskDepth { + crcArr = append(crcArr, fmt.Sprintf("%v:%v", (askDepths)[j][0], (askDepths)[j][1])) + j++ + } + } + + crc32BaseBuffer.WriteString(strings.Join(crcArr, ":")) + } + + expectCrc32 := int32(crc32.ChecksumIEEE(crc32BaseBuffer.Bytes())) + + return crc32BaseBuffer, expectCrc32 +} + +/* + 深度合并的内部方法 + 返回结果: + res:合并后的深度 + index: 最新的 ask1/bids1 的索引 +*/ +func mergeDepth(oldDepths [][]string, newDepths [][]string, method string) (res [][]string, err error) { + + oldIdx, newIdx := 0, 0 + + for oldIdx < len(oldDepths) && newIdx < len(newDepths) { + + oldItem := oldDepths[oldIdx] + newItem := newDepths[newIdx] + var oldPrice, newPrice float64 + oldPrice, err = strconv.ParseFloat(oldItem[0], 10) + if err != nil { + return + } + newPrice, err = strconv.ParseFloat(newItem[0], 10) + if err != nil { + return + } + + if oldPrice == newPrice { + if newItem[1] != "0" { + res = append(res, newItem) + } + + oldIdx++ + newIdx++ + } else { + switch method { + // 降序 + case "bids": + if oldPrice < newPrice { + res = append(res, newItem) + newIdx++ + } else { + + res = append(res, oldItem) + oldIdx++ + } + // 升序 + case "asks": + if oldPrice > newPrice { + res = append(res, newItem) + newIdx++ + } else { + + res = append(res, oldItem) + oldIdx++ + } + } + } + + } + + if oldIdx < len(oldDepths) { + res = append(res, oldDepths[oldIdx:]...) + } + + if newIdx < len(newDepths) { + res = append(res, newDepths[newIdx:]...) + } + + return +} + +/* + 深度合并,并校验 +*/ +func MergDepthData(snap DepthDetail, update DepthDetail, expChecksum int32) (res *DepthDetail, err error) { + + newAskDepths, err1 := mergeDepth(snap.Asks, update.Asks, "asks") + if err1 != nil { + return + } + + // log.Println("old Ask - ", snap.Asks) + // log.Println("update Ask - ", update.Asks) + // log.Println("new Ask - ", newAskDepths) + newBidDepths, err2 := mergeDepth(snap.Bids, update.Bids, "bids") + if err2 != nil { + return + } + // log.Println("old Bids - ", snap.Bids) + // log.Println("update Bids - ", update.Bids) + // log.Println("new Bids - ", newBidDepths) + + cBuf, checksum := CalCrc32(newAskDepths, newBidDepths) + if checksum != expChecksum { + err = errors.New("校验失败!") + log.Println("buffer:", cBuf.String()) + log.Fatal(checksum, expChecksum) + return + } + + res = &DepthDetail{ + Asks: newAskDepths, + Bids: newBidDepths, + Ts: update.Ts, + Checksum: update.Checksum, + } + + return +} diff --git a/submodules/okex/ws/wImpl/ErrData.go b/submodules/okex/ws/wImpl/ErrData.go new file mode 100644 index 0000000..020164b --- /dev/null +++ b/submodules/okex/ws/wImpl/ErrData.go @@ -0,0 +1,13 @@ +/* + 错误数据 +*/ +package wImpl + +// 服务端请求错误返回消息格式 +type ErrData struct { + Event string `json:"event"` + Code string `json:"code"` + Msg string `json:"msg"` +} + + diff --git a/submodules/okex/ws/wImpl/JRPCData.go b/submodules/okex/ws/wImpl/JRPCData.go new file mode 100644 index 0000000..cfd3223 --- /dev/null +++ b/submodules/okex/ws/wImpl/JRPCData.go @@ -0,0 +1,50 @@ +/* + JRPC请求/响应数据 +*/ +package wImpl + +import ( + "encoding/json" + . "v5sdk_go/utils" +) + +// jrpc请求结构体 +type JRPCReq struct { + Id string `json:"id"` + Op string `json:"op"` + Args []map[string]interface{} `json:"args"` +} + +func (r JRPCReq) GetType() int { + return MSG_JRPC +} + +func (r JRPCReq) ToString() string { + data, err := Struct2JsonString(r) + if err != nil { + return "" + } + return data +} + +func (r JRPCReq) Len() int { + return 1 +} + +// jrpc响应结构体 +type JRPCRsp struct { + Id string `json:"id"` + Op string `json:"op"` + Data []map[string]interface{} `json:"data"` + Code string `json:"code"` + Msg string `json:"msg"` +} + +func (r JRPCRsp) MsgType() int { + return MSG_JRPC +} + +func (r JRPCRsp) String() string { + raw, _ := json.Marshal(r) + return string(raw) +} diff --git a/submodules/okex/ws/wImpl/ReqData.go b/submodules/okex/ws/wImpl/ReqData.go new file mode 100644 index 0000000..df7afa1 --- /dev/null +++ b/submodules/okex/ws/wImpl/ReqData.go @@ -0,0 +1,47 @@ +/* + 普通订阅请求和响应的数据格式 +*/ + +package wImpl + +import ( + "encoding/json" + . "v5sdk_go/utils" +) + +// 客户端请求消息格式 +type ReqData struct { + Op string `json:"op"` + Args []map[string]string `json:"args"` +} + +func (r ReqData) GetType() int { + return MSG_NORMAL +} + +func (r ReqData) ToString() string { + data, err := Struct2JsonString(r) + if err != nil { + return "" + } + return data +} + +func (r ReqData) Len() int { + return len(r.Args) +} + +// 服务端请求响应消息格式 +type RspData struct { + Event string `json:"event"` + Arg map[string]string `json:"arg"` +} + +func (r RspData) MsgType() int { + return MSG_NORMAL +} + +func (r RspData) String() string { + raw, _ := json.Marshal(r) + return string(raw) +} diff --git a/submodules/okex/ws/wImpl/contants.go b/submodules/okex/ws/wImpl/contants.go new file mode 100644 index 0000000..c095fae --- /dev/null +++ b/submodules/okex/ws/wImpl/contants.go @@ -0,0 +1,241 @@ +package wImpl + +import ( + "regexp" +) + +/* + + */ + +const ( + MSG_NORMAL = iota + MSG_JRPC +) + +//事件 +type Event int + +/* + EventID +*/ +const ( + EVENT_UNKNOWN Event = iota + EVENT_ERROR + EVENT_PING + EVENT_LOGIN + + //订阅公共频道 + EVENT_BOOK_INSTRUMENTS + EVENT_STATUS + EVENT_BOOK_TICKERS + EVENT_BOOK_OPEN_INTEREST + EVENT_BOOK_KLINE + EVENT_BOOK_TRADE + EVENT_BOOK_ESTIMATE_PRICE + EVENT_BOOK_MARK_PRICE + EVENT_BOOK_MARK_PRICE_CANDLE_CHART + EVENT_BOOK_LIMIT_PRICE + EVENT_BOOK_ORDER_BOOK + EVENT_BOOK_ORDER_BOOK5 + EVENT_BOOK_ORDER_BOOK_TBT + EVENT_BOOK_ORDER_BOOK50_TBT + EVENT_BOOK_OPTION_SUMMARY + EVENT_BOOK_FUND_RATE + EVENT_BOOK_KLINE_INDEX + EVENT_BOOK_INDEX_TICKERS + + //订阅私有频道 + EVENT_BOOK_ACCOUNT + EVENT_BOOK_POSTION + EVENT_BOOK_ORDER + EVENT_BOOK_ALG_ORDER + EVENT_BOOK_B_AND_P + + // JRPC + EVENT_PLACE_ORDER + EVENT_PLACE_BATCH_ORDERS + EVENT_CANCEL_ORDER + EVENT_CANCEL_BATCH_ORDERS + EVENT_AMEND_ORDER + EVENT_AMEND_BATCH_ORDERS + + //订阅返回数据 + EVENT_BOOKED_DATA + EVENT_DEPTH_DATA +) + +/* + EventID,事件名称,channel + 注: 带有周期参数的频道 如 行情频道 ,需要将channel写为 正则表达模式方便 类型匹配,如 "^candle*" +*/ +var EVENT_TABLE = [][]interface{}{ + // 未知的消息 + {EVENT_UNKNOWN, "未知", ""}, + // 错误的消息 + {EVENT_ERROR, "错误", ""}, + // Ping + {EVENT_PING, "ping", ""}, + // 登陆 + {EVENT_LOGIN, "登录", ""}, + + /* + 订阅公共频道 + */ + + {EVENT_BOOK_INSTRUMENTS, "产品", "instruments"}, + {EVENT_STATUS, "status", "status"}, + {EVENT_BOOK_TICKERS, "行情", "tickers"}, + {EVENT_BOOK_OPEN_INTEREST, "持仓总量", "open-interest"}, + {EVENT_BOOK_KLINE, "K线", "candle"}, + {EVENT_BOOK_TRADE, "交易", "trades"}, + {EVENT_BOOK_ESTIMATE_PRICE, "预估交割/行权价格", "estimated-price"}, + {EVENT_BOOK_MARK_PRICE, "标记价格", "mark-price"}, + {EVENT_BOOK_MARK_PRICE_CANDLE_CHART, "标记价格K线", "mark-price-candle"}, + {EVENT_BOOK_LIMIT_PRICE, "限价", "price-limit"}, + {EVENT_BOOK_ORDER_BOOK, "400档深度", "books"}, + {EVENT_BOOK_ORDER_BOOK5, "5档深度", "books5"}, + {EVENT_BOOK_ORDER_BOOK_TBT, "tbt深度", "books-l2-tbt"}, + {EVENT_BOOK_ORDER_BOOK50_TBT, "tbt50深度", "books50-l2-tbt"}, + {EVENT_BOOK_OPTION_SUMMARY, "期权定价", "opt-summary"}, + {EVENT_BOOK_FUND_RATE, "资金费率", "funding-rate"}, + {EVENT_BOOK_KLINE_INDEX, "指数K线", "index-candle"}, + {EVENT_BOOK_INDEX_TICKERS, "指数行情", "index-tickers"}, + + /* + 订阅私有频道 + */ + {EVENT_BOOK_ACCOUNT, "账户", "account"}, + {EVENT_BOOK_POSTION, "持仓", "positions"}, + {EVENT_BOOK_ORDER, "订单", "orders"}, + {EVENT_BOOK_ALG_ORDER, "策略委托订单", "orders-algo"}, + {EVENT_BOOK_B_AND_P, "账户余额和持仓", "balance_and_position"}, + + /* + JRPC + */ + {EVENT_PLACE_ORDER, "下单", "order"}, + {EVENT_PLACE_BATCH_ORDERS, "批量下单", "batch-orders"}, + {EVENT_CANCEL_ORDER, "撤单", "cancel-order"}, + {EVENT_CANCEL_BATCH_ORDERS, "批量撤单", "batch-cancel-orders"}, + {EVENT_AMEND_ORDER, "改单", "amend-order"}, + {EVENT_AMEND_BATCH_ORDERS, "批量改单", "batch-amend-orders"}, + + /* + 订阅返回数据 + 注意:推送数据channle统一为"" + */ + {EVENT_BOOKED_DATA, "普通推送", ""}, + {EVENT_DEPTH_DATA, "深度推送", ""}, +} + +/* + 获取事件名称 +*/ +func (e Event) String() string { + for _, v := range EVENT_TABLE { + eventId := v[0].(Event) + if e == eventId { + return v[1].(string) + } + } + + return "" +} + +/* + 通过事件获取对应的channel信息 + 对于频道名称有时间周期的 通过参数 pd 传入,拼接后返回完整channel信息 +*/ +func (e Event) GetChannel(pd Period) string { + + channel := "" + + for _, v := range EVENT_TABLE { + eventId := v[0].(Event) + if e == eventId { + channel = v[2].(string) + break + } + } + + if channel == "" { + return "" + } + + return channel + string(pd) +} + +/* + 通过channel信息匹配获取事件类型 +*/ +func GetEventId(raw string) Event { + evt := EVENT_UNKNOWN + + for _, v := range EVENT_TABLE { + channel := v[2].(string) + if raw == channel { + evt = v[0].(Event) + break + } + + regexp := regexp.MustCompile(`^(.*)([1-9][0-9]?[\w])$`) + //regexp := regexp.MustCompile(`^http://www.flysnow.org/([\d]{4})/([\d]{2})/([\d]{2})/([\w-]+).html$`) + + substr := regexp.FindStringSubmatch(raw) + //fmt.Println(substr) + if len(substr) >= 2 { + if substr[1] == channel { + evt = v[0].(Event) + break + } + } + } + + return evt +} + +// 时间维度 +type Period string + +const ( + // 年 + PERIOD_1YEAR Period = "1Y" + + // 月 + PERIOD_6Mon Period = "6M" + PERIOD_3Mon Period = "3M" + PERIOD_1Mon Period = "1M" + + // 周 + PERIOD_1WEEK Period = "1W" + + // 天 + PERIOD_5DAY Period = "5D" + PERIOD_3DAY Period = "3D" + PERIOD_2DAY Period = "2D" + PERIOD_1DAY Period = "1D" + + // 小时 + PERIOD_12HOUR Period = "12H" + PERIOD_6HOUR Period = "6H" + PERIOD_4HOUR Period = "4H" + PERIOD_2HOUR Period = "2H" + PERIOD_1HOUR Period = "1H" + + // 分钟 + PERIOD_30MIN Period = "30m" + PERIOD_15MIN Period = "15m" + PERIOD_5MIN Period = "5m" + PERIOD_3MIN Period = "3m" + PERIOD_1MIN Period = "1m" + + // 缺省 + PERIOD_NONE Period = "" +) + +// 深度枚举 +const ( + DEPTH_SNAPSHOT = "snapshot" + DEPTH_UPDATE = "update" +) diff --git a/submodules/okex/ws/wImpl/contants_test.go b/submodules/okex/ws/wImpl/contants_test.go new file mode 100644 index 0000000..b4186ba --- /dev/null +++ b/submodules/okex/ws/wImpl/contants_test.go @@ -0,0 +1,28 @@ +package wImpl + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetEventId(t *testing.T) { + + id1 := GetEventId("index-candle30m") + + assert.True(t, id1 == EVENT_BOOK_KLINE_INDEX) + + id2 := GetEventId("candle1Y") + + assert.True(t, id2 == EVENT_BOOK_KLINE) + + id3 := GetEventId("orders-algo") + assert.True(t, id3 == EVENT_BOOK_ALG_ORDER) + + id4 := GetEventId("balance_and_position") + assert.True(t, id4 == EVENT_BOOK_B_AND_P) + + id5 := GetEventId("index-candle1m") + assert.True(t, id5 == EVENT_BOOK_KLINE_INDEX) + +} diff --git a/submodules/okex/ws/wInterface/IParam.go b/submodules/okex/ws/wInterface/IParam.go new file mode 100644 index 0000000..72f10e1 --- /dev/null +++ b/submodules/okex/ws/wInterface/IParam.go @@ -0,0 +1,9 @@ +package wInterface + +import . "v5sdk_go/ws/wImpl" + +// 请求数据 +type WSParam interface { + EventType() Event + ToMap() *map[string]string +} diff --git a/submodules/okex/ws/wInterface/IReqData.go b/submodules/okex/ws/wInterface/IReqData.go new file mode 100644 index 0000000..3bd299e --- /dev/null +++ b/submodules/okex/ws/wInterface/IReqData.go @@ -0,0 +1,8 @@ +package wInterface + +// 请求数据 +type WSReqData interface { + GetType() int + Len() int + ToString() string +} diff --git a/submodules/okex/ws/wInterface/IRspData.go b/submodules/okex/ws/wInterface/IRspData.go new file mode 100644 index 0000000..5282d71 --- /dev/null +++ b/submodules/okex/ws/wInterface/IRspData.go @@ -0,0 +1,6 @@ +package wInterface + +// 返回数据 +type WSRspData interface { + MsgType() int +} diff --git a/submodules/okex/ws/ws_AddBookedDataHook_test.go b/submodules/okex/ws/ws_AddBookedDataHook_test.go new file mode 100644 index 0000000..70ff328 --- /dev/null +++ b/submodules/okex/ws/ws_AddBookedDataHook_test.go @@ -0,0 +1,147 @@ +package ws + +// HOW TO RUN +// go test ws_cli.go ws_op.go ws_contants.go utils.go ws_pub_channel.go ws_pub_channel_test.go ws_priv_channel.go ws_priv_channel_test.go ws_jrpc.go ws_jrpc_test.go ws_test_AddBookedDataHook.go -v +import ( + "fmt" + "log" + "testing" + "time" + . "v5sdk_go/ws/wImpl" +) + +const ( + TRADE_ACCOUNT = iota + ISOLATE_ACCOUNT + CROSS_ACCOUNT + CROSS_ACCOUNT_B +) + +func init() { + log.SetFlags(log.LstdFlags | log.Llongfile) + +} + +func prework() *WsClient { + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err, ep) + } + return r +} +func prework_pri(t int) *WsClient { + // 模拟环境 + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + var apikey, passphrase, secretKey string + // 把账号密码写这里 + switch t { + case TRADE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case ISOLATE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT_B: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + } + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err) + } + + var res bool + start := time.Now() + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + usedTime := time.Since(start) + fmt.Println("登录成功!", usedTime.String()) + } else { + log.Fatal("登录失败!", err) + } + fmt.Println(apikey, secretKey, passphrase) + return r +} + +func TestAddBookedDataHook(t *testing.T) { + var r *WsClient + + /*订阅私有频道*/ + { + r = prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + r.AddBookMsgHook(func(ts time.Time, data MsgData) error { + // 添加你的方法 + fmt.Println("这是自定义AddBookMsgHook") + fmt.Println("当前数据是", data) + return nil + }) + + param := map[string]string{} + param["channel"] = "account" + param["ccy"] = "BTC" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(100 * time.Second) + } + + //订阅公共频道 + { + r = prework() + var res bool + var err error + + // r.AddBookMsgHook(func(ts time.Time, data MsgData) error { + // 添加你的方法 + // fmt.Println("这是公共自定义AddBookMsgHook") + // fmt.Println("当前数据是", data) + // return nil + // }) + + param := map[string]string{} + param["channel"] = "instruments" + param["instType"] = "FUTURES" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + select {} + } + +} diff --git a/submodules/okex/ws/ws_cli.go b/submodules/okex/ws/ws_cli.go new file mode 100644 index 0000000..0ed1553 --- /dev/null +++ b/submodules/okex/ws/ws_cli.go @@ -0,0 +1,725 @@ +package ws + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "regexp" + "runtime/debug" + "sync" + "time" + . "v5sdk_go/config" + . "v5sdk_go/utils" + . "v5sdk_go/ws/wImpl" + + "github.com/gorilla/websocket" +) + +// 全局回调函数 +type ReceivedDataCallback func(*Msg) error + +// 普通订阅推送数据回调函数 +type ReceivedMsgDataCallback func(time.Time, MsgData) error + +// 深度订阅推送数据回调函数 +type ReceivedDepthDataCallback func(time.Time, DepthData) error + +// websocket +type WsClient struct { + WsEndPoint string + WsApi *ApiInfo + conn *websocket.Conn + sendCh chan string //发消息队列 + resCh chan *Msg //收消息队列 + + errCh chan *Msg + regCh map[Event]chan *Msg //请求响应队列 + + quitCh chan struct{} + lock sync.RWMutex + + onMessageHook ReceivedDataCallback //全局消息回调函数 + onBookMsgHook ReceivedMsgDataCallback //普通订阅消息回调函数 + onDepthHook ReceivedDepthDataCallback //深度订阅消息回调函数 + OnErrorHook ReceivedDataCallback //错误处理回调函数 + + // 记录深度信息 + DepthDataList map[string]DepthDetail + autoDepthMgr bool // 深度数据管理(checksum等) + DepthDataLock sync.RWMutex + + isStarted bool //防止重复启动和关闭 + dailTimeout time.Duration +} + +/* + 服务端响应详细信息 + Timestamp: 接受到消息的时间 + Info: 接受到的消息字符串 +*/ +type Msg struct { + Timestamp time.Time `json:"timestamp"` + Info interface{} `json:"info"` +} + +func (this *Msg) Print() { + fmt.Println("【消息时间】", this.Timestamp.Format("2006-01-02 15:04:05.000")) + str, _ := json.Marshal(this.Info) + fmt.Println("【消息内容】", string(str)) +} + +/* + 订阅结果封装后的消息结构体 +*/ +type ProcessDetail struct { + EndPoint string `json:"endPoint"` + ReqInfo string `json:"ReqInfo"` //订阅请求 + SendTime time.Time `json:"sendTime"` //发送订阅请求的时间 + RecvTime time.Time `json:"recvTime"` //接受到订阅结果的时间 + UsedTime time.Duration `json:"UsedTime"` //耗时 + Data []*Msg `json:"data"` //订阅结果数据 +} + +func (p *ProcessDetail) String() string { + data, _ := json.Marshal(p) + return string(data) +} + +// 创建ws对象 +func NewWsClient(ep string) (r *WsClient, err error) { + if ep == "" { + err = errors.New("websocket endpoint cannot be null") + return + } + + r = &WsClient{ + WsEndPoint: ep, + sendCh: make(chan string), + resCh: make(chan *Msg), + errCh: make(chan *Msg), + regCh: make(map[Event]chan *Msg), + //cbs: make(map[Event]ReceivedDataCallback), + quitCh: make(chan struct{}), + DepthDataList: make(map[string]DepthDetail), + dailTimeout: time.Second * 10, + // 自动深度校验默认开启 + autoDepthMgr: true, + } + + return +} + +/* + 新增记录深度信息 +*/ +func (a *WsClient) addDepthDataList(key string, dd DepthDetail) error { + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + a.DepthDataList[key] = dd + return nil +} + +/* + 更新记录深度信息(如果没有记录不会更新成功) +*/ +func (a *WsClient) updateDepthDataList(key string, dd DepthDetail) error { + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + if _, ok := a.DepthDataList[key]; !ok { + return errors.New("更新失败!未发现记录" + key) + } + + a.DepthDataList[key] = dd + return nil +} + +/* + 删除记录深度信息 +*/ +func (a *WsClient) deleteDepthDataList(key string) error { + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + delete(a.DepthDataList, key) + return nil +} + +/* + 设置是否自动深度管理,开启 true,关闭 false +*/ +func (a *WsClient) EnableAutoDepthMgr(b bool) error { + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + + if len(a.DepthDataList) != 0 { + err := errors.New("当前有深度数据处于订阅中") + return err + } + + a.autoDepthMgr = b + return nil +} + +/* + 获取当前的深度快照信息(合并后的) +*/ +func (a *WsClient) GetSnapshotByChannel(data DepthData) (snapshot *DepthDetail, err error) { + key, err := json.Marshal(data.Arg) + if err != nil { + return + } + a.DepthDataLock.Lock() + defer a.DepthDataLock.Unlock() + val, ok := a.DepthDataList[string(key)] + if !ok { + return + } + snapshot = new(DepthDetail) + raw, err := json.Marshal(val) + if err != nil { + return + } + err = json.Unmarshal(raw, &snapshot) + if err != nil { + return + } + return +} + +// 设置dial超时时间 +func (a *WsClient) SetDailTimeout(tm time.Duration) { + a.dailTimeout = tm +} + +// 非阻塞启动 +func (a *WsClient) Start() error { + a.lock.RLock() + if a.isStarted { + a.lock.RUnlock() + fmt.Println("ws已经启动") + return nil + } else { + a.lock.RUnlock() + a.lock.Lock() + defer a.lock.Unlock() + // 增加超时处理 + done := make(chan struct{}) + ctx, cancel := context.WithTimeout(context.Background(), a.dailTimeout) + defer cancel() + go func() { + defer func() { + close(done) + }() + var c *websocket.Conn + fmt.Println("a.WsEndPoint: ", a.WsEndPoint) + c, _, err := websocket.DefaultDialer.Dial(a.WsEndPoint, nil) + if err != nil { + err = errors.New("dial error:" + err.Error()) + return + } + a.conn = c + + }() + select { + case <-ctx.Done(): + err := errors.New("连接超时退出!") + return err + case <-done: + + } + //TODO 自定义的推送消息回调回来试试放在这里 + go a.receive() + go a.work() + a.isStarted = true + log.Println("客户端已启动!", a.WsEndPoint) + return nil + } +} + +// 客户端退出消息channel +func (a *WsClient) IsQuit() <-chan struct{} { + return a.quitCh +} + +func (a *WsClient) work() { + defer func() { + a.Stop() + err := recover() + if err != nil { + log.Printf("work End. Recover msg: %+v", a) + debug.PrintStack() + } + + }() + + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: // 保持心跳 + // go a.Ping(1000) + go func() { + _, _, err := a.Ping(1000) + if err != nil { + fmt.Println("心跳检测失败!", err) + a.Stop() + return + } + + }() + + case <-a.quitCh: // 保持心跳 + return + case data, ok := <-a.resCh: //接收到服务端发来的消息 + if !ok { + return + } + //log.Println("接收到来自resCh的消息:", data) + if a.onMessageHook != nil { + err := a.onMessageHook(data) + if err != nil { + log.Println("执行onMessageHook函数错误!", err) + } + } + case errMsg, ok := <-a.errCh: //错误处理 + if !ok { + return + } + if a.OnErrorHook != nil { + err := a.OnErrorHook(errMsg) + if err != nil { + log.Println("执行OnErrorHook函数错误!", err) + } + } + case req, ok := <-a.sendCh: //从发送队列中取出数据发送到服务端 + if !ok { + return + } + //log.Println("接收到来自req的消息:", req) + err := a.conn.WriteMessage(websocket.TextMessage, []byte(req)) + if err != nil { + log.Printf("发送请求失败: %s\n", err) + return + } + log.Printf("[发送请求] %v\n", req) + } + } + +} + +/* + 处理接受到的消息 +*/ +func (a *WsClient) receive() { + defer func() { + a.Stop() + err := recover() + if err != nil { + log.Printf("Receive End. Recover msg: %+v", a) + debug.PrintStack() + } + + }() + + for { + messageType, message, err := a.conn.ReadMessage() + if err != nil { + if a.isStarted { + log.Println("receive message error!" + err.Error()) + } + + break + } + + txtMsg := message + switch messageType { + case websocket.TextMessage: + case websocket.BinaryMessage: + txtMsg, err = GzipDecode(message) + if err != nil { + log.Println("解压失败!") + continue + } + } + + log.Println("[收到消息]", string(txtMsg)) + + //发送结果到默认消息处理通道 + + timestamp := time.Now() + msg := &Msg{Timestamp: timestamp, Info: string(txtMsg)} + + a.resCh <- msg + + evt, data, err := a.parseMessage(txtMsg) + if err != nil { + log.Println("解析消息失败!", err) + continue + } + + //log.Println("解析消息成功!消息类型 =", evt) + + a.lock.RLock() + ch, ok := a.regCh[evt] + a.lock.RUnlock() + if !ok { + //只有推送消息才会主动创建通道和消费队列 + if evt == EVENT_BOOKED_DATA || evt == EVENT_DEPTH_DATA { + //log.Println("channel不存在!event:", evt) + //a.lock.RUnlock() + a.lock.Lock() + a.regCh[evt] = make(chan *Msg) + ch = a.regCh[evt] + a.lock.Unlock() + + //log.Println("创建", evt, "通道") + + // 创建消费队列 + go func(evt Event) { + //log.Println("创建goroutine evt:", evt) + + for msg := range a.regCh[evt] { + //log.Println(msg) + // msg.Print() + switch evt { + // 处理普通推送数据 + case EVENT_BOOKED_DATA: + fn := a.onBookMsgHook + if fn != nil { + err = fn(msg.Timestamp, msg.Info.(MsgData)) + if err != nil { + log.Println("订阅数据回调函数执行失败!", err) + } + //log.Println("函数执行成功!", err) + } + // 处理深度推送数据 + case EVENT_DEPTH_DATA: + fn := a.onDepthHook + + depData := msg.Info.(DepthData) + + // 开启深度数据管理功能的,会合并深度数据 + if a.autoDepthMgr { + a.MergeDepth(depData) + } + + // 运行用户定义回调函数 + if fn != nil { + err = fn(msg.Timestamp, msg.Info.(DepthData)) + if err != nil { + log.Println("深度回调函数执行失败!", err) + } + + } + } + + } + //log.Println("退出goroutine evt:", evt) + }(evt) + + //continue + } else { + //log.Println("程序异常!通道已关闭", evt) + continue + } + + } + + //log.Println(evt,"事件已注册",ch) + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Millisecond*1000) + select { + /* + 丢弃消息容易引发数据处理处理错误 + */ + // case <-ctx.Done(): + // log.Println("等待超时,消息丢弃 - ", data) + case ch <- &Msg{Timestamp: timestamp, Info: data}: + } + cancel() + } +} + +/* + 开启了深度数据管理功能后,系统会自动合并深度信息 +*/ +func (a *WsClient) MergeDepth(depData DepthData) (err error) { + if !a.autoDepthMgr { + return + } + + key, err := json.Marshal(depData.Arg) + if err != nil { + err = errors.New("数据错误") + return + } + + // books5 不需要做checksum + if depData.Arg["channel"] == "books5" { + a.addDepthDataList(string(key), depData.Data[0]) + return + } + + if depData.Action == "snapshot" { + + _, err = depData.CheckSum(nil) + if err != nil { + log.Println("校验失败", err) + return + } + + a.addDepthDataList(string(key), depData.Data[0]) + + } else { + + var newSnapshot *DepthDetail + a.DepthDataLock.RLock() + oldSnapshot, ok := a.DepthDataList[string(key)] + if !ok { + log.Println("深度数据错误,全量数据未发现!") + err = errors.New("数据错误") + return + } + a.DepthDataLock.RUnlock() + newSnapshot, err = depData.CheckSum(&oldSnapshot) + if err != nil { + log.Println("深度校验失败", err) + err = errors.New("校验失败") + return + } + + a.updateDepthDataList(string(key), *newSnapshot) + } + return +} + +/* + 通过ErrorCode判断事件类型 +*/ +func GetInfoFromErrCode(data ErrData) Event { + switch data.Code { + case "60001": + return EVENT_LOGIN + case "60002": + return EVENT_LOGIN + case "60003": + return EVENT_LOGIN + case "60004": + return EVENT_LOGIN + case "60005": + return EVENT_LOGIN + case "60006": + return EVENT_LOGIN + case "60007": + return EVENT_LOGIN + case "60008": + return EVENT_LOGIN + case "60009": + return EVENT_LOGIN + case "60010": + return EVENT_LOGIN + case "60011": + return EVENT_LOGIN + } + + return EVENT_UNKNOWN +} + +/* + 从error返回中解析出对应的channel + error信息样例 + {"event":"error","msg":"channel:index-tickers,instId:BTC-USDT1 doesn't exist","code":"60018"} +*/ +func GetInfoFromErrMsg(raw string) (channel string) { + reg := regexp.MustCompile(`channel:(.*?),`) + if reg == nil { + fmt.Println("MustCompile err") + return + } + //提取关键信息 + result := reg.FindAllStringSubmatch(raw, -1) + for _, text := range result { + channel = text[1] + } + return +} + +/* + 解析消息类型 +*/ +func (a *WsClient) parseMessage(raw []byte) (evt Event, data interface{}, err error) { + evt = EVENT_UNKNOWN + //log.Println("解析消息") + //log.Println("消息内容:", string(raw)) + if string(raw) == "pong" { + evt = EVENT_PING + data = raw + return + } + //log.Println(0, evt) + var rspData = RspData{} + err = json.Unmarshal(raw, &rspData) + if err == nil { + op := rspData.Event + if op == OP_SUBSCRIBE || op == OP_UNSUBSCRIBE { + channel := rspData.Arg["channel"] + evt = GetEventId(channel) + data = rspData + return + } + } + + //log.Println("ErrData") + var errData = ErrData{} + err = json.Unmarshal(raw, &errData) + if err == nil { + op := errData.Event + switch op { + case OP_LOGIN: + evt = EVENT_LOGIN + data = errData + //log.Println(3, evt) + return + case OP_ERROR: + data = errData + // TODO:细化报错对应的事件判断 + + //尝试从msg字段中解析对应的事件类型 + evt = GetInfoFromErrCode(errData) + if evt != EVENT_UNKNOWN { + return + } + evt = GetEventId(GetInfoFromErrMsg(errData.Msg)) + if evt == EVENT_UNKNOWN { + evt = EVENT_ERROR + return + } + return + } + //log.Println(5, evt) + } + + //log.Println("JRPCRsp") + var jRPCRsp = JRPCRsp{} + err = json.Unmarshal(raw, &jRPCRsp) + if err == nil { + data = jRPCRsp + evt = GetEventId(jRPCRsp.Op) + if evt != EVENT_UNKNOWN { + return + } + } + + var depthData = DepthData{} + err = json.Unmarshal(raw, &depthData) + if err == nil { + evt = EVENT_DEPTH_DATA + data = depthData + //log.Println("-->>EVENT_DEPTH_DATA", evt) + //log.Println(evt, data) + //log.Println(6) + switch depthData.Arg["channel"] { + case "books": + return + case "books-l2-tbt": + return + case "books50-l2-tbt": + return + case "books5": + return + default: + + } + } + + //log.Println("MsgData") + var msgData = MsgData{} + err = json.Unmarshal(raw, &msgData) + if err == nil { + evt = EVENT_BOOKED_DATA + data = msgData + //log.Println("-->>EVENT_BOOK_DATA", evt) + //log.Println(evt, data) + //log.Println(6) + return + } + + evt = EVENT_UNKNOWN + err = errors.New("message unknown") + return +} + +func (a *WsClient) Stop() error { + + a.lock.Lock() + defer a.lock.Unlock() + if !a.isStarted { + return nil + } + + a.isStarted = false + + if a.conn != nil { + a.conn.Close() + } + close(a.errCh) + close(a.sendCh) + close(a.resCh) + close(a.quitCh) + + for _, ch := range a.regCh { + close(ch) + } + + log.Println("ws客户端退出!") + return nil +} + +/* + 添加全局消息处理的回调函数 +*/ +func (a *WsClient) AddMessageHook(fn ReceivedDataCallback) error { + a.onMessageHook = fn + return nil +} + +/* + 添加订阅消息处理的回调函数 +*/ +func (a *WsClient) AddBookMsgHook(fn ReceivedMsgDataCallback) error { + a.onBookMsgHook = fn + return nil +} + +/* + 添加深度消息处理的回调函数 + 例如: + cli.AddDepthHook(func(ts time.Time, data DepthData) error { return nil }) +*/ +func (a *WsClient) AddDepthHook(fn ReceivedDepthDataCallback) error { + a.onDepthHook = fn + return nil +} + +/* + 添加错误类型消息处理的回调函数 +*/ +func (a *WsClient) AddErrMsgHook(fn ReceivedDataCallback) error { + a.OnErrorHook = fn + return nil +} + +/* + 判断连接是否存活 +*/ +func (a *WsClient) IsAlive() bool { + res := false + if a.conn == nil { + return res + } + res, _, _ = a.Ping(500) + return res +} diff --git a/submodules/okex/ws/ws_contants.go b/submodules/okex/ws/ws_contants.go new file mode 100644 index 0000000..69a5dfe --- /dev/null +++ b/submodules/okex/ws/ws_contants.go @@ -0,0 +1,18 @@ +package ws + +//操作符 +const ( + OP_LOGIN = "login" + OP_ERROR = "error" + OP_SUBSCRIBE = "subscribe" + OP_UNSUBSCRIBE = "unsubscribe" +) + +// instrument Type +const ( + SPOT = "SPOT" + SWAP = "SWAP" + FUTURES = "FUTURES" + OPTION = "OPTION" + ANY = "ANY" +) diff --git a/submodules/okex/ws/ws_jrpc.go b/submodules/okex/ws/ws_jrpc.go new file mode 100644 index 0000000..b27480e --- /dev/null +++ b/submodules/okex/ws/ws_jrpc.go @@ -0,0 +1,157 @@ +package ws + +import ( + "context" + "log" + "time" + . "v5sdk_go/ws/wImpl" +) + +/* + websocket交易 通用请求 + 参数说明: + evtId:封装的事件类型 + id: 请求ID + op: 请求参数op + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) jrpcReq(evtId Event, op string, id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + req := &JRPCReq{ + Id: id, + Op: op, + Args: params, + } + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + defer cancel() + ctx = context.WithValue(ctx, "detail", detail) + + msg, err := a.process(ctx, evtId, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +/* + 单个下单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) PlaceOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + op := "order" + evtId := EVENT_PLACE_ORDER + + var args []map[string]interface{} + args = append(args, param) + + return a.jrpcReq(evtId, op, id, args, timeOut...) + +} + +/* + 批量下单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) BatchPlaceOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "batch-orders" + evtId := EVENT_PLACE_BATCH_ORDERS + return a.jrpcReq(evtId, op, id, params, timeOut...) + +} + +/* + 单个撤单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) CancelOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "cancel-order" + evtId := EVENT_CANCEL_ORDER + + var args []map[string]interface{} + args = append(args, param) + + return a.jrpcReq(evtId, op, id, args, timeOut...) + +} + +/* + 批量撤单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) BatchCancelOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "batch-cancel-orders" + evtId := EVENT_CANCEL_BATCH_ORDERS + return a.jrpcReq(evtId, op, id, params, timeOut...) + +} + +/* + 单个改单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) AmendOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "amend-order" + evtId := EVENT_AMEND_ORDER + + var args []map[string]interface{} + args = append(args, param) + + return a.jrpcReq(evtId, op, id, args, timeOut...) + +} + +/* + 批量改单 + 参数说明: + id: 请求ID + params: 请求参数 + timeOut: 超时时间 +*/ +func (a *WsClient) BatchAmendOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + op := "batch-amend-orders" + evtId := EVENT_AMEND_BATCH_ORDERS + return a.jrpcReq(evtId, op, id, params, timeOut...) + +} diff --git a/submodules/okex/ws/ws_jrpc_test.go b/submodules/okex/ws/ws_jrpc_test.go new file mode 100644 index 0000000..502e7e1 --- /dev/null +++ b/submodules/okex/ws/ws_jrpc_test.go @@ -0,0 +1,186 @@ +package ws + +import ( + "fmt" + "testing" + "time" + . "v5sdk_go/ws/wImpl" +) + +func PrintDetail(d *ProcessDetail) { + fmt.Println("[详细信息]") + fmt.Println("请求地址:", d.EndPoint) + fmt.Println("请求内容:", d.ReqInfo) + fmt.Println("发送时间:", d.SendTime.Format("2006-01-02 15:04:05.000")) + fmt.Println("响应时间:", d.RecvTime.Format("2006-01-02 15:04:05.000")) + fmt.Println("耗时:", d.UsedTime.String()) + fmt.Printf("接受到 %v 条消息:\n", len(d.Data)) + for _, v := range d.Data { + fmt.Printf("[%v] %v\n", v.Timestamp.Format("2006-01-02 15:04:05.000"), v.Info) + } +} + +func (r *WsClient) makeOrder(instId string, tdMode string, side string, ordType string, px string, sz string) (orderId string, err error) { + + var res bool + var data *ProcessDetail + + param := map[string]interface{}{} + param["instId"] = instId + param["tdMode"] = tdMode + param["side"] = side + param["ordType"] = ordType + if px != "" { + param["px"] = px + } + param["sz"] = sz + + res, data, err = r.PlaceOrder("0011", param) + if err != nil { + return + } + if res && len(data.Data) == 1 { + rsp := data.Data[0].Info.(JRPCRsp) + if len(rsp.Data) == 1 { + val, ok := rsp.Data[0]["ordId"] + if !ok { + return + } + orderId = val.(string) + return + } + } + + return +} + +/* + 单个下单 +*/ +// func TestPlaceOrder(t *testing.T) { +// r := prework_pri(CROSS_ACCOUNT) +// r := prework_pri(TRADE_ACCOUNT) +// var res bool +// var err error +// var data *ProcessDetail +// +// start := time.Now() +// param := map[string]interface{}{} +// param["instId"] = "BTC-USDT" +// param["tdMode"] = "cash" +// param["side"] = "buy" +// param["ordType"] = "market" +// param["px"] = "1" +// param["sz"] = "200" +// +// res, data, err = r.PlaceOrder("0011", param) +// if res { +// usedTime := time.Since(start) +// fmt.Println("下单成功!", usedTime.String()) +// PrintDetail(data) +// } else { +// usedTime := time.Since(start) +// fmt.Println("下单失败!", usedTime.String(), err) +// } +// +// } + +/* + 批量下单 +*/ +// func TestPlaceBatchOrder(t *testing.T) { +// r := prework_pri(CROSS_ACCOUNT) +// var res bool +// var err error +// var data *ProcessDetail +// +// start := time.Now() +// var params []map[string]interface{} +// param := map[string]interface{}{} +// param["instId"] = "BTC-USDT" +// param["tdMode"] = "cash" +// param["side"] = "sell" +// param["ordType"] = "market" +// param["sz"] = "0.001" +// params = append(params, param) +// param = map[string]interface{}{} +// param["instId"] = "BTC-USDT" +// param["tdMode"] = "cash" +// param["side"] = "buy" +// param["ordType"] = "market" +// param["sz"] = "100" +// params = append(params, param) +// res, data, err = r.BatchPlaceOrders("001", params) +// usedTime := time.Since(start) +// if err != nil { +// fmt.Println("下单失败!", err, usedTime.String()) +// t.Fail() +// } +// if res { +// fmt.Println("下单成功!", usedTime.String()) +// PrintDetail(data) +// } else { +// +// fmt.Println("下单失败!", usedTime.String()) +// t.Fail() +// } +// +// } + +/* + 撤销订单 +*/ +// func TestCancelOrder(t *testing.T) { +// r := prework_pri(CROSS_ACCOUNT) +// +// 用户自定义limit限价价格 +// ordId, _ := r.makeOrder("BTC-USDT", "cash", "sell", "limit", "57000", "0.01") +// if ordId == "" { +// t.Fatal() +// } +// +// t.Log("生成挂单:orderId=", ordId) +// +// param := map[string]interface{}{} +// param["instId"] = "BTC-USDT" +// param["ordId"] = ordId +// start := time.Now() +// res, _, _ := r.CancelOrder("1", param) +// if res { +// usedTime := time.Since(start) +// fmt.Println("撤单成功!", usedTime.String()) +// } else { +// t.Fatal("撤单失败!") +// } +// } + +/* + 修改订单 +*/ +func TestAmendlOrder(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + + // 用户自定义limit限价价格 + ordId, _ := r.makeOrder("BTC-USDT", "cash", "sell", "limit", "57000", "0.01") + if ordId == "" { + t.Fatal() + } + + t.Log("生成挂单:orderId=", ordId) + + param := map[string]interface{}{} + param["instId"] = "BTC-USDT" + param["ordId"] = ordId + // 调整修改订单的参数 + //param["newSz"] = "0.02" + param["newPx"] = "57001" + + start := time.Now() + res, _, _ := r.AmendOrder("1", param) + if res { + usedTime := time.Since(start) + fmt.Println("修改订单成功!", usedTime.String()) + } else { + t.Fatal("修改订单失败!") + } +} diff --git a/submodules/okex/ws/ws_middleware.go b/submodules/okex/ws/ws_middleware.go new file mode 100644 index 0000000..800ea02 --- /dev/null +++ b/submodules/okex/ws/ws_middleware.go @@ -0,0 +1,19 @@ +package ws + +import "fmt" + +type ReqFunc func(...interface{}) (res bool, msg *Msg, err error) +type Decorator func(ReqFunc) ReqFunc + +func handler(h ReqFunc, decors ...Decorator) ReqFunc { + for i := range decors { + d := decors[len(decors)-1-i] + h = d(h) + } + return h +} + +func preprocess() (res bool, msg *Msg, err error) { + fmt.Println("preprocess") + return +} diff --git a/submodules/okex/ws/ws_op.go b/submodules/okex/ws/ws_op.go new file mode 100644 index 0000000..f3522f4 --- /dev/null +++ b/submodules/okex/ws/ws_op.go @@ -0,0 +1,532 @@ +package ws + +import ( + "context" + "errors" + "log" + "sync" + "time" + . "v5sdk_go/config" + "v5sdk_go/rest" + . "v5sdk_go/utils" + . "v5sdk_go/ws/wImpl" + . "v5sdk_go/ws/wInterface" +) + +/* + Ping服务端保持心跳。 + timeOut:超时时间(毫秒),如果不填默认为5000ms +*/ +func (a *WsClient) Ping(timeOut ...int) (res bool, detail *ProcessDetail, err error) { + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + res = true + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + msg, err := a.process(ctx, EVENT_PING, nil) + if err != nil { + res = false + log.Println("处理请求失败!", err) + return + } + detail.Data = msg + + if len(msg) == 0 { + res = false + return + } + + str := string(msg[0].Info.([]byte)) + if str != "pong" { + res = false + return + } + + return +} + +/* + 登录私有频道 +*/ +func (a *WsClient) Login(apiKey, secKey, passPhrase string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + + if apiKey == "" { + err = errors.New("ApiKey cannot be null") + return + } + + if secKey == "" { + err = errors.New("SecretKey cannot be null") + return + } + + if passPhrase == "" { + err = errors.New("Passphrase cannot be null") + return + } + + a.WsApi = &ApiInfo{ + ApiKey: apiKey, + SecretKey: secKey, + Passphrase: passPhrase, + } + + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + res = true + + timestamp := EpochTime() + + preHash := PreHashString(timestamp, rest.GET, "/users/self/verify", "") + //fmt.Println("preHash:", preHash) + var sign string + if sign, err = HmacSha256Base64Signer(preHash, secKey); err != nil { + log.Println("处理签名失败!", err) + return + } + + args := map[string]string{} + args["apiKey"] = apiKey + args["passphrase"] = passPhrase + args["timestamp"] = timestamp + args["sign"] = sign + req := &ReqData{ + Op: OP_LOGIN, + Args: []map[string]string{args}, + } + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + + msg, err := a.process(ctx, EVENT_LOGIN, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + + if len(msg) == 0 { + res = false + return + } + + info, _ := msg[0].Info.(ErrData) + + if info.Code == "0" && info.Event == OP_LOGIN { + log.Println("登录成功!") + } else { + log.Println("登录失败!") + res = false + return + } + + return +} + +/* + 等待结果响应 +*/ +func (a *WsClient) waitForResult(e Event, timeOut int) (data interface{}, err error) { + + if _, ok := a.regCh[e]; !ok { + a.lock.Lock() + a.regCh[e] = make(chan *Msg) + a.lock.Unlock() + //log.Println("注册", e, "事件成功") + } + + a.lock.RLock() + defer a.lock.RUnlock() + ch := a.regCh[e] + //log.Println(e, "等待响应!") + select { + case <-time.After(time.Duration(timeOut) * time.Millisecond): + log.Println(e, "超时未响应!") + err = errors.New(e.String() + "超时未响应!") + return + case data = <-ch: + //log.Println(data) + } + + return +} + +/* + 发送消息到服务端 +*/ +func (a *WsClient) Send(ctx context.Context, op WSReqData) (err error) { + select { + case <-ctx.Done(): + log.Println("发生失败退出!") + err = errors.New("发送超时退出!") + case a.sendCh <- op.ToString(): + } + + return +} + +func (a *WsClient) process(ctx context.Context, e Event, op WSReqData) (data []*Msg, err error) { + defer func() { + _ = recover() + }() + + var detail *ProcessDetail + if val := ctx.Value("detail"); val != nil { + detail = val.(*ProcessDetail) + } else { + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + } + defer func() { + //fmt.Println("处理完成,", e.String()) + detail.UsedTime = detail.RecvTime.Sub(detail.SendTime) + }() + + //查看事件是否被注册 + if _, ok := a.regCh[e]; !ok { + a.lock.Lock() + a.regCh[e] = make(chan *Msg) + a.lock.Unlock() + //log.Println("注册", e, "事件成功") + } else { + //log.Println("事件", e, "已注册!") + err = errors.New("事件" + e.String() + "尚未处理完毕") + return + } + + //预期请求响应的条数 + expectCnt := 1 + if op != nil { + expectCnt = op.Len() + } + recvCnt := 0 + + //等待完成通知 + wg := sync.WaitGroup{} + wg.Add(1) + // 这里要先定义go routine func(){} 是为了在里面订阅channel的内容, 我们知道一个队列要想往里塞东西,必先给他安排一个订阅它的协程 + go func(ctx context.Context) { + defer func() { + a.lock.Lock() + delete(a.regCh, e) + //log.Println("事件已注销!",e) + a.lock.Unlock() + wg.Done() + }() + + a.lock.RLock() + ch := a.regCh[e] //请求响应队列 + a.lock.RUnlock() + + //log.Println(e, "等待响应!") + done := false + ok := true + for { + var item *Msg + select { + case <-ctx.Done(): + log.Println(e, "超时未响应!") + err = errors.New(e.String() + "超时未响应!") + return + case item, ok = <-ch: + if !ok { + return + } + detail.RecvTime = time.Now() + //log.Println(e, "接受到数据", item) + // 这里只是把推送的数据显示出来,并没有做更近一步的处理,后续可以二次开发,在这个位置上进行处理 + data = append(data, item) + recvCnt++ + //log.Println(data) + if recvCnt == expectCnt { + done = true + break + } + } + if done { + break + } + } + if ok { + close(ch) + } + + }(ctx) + + // + switch e { + case EVENT_PING: + msg := "ping" + detail.ReqInfo = msg + a.sendCh <- msg + detail.SendTime = time.Now() + default: + detail.ReqInfo = op.ToString() + //这个时候ctx中已经提供了meta信息,用于发送ws请求 + err = a.Send(ctx, op) + if err != nil { + log.Println("发送[", e, "]消息失败!", err) + return + } + detail.SendTime = time.Now() + } + + wg.Wait() + return +} + +/* + 根据args请求参数判断请求类型 + 如:{"channel": "account","ccy": "BTC"} 类型为 EVENT_BOOK_ACCOUNT +*/ +func GetEventByParam(param map[string]string) (evtId Event) { + evtId = EVENT_UNKNOWN + channel, ok := param["channel"] + if !ok { + return + } + + evtId = GetEventId(channel) + return +} + +/* + 订阅频道。 + req:请求json字符串 +*/ +func (a *WsClient) Subscribe(param map[string]string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + evtid := GetEventByParam(param) + if evtid == EVENT_UNKNOWN { + err = errors.New("非法的请求参数!") + return + } + + var args []map[string]string + args = append(args, param) + + req := ReqData{ + Op: OP_SUBSCRIBE, + Args: args, + } + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + + msg, err := a.process(ctx, evtid, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + + //检查所有频道是否都更新成功 + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +/* + 取消订阅频道。 + req:请求json字符串 +*/ +func (a *WsClient) UnSubscribe(param map[string]string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + evtid := GetEventByParam(param) + if evtid == EVENT_UNKNOWN { + err = errors.New("非法的请求参数!") + return + } + + var args []map[string]string + args = append(args, param) + + req := ReqData{ + Op: OP_UNSUBSCRIBE, + Args: args, + } + + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + msg, err := a.process(ctx, evtid, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + //检查所有频道是否都更新成功 + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +/* + jrpc请求 +*/ +func (a *WsClient) Jrpc(id, op string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + evtid := GetEventId(op) + if evtid == EVENT_UNKNOWN { + err = errors.New("非法的请求参数!") + return + } + + req := JRPCReq{ + Id: id, + Op: op, + Args: params, + } + detail = &ProcessDetail{ + EndPoint: a.WsEndPoint, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + ctx = context.WithValue(ctx, "detail", detail) + msg, err := a.process(ctx, evtid, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + detail.Data = msg + + //检查所有频道是否都更新成功 + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +func (a *WsClient) PubChannel(evtId Event, op string, params []map[string]string, pd Period, timeOut ...int) (res bool, msg []*Msg, err error) { + + // 参数校验 + pa, err := checkParams(evtId, params, pd) + if err != nil { + return + } + + res = true + tm := 5000 + if len(timeOut) != 0 { + tm = timeOut[0] + } + + req := ReqData{ + Op: op, + Args: pa, + } + + ctx := context.Background() + ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) + msg, err = a.process(ctx, evtId, req) + if err != nil { + res = false + log.Println("处理请求失败!", req, err) + return + } + + //检查所有频道是否都更新成功 + + res, err = checkResult(req, msg) + if err != nil { + res = false + return + } + + return +} + +// 参数校验 +func checkParams(evtId Event, params []map[string]string, pd Period) (res []map[string]string, err error) { + + channel := evtId.GetChannel(pd) + if channel == "" { + err = errors.New("参数校验失败!未知的类型:" + evtId.String()) + return + } + log.Println(channel) + if params == nil { + tmp := make(map[string]string) + tmp["channel"] = channel + res = append(res, tmp) + } else { + //log.Println(params) + for _, param := range params { + + tmp := make(map[string]string) + for k, v := range param { + tmp[k] = v + } + + val, ok := tmp["channel"] + if !ok { + tmp["channel"] = channel + } else { + if val != channel { + err = errors.New("参数校验失败!channel应为" + channel + val) + return + } + } + + res = append(res, tmp) + } + } + + return +} diff --git a/submodules/okex/ws/ws_priv_channel.go b/submodules/okex/ws/ws_priv_channel.go new file mode 100644 index 0000000..650d5ec --- /dev/null +++ b/submodules/okex/ws/ws_priv_channel.go @@ -0,0 +1,40 @@ +package ws + +import ( + . "v5sdk_go/ws/wImpl" +) + +/* + 订阅账户频道 +*/ +func (a *WsClient) PrivAccout(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_ACCOUNT, op, params, PERIOD_NONE, timeOut...) +} + +/* + 订阅持仓频道 +*/ +func (a *WsClient) PrivPostion(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_POSTION, op, params, PERIOD_NONE, timeOut...) +} + +/* + 订阅订单频道 +*/ +func (a *WsClient) PrivBookOrder(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_ORDER, op, params, PERIOD_NONE, timeOut...) +} + +/* + 订阅策略委托订单频道 +*/ +func (a *WsClient) PrivBookAlgoOrder(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_ALG_ORDER, op, params, PERIOD_NONE, timeOut...) +} + +/* + 订阅账户余额和持仓频道 +*/ +func (a *WsClient) PrivBalAndPos(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_B_AND_P, op, params, PERIOD_NONE, timeOut...) +} diff --git a/submodules/okex/ws/ws_priv_channel_Accout_test.go b/submodules/okex/ws/ws_priv_channel_Accout_test.go new file mode 100644 index 0000000..5cac15b --- /dev/null +++ b/submodules/okex/ws/ws_priv_channel_Accout_test.go @@ -0,0 +1,99 @@ +package ws + +// HOW TO RUN +// go test ws_cli.go ws_op.go ws_contants.go utils.go ws_priv_channel.go ws_priv_channel_Accout_test.go -v + +import ( + "fmt" + "log" + "testing" + "time" +) + +const ( + TRADE_ACCOUNT = iota + ISOLATE_ACCOUNT + CROSS_ACCOUNT + CROSS_ACCOUNT_B +) + +func prework_pri(t int) *WsClient { + // 模拟环境 + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + var apikey, passphrase, secretKey string + // 把账号密码写这里 + switch t { + case TRADE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case ISOLATE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT_B: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + } + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err) + } + + var res bool + start := time.Now() + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + usedTime := time.Since(start) + fmt.Println("登录成功!", usedTime.String()) + } else { + log.Fatal("登录失败!", err) + } + fmt.Println(apikey, secretKey, passphrase) + return r +} + +// 账户频道 测试 +func TestAccout(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + var args []map[string]string + arg := make(map[string]string) + arg["ccy"] = "BTC" + args = append(args, arg) + fmt.Println("args: ", args) + start := time.Now() + res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅所有成功!", usedTime.String()) + } else { + fmt.Println("订阅所有成功!", err) + t.Fatal("订阅所有成功!", err) + } + + time.Sleep(100 * time.Second) + start = time.Now() + // res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) + // if res { + // usedTime := time.Since(start) + // fmt.Println("取消订阅所有成功!", usedTime.String()) + // } else { + // fmt.Println("取消订阅所有失败!", err) + // t.Fatal("取消订阅所有失败!", err) + // } + +} diff --git a/submodules/okex/ws/ws_priv_channel_test.go b/submodules/okex/ws/ws_priv_channel_test.go new file mode 100644 index 0000000..99cd382 --- /dev/null +++ b/submodules/okex/ws/ws_priv_channel_test.go @@ -0,0 +1,247 @@ +package ws + +import ( + "fmt" + "log" + "testing" + "time" +) + +const ( + TRADE_ACCOUNT = iota + ISOLATE_ACCOUNT + CROSS_ACCOUNT + CROSS_ACCOUNT_B +) + +func prework_pri(t int) *WsClient { + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + var apikey, passphrase, secretKey string + // 把账号密码写这里 + switch t { + case TRADE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case ISOLATE_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + case CROSS_ACCOUNT_B: + apikey = "fe468418-5e40-433f-8d04-04951286d417" + passphrase = "M4pw71Id" + secretKey = "D6D74DF9DD60A25BE2B27CA71D8F814D" + } + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err) + } + + var res bool + //start := time.Now() + res, _, err = r.Login(apikey, secretKey, passphrase) + if res { + //usedTime := time.Since(start) + //fmt.Println("登录成功!",usedTime.String()) + } else { + log.Fatal("登录失败!", err) + } + fmt.Println(apikey, secretKey, passphrase) + return r +} + +// 账户频道 测试 +func TestAccout(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + var args []map[string]string + arg := make(map[string]string) + //arg["ccy"] = "BTC" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅所有成功!", usedTime.String()) + } else { + fmt.Println("订阅所有成功!", err) + t.Fatal("订阅所有成功!", err) + } + + time.Sleep(100 * time.Second) + start = time.Now() + res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅所有成功!", usedTime.String()) + } else { + fmt.Println("取消订阅所有失败!", err) + t.Fatal("取消订阅所有失败!", err) + } + +} + +// 持仓频道 测试 +func TestPositon(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = FUTURES + arg["uly"] = "BTC-USD" + //arg["instId"] = "BTC-USD-210319" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivPostion(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60000 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PrivPostion(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 订单频道 测试 +func TestBookOrder(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + arg["instType"] = "ANY" + //arg["instType"] = "SWAP" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivBookOrder(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(6000 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PrivBookOrder(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 策略委托订单频道 测试 +func TestAlgoOrder(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = "SPOT" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivBookAlgoOrder(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PrivBookAlgoOrder(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 账户余额和持仓频道 测试 +func TestPrivBalAndPos(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + args = append(args, arg) + + start := time.Now() + res, _, err = r.PrivBalAndPos(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(600 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PrivBalAndPos(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} diff --git a/submodules/okex/ws/ws_pub_channel.go b/submodules/okex/ws/ws_pub_channel.go new file mode 100644 index 0000000..bad023f --- /dev/null +++ b/submodules/okex/ws/ws_pub_channel.go @@ -0,0 +1,141 @@ +package ws + +import ( + "errors" + . "v5sdk_go/ws/wImpl" +) + +/* + 产品频道 +*/ +func (a *WsClient) PubInstruemnts(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_INSTRUMENTS, op, params, PERIOD_NONE, timeOut...) +} + +// 获取系统维护的状态,当系统维护状态改变时推送 +func (a *WsClient) PubStatus(op string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_STATUS, op, nil, PERIOD_NONE, timeOut...) +} + +/* + 行情频道 +*/ +func (a *WsClient) PubTickers(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_TICKERS, op, params, PERIOD_NONE, timeOut...) +} + +/* + 持仓总量频道 +*/ +func (a *WsClient) PubOpenInsterest(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + return a.PubChannel(EVENT_BOOK_OPEN_INTEREST, op, params, PERIOD_NONE, timeOut...) +} + +/* + K线频道 +*/ +func (a *WsClient) PubKLine(op string, period Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_KLINE, op, params, period, timeOut...) +} + +/* + 交易频道 +*/ +func (a *WsClient) PubTrade(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_TRADE, op, params, PERIOD_NONE, timeOut...) +} + +/* + 预估交割/行权价格频道 +*/ +func (a *WsClient) PubEstDePrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_ESTIMATE_PRICE, op, params, PERIOD_NONE, timeOut...) + +} + +/* + 标记价格频道 +*/ +func (a *WsClient) PubMarkPrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_MARK_PRICE, op, params, PERIOD_NONE, timeOut...) +} + +/* + 标记价格K线频道 +*/ +func (a *WsClient) PubMarkPriceCandle(op string, pd Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_MARK_PRICE_CANDLE_CHART, op, params, pd, timeOut...) +} + +/* + 限价频道 +*/ +func (a *WsClient) PubLimitPrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_LIMIT_PRICE, op, params, PERIOD_NONE, timeOut...) +} + +/* + 深度频道 +*/ +func (a *WsClient) PubOrderBooks(op string, channel string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + switch channel { + // 400档快照 + case "books": + return a.PubChannel(EVENT_BOOK_ORDER_BOOK, op, params, PERIOD_NONE, timeOut...) + // 5档快照 + case "books5": + return a.PubChannel(EVENT_BOOK_ORDER_BOOK5, op, params, PERIOD_NONE, timeOut...) + // 400 tbt + case "books-l2-tbt": + return a.PubChannel(EVENT_BOOK_ORDER_BOOK_TBT, op, params, PERIOD_NONE, timeOut...) + // 50 tbt + case "books50-l2-tbt": + return a.PubChannel(EVENT_BOOK_ORDER_BOOK50_TBT, op, params, PERIOD_NONE, timeOut...) + + default: + err = errors.New("未知的channel") + return + } + +} + +/* + 期权定价频道 +*/ +func (a *WsClient) PubOptionSummary(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_OPTION_SUMMARY, op, params, PERIOD_NONE, timeOut...) +} + +/* + 资金费率频道 +*/ +func (a *WsClient) PubFundRate(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_FUND_RATE, op, params, PERIOD_NONE, timeOut...) +} + +/* + 指数K线频道 +*/ +func (a *WsClient) PubKLineIndex(op string, pd Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_KLINE_INDEX, op, params, pd, timeOut...) +} + +/* + 指数行情频道 +*/ +func (a *WsClient) PubIndexTickers(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { + + return a.PubChannel(EVENT_BOOK_INDEX_TICKERS, op, params, PERIOD_NONE, timeOut...) +} diff --git a/submodules/okex/ws/ws_pub_channel_test.go b/submodules/okex/ws/ws_pub_channel_test.go new file mode 100644 index 0000000..ac96eef --- /dev/null +++ b/submodules/okex/ws/ws_pub_channel_test.go @@ -0,0 +1,669 @@ +package ws + +import ( + "encoding/json" + "fmt" + "log" + "strings" + "testing" + "time" + . "v5sdk_go/ws/wImpl" +) + +func prework() *WsClient { + ep := "wss://wsaws.okex.com:8443/ws/v5/private" + + r, err := NewWsClient(ep) + if err != nil { + log.Fatal(err) + } + + err = r.Start() + if err != nil { + log.Fatal(err, ep) + } + return r +} + +// 产品频道测试 +func TestInstruemnts(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = SPOT + //arg["instType"] = OPTION + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubInstruemnts(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(3 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// status频道测试 +func TestStatus(t *testing.T) { + r := prework() + var err error + var res bool + + start := time.Now() + res, _, err = r.PubStatus(OP_SUBSCRIBE) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(10000 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubStatus(OP_UNSUBSCRIBE) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 行情频道测试 +func TestTickers(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubTickers(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(600 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubTickers(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 持仓总量频道 测试 +func TestOpenInsterest(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "LTC-USD-SWAP" + + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubOpenInsterest(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubOpenInsterest(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// K线频道测试 +func TestKLine(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + // 1分钟K + period := PERIOD_1MIN + + start := time.Now() + res, _, err = r.PubKLine(OP_SUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubKLine(OP_UNSUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 交易频道测试 +func TestTrade(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubTrade(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubTrade(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 预估交割/行权价格频道 测试 +func TestEstDePrice(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instType"] = FUTURES + arg["uly"] = "BTC-USD" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubEstDePrice(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubEstDePrice(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 标记价格频道 测试 +func TestMarkPrice(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubMarkPrice(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubMarkPrice(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 标记价格K线频道 测试s +func TestMarkPriceCandle(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + period := PERIOD_1MIN + + start := time.Now() + res, _, err = r.PubMarkPriceCandle(OP_SUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubMarkPriceCandle(OP_UNSUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 限价频道 测试 +func TestLimitPrice(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubLimitPrice(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubLimitPrice(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 深度频道 测试 +func TestOrderBooks(t *testing.T) { + r := prework() + var err error + var res bool + + /* + 设置关闭深度数据管理 + */ + // err = r.EnableAutoDepthMgr(false) + // if err != nil { + // fmt.Println("关闭自动校验失败!") + // } + + end := make(chan struct{}) + + r.AddDepthHook(func(ts time.Time, data DepthData) error { + // 对于深度类型数据处理的用户可以自定义 + + // 检测深度数据是否正常 + key, _ := json.Marshal(data.Arg) + fmt.Println("个数:", len(data.Data[0].Asks)) + checksum := data.Data[0].Checksum + fmt.Println("[自定义方法] ", string(key), ", checksum = ", checksum) + + for _, ask := range data.Data[0].Asks { + + arr := strings.Split(ask[0], ".") + //fmt.Println(arr) + if len(arr) > 1 && len(arr[1]) > 2 { + fmt.Println("ask数据异常,", checksum, "ask:", ask) + t.Fatal() + end <- struct{}{} + return nil + } else { + fmt.Println("bid数据正常,", checksum, "ask:", ask) + } + + } + + for _, bid := range data.Data[0].Bids { + + arr := strings.Split(bid[0], ".") + //fmt.Println(arr) + if len(arr) > 1 && len(arr[1]) > 2 { + fmt.Println("bid数据异常,", checksum, "bid:", bid) + t.Fatal() + end <- struct{}{} + return nil + } else { + fmt.Println("ask数据正常,", checksum, "bid:", bid) + } + + } + + // // 查看当前合并后的全量深度数据 + // snapshot, err := r.GetSnapshotByChannel(data) + // if err != nil { + // t.Fatal("深度数据不存在!") + // } + // // 展示ask/bid 前5档数据 + // fmt.Println(" Ask 5 档数据 >> ") + // for _, v := range snapshot.Asks[:5] { + // fmt.Println(" price:", v[0], " amount:", v[1]) + // } + // fmt.Println(" Bid 5 档数据 >> ") + // for _, v := range snapshot.Bids[:5] { + // fmt.Println(" price:", v[0], " amount:", v[1]) + // } + return nil + }) + + // 可选类型:books books5 books-l2-tbt + channel := "books50-l2-tbt" + + instIds := []string{"BTC-USDT"} + for _, instId := range instIds { + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = instId + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubOrderBooks(OP_SUBSCRIBE, channel, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + } + } + + select { + case <-end: + + } + //等待推送 + for _, instId := range instIds { + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = instId + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubOrderBooks(OP_UNSUBSCRIBE, channel, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + } + +} + +// 期权定价频道 测试 +func TestOptionSummary(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["uly"] = "BTC-USD" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubOptionSummary(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubOptionSummary(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 资金费率 测试 +func TestFundRate(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USD-SWAP" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubFundRate(OP_SUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(600 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubFundRate(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 指数K线频道 测试 +func TestKLineIndex(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + + arg["instId"] = "BTC-USDT" + args = append(args, arg) + period := PERIOD_1MIN + + start := time.Now() + res, _, err = r.PubKLineIndex(OP_SUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubKLineIndex(OP_UNSUBSCRIBE, period, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +// 指数行情频道 测试 +func TestIndexMarket(t *testing.T) { + r := prework() + var err error + var res bool + + var args []map[string]string + arg := make(map[string]string) + arg["instId"] = "BTC-USDT" + args = append(args, arg) + + start := time.Now() + res, _, err = r.PubIndexTickers(OP_SUBSCRIBE, args) + if err != nil { + fmt.Println("订阅失败!", err) + } + usedTime := time.Since(start) + if res { + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", usedTime.String()) + //return + } + + time.Sleep(600 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.PubIndexTickers(OP_UNSUBSCRIBE, args) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} diff --git a/submodules/okex/ws/ws_test.go b/submodules/okex/ws/ws_test.go new file mode 100644 index 0000000..8ac7e7e --- /dev/null +++ b/submodules/okex/ws/ws_test.go @@ -0,0 +1,386 @@ +package ws + +import ( + "fmt" + "log" + "testing" + "time" + . "v5sdk_go/ws/wImpl" + + "github.com/stretchr/testify/assert" +) + +func init() { + log.SetFlags(log.LstdFlags | log.Llongfile) + +} + +func TestPing(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + + res, _, _ := r.Ping() + assert.True(t, res, true) +} + +func TestWsClient_SubscribeAndUnSubscribe(t *testing.T) { + r := prework() + var err error + var res bool + + param := map[string]string{} + param["channel"] = "opt-summary" + param["uly"] = "BTC-USD" + + start := time.Now() + res, _, err = r.Subscribe(param) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.UnSubscribe(param) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + t.Fatal("取消订阅失败!", err) + } + +} + +func TestWsClient_SubscribeAndUnSubscribe_priv(t *testing.T) { + r := prework_pri(ISOLATE_ACCOUNT) + var err error + var res bool + + var params []map[string]string + params = append(params, map[string]string{"channel": "orders", "instType": SPOT, "instId": "BTC-USDT"}) + //一个失败的订阅用例 + params = append(params, map[string]string{"channel": "positions", "instType": "any"}) + + for _, v := range params { + start := time.Now() + var data *ProcessDetail + res, data, err = r.Subscribe(v) + if res { + usedTime := time.Since(start) + fmt.Println("订阅成功!", usedTime.String()) + PrintDetail(data) + } else { + fmt.Println("订阅失败!", err) + //return + } + time.Sleep(60 * time.Second) + //等待推送 + + start = time.Now() + res, _, err = r.UnSubscribe(v) + if res { + usedTime := time.Since(start) + fmt.Println("取消订阅成功!", usedTime.String()) + } else { + fmt.Println("取消订阅失败!", err) + } + + } + +} + +func TestWsClient_Jrpc(t *testing.T) { + //r := prework_pri(ISOLATE_ACCOUNT) + r := prework_pri(CROSS_ACCOUNT) + var res bool + var err error + var data *ProcessDetail + + start := time.Now() + var args []map[string]interface{} + + param := map[string]interface{}{} + param["instId"] = "BTC-USDT" + param["clOrdId"] = "SIM0dcopy16069997808063455" + param["tdMode"] = "cross" + param["side"] = "sell" + param["ordType"] = "limit" + param["px"] = "19333.3" + param["sz"] = "0.18605445" + + param1 := map[string]interface{}{} + param1["instId"] = "BTC-USDT" + param1["clOrdId"] = "SIM0dcopy16069997808063456" + param1["tdMode"] = "cross" + param1["side"] = "sell" + param1["ordType"] = "limit" + param1["px"] = "19334.2" + param1["sz"] = "0.03508913" + + param2 := map[string]interface{}{} + param2["instId"] = "BTC-USDT" + param2["clOrdId"] = "SIM0dcopy16069997808063457" + param2["tdMode"] = "cross" + param2["side"] = "sell" + param2["ordType"] = "limit" + param2["px"] = "19334.8" + param2["sz"] = "0.03658186" + + param3 := map[string]interface{}{} + param3["instId"] = "BTC-USDT" + param3["clOrdId"] = "SIM0dcopy16069997808063458" + param3["tdMode"] = "cross" + param3["side"] = "sell" + param3["ordType"] = "limit" + param3["px"] = "19334.9" + param3["sz"] = "0.5" + + param4 := map[string]interface{}{} + param4["instId"] = "BTC-USDT" + param4["clOrdId"] = "SIM0dcopy16069997808063459" + param4["tdMode"] = "cross" + param4["side"] = "sell" + param4["ordType"] = "limit" + param4["px"] = "19335.2" + param4["sz"] = "0.3" + + param5 := map[string]interface{}{} + param5["instId"] = "BTC-USDT" + param5["clOrdId"] = "SIM0dcopy16069997808063460" + param5["tdMode"] = "cross" + param5["side"] = "sell" + param5["ordType"] = "limit" + param5["px"] = "19335.9" + param5["sz"] = "0.051" + + param6 := map[string]interface{}{} + param6["instId"] = "BTC-USDT" + param6["clOrdId"] = "SIM0dcopy16069997808063461" + param6["tdMode"] = "cross" + param6["side"] = "sell" + param6["ordType"] = "limit" + param6["px"] = "19336.4" + param6["sz"] = "1" + + param7 := map[string]interface{}{} + param7["instId"] = "BTC-USDT" + param7["clOrdId"] = "SIM0dcopy16069997808063462" + param7["tdMode"] = "cross" + param7["side"] = "sell" + param7["ordType"] = "limit" + param7["px"] = "19336.8" + param7["sz"] = "0.475" + + param8 := map[string]interface{}{} + param8["instId"] = "BTC-USDT" + param8["clOrdId"] = "SIM0dcopy16069997808063463" + param8["tdMode"] = "cross" + param8["side"] = "sell" + param8["ordType"] = "limit" + param8["px"] = "19337.3" + param8["sz"] = "0.21299357" + + param9 := map[string]interface{}{} + param9["instId"] = "BTC-USDT" + param9["clOrdId"] = "SIM0dcopy16069997808063464" + param9["tdMode"] = "cross" + param9["side"] = "sell" + param9["ordType"] = "limit" + param9["px"] = "19337.5" + param9["sz"] = "0.5" + + args = append(args, param) + args = append(args, param1) + args = append(args, param2) + args = append(args, param3) + args = append(args, param4) + args = append(args, param5) + args = append(args, param6) + args = append(args, param7) + args = append(args, param8) + args = append(args, param9) + + res, data, err = r.Jrpc("okexv5wsapi001", "order", args) + if res { + usedTime := time.Since(start) + fmt.Println("下单成功!", usedTime.String()) + PrintDetail(data) + } else { + usedTime := time.Since(start) + fmt.Println("下单失败!", usedTime.String(), err) + } +} + +/* + 测试 添加全局消息回调函数 +*/ +func TestAddMessageHook(t *testing.T) { + + r := prework_pri(CROSS_ACCOUNT) + + r.AddMessageHook(func(msg *Msg) error { + // 添加你的方法 + fmt.Println("这是全局消息自定义MessageHook") + fmt.Println("当前数据是", msg) + return nil + }) + + select {} +} + +/* + 普通推送数据回调函数 +*/ +func TestAddBookedDataHook(t *testing.T) { + var r *WsClient + + /*订阅私有频道*/ + { + r = prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + r.AddBookMsgHook(func(ts time.Time, data MsgData) error { + // 添加你的方法 + fmt.Println("这是私有自定义AddBookMsgHook") + fmt.Println("当前数据是", data) + return nil + }) + + param := map[string]string{} + param["channel"] = "account" + param["ccy"] = "BTC" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(100 * time.Second) + } + + //订阅公共频道 + { + r = prework() + var res bool + var err error + + r.AddBookMsgHook(func(ts time.Time, data MsgData) error { + // 添加你的方法 + fmt.Println("这是公共自定义AddBookMsgHook") + fmt.Println("当前数据是", data) + return nil + }) + + param := map[string]string{} + param["channel"] = "instruments" + param["instType"] = "FUTURES" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + select {} + } + +} + +func TestGetInfoFromErrMsg(t *testing.T) { + a := assert.New(t) + buf := ` +"channel:index-tickers,instId:BTC-USDT1 doesn't exist" + ` + ch := GetInfoFromErrMsg(buf) + //t.Log(ch) + a.Equal("index-tickers", ch) + + //assert.True(t,ch == "index-tickers") +} + +/* + + */ +func TestParseMessage(t *testing.T) { + r := prework() + var evt Event + msg := `{"event":"error","msg":"Contract does not exist.","code":"51001"}` + + evt, _, _ = r.parseMessage([]byte(msg)) + assert.True(t, EVENT_ERROR == evt) + + msg = `{"event":"error","msg":"channel:positions,ccy:BTC doesn't exist","code":"60018"}` + evt, _, _ = r.parseMessage([]byte(msg)) + assert.True(t, EVENT_BOOK_POSTION == evt) +} + +/* + 原始方式 深度订阅 测试 +*/ +func TestSubscribeTBT(t *testing.T) { + r := prework() + var res bool + var err error + + // 添加你的方法 + r.AddDepthHook(func(ts time.Time, data DepthData) error { + //fmt.Println("这是自定义AddBookMsgHook") + fmt.Println("当前数据是:", data) + return nil + }) + + param := map[string]string{} + param["channel"] = "books-l2-tbt" + //param["channel"] = "books" + param["instId"] = "BTC-USD-SWAP" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) +} + +/* + + */ +func TestSubscribeBalAndPos(t *testing.T) { + r := prework_pri(CROSS_ACCOUNT) + var res bool + var err error + + param := map[string]string{} + + // 产品信息 + param["channel"] = "balance_and_position" + + res, _, err = r.Subscribe(param) + if res { + fmt.Println("订阅成功!") + } else { + fmt.Println("订阅失败!", err) + t.Fatal("订阅失败!", err) + //return + } + + time.Sleep(60 * time.Second) +} diff --git a/utils/crpty.go b/utils/crpty.go new file mode 100644 index 0000000..61e18d2 --- /dev/null +++ b/utils/crpty.go @@ -0,0 +1,14 @@ +package utils + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" +) + +func ComputeHmac256(message string, secret string) string { + key := []byte(secret) + h := hmac.New(sha256.New, key) + h.Write([]byte(message)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/utils/maopao.go b/utils/maopao.go new file mode 100644 index 0000000..f2c4abc --- /dev/null +++ b/utils/maopao.go @@ -0,0 +1,21 @@ +package utils + +func RecursiveBubble(ary []int64, length int) []int64 { + if length == 0 { + return ary + } + for idx, _ := range ary { + if idx >= length-1 { + break + } + temp := int64(0) + if ary[idx] < ary[idx+1] { //改变成>,换成从小到大排序 + temp = ary[idx] + ary[idx] = ary[idx+1] + ary[idx+1] = temp + } + } + length-- + RecursiveBubble(ary, length) + return ary +} diff --git a/utils/myStack.go b/utils/myStack.go new file mode 100644 index 0000000..b13cc0f --- /dev/null +++ b/utils/myStack.go @@ -0,0 +1,51 @@ +package utils + +import ( + "fmt" + "sync" +) + +type MyStack struct { + Stack []interface{} + CType string + lock sync.RWMutex + Len int +} + +func (c *MyStack) Push(obj interface{}) { + c.lock.Lock() + if c.Len == len(c.Stack) { + c.Stack = c.Stack[1:] + } + defer c.lock.Unlock() + c.Stack = append(c.Stack, obj) +} + +func (c *MyStack) Pop() error { + len := len(c.Stack) + if len > 0 { + c.lock.Lock() + defer c.lock.Unlock() + c.Stack = c.Stack[:len-1] + return nil + } + return fmt.Errorf("Pop Error: Stack is empty") +} + +func (c *MyStack) Front() (interface{}, error) { + len := len(c.Stack) + if len > 0 { + c.lock.Lock() + defer c.lock.Unlock() + return c.Stack[len-1], nil + } + return "", fmt.Errorf("Peep Error: Stack is empty") +} + +func (c *MyStack) Size() int { + return len(c.Stack) +} + +func (c *MyStack) Empty() bool { + return len(c.Stack) == 0 +} diff --git a/utils/rand.go b/utils/rand.go new file mode 100644 index 0000000..1c73e4c --- /dev/null +++ b/utils/rand.go @@ -0,0 +1,51 @@ +package utils + +import ( + "math/rand" + "reflect" + "strconv" + "time" +) + +func GetRandListChan(rg int, count int) []string { + strAry := []string{} + for i := 0; i < count; i++ { + rand.Seed(time.Now().UnixNano()) + b := rand.Intn(rg) + bs := strconv.Itoa(b) + strAry = append(strAry, bs) + } + return strAry +} + +func GetRandomString(l int) string { + str := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + bytes := []byte(str) + result := []byte{} + r := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < l; i++ { + result = append(result, bytes[r.Intn(len(bytes))]) + } + return string(result) +} + +//判断某一个值是否含在切片之中 +func In_Array(val interface{}, array interface{}) (exists bool, index int) { + exists = false + index = -1 + + switch reflect.TypeOf(array).Kind() { + case reflect.Slice: + s := reflect.ValueOf(array) + + for i := 0; i < s.Len(); i++ { + if reflect.DeepEqual(val, s.Index(i).Interface()) == true { + index = i + exists = true + return + } + } + } + + return +} diff --git a/utils/sqrt.go b/utils/sqrt.go new file mode 100644 index 0000000..a9305b1 --- /dev/null +++ b/utils/sqrt.go @@ -0,0 +1,11 @@ +package utils + +import "math" + +func Sqrt(x float64) float64 { + z := 1.0 + for math.Abs(z*z-x) > 0.000001 { + z -= (z*z - x) / (2 * z) + } + return z +} diff --git a/utils/tools.go b/utils/tools.go new file mode 100644 index 0000000..37e9ee6 --- /dev/null +++ b/utils/tools.go @@ -0,0 +1,116 @@ +package utils + +import ( + "crypto/md5" + "fmt" + "reflect" + "runtime" + "strconv" + "time" +) + +type PushRestQ func(int, []string) error + +// 获取当前函数名字 +func GetFuncName() string { + pc := make([]uintptr, 1) + runtime.Callers(2, pc) + f := runtime.FuncForPC(pc[0]) + return f.Name() +} + +func TickerWrapper(mdura time.Duration, ary []string, pr PushRestQ) { + done := make(chan bool) + idx := 0 + per2 := time.Duration(mdura) / time.Duration(len(ary)+2) + // fmt.Println("mdura, len of ary, per2: ", mdura, len(ary), per2) + if per2 < 100*time.Millisecond { + per2 = 100 * time.Millisecond + } + ticker := time.NewTicker(per2) + go func(i int) { + for { + select { + case <-ticker.C: + if i >= (len(ary) - 1) { + done <- true + break + } + go func(i int) { + err := pr(i, ary) + if err != nil { + fmt.Println("inner err: ", err) + } + }(i) + i++ + } + } + }(idx) + time.Sleep(mdura) +} + +func HashDispatch(originName string, count uint8) int { + data := []byte(originName) + bytes := md5.Sum(data) + res := uint8(0) + for _, v := range bytes { + res += v + } + res = res % count + return int(res) +} + +func IsoTime() string { + utcTime := time.Now().UTC() + iso := utcTime.String() + isoBytes := []byte(iso) + iso = string(isoBytes[:10]) + "T" + string(isoBytes[11:23]) + "Z" + return iso +} + +// 闹钟, +func Fenci(count int) bool { + tsi := time.Now().Unix() + tsi = tsi - tsi%60 + cha := tsi % (int64(count)) + if cha == 0 { + return true + } + return false +} +func ToString(val interface{}) string { + valstr := "" + if reflect.TypeOf(val).Name() == "string" { + valstr = val.(string) + } else if reflect.TypeOf(val).Name() == "float64" { + valstr = fmt.Sprintf("%f", val) + } else if reflect.TypeOf(val).Name() == "int64" { + valstr = strconv.FormatInt(val.(int64), 16) + } + return valstr +} + +func ToInt64(val interface{}) int64 { + vali := int64(0) + if reflect.TypeOf(val).Name() == "string" { + vali, _ = strconv.ParseInt(val.(string), 10, 64) + } else if reflect.TypeOf(val).Name() == "float64" { + vali = int64(val.(float64)) + } + return vali +} + +func ToFloat64(val interface{}) float64 { + valf := float64(0) + ctype := reflect.TypeOf(val).Name() + if ctype == "string" { + valf, _ = strconv.ParseFloat(val.(string), 64) + } else if reflect.TypeOf(val).Name() == "float64" { + valf = val.(float64) + } else if reflect.TypeOf(val).Name() == "int64" { + valf = float64(val.(int64)) + } else { + fmt.Println("convert err:", val, ":", ctype) + } + return valf +}