Files
aitrade/pkg/app/app.go
T
kaedwen 62809a01ea
Build and Push Docker Image / build-and-push (push) Successful in 4m7s
parallel source fetch with timeout
Signed-off-by: kaedwen <kaedwen@heinrich.blue>
2026-07-09 21:11:58 +02:00

176 lines
3.8 KiB
Go

package app
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/pheinrich/aitrade/pkg/app/client"
"github.com/pheinrich/aitrade/pkg/app/news"
"github.com/pheinrich/aitrade/pkg/app/strategy"
"github.com/pheinrich/aitrade/pkg/app/trader"
"github.com/pheinrich/aitrade/pkg/app/web"
"github.com/pheinrich/aitrade/pkg/config"
"github.com/pheinrich/aitrade/pkg/db"
"golang.org/x/sync/errgroup"
)
type Application struct {
cfg *config.Config
db db.Database
ibClient *client.IBClient
newsAgg *news.Aggregator
trader *trader.Trader
webServer *web.Server
balanceRepo *db.BalanceRepository
newsRepo *db.NewsRepository
tradeRepo *db.TradeRepository
logger *slog.Logger
}
func New(cfg *config.Config, logger *slog.Logger) (*Application, error) {
// Open database
database, err := db.New(cfg.Database.Path, logger)
if err != nil {
return nil, fmt.Errorf("failed to initialize database: %w", err)
}
// Create IB client
ibClient := client.New(&cfg.IBGateway, logger)
// Create repositories
balanceRepo := db.NewBalanceRepository(database)
newsRepo := db.NewNewsRepository(database)
tradeRepo := db.NewTradeRepository(database)
whitelistRepo := db.NewWhitelistRepository(database)
positionRepo := db.NewPositionRepository(database)
// Create LLM scorer (optional)
var llmScorer *news.LLMScorer
if cfg.LLMScorer.Enabled {
llmScorer = news.NewLLMScorer(
&cfg.LLMScorer,
news.NewAnalyzer(), // Fallback analyzer
logger,
)
logger.Info("LLM scorer enabled",
slog.String("model", cfg.LLMScorer.ModelName),
slog.String("endpoint", cfg.LLMScorer.Endpoint),
slog.Float64("ensemble_weight", cfg.LLMScorer.EnsembleWeight))
}
// Create news aggregator
fetchTimeout := cfg.News.FetchTimeout.Duration
if fetchTimeout == 0 {
fetchTimeout = 5 * time.Minute // Default timeout
}
newsAgg := news.NewAggregator(newsRepo, cfg.News.PollInterval.Duration, fetchTimeout, llmScorer, logger)
// Add news sources from config
if len(cfg.News.Sources) > 0 {
newsAgg.AddSourcesFromConfig(cfg.News.Sources, cfg.News.DefaultRateLimit)
}
// Create trading strategy
tradingStrategy := strategy.NewStrategy(
cfg.Trading.Strategy,
cfg.Trading.StopLossEnabled,
cfg.Trading.StopLossPercent,
)
// Create trader
traderInstance := trader.NewTrader(
ibClient,
tradeRepo,
newsRepo,
balanceRepo,
whitelistRepo,
positionRepo,
tradingStrategy,
&cfg.Trading,
logger,
)
// Create web server
webServer, err := web.NewServer(
&cfg.Web,
&cfg.Trading,
&cfg.OIDC,
tradeRepo,
balanceRepo,
newsRepo,
whitelistRepo,
traderInstance,
logger,
)
if err != nil {
database.Close()
return nil, fmt.Errorf("failed to create web server: %w", err)
}
app := &Application{
cfg: cfg,
db: database,
ibClient: ibClient,
newsAgg: newsAgg,
trader: traderInstance,
webServer: webServer,
balanceRepo: balanceRepo,
newsRepo: newsRepo,
tradeRepo: tradeRepo,
logger: logger,
}
// Connect news aggregator to web server for SSE notifications
newsAgg.SetNewsUpdateCallback(func() {
webServer.BroadcastSSE("news.updated")
})
return app, nil
}
func (a *Application) Run(ctx context.Context) error {
a.logger.Info("application starting")
g, ctx := errgroup.WithContext(ctx)
// Start IB Gateway client
g.Go(func() error {
return a.ibClient.Run(ctx)
})
// Start news aggregator
g.Go(func() error {
return a.newsAgg.Run(ctx)
})
// Start trader
g.Go(func() error {
return a.trader.Run(ctx)
})
// Start web server
g.Go(func() error {
return a.webServer.Run(ctx)
})
err := g.Wait()
if err != nil && err != context.Canceled {
return err
}
a.logger.Info("shutting down")
return nil
}
func (a *Application) Close() error {
if a.ibClient != nil {
a.ibClient.Disconnect()
}
if a.db != nil {
return a.db.Close()
}
return nil
}