init code
This commit is contained in:
52
internal/services/controllers/market.go
Normal file
52
internal/services/controllers/market.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"me.thuanle/bbot/internal/configs/strategy"
|
||||
"me.thuanle/bbot/internal/data"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// GetTopPrices returns the top symbol, price and rate
|
||||
func GetTopPrices() ([]string, []float64, []float64) {
|
||||
n := len(strategy.TopPriceSymbols)
|
||||
topSym := make([]string, n)
|
||||
topPrice := make([]float64, n)
|
||||
topRate := make([]float64, n)
|
||||
|
||||
for i, sym := range strategy.TopPriceSymbols {
|
||||
price, rate, _, ok := data.Market.GetFuturePrice(sym)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
topSym[i] = sym
|
||||
topPrice[i] = price
|
||||
topRate[i] = rate
|
||||
|
||||
}
|
||||
return topSym, topPrice, topRate
|
||||
}
|
||||
|
||||
// GetTopFundingFee returns the top funding fee: symbol, rate and cd time
|
||||
func GetTopFundingFee() ([]string, []float64, []int64) {
|
||||
allRates, allCd := data.Market.GetAllFundRate()
|
||||
var resSym []string
|
||||
for sym, rate := range allRates {
|
||||
if strategy.FundRateBuyThreshold < rate && rate < strategy.FundRateSellThreshold {
|
||||
continue
|
||||
}
|
||||
|
||||
resSym = append(resSym, sym)
|
||||
}
|
||||
|
||||
sort.Slice(resSym, func(i, j int) bool {
|
||||
return allRates[resSym[i]] < allRates[resSym[j]]
|
||||
})
|
||||
|
||||
resRate := make([]float64, len(resSym))
|
||||
resCd := make([]int64, len(resSym))
|
||||
for i, sym := range resSym {
|
||||
resRate[i] = allRates[sym]
|
||||
resCd[i] = allCd[sym]
|
||||
}
|
||||
return resSym, resRate, resCd
|
||||
}
|
||||
9
internal/services/tele/chat/helper/wrap.go
Normal file
9
internal/services/tele/chat/helper/wrap.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package helper
|
||||
|
||||
func Pre(msg string) string {
|
||||
return "<pre>" + msg + "</pre>"
|
||||
}
|
||||
|
||||
func Code(msg string) string {
|
||||
return "<code>" + msg + "</code>"
|
||||
}
|
||||
39
internal/services/tele/chat/reply.go
Normal file
39
internal/services/tele/chat/reply.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gopkg.in/telebot.v3"
|
||||
"me.thuanle/bbot/internal/services/tele/chat/helper"
|
||||
)
|
||||
|
||||
func ReplyMessage(context telebot.Context, formattedText string, opts ...interface{}) error {
|
||||
if opts == nil {
|
||||
return context.Reply(formattedText, telebot.ModeHTML, telebot.NoPreview)
|
||||
}
|
||||
return context.Reply(formattedText, append(opts, telebot.ModeHTML, telebot.NoPreview)...)
|
||||
}
|
||||
|
||||
func ReplyMessagef(context telebot.Context, menu *telebot.ReplyMarkup, format string, v ...interface{}) error {
|
||||
if menu != nil {
|
||||
return ReplyMessage(context, fmt.Sprintf(format, v...), menu)
|
||||
} else {
|
||||
return ReplyMessage(context, fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func ReplyMessageCode(context telebot.Context, message string) error {
|
||||
return ReplyMessage(context, helper.Code(message))
|
||||
}
|
||||
|
||||
func ReplyMessagePre(context telebot.Context, message string) error {
|
||||
return ReplyMessage(context, helper.Pre(message))
|
||||
}
|
||||
|
||||
func ReplyMessageJson(context telebot.Context, obj any) error {
|
||||
jsonStr, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ReplyMessagePre(context, string(jsonStr))
|
||||
}
|
||||
45
internal/services/tele/command.go
Normal file
45
internal/services/tele/command.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package tele
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/telebot.v3"
|
||||
"me.thuanle/bbot/internal/services/tele/commands"
|
||||
"me.thuanle/bbot/internal/services/tele/middlewares"
|
||||
)
|
||||
|
||||
var commandList = []telebot.Command{
|
||||
{
|
||||
Text: "p",
|
||||
Description: "(p) - Get mark price",
|
||||
},
|
||||
{
|
||||
Text: "fee",
|
||||
Description: "(f) - show top funding fee",
|
||||
},
|
||||
}
|
||||
|
||||
func setupCommands(b *telebot.Bot) error {
|
||||
if err := b.SetCommands(commandList); err != nil {
|
||||
log.Fatal().Err(err).Msg("setup telebot commands")
|
||||
return err
|
||||
}
|
||||
|
||||
b.Use(middlewares.IgnoreBot)
|
||||
b.Use(middlewares.SendErrorMiddleware)
|
||||
|
||||
//welcome
|
||||
b.Handle("/start", commands.OnStart)
|
||||
|
||||
//general
|
||||
b.Handle("/ip", commands.OnGetIp)
|
||||
|
||||
//info
|
||||
b.Handle("/p", commands.OnGetTopPrices)
|
||||
b.Handle("/fee", commands.OnGetTopFundingFee)
|
||||
|
||||
//any text
|
||||
b.Handle(telebot.OnText, commands.OnChatHandler)
|
||||
b.Handle(telebot.OnSticker, commands.OnStickerHandler)
|
||||
|
||||
return nil
|
||||
}
|
||||
43
internal/services/tele/commands/chat.go
Normal file
43
internal/services/tele/commands/chat.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/telebot.v3"
|
||||
"me.thuanle/bbot/internal/configs/tele"
|
||||
"me.thuanle/bbot/internal/helper/binancex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func OnChatHandler(context telebot.Context) error {
|
||||
text := strings.ToLower(context.Text())
|
||||
|
||||
switch {
|
||||
case text == "p":
|
||||
return OnGetTopPrices(context)
|
||||
case text == "f" || text == "fee":
|
||||
return OnGetTopFundingFee(context)
|
||||
case binancex.IsToken(text):
|
||||
return OnTokenInfo(context)
|
||||
default:
|
||||
//ignore it
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func OnStickerHandler(context telebot.Context) error {
|
||||
stickerId := context.Message().Sticker.UniqueID
|
||||
switch stickerId {
|
||||
case tele.StickerTop:
|
||||
return OnGetTopPrices(context)
|
||||
default:
|
||||
token, ok := tele.Sticker2TokenMap[stickerId]
|
||||
if !ok {
|
||||
log.Debug().
|
||||
Any("sticker", context.Message().Sticker).
|
||||
Msg("Sticker received")
|
||||
return nil
|
||||
}
|
||||
|
||||
return OnTokenInfoByToken(context, token)
|
||||
}
|
||||
}
|
||||
12
internal/services/tele/commands/general.go
Normal file
12
internal/services/tele/commands/general.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"gopkg.in/telebot.v3"
|
||||
"me.thuanle/bbot/internal/services/tele/chat"
|
||||
"me.thuanle/bbot/internal/utils/netx"
|
||||
)
|
||||
|
||||
func OnGetIp(context telebot.Context) error {
|
||||
ip := netx.GetPublicIp()
|
||||
return chat.ReplyMessageCode(context, ip)
|
||||
}
|
||||
5
internal/services/tele/commands/helper/error.go
Normal file
5
internal/services/tele/commands/helper/error.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package helper
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrorParamCountMismatch = errors.New("param count mismatched")
|
||||
41
internal/services/tele/commands/helper/handler.go
Normal file
41
internal/services/tele/commands/helper/handler.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"gopkg.in/telebot.v3"
|
||||
)
|
||||
|
||||
func throwErrorParamCountMismatch(_ telebot.Context) error {
|
||||
return ErrorParamCountMismatch
|
||||
}
|
||||
|
||||
// Base on the number of Args in context, it will execute the (n+1)-th function in the list
|
||||
// - If the context have 0 argument, it will call funcs[0]
|
||||
// - If the context have 1 argument, it will call funcs[1]
|
||||
// Otherwise, it will call `other` function
|
||||
func HandleZ0nArgs(other telebot.HandlerFunc, funcs ...telebot.HandlerFunc) telebot.HandlerFunc {
|
||||
return func(context telebot.Context) error {
|
||||
args := context.Args()
|
||||
|
||||
if len(args) > len(funcs)-1 {
|
||||
return other(context)
|
||||
}
|
||||
return funcs[len(args)](context)
|
||||
}
|
||||
}
|
||||
|
||||
// Base on the number of Args in context, it will execute the (n+1)-th function in the list
|
||||
// - If the context have 0 argument, it will call funcs[0]
|
||||
// - If the context have 1 argument, it will call funcs[1]
|
||||
// Otherwise, it will throw error
|
||||
func Handle0nArgs(funcs ...telebot.HandlerFunc) telebot.HandlerFunc {
|
||||
return HandleZ0nArgs(throwErrorParamCountMismatch, funcs...)
|
||||
}
|
||||
|
||||
func HandleGetSet(getFunc telebot.HandlerFunc, setFunc telebot.HandlerFunc) telebot.HandlerFunc {
|
||||
return func(context telebot.Context) error {
|
||||
if len(context.Args()) == 0 {
|
||||
return getFunc(context)
|
||||
}
|
||||
return setFunc(context)
|
||||
}
|
||||
}
|
||||
18
internal/services/tele/commands/market.go
Normal file
18
internal/services/tele/commands/market.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"gopkg.in/telebot.v3"
|
||||
"me.thuanle/bbot/internal/services/controllers"
|
||||
"me.thuanle/bbot/internal/services/tele/chat"
|
||||
"me.thuanle/bbot/internal/services/tele/view"
|
||||
)
|
||||
|
||||
func OnGetTopPrices(context telebot.Context) error {
|
||||
sym, price, rate := controllers.GetTopPrices()
|
||||
return chat.ReplyMessagePre(context, view.RenderOnGetTopPricesMessage(sym, price, rate))
|
||||
}
|
||||
|
||||
func OnGetTopFundingFee(context telebot.Context) error {
|
||||
fee, float64s, cds := controllers.GetTopFundingFee()
|
||||
return chat.ReplyMessagePre(context, view.RenderOnGetTopFundingFeeMessage(fee, float64s, cds))
|
||||
}
|
||||
11
internal/services/tele/commands/start.go
Normal file
11
internal/services/tele/commands/start.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package commands
|
||||
|
||||
import "gopkg.in/telebot.v3"
|
||||
|
||||
const what = `Welcome on board! Here is some tips:
|
||||
- /p to get pricing
|
||||
- /l to list current positions`
|
||||
|
||||
func OnStart(context telebot.Context) error {
|
||||
return context.Send(what)
|
||||
}
|
||||
49
internal/services/tele/commands/token.go
Normal file
49
internal/services/tele/commands/token.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"gopkg.in/telebot.v3"
|
||||
"me.thuanle/bbot/internal/configs/tele"
|
||||
"me.thuanle/bbot/internal/data"
|
||||
"me.thuanle/bbot/internal/helper/binancex"
|
||||
"me.thuanle/bbot/internal/services/tele/chat"
|
||||
"me.thuanle/bbot/internal/services/tele/view"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func showStickerMode(context telebot.Context, token string) {
|
||||
token = strings.ToUpper(token)
|
||||
stickerIdx, ok := tele.Token2StickerIdxMap[token]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
stickers, err := context.Bot().StickerSet(tele.StickerSet)
|
||||
if err == nil && stickers != nil {
|
||||
_ = context.Reply(&stickers.Stickers[stickerIdx])
|
||||
}
|
||||
}
|
||||
|
||||
func OnTokenInfoByToken(context telebot.Context, token string) error {
|
||||
symbols := binancex.Token2Symbols(token)
|
||||
if len(symbols) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
showStickerMode(context, token)
|
||||
|
||||
fp, fundRate, fundTime, ok := data.Market.GetFuturePrice(symbols[0])
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
sSymbol := binancex.Future2SpotSymbol(symbols[0])
|
||||
sp, _ := data.Market.GetSpotPrice(sSymbol)
|
||||
|
||||
_ = chat.ReplyMessage(context, view.RenderOnPriceMessage(symbols[0], sp, fp, fundRate, fundTime))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func OnTokenInfo(context telebot.Context) error {
|
||||
text := strings.ToLower(context.Text())
|
||||
|
||||
return OnTokenInfoByToken(context, text)
|
||||
}
|
||||
19
internal/services/tele/middlewares/error.go
Normal file
19
internal/services/tele/middlewares/error.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/telebot.v3"
|
||||
)
|
||||
|
||||
func SendErrorMiddleware(next telebot.HandlerFunc) telebot.HandlerFunc {
|
||||
return func(c telebot.Context) error {
|
||||
err := next(c) // continue execution chain
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("Chat Middleware error reply")
|
||||
return c.Reply(fmt.Sprintf("Error: %v", err), telebot.RemoveKeyboard)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
14
internal/services/tele/middlewares/ignore_bot.go
Normal file
14
internal/services/tele/middlewares/ignore_bot.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"gopkg.in/telebot.v3"
|
||||
)
|
||||
|
||||
func IgnoreBot(next telebot.HandlerFunc) telebot.HandlerFunc {
|
||||
return func(c telebot.Context) error {
|
||||
if c.Sender().IsBot {
|
||||
return nil
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
51
internal/services/tele/tele_service.go
Normal file
51
internal/services/tele/tele_service.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package tele
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/telebot.v3"
|
||||
"me.thuanle/bbot/internal/configs/key"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
b *telebot.Bot
|
||||
}
|
||||
|
||||
var (
|
||||
ts *Service
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func Ins() *Service {
|
||||
if ts != nil {
|
||||
return ts
|
||||
}
|
||||
once.Do(func() {
|
||||
pref := telebot.Settings{
|
||||
Token: os.Getenv(key.TelegramToken),
|
||||
Poller: &telebot.LongPoller{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
b, err := telebot.NewBot(pref)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to create bot")
|
||||
}
|
||||
|
||||
err = setupCommands(b)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to setup commands")
|
||||
}
|
||||
|
||||
ts = &Service{
|
||||
b: b,
|
||||
}
|
||||
})
|
||||
return ts
|
||||
}
|
||||
|
||||
func (t *Service) Start() {
|
||||
log.Info().Msg("Starting telegram service")
|
||||
t.b.Start()
|
||||
}
|
||||
34
internal/services/tele/view/fee.go
Normal file
34
internal/services/tele/view/fee.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"me.thuanle/bbot/internal/helper/binancex"
|
||||
"me.thuanle/bbot/internal/services/tele/view/helper"
|
||||
"me.thuanle/bbot/internal/utils/opx"
|
||||
"me.thuanle/bbot/internal/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
func IconOfFundingFeeDirection(fund float64) string {
|
||||
return opx.Ite(fund > 0, "📉", "📈")
|
||||
}
|
||||
|
||||
func RenderOnGetTopFundingFeeMessage(sym []string, rate []float64, cd []int64) string {
|
||||
t := helper.NewNoBorderTableWriter("Symbol", "Rate", "Cd")
|
||||
|
||||
flag := true
|
||||
for i, s := range sym {
|
||||
if rate[i] > 0 && flag {
|
||||
t.AppendRow(table.Row{"...", "...", "..."})
|
||||
flag = false
|
||||
}
|
||||
t.AppendRow(table.Row{
|
||||
binancex.Symbol2Token(s),
|
||||
fmt.Sprintf("%.3f%%", rate[i]*100),
|
||||
fmt.Sprintf("%s", timex.CdMinuteStringTime(time.UnixMilli(cd[i]))),
|
||||
})
|
||||
}
|
||||
|
||||
return t.Render()
|
||||
}
|
||||
27
internal/services/tele/view/helper/table.go
Normal file
27
internal/services/tele/view/helper/table.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
func NewNoBorderTableWriter(headers ...string) table.Writer {
|
||||
t := table.NewWriter()
|
||||
t.Style().Options.DrawBorder = false
|
||||
t.Style().Options.SeparateColumns = false
|
||||
|
||||
var configs []table.ColumnConfig
|
||||
configs = append(configs, table.ColumnConfig{Number: 1, AlignHeader: text.AlignCenter, Align: text.AlignLeft, AlignFooter: text.AlignLeft})
|
||||
for i := 2; i <= len(headers); i++ {
|
||||
configs = append(configs, table.ColumnConfig{Number: i, AlignHeader: text.AlignCenter, Align: text.AlignRight, AlignFooter: text.AlignRight})
|
||||
}
|
||||
t.SetColumnConfigs(configs)
|
||||
|
||||
var h table.Row
|
||||
for _, header := range headers {
|
||||
h = append(h, header)
|
||||
}
|
||||
t.AppendHeader(h)
|
||||
|
||||
return t
|
||||
}
|
||||
60
internal/services/tele/view/price.go
Normal file
60
internal/services/tele/view/price.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"me.thuanle/bbot/internal/helper/binancex"
|
||||
"me.thuanle/bbot/internal/services/tele/view/helper"
|
||||
"me.thuanle/bbot/internal/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
func RenderPrice(price float64) string {
|
||||
mFmt := message.NewPrinter(language.AmericanEnglish)
|
||||
switch {
|
||||
case price > 1000:
|
||||
return mFmt.Sprintf("%0.0f", price)
|
||||
case price > 100:
|
||||
return mFmt.Sprintf("%0.1f", price)
|
||||
case price > 10:
|
||||
return mFmt.Sprintf("%0.2f", price)
|
||||
case price > 1:
|
||||
return mFmt.Sprintf("%0.3f", price)
|
||||
case price > 0.1:
|
||||
return mFmt.Sprintf("%0.4f", price)
|
||||
case price > 0.01:
|
||||
return mFmt.Sprintf("%0.5f", price)
|
||||
case price > 0.001:
|
||||
return mFmt.Sprintf("%0.6f", price)
|
||||
case price > 0.0001:
|
||||
return mFmt.Sprintf("%0.7f", price)
|
||||
default:
|
||||
return mFmt.Sprintf("%0.8f", price)
|
||||
}
|
||||
}
|
||||
|
||||
func RenderOnPriceMessage(symbol string, spotPrice float64, futurePrice float64, fundrate float64, fundtime int64) string {
|
||||
return fmt.Sprintf(
|
||||
"%s: %s\n"+
|
||||
"Fund rate: %.4f%% %s in %s\n"+
|
||||
"Spot: %s",
|
||||
RenderSymbolInfo(symbol), RenderPrice(futurePrice),
|
||||
fundrate*100, IconOfFundingFeeDirection(fundrate), timex.CdMinuteStringTime(time.UnixMilli(fundtime)),
|
||||
RenderPrice(spotPrice),
|
||||
)
|
||||
}
|
||||
|
||||
func RenderOnGetTopPricesMessage(symbols []string, price []float64, fundRate []float64) string {
|
||||
t := helper.NewNoBorderTableWriter("", "Price", "Rate")
|
||||
mFmt := message.NewPrinter(language.AmericanEnglish)
|
||||
for i, symbol := range symbols {
|
||||
t.AppendRow(table.Row{
|
||||
binancex.Symbol2Token(symbol),
|
||||
mFmt.Sprintf("%.2f", price[i]),
|
||||
mFmt.Sprintf("%.3f%%", fundRate[i]*100),
|
||||
})
|
||||
}
|
||||
return t.Render()
|
||||
}
|
||||
11
internal/services/tele/view/symbol.go
Normal file
11
internal/services/tele/view/symbol.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"me.thuanle/bbot/internal/helper/binancex"
|
||||
)
|
||||
|
||||
func RenderSymbolInfo(sym string) string {
|
||||
token := binancex.Symbol2Token(sym)
|
||||
return fmt.Sprintf("<a href=\"https://www.binance.com/futures/%s\">%s</a>", sym, token)
|
||||
}
|
||||
Reference in New Issue
Block a user