package usecase import ( "context" "errors" "fmt" "log/slog" "sync" "time" "golang.org/x/sync/errgroup" "cryptoHermes/internal/entity" ) var supportedSymbols = map[string]bool{ "BTCUSDT": true, "ETHUSDT": true, } var supportedIntervals = []string{"15m", "1h", "4h", "1d", "1w"} const ( klineWindowSize = 300 klineMinForOK = 200 derivativePeriod = "1h" fundingHistoryLen = 100 oiHistoryLen = 200 longShortLen = 200 takerHistoryLen = 200 ) type MarketContextUsecase struct { marketData MarketDataProvider derivatives DerivativesProvider klineRepo KlineRepository fundingRepo FundingRepository oiRepo OpenInterestRepository lsRepo LongShortRatioRepository log *slog.Logger } func NewMarketContextUsecase( marketData MarketDataProvider, derivatives DerivativesProvider, klineRepo KlineRepository, fundingRepo FundingRepository, oiRepo OpenInterestRepository, lsRepo LongShortRatioRepository, log *slog.Logger, ) *MarketContextUsecase { return &MarketContextUsecase{ marketData: marketData, derivatives: derivatives, klineRepo: klineRepo, fundingRepo: fundingRepo, oiRepo: oiRepo, lsRepo: lsRepo, log: log, } } func (u *MarketContextUsecase) Build(ctx context.Context, symbol string) (*entity.MarketContext, error) { if !supportedSymbols[symbol] { return nil, fmt.Errorf("unsupported symbol: %s", symbol) } var ( snapshot *entity.Ticker24h currentFund *entity.FundingRate currentOI *entity.OpenInterest warnMu sync.Mutex warnings []string ) addWarn := func(w string) { warnMu.Lock() warnings = append(warnings, w) warnMu.Unlock() } g, gctx := errgroup.WithContext(ctx) g.Go(func() error { s, err := u.marketData.GetTicker24h(gctx, symbol) if err != nil { addWarn("snapshot fetch failed: " + err.Error()) return nil } snapshot = s return nil }) g.Go(func() error { f, err := u.derivatives.GetCurrentFunding(gctx, symbol) if err != nil { addWarn("current funding fetch failed: " + err.Error()) return nil } currentFund = f return nil }) g.Go(func() error { oi, err := u.derivatives.GetCurrentOpenInterest(gctx, symbol) if err != nil { addWarn("current OI fetch failed: " + err.Error()) return nil } currentOI = oi return nil }) _ = g.Wait() klines := make(map[string][]entity.Kline, len(supportedIntervals)) for _, iv := range supportedIntervals { rows, err := u.klineRepo.FindRecent(ctx, symbol, iv, klineWindowSize) if err != nil { addWarn(fmt.Sprintf("klines DB query failed for %s: %v", iv, err)) rows = nil } if len(rows) < klineMinForOK { addWarn(fmt.Sprintf("klines %s only %d in DB, falling back to Binance", iv, len(rows))) fresh, ferr := u.marketData.GetKlines(ctx, symbol, iv, klineWindowSize) if ferr != nil { addWarn(fmt.Sprintf("klines fallback %s failed: %v", iv, ferr)) } else { closed := make([]entity.Kline, 0, len(fresh)) for _, k := range fresh { if k.IsClosed { closed = append(closed, k) } } go func(items []entity.Kline) { bgCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := u.klineRepo.UpsertMany(bgCtx, items); err != nil { u.log.Error("background_kline_upsert_failed", "err", err) } }(closed) rows = closed } } klines[iv] = rows } fundingHist, err := u.fundingRepo.FindRecent(ctx, symbol, fundingHistoryLen) if err != nil { addWarn("funding history query failed: " + err.Error()) } oiHist, err := u.oiRepo.FindRecent(ctx, symbol, derivativePeriod, oiHistoryLen) if err != nil { addWarn("OI history query failed: " + err.Error()) } globalLS, err := u.lsRepo.FindRecent(ctx, symbol, derivativePeriod, entity.RatioTypeGlobalAccount, longShortLen) if err != nil { addWarn("global long/short query failed: " + err.Error()) } topLS, err := u.lsRepo.FindRecent(ctx, symbol, derivativePeriod, entity.RatioTypeTopTraderPosition, longShortLen) if err != nil { addWarn("top trader position long/short query failed: " + err.Error()) } out := &entity.MarketContext{ Symbol: symbol, GeneratedAt: time.Now().UnixMilli(), Snapshot: snapshot, Klines: klines, Derivatives: entity.DerivativesBundle{ Funding: entity.FundingBundle{ Current: currentFund, History: fundingHist, }, OpenInterest: entity.OpenInterestBundle{ Current: currentOI, History: oiHist, }, LongShortRatio: entity.LongShortBundle{ Global: globalLS, TopTraderPosition: topLS, }, TakerBuySellVolume: nil, }, Technical: entity.TechnicalStructure{ Support: []entity.TechnicalLevel{}, Resistance: []entity.TechnicalLevel{}, }, DataQuality: entity.DataQuality{ Source: "binance", Warnings: warnings, }, } if out.DataQuality.Warnings == nil { out.DataQuality.Warnings = []string{} } if snapshot == nil { return out, errors.New("market snapshot unavailable") } return out, nil }