Files
cryptoHermes/internal/usecase/market_context.go
dela 0dabdb29ac feat(context): 接入 IndicatorUsecase,填实 Technical 字段
- MarketContextUsecase 增 IndicatorComputer 字段;构造器增参
- 用既有常量 derivativePeriod ("1h") 作为指标计算的 primary interval
  —— 与 LSR 采样时基对齐;未来切换 primary 改一个常量即可
- 替换原 hard-coded 空 Technical(market_context.go l.186-189)
- app.go 注入 usecase.NewIndicatorUsecase()

端到端 smoke 留给 commit 7 在所有测试就位后做(make docker-up + migrate-up + run)。
2026-05-24 20:32:30 +08:00

206 lines
5.0 KiB
Go

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
indicator IndicatorComputer
log *slog.Logger
}
func NewMarketContextUsecase(
marketData MarketDataProvider,
derivatives DerivativesProvider,
klineRepo KlineRepository,
fundingRepo FundingRepository,
oiRepo OpenInterestRepository,
lsRepo LongShortRatioRepository,
indicator IndicatorComputer,
log *slog.Logger,
) *MarketContextUsecase {
return &MarketContextUsecase{
marketData: marketData,
derivatives: derivatives,
klineRepo: klineRepo,
fundingRepo: fundingRepo,
oiRepo: oiRepo,
lsRepo: lsRepo,
indicator: indicator,
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: u.indicator.Compute(klines[derivativePeriod], globalLS),
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
}