refactor server routes

This commit is contained in:
c9s 2021-02-04 16:44:14 +08:00
parent d5b1472560
commit c1b2114dd2

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@ -44,16 +45,12 @@ type Server struct {
Environ *bbgo.Environment Environ *bbgo.Environment
Trader *bbgo.Trader Trader *bbgo.Trader
Setup *Setup Setup *Setup
Bind string
OpenInBrowser bool OpenInBrowser bool
srv *http.Server srv *http.Server
} }
func (s *Server) Run(ctx context.Context) error { func (s *Server) newEngine() *gin.Engine {
userConfig := s.Config
environ := s.Environ
r := gin.Default() r := gin.Default()
r.Use(cors.New(cors.Config{ r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"}, AllowOrigins: []string{"*"},
@ -76,7 +73,7 @@ func (s *Server) Run(ctx context.Context) error {
} }
r.GET("/api/trades", func(c *gin.Context) { 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"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "database is not configured"})
return return
} }
@ -91,7 +88,7 @@ func (s *Server) Run(ctx context.Context) error {
return return
} }
trades, err := environ.TradeService.Query(service.QueryTradesOptions{ trades, err := s.Environ.TradeService.Query(service.QueryTradesOptions{
Exchange: types.ExchangeName(exchange), Exchange: types.ExchangeName(exchange),
Symbol: symbol, Symbol: symbol,
LastGID: lastGID, LastGID: lastGID,
@ -108,91 +105,8 @@ func (s *Server) Run(ctx context.Context) error {
}) })
}) })
r.GET("/api/orders/closed", func(c *gin.Context) { r.GET("/api/orders/closed", s.listClosedOrders)
if environ.OrderService == nil { r.GET("/api/trading-volume", s.tradingVolume)
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.POST("/api/sessions/test", func(c *gin.Context) { r.POST("/api/sessions/test", func(c *gin.Context) {
var sessionConfig bbgo.ExchangeSession var sessionConfig bbgo.ExchangeSession
@ -212,12 +126,12 @@ func (s *Server) Run(ctx context.Context) error {
} }
var anyErr error var anyErr error
_, openOrdersErr := session.Exchange.QueryOpenOrders(ctx, "BTCUSDT") _, openOrdersErr := session.Exchange.QueryOpenOrders(c, "BTCUSDT")
if openOrdersErr != nil { if openOrdersErr != nil {
anyErr = openOrdersErr anyErr = openOrdersErr
} }
_, balanceErr := session.Exchange.QueryAccountBalances(ctx) _, balanceErr := session.Exchange.QueryAccountBalances(c)
if balanceErr != nil { if balanceErr != nil {
anyErr = balanceErr anyErr = balanceErr
} }
@ -232,7 +146,7 @@ func (s *Server) Run(ctx context.Context) error {
r.GET("/api/sessions", func(c *gin.Context) { r.GET("/api/sessions", func(c *gin.Context) {
var sessions []*bbgo.ExchangeSession var sessions []*bbgo.ExchangeSession
for _, session := range environ.Sessions() { for _, session := range s.Environ.Sessions() {
sessions = append(sessions, session) sessions = append(sessions, session)
} }
@ -260,120 +174,33 @@ func (s *Server) Run(ctx context.Context) error {
return return
} }
if userConfig.Sessions == nil { if s.Config.Sessions == nil {
userConfig.Sessions = make(map[string]*bbgo.ExchangeSession) 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{ c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(), "error": err.Error(),
}) })
return return
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{"success": true})
"success": true,
})
}) })
r.GET("/api/assets", func(c *gin.Context) { r.GET("/api/assets", s.listAssets)
totalAssets := types.AssetMap{} r.GET("/api/sessions/:session", s.listSessions)
r.GET("/api/sessions/:session/trades", s.listSessionTrades)
for _, session := range environ.Sessions() { r.GET("/api/sessions/:session/open-orders", s.listSessionOpenOrders)
balances := session.Account.Balances() r.GET("/api/sessions/:session/account", s.getSessionAccount)
r.GET("/api/sessions/:session/account/balances", s.getSessionAccountBalance)
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/sessions/:session/symbols", func(c *gin.Context) { r.GET("/api/sessions/:session/symbols", func(c *gin.Context) {
sessionName := c.Param("session") sessionName := c.Param("session")
session, ok := environ.Session(sessionName) session, ok := s.Environ.Session(sessionName)
if !ok { if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)}) 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.GET("/api/strategies/single", s.listStrategies)
r.NoRoute(s.pkgerHandler) 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 s.OpenInBrowser {
if runtime.GOOS == "darwin" { s.openBrowser(ctx, bind)
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)
}
} }
bind := DefaultBindAddress s.srv = newServer(r, bind)
if len(s.Bind) > 0 { return listenAndServe(s.srv)
bind = s.Bind }
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, Addr: bind,
Handler: r, 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 var err error
defer func() { 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 { if err != http.ErrServerClosed {
return err return err
} }
@ -576,6 +441,40 @@ func (s *Server) setupRestart(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true}) 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) { func (s *Server) listStrategies(c *gin.Context) {
var stashes []map[string]interface{} var stashes []map[string]interface{}
@ -597,23 +496,113 @@ func (s *Server) listStrategies(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"strategies": stashes}) c.JSON(http.StatusOK, gin.H{"strategies": stashes})
} }
func (s *Server) setupSaveConfig(c *gin.Context) { func (s *Server) listSessions(c *gin.Context) {
userConfig := s.Config sessionName := c.Param("session")
environ := s.Environ 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"}) c.JSON(http.StatusBadRequest, gin.H{"error": "session is not configured"})
return return
} }
envVars, err := collectSessionEnvVars(userConfig.Sessions) envVars, err := collectSessionEnvVars(s.Config.Sessions)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
if len(s.Environ.MysqlURL) > 0 { if len(s.Environ.MysqlURL) > 0 {
envVars["MYSQL_URL"] = environ.MysqlURL envVars["MYSQL_URL"] = s.Environ.MysqlURL
} }
dotenvFile := ".env.local" dotenvFile := ".env.local"
@ -627,7 +616,7 @@ func (s *Server) setupSaveConfig(c *gin.Context) {
return return
} }
out, err := userConfig.YAML() out, err := s.Config.YAML()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@ -711,6 +700,58 @@ func collectSessionEnvVars(sessions map[string]*bbgo.ExchangeSession) (envVars m
return 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 { func getJSON(url string, data interface{}) error {
var client = &http.Client{Timeout: 500 * time.Millisecond} var client = &http.Client{Timeout: 500 * time.Millisecond}
r, err := client.Get(url) r, err := client.Get(url)