package app import ( "context" "fmt" "log/slog" "os" "os/signal" "syscall" "time" "github.com/gofiber/fiber/v2" "cryptoHermes/config" "cryptoHermes/internal/controller/restapi" "cryptoHermes/internal/repo/persistent/postgres" "cryptoHermes/internal/repo/webapi/binance" coinglassapi "cryptoHermes/internal/repo/webapi/coinglass" "cryptoHermes/internal/usecase" "cryptoHermes/internal/worker" pgpkg "cryptoHermes/pkg/postgres" ) func Run(cfg *config.Config, log *slog.Logger) error { rootCtx, cancel := context.WithCancel(context.Background()) defer cancel() pool, err := pgpkg.NewPool(rootCtx, pgpkg.Options{ DSN: cfg.Postgres.DSN, MaxConns: cfg.Postgres.MaxConns, MinConns: cfg.Postgres.MinConns, }) if err != nil { return fmt.Errorf("postgres: %w", err) } defer pool.Close() binanceClient := binance.NewClient(binance.ClientOptions{ BaseURL: cfg.Binance.BaseURL, Timeout: cfg.Binance.Timeout, RetryCount: cfg.Binance.RetryCount, RPS: cfg.Binance.RPS, Burst: cfg.Binance.Burst, }) klineRepo := postgres.NewKlineRepo(pool) fundingRepo := postgres.NewFundingRepo(pool) oiRepo := postgres.NewOpenInterestRepo(pool) lsRepo := postgres.NewLongShortRatioRepo(pool) takerRepo := postgres.NewTakerVolumeRepo(pool) cgLiqRepo := postgres.NewCoinglassLiqHeatMapRepo(pool) contextUC := usecase.NewMarketContextUsecase( binanceClient, binanceClient, klineRepo, fundingRepo, oiRepo, lsRepo, usecase.NewIndicatorUsecase(), usecase.NewDerivativesSignalUsecase(), log, ) queryUC := usecase.NewMarketQueryUsecase( binanceClient, fundingRepo, oiRepo, lsRepo, ) cgQueryUC := usecase.NewCoinglassQueryUsecase(cgLiqRepo) var collector *worker.Collector if cfg.Collector.Enabled { collector = worker.NewCollector(worker.Deps{ MarketData: binanceClient, Derivatives: binanceClient, KlineRepo: klineRepo, FundingRepo: fundingRepo, OIRepo: oiRepo, LSRepo: lsRepo, TakerRepo: takerRepo, Symbols: cfg.Collector.Symbols, Intervals: cfg.Collector.Intervals, Limit: cfg.Collector.DefaultLimit, Logger: log, }) } var cgCollector *worker.CoinglassCollector if cfg.Coinglass.Enabled { cgClient, err := coinglassapi.NewClient(coinglassapi.ClientOptions{ BaseURL: cfg.Coinglass.BaseURL, TOTPSecret: cfg.Coinglass.TOTPSecret, DataAESKey: cfg.Coinglass.DataAESKey, Timeout: cfg.Coinglass.Timeout, FakeSuccessRetry: cfg.Coinglass.FakeSuccessRetry, FakeSuccessRetryWait: cfg.Coinglass.FakeSuccessRetryWait, }) if err != nil { return fmt.Errorf("coinglass client: %w", err) } cgCollector = worker.NewCoinglassCollector(worker.CoinglassCollectorDeps{ Provider: cgClient, LiqHeatMapRepo: cgLiqRepo, Symbols: cfg.Collector.Symbols, LiqHeatMapInterval: cfg.Coinglass.LiqHeatMapInterval, LiqHeatMapLimit: cfg.Coinglass.LiqHeatMapLimit, BackoffInitial: cfg.Coinglass.BackoffInitial, BackoffMax: cfg.Coinglass.BackoffMax, Logger: log, }) } var sched *Scheduler if collector != nil || cgCollector != nil { sched = NewScheduler(collector, log) if cgCollector != nil { sched.WithCoinglass(cgCollector, cfg.Coinglass.LiqHeatMapCron, cfg.Coinglass.StartupJitterMax) } sched.Start(rootCtx) defer sched.Stop() } fiberApp := fiber.New(fiber.Config{ AppName: cfg.App.Name, ReadTimeout: cfg.HTTP.ReadTimeout, WriteTimeout: cfg.HTTP.WriteTimeout, IdleTimeout: cfg.HTTP.IdleTimeout, ErrorHandler: restapi.ErrorHandler, }) restapi.Register(fiberApp, restapi.Deps{ Logger: log, MarketContext: contextUC, MarketQuery: queryUC, CoinglassQuery: cgQueryUC, CoinglassEnabled: cfg.Coinglass.Enabled, MarketData: binanceClient, KlineRepo: klineRepo, }) listenErr := make(chan error, 1) go func() { addr := fmt.Sprintf(":%d", cfg.App.Port) log.Info("http_listening", "addr", addr) listenErr <- fiberApp.Listen(addr) }() stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) select { case err := <-listenErr: return err case sig := <-stop: log.Info("shutdown_signal", "signal", sig.String()) } shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownCancel() return fiberApp.ShutdownWithContext(shutdownCtx) }