texus/core/core.go

726 lines
20 KiB
Go
Raw Normal View History

2024-12-02 14:03:35 +08:00
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
}