first add

This commit is contained in:
zhangkun 2024-12-02 14:03:35 +08:00
commit bf3c65e488
109 changed files with 15932 additions and 0 deletions

BIN
.README.md.swp Normal file

Binary file not shown.

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
vendor/

24
README.md Normal file
View File

@ -0,0 +1,24 @@
## Init
cd submodule
git submodule add baidu:/root/repos/go/okexV5Api okex
cd ../
git pull
git submodule init
git submodule update --force --recursive --init --remote
go mod tidy
go mod vendor
## How TO RUN
go run main.go
环境分成两个test和production相关配置都在basicConfig.json中。
## 架构
### 欧易文档首页:
https://www.okx.com/docs-v5/zh/#overview

85
configs/basicConfig.json Normal file
View File

@ -0,0 +1,85 @@
{
"test": {
"redis": {
"url": "localhost:6379",
"password": "",
"index": 3,
"description": ""
},
"credentialReadOnly": {
"secretKey": "D6D74DF9DD60A25BE2B27CA71D8F814D",
"baseUrl": "https://aws.okx.com",
"okAccessKey": "fe468418-5e40-433f-8d04-04951286d417",
"okAccessPassphrase": "M4pw71Id",
"env": "realPlate",
"ctype": "readOnly"
},
"connect": {
"loginSubUrl": "/users/self/verify",
"wsPrivateBaseUrl": "wsaws.okx.com:8443/ws/v5/private",
"wsPublicBaseUrl": "wsaws.okx.com:8443/ws/v5/public",
"restBaseUrl": "https://aws.okx.com"
},
"threads ":{
"maxLenTickerStream": 512,
"maxLenCandleStream": 1280,
"maxCandles":7,
"asyncChannels":40,
"maxTickers":3,
"restPeriod": 180,
"waitWs": 120
},
},
"stage": {
"redis": {
"url": "localhost:6379",
"password": "",
"index": 4,
"description": ""
},
"credentialReadOnly": {
"secretKey": "D6D74DF9DD60A25BE2B27CA71D8F814D",
"baseUrl": "https://aws.okx.com",
"okAccessKey": "fe468418-5e40-433f-8d04-04951286d417",
"okAccessPassphrase": "M4pw71Id",
"env": "realPlate",
"ctype": "readOnly"
},
"connect": {
"loginSubUrl": "/users/self/verify",
"wsPrivateBaseUrl": "wsaws.okx.com:8443/ws/v5/private",
"wsPublicBaseUrl": "wsaws.okx.com:8443/ws/v5/public",
"restBaseUrl": "https://aws.okx.com"
}
},
"production": {
"redis": {
"url": "localhost:6379",
"password": "",
"index": 5
},
"credentialReadOnly": {
"secretKey": "D6D74DF9DD60A25BE2B27CA71D8F814D",
"baseUrl": "https://aws.okx.com",
"okAccessKey": "fe468418-5e40-433f-8d04-04951286d417",
"env": "realPlate",
"okAccessPassphrase": "M4pw71Id",
"ctype": "readOnly"
},
"credentialMutable": {
"secretKey": "49F354BBEEA3D917FF190F94525ACEB7",
"baseUrl": "https://aws.okx.com",
"okAccessKey": "98afba4e-531b-4ec7-af2e-16e270b6b576",
"env": "realPlate",
"okAccessPassphrase": "jitbyw-kArrac-zydva2",
"ctype": "Mutable"
},
"connect": {
"userId": "169408628405739520",
"loginSubUrl": "/users/self/verify",
"wsPrivateBaseUrl": "wsaws.okx.com:8443/ws/v5/private",
"wsPublicBaseUrl": "wsaws.okx.com:8443/ws/v5/public",
"restBaseUrl": "https://aws.okx.com"
}
}
}

450
core/candle.go Normal file
View File

@ -0,0 +1,450 @@
package core
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
"v5sdk_go/rest"
simple "github.com/bitly/go-simplejson"
"github.com/go-redis/redis"
"phyer.click/tunas/utils"
)
type Candle struct {
core *Core
InstId string
Period string
Data []interface{}
From string
}
type MaX struct {
Core *Core
InstId string
Period string
KeyName string
Count int
Ts int64
Value float64
Data []interface{}
From string
}
type MatchCheck struct {
Minutes int64
Matched bool
}
func (mc *MatchCheck) SetMatched(value bool) {
mc.Matched = value
}
func (core *Core) GetCandlesWithRest(instId string, kidx int, dura time.Duration, maxCandles int) error {
ary := []string{}
wsary := core.Cfg.CandleDimentions
for k, v := range wsary {
matched := false
// 这个算法的目的是越靠后的candles维度被命中的概率越低第一个百分之百命中后面开始越来越低, 每分钟都会发生这样的计算,
// 因为维度多了的话,照顾不过来
rand.New(rand.NewSource(time.Now().UnixNano()))
rand.Seed(time.Now().UnixNano())
n := (k*2 + 2) * 3
if n < 1 {
n = 1
}
b := rand.Intn(n)
if b < 8 {
matched = true
}
if matched {
ary = append(ary, v)
}
}
mdura := dura/(time.Duration(len(ary)+1)) - 50*time.Millisecond
// fmt.Println("loop4 Ticker Start instId, dura: ", instId, dura, dura/10, mdura, len(ary), " idx: ", kidx)
// time.Duration(len(ary)+1)
ticker := time.NewTicker(mdura)
done := make(chan bool)
idx := 0
go func(i int) {
for {
select {
case <-ticker.C:
if i >= (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()
}

122
core/config.go Normal file
View File

@ -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
}

12
core/const.go Normal file
View File

@ -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"

725
core/core.go Normal file
View File

@ -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, &params)
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, &param)
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
}

68
core/ticker.go Normal file
View File

@ -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
}

23
go.mod Normal file
View File

@ -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
)

146
go.sum Normal file
View File

@ -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=

View File

@ -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": []
}]
}

56
jsonResp/order.json Normal file
View File

@ -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": ""
}]
}

55
jsonResp/order2.json Normal file
View File

@ -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"
}]
}

2248
jsonResp/position.json Normal file

File diff suppressed because it is too large Load Diff

639
main.go Normal file
View File

@ -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个周期去取之前的记录,对于2D5D等数据可以用来补全数据, 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 {}
}

44
models/account.go Normal file
View File

@ -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":""
// }
// ]
// }

10
models/candle.go Normal file
View File

@ -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"` // 交易量,以币为单位
}

21
models/globalCoin.go Normal file
View File

@ -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{}

40
models/instrument.go Normal file
View File

@ -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:
// }

20
models/ticker.go Normal file
View File

@ -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
}

1
okex/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
ws/vendor/

21
okex/LICENSE Normal file
View File

@ -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.

32
okex/config/conf.go Normal file
View File

@ -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
}

3
okex/config/go.mod Normal file
View File

@ -0,0 +1,3 @@
module v5sdk_go/config
go 1.14

8
okex/go.mod Normal file
View File

@ -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
)

12
okex/go.sum Normal file
View File

@ -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=

237
okex/main.go Normal file
View File

@ -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()
}

283
okex/readme.md Normal file
View File

@ -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

35
okex/rest/contants.go Normal file
View File

@ -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"
)

3
okex/rest/go.mod Normal file
View File

@ -0,0 +1,3 @@
module v5sdk_go/rest
go 1.14

21
okex/rest/go.sum Normal file
View File

@ -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=

331
okex/rest/rest.go Normal file
View File

@ -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)
}
}

100
okex/rest/rest_test.go Normal file
View File

@ -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", &param)
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", &param)
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)
}

8
okex/utils/go.mod Normal file
View File

@ -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
)

7
okex/utils/go.sum Normal file
View File

@ -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=

102
okex/utils/utils.go Normal file
View File

@ -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
}

17
okex/utils/utils_test.go Normal file
View File

@ -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)
}

8
okex/ws/go.mod Normal file
View File

@ -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
)

28
okex/ws/go.sum Normal file
View File

@ -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=

75
okex/ws/utils.go Normal file
View File

@ -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
}

226
okex/ws/wImpl/BookData.go Normal file
View File

@ -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
}

13
okex/ws/wImpl/ErrData.go Normal file
View File

@ -0,0 +1,13 @@
/*
错误数据
*/
package wImpl
// 服务端请求错误返回消息格式
type ErrData struct {
Event string `json:"event"`
Code string `json:"code"`
Msg string `json:"msg"`
}

50
okex/ws/wImpl/JRPCData.go Normal file
View File

@ -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)
}

47
okex/ws/wImpl/ReqData.go Normal file
View File

@ -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)
}

241
okex/ws/wImpl/contants.go Normal file
View File

@ -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"
)

View File

@ -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)
}

View File

@ -0,0 +1,9 @@
package wInterface
import . "v5sdk_go/ws/wImpl"
// 请求数据
type WSParam interface {
EventType() Event
ToMap() *map[string]string
}

View File

@ -0,0 +1,8 @@
package wInterface
// 请求数据
type WSReqData interface {
GetType() int
Len() int
ToString() string
}

View File

@ -0,0 +1,6 @@
package wInterface
// 返回数据
type WSRspData interface {
MsgType() int
}

View File

@ -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 {}
}
}

725
okex/ws/ws_cli.go Normal file
View File

@ -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
}

18
okex/ws/ws_contants.go Normal file
View File

@ -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"
)

157
okex/ws/ws_jrpc.go Normal file
View File

@ -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...)
}

186
okex/ws/ws_jrpc_test.go Normal file
View File

@ -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("修改订单失败!")
}
}

19
okex/ws/ws_middleware.go Normal file
View File

@ -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
}

532
okex/ws/ws_op.go Normal file
View File

@ -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
}

View File

@ -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...)
}

View File

@ -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)
// }
}

View File

@ -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)
}
}

141
okex/ws/ws_pub_channel.go Normal file
View File

@ -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...)
}

View File

@ -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)
}
}

386
okex/ws/ws_test.go Normal file
View File

@ -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)
}

73
private/balance.go Normal file
View File

@ -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
}

107
private/order.go Normal file
View File

@ -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
// }

21
submodules/okex/LICENSE Normal file
View File

@ -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.

View File

@ -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
}

View File

@ -0,0 +1,3 @@
module v5sdk_go/config
go 1.14

8
submodules/okex/go.mod Normal file
View File

@ -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
)

12
submodules/okex/go.sum Normal file
View File

@ -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=

237
submodules/okex/main.go Normal file
View File

@ -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()
}

283
submodules/okex/readme.md Normal file
View File

@ -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

View File

@ -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"
)

View File

View File

@ -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)
}
}

View File

@ -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", &param)
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", &param)
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)
}

View File

View File

@ -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
}

View File

@ -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)
}

View File

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,13 @@
/*
错误数据
*/
package wImpl
// 服务端请求错误返回消息格式
type ErrData struct {
Event string `json:"event"`
Code string `json:"code"`
Msg string `json:"msg"`
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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"
)

View File

@ -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)
}

View File

@ -0,0 +1,9 @@
package wInterface
import . "v5sdk_go/ws/wImpl"
// 请求数据
type WSParam interface {
EventType() Event
ToMap() *map[string]string
}

View File

@ -0,0 +1,8 @@
package wInterface
// 请求数据
type WSReqData interface {
GetType() int
Len() int
ToString() string
}

View File

@ -0,0 +1,6 @@
package wInterface
// 返回数据
type WSRspData interface {
MsgType() int
}

View File

@ -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 {}
}
}

View File

@ -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
}

View File

@ -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"
)

View File

@ -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...)
}

View File

@ -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("修改订单失败!")
}
}

View File

@ -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
}

532
submodules/okex/ws/ws_op.go Normal file
View File

@ -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
}

View File

@ -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...)
}

View File

@ -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)
// }
}

View File

@ -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)
}
}

Some files were not shown because too many files have changed in this diff Show More