crypto_alert_bot/internal/provider/bybit/bybit.go
2026-02-26 16:02:11 +03:00

168 lines
4.5 KiB
Go

// package bybit provides rates of cryptocurrencies from bybit exchange orderbook.
package bybit
import (
"context"
"fmt"
"log/slog"
"net/http"
"strconv"
"time"
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/config"
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities"
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/provider"
"github.com/shopspring/decimal"
)
const (
// mainnetURL = "https://api.bybit.com"
httpTimeout = time.Second * 10
)
type Bybit struct {
log *slog.Logger
cfg *config.Bybit
client *http.Client
}
func New(log *slog.Logger, cfg *config.Bybit) provider.Provider {
return &Bybit{
log: log,
cfg: cfg,
client: &http.Client{
Timeout: httpTimeout,
},
}
}
func (b *Bybit) symbol(pair *entities.Instrument) string {
return fmt.Sprintf("%s%s", pair.BaseCurrency, pair.QuoteCurrency)
}
// Price returns the current price of the pair (base currency / quote currency).
// e.g. BTC/USDT.
func (b *Bybit) Price(ctx context.Context, instrument entities.Instrument) (*entities.Price, error) {
// build request
req := marketOrderbookReq{
Category: categorySpot,
Symbol: b.symbol(&instrument),
}
var resp marketOrderbookResp
// make request
err := b.makeRequest(ctx, http.MethodGet, "/v5/market/orderbook", req, &resp)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
// get results
if len(resp.A) == 0 || len(resp.B) == 0 {
return nil, fmt.Errorf("incorrect response: nil data")
}
if len(resp.A[0]) != 2 || len(resp.B[0]) != 2 {
return nil, fmt.Errorf("incorrect response: nil prices")
}
askPrice := decimal.RequireFromString(resp.A[0][0])
bidPrice := decimal.RequireFromString(resp.B[0][0])
spread := askPrice.Sub(bidPrice)
return &entities.Price{
Ask: askPrice,
Bid: bidPrice,
Spread: spread,
Instrument: instrument,
}, nil
}
const klineLimit = 1000
var intervalToBybitIntervalMapper = map[provider.KlineInterval]string{
provider.Kline1m: "1",
provider.Kline3m: "3",
provider.Kline5m: "5",
provider.Kline15m: "15",
provider.Kline30m: "30",
provider.Kline1H: "60",
provider.Kline4H: "240",
provider.Kline6H: "360",
provider.Kline12H: "720",
provider.Kline1D: "D",
provider.Kline1W: "W",
}
// intervalDuration returns the time.Duration corresponding to a Bybit kline interval string.
func intervalBybit(interval provider.KlineInterval) (string, error) {
i, ok := intervalToBybitIntervalMapper[interval]
if !ok {
return "", fmt.Errorf("interval not found")
}
return i, nil
}
// Candles returns OHLC candles for the given interval in the [from, to) range.
// It paginates automatically when the range exceeds klineLimit candles per request.
func (b *Bybit) Candles(ctx context.Context, instrument entities.Instrument, from, to time.Time, interval provider.KlineInterval) ([]entities.Candle, error) {
dur := interval.ToDuration()
limit := klineLimit
var allCandles []entities.Candle
batchFrom := from
for batchFrom.Before(to) {
batchTo := batchFrom.Add(time.Duration(limit) * dur)
if batchTo.After(to) {
batchTo = to
}
bybitInterval, err := intervalBybit(interval)
if err != nil {
return nil, err
}
req := marketKlineReq{
Category: categorySpot,
Symbol: b.symbol(&instrument),
Interval: bybitInterval,
Start: strconv.FormatInt(batchFrom.UnixMilli(), 10),
End: strconv.FormatInt(batchTo.UnixMilli(), 10),
Limit: &limit,
}
var resp marketKlineResp
if err := b.makeRequest(ctx, http.MethodGet, "/v5/market/kline", req, &resp); err != nil {
return nil, fmt.Errorf("failed to get kline [%s, %s]: %w", batchFrom.Format(time.RFC3339), batchTo.Format(time.RFC3339), err)
}
for _, item := range resp.List {
if len(item) < 4 {
b.log.Error("bybit candles: length of elements less then 4", "len", len(item))
continue
}
startMs, err := strconv.ParseInt(item[0], 10, 64)
if err != nil {
b.log.Error("bybit candles: failed to parse start time", "err", err)
continue
}
high, err := decimal.NewFromString(item[2])
if err != nil {
b.log.Error("bybit candles: failed to parse candle high price", "err", err)
continue
}
low, err := decimal.NewFromString(item[3])
if err != nil {
b.log.Error("bybit candles: failed to parse candle low price", "err", err)
continue
}
allCandles = append(allCandles, entities.Candle{
OpenTime: time.UnixMilli(startMs),
High: high,
Low: low,
})
}
batchFrom = batchTo
}
return allCandles, nil
}