168 lines
4.5 KiB
Go
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
|
|
}
|