diff --git a/api/pub.proto b/api/pub.proto index 4b39b2a..3d1cb36 100644 --- a/api/pub.proto +++ b/api/pub.proto @@ -242,3 +242,12 @@ message BacktestTradeOrder { int64 entry_time = 20; repeated int64 trades = 21; } + +// 登录认证对象 +message RpcSubject { + int64 uid = 1; + string account = 2; + int64 time = 3; // ms + map extra = 4; +} + diff --git a/cmd/admin/main.go b/cmd/admin/main.go index 7f1fe17..415f3a6 100644 --- a/cmd/admin/main.go +++ b/cmd/admin/main.go @@ -7,11 +7,14 @@ import ( "sig-pub/pkg/utils/exit" ) -// curl server: /api/sig/backtest/log +type AdminConf struct { + AesTokenKey string `toml:"aesTokenKey"` +} func main() { // load config conf := config.MustLoadConfig(new(config.Configuration), "config/config.toml") + config.SetDefaultConfigPath("config/admin.toml") // database db, err := conf.Database.Postgres.NewGormDB() diff --git a/cmd/test/test.go b/cmd/test/test.go index 9f1fec1..44bb6b9 100644 --- a/cmd/test/test.go +++ b/cmd/test/test.go @@ -1,7 +1,10 @@ package main import ( + "fmt" + "path/filepath" "sig-pub/pkg/zlog" + "strings" "github.com/govalues/decimal" ) @@ -9,7 +12,14 @@ import ( func main() { // curl -H 'Content-Type: application/json' --data-binary "@vmdata.json" -X POST http://localhost:8428/api/v1/import - testDecimalScale() + // testDecimalScale() + file := "config.toml" + ext := filepath.Ext(file) + filename, _ := strings.CutSuffix(file, ext) + filename2 := strings.Replace(file, ext, "", -1) + fmt.Println(ext) + fmt.Println(filename) + fmt.Println(filename2) } type BTC struct { diff --git a/config/admin.toml b/config/admin.toml new file mode 100644 index 0000000..a1fc42d --- /dev/null +++ b/config/admin.toml @@ -0,0 +1,2 @@ + +aesTokenKey = "VzKw8Vx+K8k1nO9fmPjhv8o+8l4vqpF+fjsbvXf0j4o=" diff --git a/config/exchange.toml b/config/exchange.toml index 19bb205..caa8ef0 100644 --- a/config/exchange.toml +++ b/config/exchange.toml @@ -19,8 +19,8 @@ marketSubscribeLimit = 16 consumeBatch = 1024 consumeLater = 2000 # 时间到达later或者数据累计到batch触发consume # httpProxy = "" -httpProxy = "http://192.168.1.5:7890" -# httpProxy = "http://10.255.183.209:7890" +# httpProxy = "http://192.168.1.5:7890" +httpProxy = "http://10.255.183.209:7890" # 模拟盘API交易地址如下: # REST:https://www.okx.com diff --git a/internal/admin/args/auth_args.go b/internal/admin/args/auth_args.go new file mode 100644 index 0000000..7dc9a21 --- /dev/null +++ b/internal/admin/args/auth_args.go @@ -0,0 +1,24 @@ +package args + +import "errors" + +type LoginReq struct { + Account string `json:"account"` + Password string `json:"password"` +} + +func (arg LoginReq) Validate() (err error) { + if len(arg.Account) == 0 { + err = errors.New("account is empty") + return + } + if len(arg.Account) > 36 { + err = errors.New("account length must be at most 36") + return + } + if len(arg.Password) < 6 || len(arg.Password) > 128 { + err = errors.New("password length must be at least 6 and at most 128") + return + } + return +} diff --git a/internal/admin/repoitory/user_repository.go b/internal/admin/repoitory/user_repository.go new file mode 100644 index 0000000..6cda0c0 --- /dev/null +++ b/internal/admin/repoitory/user_repository.go @@ -0,0 +1,48 @@ +package repository + +import ( + "sig-pub/pkg/data" + "sig-pub/pkg/data/entity" + "sig-pub/pkg/storage/persist" + "sig-pub/pkg/utils/rands" +) + +type UserRepository struct { + db *persist.DB +} + +func NewUserRepository(db *persist.DB) *UserRepository { + return &UserRepository{ + db: db, + } +} + +func (s *UserRepository) ListAllInstanceId() (insts []string, err error) { + err = s.db.Select(&insts, `select inst_id from t_trade_instance where status != ?`, data.StatusDeleted) + return +} + +func (dao *UserRepository) FindByAccount(account string) (user *entity.User, err error) { + user = &entity.User{} + err = dao.db.Select(user, `select * from t_user where account=?`, account) + return user, nil +} + +func (dao *UserRepository) NextUid() (nextUid int64, err error) { + maxUid := int64(0) + err = dao.db.Select(&maxUid, "uid=(select userid from t_user)") + if err != nil { + return + } + if maxUid == 0 { + maxUid = 10000 + } + + nextUid = maxUid + rands.RandN(10) + return +} + +func (dao *UserRepository) Create(user *entity.User) (err error) { + err = dao.db.Insert(user) + return +} diff --git a/internal/admin/service/auth_service.go b/internal/admin/service/auth_service.go new file mode 100644 index 0000000..289969b --- /dev/null +++ b/internal/admin/service/auth_service.go @@ -0,0 +1,95 @@ +package service + +import ( + "encoding/base64" + "net/http" + "sig-pub/internal/admin/args" + repository "sig-pub/internal/admin/repoitory" + "sig-pub/pkg/config" + "sig-pub/pkg/data/entity" + "sig-pub/pkg/resp" + "sig-pub/pkg/session" + "time" + + "github.com/gin-gonic/gin" +) + +type AuthService struct { + repo *repository.UserRepository + aesKeyBytes []byte +} + +func NewAuthService(repo *repository.UserRepository) *AuthService { + return &AuthService{ + repo: repo, + } +} + +func (svc *AuthService) Route(group *gin.RouterGroup) { + aesKey, err := config.GetString("aesTokenKey") + if err != nil { + panic(err) + } + svc.aesKeyBytes, err = base64.StdEncoding.DecodeString(aesKey) + if err != nil { + panic(err) + } +} + +func (s *AuthService) Login(ctx *gin.Context) { + var req args.LoginReq + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, resp.Fail(err.Error())) + return + } + if err := req.Validate(); err != nil { + ctx.JSON(http.StatusBadRequest, resp.Fail(err.Error())) + return + } + user, err := s.repo.FindByAccount(req.Account) + if err != nil { + ctx.JSON(http.StatusInternalServerError, resp.Fail(err.Error())) + return + } + if len(req.Password) < 6 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "password length must be at least 6"}) + return + } + + if user == nil { + // 不存在注册 + now := time.Now().UnixMilli() + uid, err := s.repo.NextUid() + if err != nil { + ctx.JSON(http.StatusInternalServerError, resp.Fail(err.Error())) + return + } + user = &entity.User{ + UserId: uid, + Account: req.Account, + Username: req.Account, + Password: req.Password, + CreateAt: now, + UpdateAt: now, + } + err = s.repo.Create(user) + if err != nil { + ctx.JSON(http.StatusInternalServerError, resp.Fail(err.Error())) + return + } + // return nil, errors.New("not found account " + req.Account) + } + // verify password login + if req.Password != user.Password { + ctx.JSON(http.StatusBadRequest, resp.Fail("account or password error")) + return + } + + // generate token + rpcSubject := session.NewRpcSubject(user.UserId, user.Account, nil) + _ = rpcSubject + // session.RpcSubjectGenToken() + + // res := &auth.ResLogin{Token: token, Subject: subject} + return +} diff --git a/internal/admin/service/service.go b/internal/admin/service/service.go index b2e7a71..f2c49b9 100644 --- a/internal/admin/service/service.go +++ b/internal/admin/service/service.go @@ -11,7 +11,9 @@ import ( func Init(group *gin.RouterGroup, rdb *persist.DB) { backtestRepo := repository.NewBacktestRepository(rdb) marketRepo := repository.NewMarketRepository(rdb) + userRepo := repository.NewUserRepository(rdb) NewBacktestService(backtestRepo).Route(group.Group("/backtest")) NewMarketService(marketRepo).Route(group.Group("/market")) + NewAuthService(userRepo).Route(group.Group("/auth")) } diff --git a/pkg/config/loader.go b/pkg/config/loader.go index faf0b9e..0eca566 100644 --- a/pkg/config/loader.go +++ b/pkg/config/loader.go @@ -14,23 +14,39 @@ import ( func parseConfPath(confPath string) (filePath, fileName, confType string) { filePath, fileName = filepath.Split(confPath) ext := filepath.Ext(fileName) - fileName = strings.Replace(fileName, ext, "", -1) - confType = strings.Replace(ext, ".", "", 1) + fileName, _ = strings.CutSuffix(fileName, ext) + confType, _ = strings.CutPrefix(ext, ".") + confType = strings.ToLower(confType) return } +func LoadConfig[T any](conf T, confPathArg ...string) (T, error) { + v, err := loadViper(confPathArg...) + if err != nil { + return conf, err + } + if err := v.Unmarshal(conf); err != nil { + err = errors.New("viper unmarshal config failed: " + err.Error()) + return conf, err + } + return conf, nil +} + func MustLoadConfig[T any](conf T, confPathArg ...string) T { - err := LoadConfig(conf, confPathArg...) + v, err := loadViper(confPathArg...) if err != nil { panic(err) } + if err := v.Unmarshal(conf); err != nil { + panic(errors.New("viper unmarshal config failed: " + err.Error())) + } return conf } -// LoadConfig load config.toml +// loadViper load config.toml // appConf service custom config // return common service configuration -func LoadConfig[T any](conf T, confPathArg ...string) error { +func loadViper(confPathArg ...string) (v *viper.Viper, err error) { confPath := "" confName := "config" confType := "toml" @@ -54,7 +70,7 @@ func LoadConfig[T any](conf T, confPathArg ...string) error { } zlog.Infof("use config file: %s, env prefix=%s\n", filePath, envPrefix) - v := viper.New() + v = viper.New() v.AddConfigPath(confPath) v.SetConfigName(confName) v.SetConfigType(confType) @@ -63,11 +79,7 @@ func LoadConfig[T any](conf T, confPathArg ...string) error { v.AllowEmptyEnv(true) if err := v.ReadInConfig(); err != nil { - return errors.New("viper read config fail: " + err.Error()) + return nil, errors.New("viper read config fail: " + err.Error()) } - if err := v.Unmarshal(conf); err != nil { - return errors.New("viper unmarshal config failed: " + err.Error()) - } - - return nil + return v, nil } diff --git a/pkg/config/namespace.go b/pkg/config/namespace.go new file mode 100644 index 0000000..1976b7a --- /dev/null +++ b/pkg/config/namespace.go @@ -0,0 +1,34 @@ +package config + +import "github.com/spf13/viper" + +var ( + defaultConfigPath = "config/config.toml" + namespaceConfigPath = map[string]string{} +) + +func SetDefaultConfigPath(confPath string) { + defaultConfigPath = confPath +} + +func SetNamespaceConfigPath(namespace string, confPath string) { + namespaceConfigPath[namespace] = confPath +} + +func getViper(namespace ...string) (v *viper.Viper, err error) { + confPath := defaultConfigPath + if len(namespace) > 0 { + confPath = namespaceConfigPath[namespace[0]] + } + v, err = loadViper(confPath) + return +} + +func GetString(key string, namespace ...string) (value string, err error) { + v, err := getViper(namespace...) + if err != nil { + return + } + value = v.GetString(key) + return +} diff --git a/pkg/data/entity/user.go b/pkg/data/entity/user.go new file mode 100644 index 0000000..b4b7b77 --- /dev/null +++ b/pkg/data/entity/user.go @@ -0,0 +1,16 @@ +package entity + +// User 用户 +type User struct { + UserId int64 `gorm:"column:userid;primaryKey;" json:"userId"` // 用户id + Account string `gorm:"column:account" json:"account"` // 登录账号 + Username string `gorm:"column:username" json:"username"` // 用户名 + Password string `gorm:"column:password" json:"password"` // 密码 + Avatar string `gorm:"column:avatar" json:"avatar"` // 头像 + CreateAt int64 `gorm:"column:create_at" json:"createAt"` // 创建时间 + UpdateAt int64 `gorm:"column:update_at" json:"updateAt"` // 更新时间 +} + +func (User) TableName() string { + return "t_user" +} diff --git a/pkg/indicator/indicator_registry.go b/pkg/indicator/indicator_registry.go index 88e9b1d..a59cc15 100644 --- a/pkg/indicator/indicator_registry.go +++ b/pkg/indicator/indicator_registry.go @@ -29,6 +29,7 @@ func (r *IndicatorRegistry) Init() (err error) { r.MustRegistIndicator(&SuperTrend{}) r.MustRegistIndicator(&ADX{}) r.MustRegistIndicator(&KDJ{}) + r.MustRegistIndicator(&RVI{}) return } diff --git a/pkg/indicator/rvi.go b/pkg/indicator/rvi.go new file mode 100644 index 0000000..d9ccd06 --- /dev/null +++ b/pkg/indicator/rvi.go @@ -0,0 +1,92 @@ +package indicator + +import ( + "sig-pub/pkg/types" +) + +// RVI Relative Vigor Index +// RVI = SMA(Num, N) / SMA(Denom, N) +// Num = (Close-Open) + 2*(Close_1-Open_1) + 2*(Close_2-Open_2) + (Close_3-Open_3) +// Denom = (High-Low) + 2*(High_1-Low_1) + 2*(High_2-Low_2) + (High_3-Low_3) +// Signal = (RVI + 2*RVI_1 + 2*RVI_2 + RVI_3) / 6 +type RVI struct { +} + +func (c *RVI) Meta() IndicatorMeta { + return IndicatorMeta{ + Name: "RVI", + Input: []types.InputArg{ + {Name: "window", Type: types.InputTypeUInt, Desc: "窗口大小", Default: 10}, + }, + State: []string{"sig"}, + Plots: []Plot{ + {Name: "RVI", State: "vector", Type: PlotLine, Props: PlotProps{"color": ColorGreen}}, + {Name: "Signal", State: "sig", Type: PlotLine, Props: PlotProps{"color": ColorRed}}, + }, + } +} + +func (c *RVI) CandlePeriods(ctx IIndicatorContext) int16 { + return ctx.Input().Int16("window") + 3 +} + +func (c *RVI) Calculate(ctx IIndicatorContext) (vector float64) { + window := ctx.Input().Int16("window") + + count := window + 3 + klines := ctx.Series(0, count) + + if len(klines) < int(count) { + return + } + + var numSum, denomSum float64 + + for i := int16(0); i < window; i++ { + k0 := klines[i] + k1 := klines[i+1] + k2 := klines[i+2] + k3 := klines[i+3] + + // CO = Close - Open + co0 := k0.CloseF64() - k0.OpenF64() + co1 := k1.CloseF64() - k1.OpenF64() + co2 := k2.CloseF64() - k2.OpenF64() + co3 := k3.CloseF64() - k3.OpenF64() + + // HL = High - Low + hl0 := k0.HighF64() - k0.LowF64() + hl1 := k1.HighF64() - k1.LowF64() + hl2 := k2.HighF64() - k2.LowF64() + hl3 := k3.HighF64() - k3.LowF64() + + val1 := (co0 + 2*co1 + 2*co2 + co3) / 6.0 + val2 := (hl0 + 2*hl1 + 2*hl2 + hl3) / 6.0 + + numSum += val1 + denomSum += val2 + } + + var rvi float64 + if denomSum != 0 { + rvi = numSum / denomSum + } + + ctx.State().Set("rvi", rvi) + + // Calculate Signal + // Signal = (RVI + 2*RVI_1 + 2*RVI_2 + RVI_3) / 6 + rvi1, ok1 := ctx.State().Get("rvi", 1) + rvi2, ok2 := ctx.State().Get("rvi", 2) + rvi3, ok3 := ctx.State().Get("rvi", 3) + + var sig float64 + if ok1 && ok2 && ok3 { + sig = (rvi + 2*rvi1 + 2*rvi2 + rvi3) / 6.0 + } else { + sig = rvi + } + ctx.State().Set("sig", sig) + + return rvi +} diff --git a/pkg/session/session.go b/pkg/session/session.go new file mode 100644 index 0000000..7614183 --- /dev/null +++ b/pkg/session/session.go @@ -0,0 +1,28 @@ +package session + +import ( + "encoding/base64" + "sig-pub/api/pb" + "sig-pub/pkg/utils/security" + "time" + + "google.golang.org/protobuf/proto" +) + +func NewRpcSubject(userId int64, account string, extra map[string]string) (subject *pb.RpcSubject) { + subject = &pb.RpcSubject{Uid: userId, Account: account, Time: time.Now().UnixMilli(), Extra: extra} + return +} + +func RpcSubjectGenToken(aesKey []byte, subject *pb.RpcSubject) (token string, err error) { + bytes, err := proto.Marshal(subject) + if err != nil { + return + } + t, err := security.EncryptAesCBC(bytes, aesKey) + if err != nil { + return + } + token = base64.URLEncoding.EncodeToString(t) + return +} diff --git a/pkg/utils/rands/rand.go b/pkg/utils/rands/rand.go new file mode 100644 index 0000000..50c4bd6 --- /dev/null +++ b/pkg/utils/rands/rand.go @@ -0,0 +1,12 @@ +package rands + +import "math/rand/v2" + +func RandN(n int64, rd ...*rand.Rand) (ret int64) { + crd := rad + if len(rd) > 0 && rd[0] != nil { + crd = rd[0] + } + ret = crd.Int64N(int64(n)) + return +}