candles based alerts
This commit is contained in:
parent
bec3b7de5b
commit
999f675da9
11 changed files with 316 additions and 15 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/config"
|
||||
|
|
@ -73,3 +74,95 @@ func (b *Bybit) Price(ctx context.Context, instrument entities.Instrument) (*ent
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,3 +30,21 @@ type marketOrderbookResp struct {
|
|||
Seq int `json:"seq"` // Cross sequence.
|
||||
Cts int64 `json:"cts"` // The timestamp from the matching engine when this orderbook data is produced. It can be correlated with T from public trade channel.
|
||||
}
|
||||
|
||||
type marketKlineReq struct {
|
||||
Category string `json:"category"` // Product type. spot,linear,inverse
|
||||
Symbol string `json:"symbol"` // Symbol name, like BTCUSDT, uppercase only
|
||||
Interval string `json:"interval"` // Kline interval. 1,3,5,15,30,60,120,240,360,720,D,W,M
|
||||
Start string `json:"start,omitempty"` // unix timestamp in ms
|
||||
End string `json:"end,omitempty"` // unix timestamp in ms
|
||||
Limit *int `json:"limit,omitempty"` // Limit for data size per page. [1, 1000]. Default: 200
|
||||
}
|
||||
|
||||
// marketKlineResp contains the result of /v5/market/kline.
|
||||
// List entries: [startTime, open, high, low, close, volume, turnover].
|
||||
// Returned in descending order (newest first).
|
||||
type marketKlineResp struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Category string `json:"category"`
|
||||
List [][]string `json:"list"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,11 +71,16 @@ func (b *Bybit) getRequest(ctx context.Context, endPoint string, params any) ([]
|
|||
}
|
||||
query := make(url.Values)
|
||||
for k, v := range q {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
query.Add(k, fmt.Sprint(v))
|
||||
}
|
||||
queryString = query.Encode()
|
||||
}
|
||||
|
||||
fmt.Println("req:", b.cfg.BaseURL+endPoint+"?"+queryString)
|
||||
|
||||
// make request
|
||||
request, err := http.NewRequest("GET", b.cfg.BaseURL+endPoint+"?"+queryString, nil)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue