diff --git a/go.mod b/go.mod index b479f1385..6bfba370c 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,15 @@ require ( github.com/c9s/requestgen v1.3.0 github.com/c9s/rockhopper v1.2.1-0.20220426104534-f27cbb09846c github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 + github.com/evanphx/json-patch/v5 v5.6.0 + github.com/fatih/camelcase v1.0.0 github.com/fatih/color v1.13.0 + github.com/gertd/go-pluralize v0.2.1 github.com/gin-contrib/cors v1.3.1 github.com/gin-gonic/gin v1.7.0 github.com/go-redis/redis/v8 v8.8.0 github.com/go-sql-driver/mysql v1.6.0 + github.com/gofrs/flock v0.8.1 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/jmoiron/sqlx v1.3.4 @@ -57,7 +61,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/denisenkom/go-mssqldb v0.12.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -65,7 +68,6 @@ require ( github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/go-test/deep v1.0.6 // indirect - github.com/gofrs/flock v0.8.1 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 // indirect github.com/golang/protobuf v1.5.2 // indirect diff --git a/go.sum b/go.sum index 3eb306306..6d3a17450 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,7 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= @@ -125,6 +126,8 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ 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/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= +github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= @@ -686,8 +689,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/pkg/service/database.go b/pkg/service/database.go index 0a71b7a91..3719b8231 100644 --- a/pkg/service/database.go +++ b/pkg/service/database.go @@ -11,6 +11,9 @@ import ( sqlite3Migrations "github.com/c9s/bbgo/pkg/migrations/sqlite3" ) +// reflect cache for database +var dbCache = NewReflectCache() + type DatabaseService struct { Driver string DSN string @@ -40,6 +43,12 @@ func (s *DatabaseService) Connect() error { return err } +func (s *DatabaseService) Insert(record interface{}) error { + sql := dbCache.InsertSqlOf(record) + _, err := s.DB.NamedExec(sql, record) + return err +} + func (s *DatabaseService) Close() error { return s.DB.Close() } diff --git a/pkg/service/profit.go b/pkg/service/profit.go index 15af10261..9396e6953 100644 --- a/pkg/service/profit.go +++ b/pkg/service/profit.go @@ -13,10 +13,6 @@ type ProfitService struct { DB *sqlx.DB } -func NewProfitService(db *sqlx.DB) *ProfitService { - return &ProfitService{db} -} - func (s *ProfitService) Load(ctx context.Context, id int64) (*types.Trade, error) { var trade types.Trade diff --git a/pkg/service/reflect.go b/pkg/service/reflect.go new file mode 100644 index 000000000..f8d938b87 --- /dev/null +++ b/pkg/service/reflect.go @@ -0,0 +1,154 @@ +package service + +import ( + "reflect" + "strings" + + "github.com/fatih/camelcase" + gopluralize "github.com/gertd/go-pluralize" +) + +var pluralize = gopluralize.NewClient() + +func tableNameOf(record interface{}) string { + rt := reflect.TypeOf(record) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + typeName := rt.Name() + tableName := strings.Join(camelcase.Split(typeName), "_") + tableName = strings.ToLower(tableName) + return pluralize.Plural(tableName) +} + +func placeholdersOf(record interface{}) []string { + rt := reflect.TypeOf(record) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + if rt.Kind() != reflect.Struct { + return nil + } + + var dbFields []string + for i := 0; i < rt.NumField(); i++ { + fieldType := rt.Field(i) + if tag, ok := fieldType.Tag.Lookup("db"); ok { + dbFields = append(dbFields, ":"+tag) + } + } + + return dbFields +} + +func fieldsNamesOf(record interface{}) []string { + rt := reflect.TypeOf(record) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + if rt.Kind() != reflect.Struct { + return nil + } + + var dbFields []string + for i := 0; i < rt.NumField(); i++ { + fieldType := rt.Field(i) + if tag, ok := fieldType.Tag.Lookup("db"); ok { + dbFields = append(dbFields, tag) + } + } + + return dbFields +} + +type ReflectCache struct { + tableNames map[string]string + fields map[string][]string + placeholders map[string][]string + insertSqls map[string]string +} + +func NewReflectCache() *ReflectCache { + return &ReflectCache{ + tableNames: make(map[string]string), + fields: make(map[string][]string), + placeholders: make(map[string][]string), + insertSqls: make(map[string]string), + } +} + +func (c *ReflectCache) InsertSqlOf(t interface{}) string { + rt := reflect.TypeOf(t) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + typeName := rt.Name() + sql, ok := c.insertSqls[typeName] + if ok { + return sql + } + + tableName := dbCache.TableNameOf(t) + fields := dbCache.FieldsOf(t) + placeholders := dbCache.PlaceholderOf(t) + fieldClause := strings.Join(fields, ", ") + placeholderClause := strings.Join(placeholders, ", ") + + sql = `INSERT INTO ` + tableName + ` (` + fieldClause + `) VALUES (` + placeholderClause + `)` + c.insertSqls[typeName] = sql + return sql +} + +func (c *ReflectCache) TableNameOf(t interface{}) string { + rt := reflect.TypeOf(t) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + typeName := rt.Name() + tableName, ok := c.tableNames[typeName] + if ok { + return tableName + } + + tableName = tableNameOf(t) + c.tableNames[typeName] = tableName + return tableName +} + +func (c *ReflectCache) PlaceholderOf(t interface{}) []string { + rt := reflect.TypeOf(t) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + typeName := rt.Name() + placeholders, ok := c.placeholders[typeName] + if ok { + return placeholders + } + + placeholders = placeholdersOf(t) + c.placeholders[typeName] = placeholders + return placeholders +} + +func (c *ReflectCache) FieldsOf(t interface{}) []string { + rt := reflect.TypeOf(t) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + typeName := rt.Name() + fields, ok := c.fields[typeName] + if ok { + return fields + } + + fields = fieldsNamesOf(t) + c.fields[typeName] = fields + return fields +} diff --git a/pkg/service/reflect_test.go b/pkg/service/reflect_test.go new file mode 100644 index 000000000..b2299aff0 --- /dev/null +++ b/pkg/service/reflect_test.go @@ -0,0 +1,71 @@ +package service + +import ( + "reflect" + "testing" + + "github.com/c9s/bbgo/pkg/types" +) + +func Test_tableNameOf(t *testing.T) { + type args struct { + record interface{} + } + tests := []struct { + name string + args args + want string + }{ + { + name: "MarginInterest", + args: args{record: &types.MarginInterest{}}, + want: "margin_interests", + }, + { + name: "MarginLoan", + args: args{record: &types.MarginLoan{}}, + want: "margin_loans", + }, + { + name: "MarginRepay", + args: args{record: &types.MarginRepay{}}, + want: "margin_repays", + }, + { + name: "MarginLiquidation", + args: args{record: &types.MarginLiquidation{}}, + want: "margin_liquidations", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tableNameOf(tt.args.record); got != tt.want { + t.Errorf("tableNameOf() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_fieldsNamesOf(t *testing.T) { + type args struct { + record interface{} + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "MarginInterest", + args: args{record: &types.MarginInterest{}}, + want: []string{"asset", "principle", "interest", "interest_rate", "isolated_symbol", "time"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := fieldsNamesOf(tt.args.record); !reflect.DeepEqual(got, tt.want) { + t.Errorf("fieldsNamesOf() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index b485f9b9f..675c89f2b 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -61,7 +61,7 @@ type Strategy struct { ExchangeSession *bbgo.ExchangeSession - marginBorrowRepay types.MarginBorrowRepay + marginBorrowRepay types.MarginBorrowRepayService } func (s *Strategy) ID() string { @@ -329,9 +329,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ExchangeSession = session - marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepay) + marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepayService) if !ok { - return fmt.Errorf("exchange %s does not implement types.MarginBorrowRepay", session.ExchangeName) + return fmt.Errorf("exchange %s does not implement types.MarginBorrowRepayService", session.ExchangeName) } s.marginBorrowRepay = marginBorrowRepay diff --git a/pkg/types/margin.go b/pkg/types/margin.go index ca15127ca..a907eb1de 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -52,8 +52,8 @@ type MarginExchange interface { GetMarginSettings() MarginSettings } -// MarginBorrowRepay provides repay and borrow actions of an crypto exchange -type MarginBorrowRepay interface { +// MarginBorrowRepayService provides repay and borrow actions of an crypto exchange +type MarginBorrowRepayService interface { RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error)