init; project structure & bybit provider
This commit is contained in:
commit
ae096d4820
14 changed files with 482 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.yml
|
||||
*.yaml
|
||||
5
Makefile
Normal file
5
Makefile
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
run:
|
||||
CONFIG_PATH=./internal/config/local.yml go run ./cmd/app/main.go
|
||||
|
||||
build:
|
||||
go build -o crypro_alert_bot ./cmd/app/main.go
|
||||
28
cmd/app/main.go
Normal file
28
cmd/app/main.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto_alert_bot/internal/config"
|
||||
"crypto_alert_bot/internal/entities"
|
||||
"crypto_alert_bot/internal/logger"
|
||||
"crypto_alert_bot/internal/provider/bybit"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// read config
|
||||
cfg := config.MustLoad()
|
||||
// init logger
|
||||
log := logger.NewAppLogger(&cfg.Logger)
|
||||
log.Info("app started")
|
||||
// init telegram bot
|
||||
b := bybit.New(log, &cfg.Providers.Bybit)
|
||||
price, err := b.Price(context.Background(), entities.Pair{
|
||||
BaseCurrency: "BTC",
|
||||
QuoteCurrency: "USDT",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("%+v\n", price)
|
||||
}
|
||||
28
go.mod
Normal file
28
go.mod
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
module crypto_alert_bot
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/log v0.4.2 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||
)
|
||||
47
go.sum
Normal file
47
go.sum
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||
1
internal/bot/telegram/telegram.go
Normal file
1
internal/bot/telegram/telegram.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package telegram
|
||||
45
internal/config/config.go
Normal file
45
internal/config/config.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Logger Logger `yaml:"logger"`
|
||||
Providers struct {
|
||||
Bybit Bybit `yaml:"bybit"`
|
||||
} `yaml:"providers"`
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
ServiceName string `yaml:"service_name" env-required:"true"` // service name for printing in logs
|
||||
Encoding string `yaml:"encoding" env-default:"json"` // console/json
|
||||
Level string `yaml:"level" env-default:"info"` // debug/info/warn/error
|
||||
}
|
||||
|
||||
type Bybit struct {
|
||||
BaseURL string `yaml:"base_url" env-default:"https://api.bybit.com"` // bybit api url
|
||||
}
|
||||
|
||||
// MustLoad returns config or panic.
|
||||
func MustLoad() *Config {
|
||||
configPath := os.Getenv("CONFIG_PATH")
|
||||
if configPath == "" {
|
||||
panic("CONFIG_PATH is not set")
|
||||
}
|
||||
// check if file exists
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
panic(fmt.Sprintf("config file does not exist: %s", configPath))
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
// read config
|
||||
if err := cleanenv.ReadConfig(configPath, &cfg); err != nil {
|
||||
panic(fmt.Errorf("cannot read config: %w", err))
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
6
internal/entities/pair.go
Normal file
6
internal/entities/pair.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package entities
|
||||
|
||||
type Pair struct {
|
||||
BaseCurrency string // base currency of the pair. e.g. BTC.
|
||||
QuoteCurrency string // quote currency of the pair. e.g. USDT.
|
||||
}
|
||||
10
internal/entities/price.go
Normal file
10
internal/entities/price.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package entities
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
type Price struct {
|
||||
Ask decimal.Decimal // limit seller / market buyer. ask > bid.
|
||||
Bid decimal.Decimal // limit buyer / market seller. bid < ask.
|
||||
Spread decimal.Decimal // delta between ask and bid.
|
||||
Pair Pair // trading pair
|
||||
}
|
||||
54
internal/logger/logger.go
Normal file
54
internal/logger/logger.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"crypto_alert_bot/internal/config"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
prettyLogger "github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
var levelAdapter = map[string]slog.Level{
|
||||
"debug": slog.LevelDebug,
|
||||
"info": slog.LevelInfo,
|
||||
"warn": slog.LevelWarn,
|
||||
"error": slog.LevelError,
|
||||
}
|
||||
|
||||
const (
|
||||
encodingConsole = "console"
|
||||
encodingJSON = "json"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceNameKey = "service_name"
|
||||
)
|
||||
|
||||
func NewAppLogger(cfg *config.Logger) *slog.Logger {
|
||||
var log *slog.Logger
|
||||
// select log level
|
||||
level, ok := levelAdapter[cfg.Level]
|
||||
if !ok {
|
||||
panic(fmt.Errorf("logger level not correct: %s", level))
|
||||
}
|
||||
// make handler
|
||||
switch cfg.Encoding {
|
||||
case encodingConsole:
|
||||
handler := prettyLogger.NewWithOptions(os.Stdout, prettyLogger.Options{
|
||||
Level: prettyLogger.Level(level),
|
||||
ReportTimestamp: true,
|
||||
ReportCaller: true,
|
||||
})
|
||||
log = slog.New(handler)
|
||||
case encodingJSON:
|
||||
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
|
||||
log = slog.New(handler)
|
||||
default:
|
||||
panic(fmt.Errorf("logger encoding is not correct: %s", cfg.Encoding))
|
||||
}
|
||||
// add field with service name
|
||||
log = log.With(serviceNameKey, cfg.ServiceName)
|
||||
|
||||
return log
|
||||
}
|
||||
74
internal/provider/bybit/bybit.go
Normal file
74
internal/provider/bybit/bybit.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// package bybit provides rates of cryptocurrencies from bybit exchange orderbook.
|
||||
package bybit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto_alert_bot/internal/config"
|
||||
"crypto_alert_bot/internal/entities"
|
||||
"crypto_alert_bot/internal/provider"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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.Pair) 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, pair entities.Pair) (*entities.Price, error) {
|
||||
// build request
|
||||
req := marketOrderbookReq{
|
||||
Category: categorySpot,
|
||||
Symbol: b.symbol(&pair),
|
||||
}
|
||||
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,
|
||||
Pair: pair,
|
||||
}, nil
|
||||
}
|
||||
32
internal/provider/bybit/models.go
Normal file
32
internal/provider/bybit/models.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package bybit
|
||||
|
||||
const (
|
||||
categorySpot string = "spot"
|
||||
categoryLinear string = "linear"
|
||||
categoryInverse string = "inverse"
|
||||
categoryOption string = "option"
|
||||
)
|
||||
|
||||
type marketOrderbookReq struct {
|
||||
// Product type. spot, linear, inverse, option
|
||||
Category string `json:"category"`
|
||||
// Symbol name, like BTCUSDT, uppercase only
|
||||
Symbol string `json:"symbol"`
|
||||
/*
|
||||
Limit size for each bid and ask
|
||||
spot: [1, 200]. Default: 1.
|
||||
linear&inverse: [1, 500]. Default: 25.
|
||||
option: [1, 25]. Default: 1.
|
||||
*/
|
||||
Limit *int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
type marketOrderbookResp struct {
|
||||
S string `json:"s"` // Symbol name.
|
||||
A [][]string `json:"a"` // Ask, seller. Sorted by price in ascending order. a[0]: Ask price, a[1]: Ask size.
|
||||
B [][]string `json:"b"` // Bid, buyer. Sorted by price in descending order. b[0]: Bid price, b[1]: Bid size.
|
||||
Ts int64 `json:"ts"` // The timestamp (ms) that the system generates the data.
|
||||
U int `json:"u"` // Update ID, is always in sequence.
|
||||
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.
|
||||
}
|
||||
138
internal/provider/bybit/request.go
Normal file
138
internal/provider/bybit/request.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package bybit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type response struct {
|
||||
RetCode int `json:"retCode"`
|
||||
RetMsg string `json:"retMsg"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Time int64 `json:"time"`
|
||||
RetExtInfo json.RawMessage `json:"retExtInfo"`
|
||||
}
|
||||
|
||||
func (b *Bybit) makeRequest(ctx context.Context, method string, endpoint string, params any, v any) error {
|
||||
// send request
|
||||
var (
|
||||
body []byte
|
||||
err error
|
||||
)
|
||||
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
body, err = b.getRequest(ctx, endpoint, params)
|
||||
case http.MethodPost:
|
||||
body, err = b.postRequest(ctx, endpoint, params)
|
||||
default:
|
||||
return fmt.Errorf("method not correct")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
|
||||
b.log.Debug("bybit request", "endpoint", endpoint, "params", params, "body", string(body))
|
||||
|
||||
var resp response
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if resp.RetCode != 0 {
|
||||
return fmt.Errorf("response not ok: code: %d, msg: %s, info: %s, result: %s", resp.RetCode, resp.RetMsg, resp.RetExtInfo, resp.Result)
|
||||
}
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(resp.Result, v); err != nil {
|
||||
return fmt.Errorf("failed to parse result: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bybit) getRequest(ctx context.Context, endPoint string, params any) ([]byte, error) {
|
||||
// params to query
|
||||
queryString := ""
|
||||
if params != nil {
|
||||
b, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var q map[string]any
|
||||
if err := json.Unmarshal(b, &q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := make(url.Values)
|
||||
for k, v := range q {
|
||||
query.Add(k, fmt.Sprint(v))
|
||||
}
|
||||
queryString = query.Encode()
|
||||
}
|
||||
|
||||
// make request
|
||||
request, err := http.NewRequest("GET", b.cfg.BaseURL+endPoint+"?"+queryString, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
// b.setRequestAPIHeaders(request, []byte(queryString))
|
||||
|
||||
// get response
|
||||
response, err := b.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (b *Bybit) postRequest(ctx context.Context, endPoint string, params any) ([]byte, error) {
|
||||
// params to json
|
||||
jsonData, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal params: %w", err)
|
||||
}
|
||||
|
||||
// make request
|
||||
request, err := http.NewRequest("POST", b.cfg.BaseURL+endPoint, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
// b.setRequestAPIHeaders(request, jsonData)
|
||||
|
||||
// get response
|
||||
response, err := b.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
12
internal/provider/provider.go
Normal file
12
internal/provider/provider.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto_alert_bot/internal/entities"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
// Price returns the current price of the pair (base currency / quote currency).
|
||||
// e.g. BTC/USDT.
|
||||
Price(ctx context.Context, pair entities.Pair) (*entities.Price, error)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue