Browse Source

indicator rvi, auth service

main
strange 7 months ago
parent
commit
cf64c6bdab
  1. 9
      api/pub.proto
  2. 5
      cmd/admin/main.go
  3. 12
      cmd/test/test.go
  4. 2
      config/admin.toml
  5. 4
      config/exchange.toml
  6. 24
      internal/admin/args/auth_args.go
  7. 48
      internal/admin/repoitory/user_repository.go
  8. 95
      internal/admin/service/auth_service.go
  9. 2
      internal/admin/service/service.go
  10. 36
      pkg/config/loader.go
  11. 34
      pkg/config/namespace.go
  12. 16
      pkg/data/entity/user.go
  13. 1
      pkg/indicator/indicator_registry.go
  14. 92
      pkg/indicator/rvi.go
  15. 28
      pkg/session/session.go
  16. 12
      pkg/utils/rands/rand.go

9
api/pub.proto

@ -242,3 +242,12 @@ message BacktestTradeOrder {
int64 entry_time = 20; int64 entry_time = 20;
repeated int64 trades = 21; repeated int64 trades = 21;
} }
//
message RpcSubject {
int64 uid = 1;
string account = 2;
int64 time = 3; // ms
map<string, string> extra = 4;
}

5
cmd/admin/main.go

@ -7,11 +7,14 @@ import (
"sig-pub/pkg/utils/exit" "sig-pub/pkg/utils/exit"
) )
// curl server: /api/sig/backtest/log type AdminConf struct {
AesTokenKey string `toml:"aesTokenKey"`
}
func main() { func main() {
// load config // load config
conf := config.MustLoadConfig(new(config.Configuration), "config/config.toml") conf := config.MustLoadConfig(new(config.Configuration), "config/config.toml")
config.SetDefaultConfigPath("config/admin.toml")
// database // database
db, err := conf.Database.Postgres.NewGormDB() db, err := conf.Database.Postgres.NewGormDB()

12
cmd/test/test.go

@ -1,7 +1,10 @@
package main package main
import ( import (
"fmt"
"path/filepath"
"sig-pub/pkg/zlog" "sig-pub/pkg/zlog"
"strings"
"github.com/govalues/decimal" "github.com/govalues/decimal"
) )
@ -9,7 +12,14 @@ import (
func main() { func main() {
// curl -H 'Content-Type: application/json' --data-binary "@vmdata.json" -X POST http://localhost:8428/api/v1/import // 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 { type BTC struct {

2
config/admin.toml

@ -0,0 +1,2 @@
aesTokenKey = "VzKw8Vx+K8k1nO9fmPjhv8o+8l4vqpF+fjsbvXf0j4o="

4
config/exchange.toml

@ -19,8 +19,8 @@ marketSubscribeLimit = 16
consumeBatch = 1024 consumeBatch = 1024
consumeLater = 2000 # 时间到达later或者数据累计到batch触发consume consumeLater = 2000 # 时间到达later或者数据累计到batch触发consume
# httpProxy = "" # httpProxy = ""
httpProxy = "http://192.168.1.5:7890" # httpProxy = "http://192.168.1.5:7890"
# httpProxy = "http://10.255.183.209:7890" httpProxy = "http://10.255.183.209:7890"
# 模拟盘API交易地址如下: # 模拟盘API交易地址如下:
# REST:https://www.okx.com # REST:https://www.okx.com

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

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

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

2
internal/admin/service/service.go

@ -11,7 +11,9 @@ import (
func Init(group *gin.RouterGroup, rdb *persist.DB) { func Init(group *gin.RouterGroup, rdb *persist.DB) {
backtestRepo := repository.NewBacktestRepository(rdb) backtestRepo := repository.NewBacktestRepository(rdb)
marketRepo := repository.NewMarketRepository(rdb) marketRepo := repository.NewMarketRepository(rdb)
userRepo := repository.NewUserRepository(rdb)
NewBacktestService(backtestRepo).Route(group.Group("/backtest")) NewBacktestService(backtestRepo).Route(group.Group("/backtest"))
NewMarketService(marketRepo).Route(group.Group("/market")) NewMarketService(marketRepo).Route(group.Group("/market"))
NewAuthService(userRepo).Route(group.Group("/auth"))
} }

36
pkg/config/loader.go

@ -14,23 +14,39 @@ import (
func parseConfPath(confPath string) (filePath, fileName, confType string) { func parseConfPath(confPath string) (filePath, fileName, confType string) {
filePath, fileName = filepath.Split(confPath) filePath, fileName = filepath.Split(confPath)
ext := filepath.Ext(fileName) ext := filepath.Ext(fileName)
fileName = strings.Replace(fileName, ext, "", -1) fileName, _ = strings.CutSuffix(fileName, ext)
confType = strings.Replace(ext, ".", "", 1) confType, _ = strings.CutPrefix(ext, ".")
confType = strings.ToLower(confType)
return 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 { func MustLoadConfig[T any](conf T, confPathArg ...string) T {
err := LoadConfig(conf, confPathArg...) v, err := loadViper(confPathArg...)
if err != nil { if err != nil {
panic(err) panic(err)
} }
if err := v.Unmarshal(conf); err != nil {
panic(errors.New("viper unmarshal config failed: " + err.Error()))
}
return conf return conf
} }
// LoadConfig load config.toml // loadViper load config.toml
// appConf service custom config // appConf service custom config
// return common service configuration // return common service configuration
func LoadConfig[T any](conf T, confPathArg ...string) error { func loadViper(confPathArg ...string) (v *viper.Viper, err error) {
confPath := "" confPath := ""
confName := "config" confName := "config"
confType := "toml" 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) zlog.Infof("use config file: %s, env prefix=%s\n", filePath, envPrefix)
v := viper.New() v = viper.New()
v.AddConfigPath(confPath) v.AddConfigPath(confPath)
v.SetConfigName(confName) v.SetConfigName(confName)
v.SetConfigType(confType) v.SetConfigType(confType)
@ -63,11 +79,7 @@ func LoadConfig[T any](conf T, confPathArg ...string) error {
v.AllowEmptyEnv(true) v.AllowEmptyEnv(true)
if err := v.ReadInConfig(); err != nil { 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 v, nil
return errors.New("viper unmarshal config failed: " + err.Error())
}
return nil
} }

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

16
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"
}

1
pkg/indicator/indicator_registry.go

@ -29,6 +29,7 @@ func (r *IndicatorRegistry) Init() (err error) {
r.MustRegistIndicator(&SuperTrend{}) r.MustRegistIndicator(&SuperTrend{})
r.MustRegistIndicator(&ADX{}) r.MustRegistIndicator(&ADX{})
r.MustRegistIndicator(&KDJ{}) r.MustRegistIndicator(&KDJ{})
r.MustRegistIndicator(&RVI{})
return return
} }

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

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

12
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
}
Loading…
Cancel
Save