Files
trahn-trade/trahn-trade-backend/cmd/server/main.go
2026-02-22 15:21:18 -05:00

152 lines
4.1 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/kjannette/trahn-backend/internal/api"
"github.com/kjannette/trahn-backend/internal/bot"
"github.com/kjannette/trahn-backend/internal/config"
"github.com/kjannette/trahn-backend/internal/db"
"github.com/kjannette/trahn-backend/internal/external"
"github.com/kjannette/trahn-backend/internal/notifications"
"github.com/kjannette/trahn-backend/internal/repository"
"github.com/kjannette/trahn-backend/internal/scheduler"
)
const banner = `
╔══════════════════════════════════════╗
║ TRAHN Grid Trading Bot v0.2 ║
║ ║
╚══════════════════════════════════════╝
`
const apiPort = 3001
func main() {
fmt.Print(banner)
cfg, err := config.Load()
if err != nil {
fmt.Fprintf(os.Stderr, "config load error: %v\n", err)
os.Exit(1)
}
if err := cfg.Validate(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
cfg.Print()
// Database
fmt.Printf("\n[DB] Connecting to %s:%d/%s ...\n", cfg.DBHost, cfg.DBPort, cfg.DBName)
pool, err := db.Connect(cfg.DSN())
if err != nil {
fmt.Fprintf(os.Stderr, "[DB] Connection failed: %v\n", err)
os.Exit(1)
}
defer func() {
pool.Close()
fmt.Println("[DB] Connection pool closed")
}()
if err := db.TestConnection(pool); err != nil {
fmt.Fprintf(os.Stderr, "[DB] Test query failed: %v\n", err)
os.Exit(1)
}
// Repos
priceRepo := repository.NewPriceRepo(pool)
tradeRepo := repository.NewTradeRepo(pool)
srRepo := repository.NewSRRepo(pool)
gridRepo := repository.NewGridStateRepo(pool)
// Shared Dune client (single instance for bot + scheduler)
var dune *external.DuneClient
if cfg.DuneAPIKey != "" {
dune = external.NewDuneClient(cfg.DuneAPIKey, external.DuneOptions{
Method: cfg.SRMethod,
LookbackDays: cfg.SRLookbackDays,
RefreshHours: cfg.SRRefreshHours,
})
// Warm cache from DB if a recent S/R record exists
if latest, err := srRepo.GetLatest(context.Background()); err == nil && latest != nil {
dune.SeedCache(&external.SRResult{
Support: latest.Support,
Resistance: latest.Resistance,
Midpoint: latest.Midpoint,
Method: latest.Method,
LookbackDays: latest.LookbackDays,
FetchedAt: latest.Timestamp,
})
}
}
// Notifications
notify := notifications.NewSender(cfg.WebhookURL, cfg.BotName)
// Graceful shutdown context
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// 1. API server
srv := api.NewServer(pool, apiPort, cfg.APIKey, cfg.CORSAllowOrigin)
go func() {
if err := srv.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
fmt.Fprintf(os.Stderr, "[API] Server error: %v\n", err)
os.Exit(1)
}
}()
// 2. Grid bot (shares the Dune client)
botService := bot.NewService()
if err := botService.Start(ctx, cfg, priceRepo, tradeRepo, gridRepo, notify, dune); err != nil {
fmt.Fprintf(os.Stderr, "[BOT] Start failed: %v\n", err)
os.Exit(1)
}
// 3. S/R Scheduler (shares the same Dune client)
var srSched *scheduler.SRScheduler
if dune != nil {
srSched = scheduler.NewSRScheduler(dune, srRepo, scheduler.SRSchedulerConfig{
CronInterval: 1 * time.Hour,
SRChangeThreshold: 5,
GetBotState: botService.BotState,
OnGridRecalculate: func(sr *external.SRResult) {
botService.InitializeGrid(ctx)
},
})
srSched.Start()
} else {
fmt.Println("[SCHEDULER] Skipped - no Dune API key configured")
}
fmt.Println("\nAll services started successfully")
// Wait for shutdown signal
<-ctx.Done()
fmt.Println("\nShutting down gracefully...")
if srSched != nil {
srSched.Stop()
}
botService.Stop()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
fmt.Fprintf(os.Stderr, "[API] Shutdown error: %v\n", err)
}
fmt.Println("[API] Server closed")
fmt.Println("Shutdown complete")
}