From c1b2114dd27e32e03e396b7480a5f8f34c722be5 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 Feb 2021 16:44:14 +0800 Subject: [PATCH] refactor server routes --- pkg/server/routes.go | 475 +++++++++++++++++++++++-------------------- 1 file changed, 258 insertions(+), 217 deletions(-) diff --git a/pkg/server/routes.go b/pkg/server/routes.go index c2edbbcbd..77411f44c 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net" "net/http" "os" "os/exec" @@ -44,16 +45,12 @@ type Server struct { Environ *bbgo.Environment Trader *bbgo.Trader Setup *Setup - Bind string OpenInBrowser bool srv *http.Server } -func (s *Server) Run(ctx context.Context) error { - userConfig := s.Config - environ := s.Environ - +func (s *Server) newEngine() *gin.Engine { r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, @@ -76,7 +73,7 @@ func (s *Server) Run(ctx context.Context) error { } r.GET("/api/trades", func(c *gin.Context) { - if environ.TradeService == nil { + if s.Environ.TradeService == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database is not configured"}) return } @@ -91,7 +88,7 @@ func (s *Server) Run(ctx context.Context) error { return } - trades, err := environ.TradeService.Query(service.QueryTradesOptions{ + trades, err := s.Environ.TradeService.Query(service.QueryTradesOptions{ Exchange: types.ExchangeName(exchange), Symbol: symbol, LastGID: lastGID, @@ -108,91 +105,8 @@ func (s *Server) Run(ctx context.Context) error { }) }) - r.GET("/api/orders/closed", func(c *gin.Context) { - if environ.OrderService == nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database is not configured"}) - return - } - - exchange := c.Query("exchange") - symbol := c.Query("symbol") - gidStr := c.DefaultQuery("gid", "0") - - lastGID, err := strconv.ParseInt(gidStr, 10, 64) - if err != nil { - logrus.WithError(err).Error("last gid parse error") - c.Status(http.StatusBadRequest) - return - } - - orders, err := environ.OrderService.Query(service.QueryOrdersOptions{ - Exchange: types.ExchangeName(exchange), - Symbol: symbol, - LastGID: lastGID, - Ordering: "DESC", - }) - if err != nil { - c.Status(http.StatusBadRequest) - logrus.WithError(err).Error("order query error") - return - } - - c.JSON(http.StatusOK, gin.H{ - "orders": orders, - }) - }) - - r.GET("/api/trading-volume", func(c *gin.Context) { - if environ.TradeService == nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database is not configured"}) - return - } - - period := c.DefaultQuery("period", "day") - segment := c.DefaultQuery("segment", "exchange") - startTimeStr := c.Query("start-time") - - var startTime time.Time - - if startTimeStr != "" { - v, err := time.Parse(time.RFC3339, startTimeStr) - if err != nil { - c.Status(http.StatusBadRequest) - logrus.WithError(err).Error("start-time format incorrect") - return - } - startTime = v - - } else { - switch period { - case "day": - startTime = time.Now().AddDate(0, 0, -30) - - case "month": - startTime = time.Now().AddDate(0, -6, 0) - - case "year": - startTime = time.Now().AddDate(-2, 0, 0) - - default: - startTime = time.Now().AddDate(0, 0, -7) - - } - } - - rows, err := environ.TradeService.QueryTradingVolume(startTime, service.TradingVolumeQueryOptions{ - SegmentBy: segment, - GroupByPeriod: period, - }) - if err != nil { - logrus.WithError(err).Error("trading volume query error") - c.Status(http.StatusInternalServerError) - return - } - - c.JSON(http.StatusOK, gin.H{"tradingVolumes": rows}) - return - }) + r.GET("/api/orders/closed", s.listClosedOrders) + r.GET("/api/trading-volume", s.tradingVolume) r.POST("/api/sessions/test", func(c *gin.Context) { var sessionConfig bbgo.ExchangeSession @@ -212,12 +126,12 @@ func (s *Server) Run(ctx context.Context) error { } var anyErr error - _, openOrdersErr := session.Exchange.QueryOpenOrders(ctx, "BTCUSDT") + _, openOrdersErr := session.Exchange.QueryOpenOrders(c, "BTCUSDT") if openOrdersErr != nil { anyErr = openOrdersErr } - _, balanceErr := session.Exchange.QueryAccountBalances(ctx) + _, balanceErr := session.Exchange.QueryAccountBalances(c) if balanceErr != nil { anyErr = balanceErr } @@ -232,7 +146,7 @@ func (s *Server) Run(ctx context.Context) error { r.GET("/api/sessions", func(c *gin.Context) { var sessions []*bbgo.ExchangeSession - for _, session := range environ.Sessions() { + for _, session := range s.Environ.Sessions() { sessions = append(sessions, session) } @@ -260,120 +174,33 @@ func (s *Server) Run(ctx context.Context) error { return } - if userConfig.Sessions == nil { - userConfig.Sessions = make(map[string]*bbgo.ExchangeSession) + if s.Config.Sessions == nil { + s.Config.Sessions = make(map[string]*bbgo.ExchangeSession) } - userConfig.Sessions[sessionConfig.Name] = session + s.Config.Sessions[sessionConfig.Name] = session - environ.AddExchangeSession(sessionConfig.Name, session) + s.Environ.AddExchangeSession(sessionConfig.Name, session) - if err := session.Init(ctx, environ); err != nil { + if err := session.Init(c, s.Environ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - }) + c.JSON(http.StatusOK, gin.H{"success": true}) }) - r.GET("/api/assets", func(c *gin.Context) { - totalAssets := types.AssetMap{} - - for _, session := range environ.Sessions() { - balances := session.Account.Balances() - - if err := session.UpdatePrices(ctx); err != nil { - logrus.WithError(err).Error("price update failed") - c.Status(http.StatusInternalServerError) - return - } - - assets := balances.Assets(session.LastPrices()) - - for currency, asset := range assets { - totalAssets[currency] = asset - } - } - - c.JSON(http.StatusOK, gin.H{"assets": totalAssets}) - }) - - r.GET("/api/sessions/:session", func(c *gin.Context) { - sessionName := c.Param("session") - session, ok := environ.Session(sessionName) - - if !ok { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) - return - } - - c.JSON(http.StatusOK, gin.H{"session": session}) - }) - - r.GET("/api/sessions/:session/trades", func(c *gin.Context) { - sessionName := c.Param("session") - session, ok := environ.Session(sessionName) - - if !ok { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) - return - } - - c.JSON(http.StatusOK, gin.H{"trades": session.Trades}) - }) - - r.GET("/api/sessions/:session/open-orders", func(c *gin.Context) { - sessionName := c.Param("session") - session, ok := environ.Session(sessionName) - - if !ok { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) - return - } - - marketOrders := make(map[string][]types.Order) - for symbol, orderStore := range session.OrderStores() { - marketOrders[symbol] = orderStore.Orders() - } - - c.JSON(http.StatusOK, gin.H{"orders": marketOrders}) - }) - - r.GET("/api/sessions/:session/account", func(c *gin.Context) { - sessionName := c.Param("session") - session, ok := environ.Session(sessionName) - - if !ok { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) - return - } - - c.JSON(http.StatusOK, gin.H{"account": session.Account}) - }) - - r.GET("/api/sessions/:session/account/balances", func(c *gin.Context) { - sessionName := c.Param("session") - session, ok := environ.Session(sessionName) - - if !ok { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) - return - } - - if session.Account == nil { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("the account of session %s is nil", sessionName)}) - return - } - - c.JSON(http.StatusOK, gin.H{"balances": session.Account.Balances()}) - }) + r.GET("/api/assets", s.listAssets) + r.GET("/api/sessions/:session", s.listSessions) + r.GET("/api/sessions/:session/trades", s.listSessionTrades) + r.GET("/api/sessions/:session/open-orders", s.listSessionOpenOrders) + r.GET("/api/sessions/:session/account", s.getSessionAccount) + r.GET("/api/sessions/:session/account/balances", s.getSessionAccountBalance) r.GET("/api/sessions/:session/symbols", func(c *gin.Context) { sessionName := c.Param("session") - session, ok := environ.Session(sessionName) + session, ok := s.Environ.Session(sessionName) if !ok { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) @@ -407,29 +234,67 @@ func (s *Server) Run(ctx context.Context) error { r.GET("/api/strategies/single", s.listStrategies) r.NoRoute(s.pkgerHandler) + return r +} + +func (s *Server) Run(ctx context.Context, bindArgs ...string) error { + r := s.newEngine() + bind := resolveBind(bindArgs) if s.OpenInBrowser { - if runtime.GOOS == "darwin" { - baseURL := "http://" + DefaultBindAddress - if len(s.Bind) > 0 { - baseURL = "http://" + s.Bind - } - - go pingAndOpenURL(ctx, baseURL) - } else { - logrus.Warnf("%s is not supported for opening browser automatically", runtime.GOOS) - } + s.openBrowser(ctx, bind) } - bind := DefaultBindAddress - if len(s.Bind) > 0 { - bind = s.Bind + s.srv = newServer(r, bind) + return listenAndServe(s.srv) +} + +func (s *Server) openBrowser(ctx context.Context, bind string) { + if runtime.GOOS == "darwin" { + baseURL := "http://" + bind + go pingAndOpenURL(ctx, baseURL) + } else { + logrus.Warnf("%s is not supported for opening browser automatically", runtime.GOOS) + } +} + +func resolveBind(a []string) string { + switch len(a) { + case 0: + return DefaultBindAddress + + case 1: + return a[0] + + default: + panic("too many parameters for binding") } - s.srv = &http.Server{ + return "" +} + +func newServer(r http.Handler, bind string) *http.Server { + return &http.Server{ Addr: bind, Handler: r, } +} +func serve(srv *http.Server, l net.Listener) (err error) { + defer func() { + if err != nil && err != http.ErrServerClosed { + logrus.WithError(err).Error("unexpected http server error") + } + }() + + err = srv.Serve(l) + if err != http.ErrServerClosed { + return err + } + + return nil +} + +func listenAndServe(srv *http.Server) error { var err error defer func() { @@ -438,7 +303,7 @@ func (s *Server) Run(ctx context.Context) error { } }() - err = s.srv.ListenAndServe() + err = srv.ListenAndServe() if err != http.ErrServerClosed { return err } @@ -576,6 +441,40 @@ func (s *Server) setupRestart(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true}) } +func (s *Server) listClosedOrders(c *gin.Context) { + if s.Environ.OrderService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "database is not configured"}) + return + } + + exchange := c.Query("exchange") + symbol := c.Query("symbol") + gidStr := c.DefaultQuery("gid", "0") + + lastGID, err := strconv.ParseInt(gidStr, 10, 64) + if err != nil { + logrus.WithError(err).Error("last gid parse error") + c.Status(http.StatusBadRequest) + return + } + + orders, err := s.Environ.OrderService.Query(service.QueryOrdersOptions{ + Exchange: types.ExchangeName(exchange), + Symbol: symbol, + LastGID: lastGID, + Ordering: "DESC", + }) + if err != nil { + c.Status(http.StatusBadRequest) + logrus.WithError(err).Error("order query error") + return + } + + c.JSON(http.StatusOK, gin.H{ + "orders": orders, + }) +} + func (s *Server) listStrategies(c *gin.Context) { var stashes []map[string]interface{} @@ -597,23 +496,113 @@ func (s *Server) listStrategies(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"strategies": stashes}) } -func (s *Server) setupSaveConfig(c *gin.Context) { - userConfig := s.Config - environ := s.Environ +func (s *Server) listSessions(c *gin.Context) { + sessionName := c.Param("session") + session, ok := s.Environ.Session(sessionName) - if len(userConfig.Sessions) == 0 { + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) + return + } + + c.JSON(http.StatusOK, gin.H{"session": session}) +} + +func (s *Server) listSessionTrades(c *gin.Context) { + sessionName := c.Param("session") + session, ok := s.Environ.Session(sessionName) + + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) + return + } + + c.JSON(http.StatusOK, gin.H{"trades": session.Trades}) +} + +func (s *Server) getSessionAccount(c *gin.Context) { + sessionName := c.Param("session") + session, ok := s.Environ.Session(sessionName) + + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) + return + } + + c.JSON(http.StatusOK, gin.H{"account": session.Account}) +} + +func (s *Server) getSessionAccountBalance(c *gin.Context) { + sessionName := c.Param("session") + session, ok := s.Environ.Session(sessionName) + + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) + return + } + + if session.Account == nil { + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("the account of session %s is nil", sessionName)}) + return + } + + c.JSON(http.StatusOK, gin.H{"balances": session.Account.Balances()}) +} + +func (s *Server) listSessionOpenOrders(c *gin.Context) { + sessionName := c.Param("session") + session, ok := s.Environ.Session(sessionName) + + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) + return + } + + marketOrders := make(map[string][]types.Order) + for symbol, orderStore := range session.OrderStores() { + marketOrders[symbol] = orderStore.Orders() + } + + c.JSON(http.StatusOK, gin.H{"orders": marketOrders}) +} + +func (s *Server) listAssets(c *gin.Context) { + totalAssets := types.AssetMap{} + + for _, session := range s.Environ.Sessions() { + balances := session.Account.Balances() + + if err := session.UpdatePrices(c); err != nil { + logrus.WithError(err).Error("price update failed") + c.Status(http.StatusInternalServerError) + return + } + + assets := balances.Assets(session.LastPrices()) + + for currency, asset := range assets { + totalAssets[currency] = asset + } + } + + c.JSON(http.StatusOK, gin.H{"assets": totalAssets}) + +} + +func (s *Server) setupSaveConfig(c *gin.Context) { + if len(s.Config.Sessions) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "session is not configured"}) return } - envVars, err := collectSessionEnvVars(userConfig.Sessions) + envVars, err := collectSessionEnvVars(s.Config.Sessions) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if len(s.Environ.MysqlURL) > 0 { - envVars["MYSQL_URL"] = environ.MysqlURL + envVars["MYSQL_URL"] = s.Environ.MysqlURL } dotenvFile := ".env.local" @@ -627,7 +616,7 @@ func (s *Server) setupSaveConfig(c *gin.Context) { return } - out, err := userConfig.YAML() + out, err := s.Config.YAML() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -711,6 +700,58 @@ func collectSessionEnvVars(sessions map[string]*bbgo.ExchangeSession) (envVars m return } +func (s *Server) tradingVolume(c *gin.Context) { + if s.Environ.TradeService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "database is not configured"}) + return + } + + period := c.DefaultQuery("period", "day") + segment := c.DefaultQuery("segment", "exchange") + startTimeStr := c.Query("start-time") + + var startTime time.Time + + if startTimeStr != "" { + v, err := time.Parse(time.RFC3339, startTimeStr) + if err != nil { + c.Status(http.StatusBadRequest) + logrus.WithError(err).Error("start-time format incorrect") + return + } + startTime = v + + } else { + switch period { + case "day": + startTime = time.Now().AddDate(0, 0, -30) + + case "month": + startTime = time.Now().AddDate(0, -6, 0) + + case "year": + startTime = time.Now().AddDate(-2, 0, 0) + + default: + startTime = time.Now().AddDate(0, 0, -7) + + } + } + + rows, err := s.Environ.TradeService.QueryTradingVolume(startTime, service.TradingVolumeQueryOptions{ + SegmentBy: segment, + GroupByPeriod: period, + }) + if err != nil { + logrus.WithError(err).Error("trading volume query error") + c.Status(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, gin.H{"tradingVolumes": rows}) + return +} + func getJSON(url string, data interface{}) error { var client = &http.Client{Timeout: 500 * time.Millisecond} r, err := client.Get(url)