Files
cryptoHermes/internal/usecase/market_context.go
dela fa769331c2 feat(market): Phase 1 扩展 — 更多 symbol + Bollinger/Vegas + 衍生品信号
- collector.symbols 与 supportedSymbols 加 SOL/BNB/DOGE,env-default 与 README 同步
- entity 追加 TechnicalStructure.Intervals(每周期 Bollinger + Vegas,omitempty 不破坏既有字段)与 DerivativesBundle.Signal
- 新增纯解析 usecase derivatives_signal.go:fundingBias 绝对阈值、oiSignal 用 P20 baseline + 价格方向(OI up + 价格 up/down → long/short_building,funding 不参与方向)、lsrRegime 绝对阈值;signal 数据不足走 warnings 进 DataQuality
- indicator.go 加 sma/stddev/ema/bollinger/vegas + computeIntervalTechnicals;IndicatorComputer 签名收 klines map
- harness 同步:G12 扩入 derivatives_signal.go(含 Makefile G12.5/6 + ADR-0002 适用范围 + project-map / harness-health 更新)
2026-05-24 23:19:57 +08:00

221 lines
5.4 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,
"SOLUSDT": true,
"BNBUSDT": true,
"DOGEUSDT": 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
derivSignal DerivativesSignalComputer
log *slog.Logger
}
func NewMarketContextUsecase(
marketData MarketDataProvider,
derivatives DerivativesProvider,
klineRepo KlineRepository,
fundingRepo FundingRepository,
oiRepo OpenInterestRepository,
lsRepo LongShortRatioRepository,
indicator IndicatorComputer,
derivSignal DerivativesSignalComputer,
log *slog.Logger,
) *MarketContextUsecase {
return &MarketContextUsecase{
marketData: marketData,
derivatives: derivatives,
klineRepo: klineRepo,
fundingRepo: fundingRepo,
oiRepo: oiRepo,
lsRepo: lsRepo,
indicator: indicator,
derivSignal: derivSignal,
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())
}
derivBundle := 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,
}
if u.derivSignal != nil {
signal, sigWarnings := u.derivSignal.Compute(currentFund, oiHist, globalLS, klines[derivativePeriod])
derivBundle.Signal = signal
for _, w := range sigWarnings {
addWarn(w)
}
}
out := &entity.MarketContext{
Symbol: symbol,
GeneratedAt: time.Now().UnixMilli(),
Snapshot: snapshot,
Klines: klines,
Derivatives: derivBundle,
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
}