360 lines
9.3 KiB
Go
360 lines
9.3 KiB
Go
package okex
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/bitly/go-simplejson"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.qtrade.icu/coin-quant/exchange/ws"
|
|
. "git.qtrade.icu/coin-quant/trademodel"
|
|
"github.com/mitchellh/mapstructure"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func (b *OkxTrader) runPrivate() (err error) {
|
|
b.wsUser, err = ws.NewWSConn(WSOkexPrivate, func(ws *ws.WSConn) error {
|
|
login := OPParam{
|
|
OP: "login",
|
|
Args: []interface{}{NewLoginArg(b.cfg.ApiKey, b.cfg.Passphrase, b.cfg.SecretKey)},
|
|
}
|
|
return ws.WriteMsg(login)
|
|
}, b.parseUserMsg)
|
|
return
|
|
}
|
|
|
|
type LoginArg struct {
|
|
ApiKey string `json:"apiKey"`
|
|
Passphrase string `json:"passphrase"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
Sign string `json:"sign"`
|
|
}
|
|
|
|
func NewLoginArg(apiKey, pass, secret string) *LoginArg {
|
|
a := new(LoginArg)
|
|
a.ApiKey = apiKey
|
|
a.Passphrase = pass
|
|
t := time.Now()
|
|
a.Timestamp = t.Unix()
|
|
src := fmt.Sprintf("%dGET/users/self/verify", a.Timestamp)
|
|
h := hmac.New(sha256.New, []byte(secret))
|
|
h.Write([]byte(src))
|
|
ret := h.Sum(nil)
|
|
n := base64.StdEncoding.EncodedLen(len(ret))
|
|
dst := make([]byte, n)
|
|
base64.StdEncoding.Encode(dst, ret)
|
|
a.Sign = string(dst)
|
|
return a
|
|
}
|
|
|
|
type OPArg struct {
|
|
Channel string `json:"channel"`
|
|
InstType string `json:"instType"`
|
|
Uly string `json:"uly,omitempty"`
|
|
InstID string `json:"instId,omitempty"`
|
|
}
|
|
|
|
type OPParam struct {
|
|
OP string `json:"op"`
|
|
Args []interface{} `json:"args"`
|
|
}
|
|
|
|
func (b *OkxTrader) getSymbolSub() (p OPParam) {
|
|
p.OP = "subscribe"
|
|
p.Args = append(p.Args, OPArg{Channel: "orders", InstType: "ANY"})
|
|
p.Args = append(p.Args, OPArg{Channel: "orders-algo", InstType: "ANY"})
|
|
p.Args = append(p.Args, OPArg{Channel: "algo-advance", InstType: "ANY"})
|
|
p.Args = append(p.Args, OPArg{Channel: "positions", InstType: "ANY"})
|
|
return
|
|
}
|
|
|
|
// {"arg":{"channel":"trades","instId":"BSV-USD-210924"},"data":[{"instId":"BSV-USD-210924","tradeId":"1957771","px":"166.5","sz":"22","side":"sell","ts":"1621862533713"}]}
|
|
// {"arg":{"channel":"books","instId":"BSV-USD-210924"},"action":"update","data":[{"asks":[["167.4","54","0","1"]],"bids":[["164.81","0","0","0"],["164.69","79","0","1"]],"ts":"1621862579059","checksum":71082836}]}
|
|
// books5 {"arg":{"channel":"books5","instId":"BSV-USD-210924"},"data":[{"asks":[["166.35","20","0","1"],["166.4","135","0","2"],["166.42","86","0","1"],["166.45","310","0","1"],["166.46","61","0","2"]],"bids":[["166.14","33","0","1"],["166.07","106","0","1"],["166.05","2","0","1"],["166.04","97","0","1"],["165.98","20","0","1"]],"instId":"BSV-USD-210924","ts":"1621862688397"}]}
|
|
|
|
func (b *OkxTrader) parseUserMsg(message []byte) (err error) {
|
|
var sj *simplejson.Json
|
|
var evt string
|
|
var channel string
|
|
|
|
sj, err = simplejson.NewJson(message)
|
|
if err != nil {
|
|
log.Warnf("parse json error:%s", string(message))
|
|
return
|
|
}
|
|
evtValue, ok := sj.CheckGet("event")
|
|
if ok {
|
|
evt, err = evtValue.String()
|
|
if err != nil {
|
|
log.Warnf("login error:%s %s", string(message), err.Error())
|
|
return
|
|
}
|
|
switch evt {
|
|
case "error":
|
|
log.Errorf("recv error: %s", string(message))
|
|
return
|
|
case "login":
|
|
var code string
|
|
code, err = sj.Get("code").String()
|
|
if code != "0" {
|
|
log.Warnf("login error:%s %s", string(message), err.Error())
|
|
return
|
|
}
|
|
param := b.getSymbolSub()
|
|
err = b.wsUser.WriteMsg(param)
|
|
if err != nil {
|
|
return
|
|
}
|
|
default:
|
|
}
|
|
return
|
|
}
|
|
arg, ok := sj.CheckGet("arg")
|
|
if !ok {
|
|
return
|
|
}
|
|
channelValue, ok := arg.CheckGet("channel")
|
|
if !ok {
|
|
return
|
|
}
|
|
channel, err = channelValue.String()
|
|
if err != nil {
|
|
log.Warnf("channelValue %v is not string error:%s", channelValue, err.Error())
|
|
return
|
|
}
|
|
switch channel {
|
|
case "orders":
|
|
var orders []OrderNormal
|
|
orders, err = parseOkexOrder(sj.Get("data"))
|
|
if err != nil {
|
|
log.Warnf("parseOkexOrder error:%s, %s", orders, err.Error())
|
|
return
|
|
}
|
|
for _, v := range orders {
|
|
o := v.GetOrder()
|
|
if o == nil {
|
|
continue
|
|
}
|
|
o.Status = OrderStatusFilled
|
|
b.ordersCache.Delete(v.OrdID)
|
|
b.stopOrdersCache.Delete(v.OrdID)
|
|
if b.tradeCb != nil {
|
|
b.tradeCb(o)
|
|
}
|
|
}
|
|
|
|
case "orders-algo":
|
|
// 算法单最终还是会生成一个普通单子
|
|
var algoOrders []AlgoOrder
|
|
algoOrders, err = parseOkexAlgoOrder(sj.Get("data"))
|
|
if err != nil {
|
|
log.Warnf("parseOkexAlgoOrder error:%s, %s", algoOrders, err.Error())
|
|
return
|
|
}
|
|
for _, v := range algoOrders {
|
|
if v.State == "filled" {
|
|
b.stopOrdersCache.Delete(v.AlgoID)
|
|
}
|
|
}
|
|
case "algo-advance":
|
|
case "positions":
|
|
var pos []OKEXPos
|
|
pos, err = parseOkexPos(sj.Get("data"))
|
|
if err != nil {
|
|
log.Warnf("parseOkexPos error:%s, %s", pos, err.Error())
|
|
return
|
|
}
|
|
for _, v := range pos {
|
|
t := v.GetPos()
|
|
if t == nil {
|
|
continue
|
|
}
|
|
if b.positionCb != nil {
|
|
b.positionCb(t)
|
|
}
|
|
}
|
|
default:
|
|
}
|
|
return
|
|
}
|
|
|
|
func (o *OrderNormal) GetOrder() (ret *Order) {
|
|
if o.State != "filled" {
|
|
return
|
|
}
|
|
var side = transSide(o.Side)
|
|
ret = &Order{
|
|
OrderID: o.OrdID,
|
|
Symbol: o.InstID,
|
|
// Currency:o.
|
|
Amount: parseFloat(o.Sz),
|
|
Price: parseFloat(o.AvgPx),
|
|
Status: o.State,
|
|
Side: side,
|
|
Time: parseTime(o.FillTime),
|
|
}
|
|
return
|
|
}
|
|
|
|
func transSide(oSide string) (side string) {
|
|
switch oSide {
|
|
case "buy":
|
|
side = "long"
|
|
case "sell":
|
|
side = "short"
|
|
default:
|
|
side = oSide
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseTime(v string) time.Time {
|
|
n, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
log.Errorf("parseTime failed: %s", err.Error())
|
|
return time.Now()
|
|
}
|
|
return time.Unix(n/1000, (n%1000)*int64(time.Millisecond))
|
|
}
|
|
|
|
// {"accFillSz":"0","amendResult":"","avgPx":"0","cTime":"1639669567849","category":"normal","ccy":"","clOrdId":"","code":"0","execType":"","fee":"0","feeCcy":"USDT","fillFee":"0","fillFeeCcy":"","fillNotionalUsd":"","fillPx":"","fillSz":"0","fillTime":"","instId":"SOL-USDT-SWAP","instType":"SWAP","lever":"10","msg":"","notionalUsd":"185.666","ordId":"391737792421838866","ordType":"limit","pnl":"0","posSide":"short","px":"200","rebate":"0","rebateCcy":"USDT","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"last","source":"","state":"live","sz":"1","tag":"","tdMode":"isolated","tgtCcy":"","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"last","tradeId":"","uTime":"1639669567849"}
|
|
func parseOkexOrder(sj *simplejson.Json) (orders []OrderNormal, err error) {
|
|
arr, err := sj.Array()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var ret map[string]interface{}
|
|
var ok bool
|
|
for _, v := range arr {
|
|
ret, ok = v.(map[string]interface{})
|
|
if !ok {
|
|
err = errors.New("parseOrderBook error")
|
|
return
|
|
}
|
|
var o OrderNormal
|
|
err = mapstructure.Decode(ret, &o)
|
|
if err != nil {
|
|
return
|
|
}
|
|
orders = append(orders, o)
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseOkexPos(sj *simplejson.Json) (trades []OKEXPos, err error) {
|
|
arr, err := sj.Array()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var ret map[string]interface{}
|
|
var ok bool
|
|
for _, v := range arr {
|
|
ret, ok = v.(map[string]interface{})
|
|
if !ok {
|
|
err = errors.New("parseOkexTrades error")
|
|
return
|
|
}
|
|
var t OKEXPos
|
|
err = mapstructure.Decode(ret, &t)
|
|
if err != nil {
|
|
return
|
|
}
|
|
trades = append(trades, t)
|
|
}
|
|
return
|
|
}
|
|
|
|
type OKEXPos struct {
|
|
Adl string `json:"adl"`
|
|
AvailPos string `json:"availPos"`
|
|
AvgPx string `json:"avgPx"`
|
|
CTime string `json:"cTime"`
|
|
Ccy string `json:"ccy"`
|
|
DeltaBS string `json:"deltaBS"`
|
|
DeltaPA string `json:"deltaPA"`
|
|
GammaBS string `json:"gammaBS"`
|
|
GammaPA string `json:"gammaPA"`
|
|
Imr string `json:"imr"`
|
|
InstID string `json:"instId"`
|
|
InstType string `json:"instType"`
|
|
Interest string `json:"interest"`
|
|
Last string `json:"last"`
|
|
Lever string `json:"lever"`
|
|
Liab string `json:"liab"`
|
|
LiabCcy string `json:"liabCcy"`
|
|
LiqPx string `json:"liqPx"`
|
|
MarkPx string `json:"markPx"`
|
|
Margin string `json:"margin"`
|
|
MgnMode string `json:"mgnMode"`
|
|
MgnRatio string `json:"mgnRatio"`
|
|
Mmr string `json:"mmr"`
|
|
NotionalUsd string `json:"notionalUsd"`
|
|
OptVal string `json:"optVal"`
|
|
PTime string `json:"pTime"`
|
|
Pos string `json:"pos"`
|
|
PosCcy string `json:"posCcy"`
|
|
PosID string `json:"posId"`
|
|
PosSide string `json:"posSide"`
|
|
ThetaBS string `json:"thetaBS"`
|
|
ThetaPA string `json:"thetaPA"`
|
|
TradeID string `json:"tradeId"`
|
|
UTime string `json:"uTime"`
|
|
Upl string `json:"upl"`
|
|
UplRatio string `json:"uplRatio"`
|
|
VegaBS string `json:"vegaBS"`
|
|
VegaPA string `json:"vegaPA"`
|
|
}
|
|
|
|
func (ot *OKEXPos) GetPos() (pos *Position) {
|
|
var typ int
|
|
hold := parseFloat(ot.Pos)
|
|
if ot.PosSide == "long" {
|
|
typ = Long
|
|
} else if ot.PosSide == "short" {
|
|
typ = Short
|
|
hold = 0 - hold
|
|
} else if ot.PosSide == "net" {
|
|
if hold > 0 {
|
|
typ = Long
|
|
} else {
|
|
typ = Short
|
|
}
|
|
}
|
|
price := parseFloat(ot.AvgPx)
|
|
pos = &Position{
|
|
Symbol: ot.InstID,
|
|
Type: typ,
|
|
Hold: hold,
|
|
Price: price,
|
|
ProfitRatio: parseFloat(ot.Upl),
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseOkexAlgoOrder(sj *simplejson.Json) (orders []AlgoOrder, err error) {
|
|
arr, err := sj.Array()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var ret map[string]interface{}
|
|
var ok bool
|
|
for _, v := range arr {
|
|
ret, ok = v.(map[string]interface{})
|
|
if !ok {
|
|
err = errors.New("parseOrderBook error")
|
|
return
|
|
}
|
|
var o AlgoOrder
|
|
err = mapstructure.Decode(ret, &o)
|
|
if err != nil {
|
|
return
|
|
}
|
|
orders = append(orders, o)
|
|
}
|
|
return
|
|
}
|