29 Commits

Author SHA1 Message Date
thuanle 391169761a Merge pull request 'perf: parallelize token source price requests' (#20) from feat/parallel-price-lookup-v2 into feat/token-message-rich
Reviewed-on: #20
2026-04-26 19:15:53 +07:00
thuanle a54ec0dbb7 Merge branch 'feat/token-message-rich' into feat/parallel-price-lookup-v2 2026-04-26 19:00:45 +07:00
thuanle a4644f5982 Merge branch 'main' into feat/token-message-rich 2026-04-26 18:39:53 +07:00
thuanle a36eb7aad0 perf: run token price source lookups in parallel
Fetch alpha, futures, and spot sources concurrently in token lookup flow to reduce end-to-end response latency while preserving independent source behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 18:26:33 +07:00
thuanle 82b0f2f1a8 feat: fetch alpha price by symbol ticker
Keep alpha token existence from list cache and fetch live alpha price via symbol ticker endpoint for richer token response accuracy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 18:22:48 +07:00
thuanle 6e8a2e8f68 fix: decouple token spot/future/alpha lookups
Collect token market data independently for spot, futures, and alpha so any available source can be rendered even if others fail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 18:13:13 +07:00
thuanle 194a235065 docs: document required environment variables
Build Docker Image / build (amd64) (push) Successful in 59s
Expand README with env setup, required TELEGRAM_TOKEN,
optional LOG_ENV and ADMIN_CHAT_ID, plus run notes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 17:50:41 +07:00
thuanle c841ca4421 Merge pull request 'Add rich unified token price message' (#17) from feat/token-message-rich into main
Build Docker Image / build (amd64) (push) Successful in 1m2s
2026-04-26 17:41:17 +07:00
thuanle 95e9217ef4 fix: render basis with explicit sign and absolute delta
Format basis row as +/-$abs(delta) to match the spec and avoid
awkward %+s formatting behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 17:39:57 +07:00
thuanle 88de029e14 feat: unify token message composition across sources
Remove alpha-first early return and build one rich message from all
available sources (spot/future/alpha) with conditional rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 17:19:59 +07:00
thuanle 2f875032f6 feat: add rich token message renderer with conditional rows
Introduce unified rich token message rendering with fixed row order
and conditional visibility based on available sources.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 17:18:34 +07:00
thuanle b376eba674 Ignore local worktree directory
Add .worktrees to gitignore to prevent worktree contents from being
tracked in the repository.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 17:14:52 +07:00
thuanle e565503f24 Add rich token message design spec
Document unified Telegram token message format that includes
all available sources (spot/future/alpha) with conditional rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 17:09:23 +07:00
thuanle 77036f2211 Merge pull request 'Update module github.com/jedib0t/go-pretty/v6 to v6.7.10' (#14) from renovate/github.com-jedib0t-go-pretty-v6-6.x into main
Build Docker Image / build (amd64) (push) Successful in 1m11s
2026-04-26 16:52:45 +07:00
renovatebot d13c76a93d Update module github.com/jedib0t/go-pretty/v6 to v6.7.10 2026-04-26 16:52:29 +07:00
thuanle cdd5f8d4af Merge pull request 'Update module github.com/go-resty/resty/v2 to v2.17.2' (#13) from renovate/github.com-go-resty-resty-v2-2.x into main
Build Docker Image / build (amd64) (push) Successful in 2m16s
2026-04-26 16:51:22 +07:00
renovatebot 93b6c42842 Update module github.com/go-resty/resty/v2 to v2.17.2 2026-04-26 16:49:28 +07:00
thuanle ee6c181cd6 Merge pull request 'Remove ETH special portfolio tracking' (#16) from chore/remove-eth-special-case into main
Build Docker Image / build (amd64) (push) Successful in 1m0s
Reviewed-on: #16
2026-04-26 16:45:36 +07:00
thuanle 4cab108549 Merge branch 'main' into chore/remove-eth-special-case 2026-04-26 16:45:27 +07:00
thuanle 733cf48e41 Merge pull request 'Rewrite price lookup from WebSocket to REST API' (#15) from feat/rest-price-lookup into main
Build Docker Image / build (amd64) (push) Successful in 1m37s
Reviewed-on: #15
2026-04-26 16:19:28 +07:00
thuanle 914beea5ce Address PR #15 round 2: fail-closed admin guard, sync init, error reporting
1. /refresh now fail-closed: rejects all if ADMIN_CHAT_ID unset or invalid
2. Initial pair cache fill is synchronous — bot waits before accepting queries
3. /refresh reports failure when API fetch fails instead of always saying success

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 16:03:22 +07:00
thuanle c7128ff516 Address PR #15 review: batch API calls and admin-guard /refresh
1. Add GetAllPremiumIndex() to fetch all futures data in one call,
   used by GetTopPrices instead of per-symbol sequential calls.
2. Add ADMIN_CHAT_ID env check to /refresh command to restrict
   access to authorized users only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 15:40:00 +07:00
thuanle 59da49c17f Remove ETH special portfolio tracking
Treat ETH like any other token — display only spot/future price
and funding rate, removing personal portfolio calculations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 15:14:39 +07:00
thuanle 9c39423315 Rewrite price lookup from WebSocket to REST API
Replace unreliable WebSocket connections with on-demand REST API calls
for spot and futures prices. Add cached trading pair list (refreshed
hourly) for symbol validation, and /refresh command for manual updates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 15:14:09 +07:00
thuanle c5c1cdf0d5 Add hourly Alpha token cache refresh
Build Docker Image / build (amd64) (push) Successful in 58s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 01:58:50 +07:00
thuanle ced55e5f16 Add dedicated Alpha token price display with 24h change
Build Docker Image / build (amd64) (push) Successful in 1m31s
Alpha tokens now show a simplified format with price and 24h change
instead of the regular token format with empty futures/funding data.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 01:51:56 +07:00
thuanle a5ff76fc96 Update dependencies: go-binance v2.8.11, lo v1.53.0, x/text v0.36.0, telebot v3.3.8
Build Docker Image / build (amd64) (push) Successful in 5m21s
Also update Dockerfile to golang:1.26 and go directive to 1.25.0.
Supersedes all pending renovate PR branches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 01:27:12 +07:00
thuanle 87e69ec8d0 Remove Alpha token price caching in spotPrice map
Build Docker Image / build (amd64) (push) Successful in 2m21s
Alpha token prices are now looked up directly from Alpha data instead
of being cached into spotPrice. Remove unused ensureAlphaCacheLoaded
and shouldRefreshAlphaCache functions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 01:14:40 +07:00
thuanle 440fa3bacd Fix concurrent map read/write race condition in MarketData
Add sync.RWMutex to protect future and spot price maps accessed by
WebSocket handlers (writers) and Telegram bot handlers (readers).
Also adds Alpha token support with caching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:15:22 +07:00
21 changed files with 1227 additions and 179 deletions
+1
View File
@@ -1 +1,2 @@
/.env
/.worktrees/
+1 -1
View File
@@ -1,5 +1,5 @@
# DEB
FROM golang:1.22 AS deps
FROM golang:1.26 AS deps
WORKDIR /app
+30
View File
@@ -1 +1,31 @@
# crypto price bot
Telegram bot tra giá crypto (Spot / Future / Alpha), funding rate và margin APR.
## Environment variables
Tạo file `.env` ở root project với các biến sau:
```env
# BẮT BUỘC: token bot Telegram
TELEGRAM_TOKEN=your_telegram_bot_token
# TÙY CHỌN: bật/tắt log env mode (mặc định true nếu không set)
# giá trị hợp lệ: true / false
LOG_ENV=true
# TÙY CHỌN nhưng KHUYẾN NGHỊ: Telegram user id được phép dùng /refresh
# nếu không set hoặc set sai format -> /refresh sẽ bị từ chối cho tất cả user
ADMIN_CHAT_ID=123456789
```
## Run
```bash
go run ./cmd/tele
```
## Notes
- `TELEGRAM_TOKEN` sai/thiếu: bot sẽ fail khi khởi tạo Telegram service.
- `/refresh` dùng để refresh trading pair cache thủ công và chỉ cho `ADMIN_CHAT_ID`.
@@ -0,0 +1,130 @@
# Token message rich design
## Context
Current token message flow returns early for Alpha tokens, so users cannot see Spot/Future together with Alpha when multiple sources are available. Message content is also rigid and less informative for partial-source cases.
Goal: redesign Telegram token message so each available price source is shown in one rich message, with stable ordering and conditional rows.
## Scope
Applies to token query message rendering in Telegram bot (`OnTokenInfoByToken` flow and price view formatting).
Out of scope:
- changing market data fetch logic
- changing command routing
- trend arrows / historical diff
## Requirements
1. Rich, multi-line Telegram message format.
2. Spot is primary source when present.
3. Show all available sources in a single message: Spot, Future, Alpha.
4. Hide rows for missing data.
5. Show Basis only when both Spot and Future exist.
6. Show Alpha 24h when Alpha data exists.
7. No trend arrows.
8. Preserve current behavior of no reply when no Spot/Future/Alpha data exists.
## Message format
Fixed row order (hide missing rows):
1. `🪙 TOKEN`
2. `💵 Spot: ...` (primary if exists)
3. `📈 Future: ...`
4. `🅰️ Alpha: ...`
5. `🧭 Basis: Future-Spot` (only if Spot+Future)
6. `💸 Funding: ... in ...` (if Future exists)
7. `🏦 Margin: ... APR` (if margin exists)
8. `📊 Alpha 24h: ...` (if Alpha change exists)
## Fallback rules
- **All three available**: render all rows above (except any missing optional fields).
- **Spot + Future**: render Spot, Future, Basis, Funding, Margin.
- **Alpha only**: render Alpha and Alpha 24h.
- **Future only**: render Future, Funding, Margin.
- **Spot only**: render Spot, Margin.
- **No Spot/Future/Alpha**: no reply.
## Data flow behavior
1. Resolve token to candidate futures symbol(s) using existing resolver.
2. Collect available values from Spot/Future/Alpha/margin sources.
3. Build one unified message model.
4. Render rows in fixed order; each row is conditional on availability.
5. Send single Telegram message.
Important behavioral change: remove alpha-first early-return behavior; Alpha becomes a peer data source in unified rendering.
## Formatting notes
- Reuse existing numeric formatter (`RenderPrice`) for price values.
- Keep current funding icon and countdown style.
- Basis format:
- absolute delta: `Future - Spot`
- percent delta: `(Future-Spot)/Spot*100`
- sign preserved (`+`/`-`)
## Example outputs
### A) Spot + Future + Alpha
```text
🪙 ETH
💵 Spot: $3,245
📈 Future: $3,251
🅰️ Alpha: $3,248
🧭 Basis: +$6 (+0.18%)
💸 Funding: +0.0123% 🟢 in 05m
🏦 Margin: 7.665% APR
📊 Alpha 24h: 🔴 -3.21%
```
### B) Spot + Future
```text
🪙 SOL
💵 Spot: $182.4
📈 Future: $183.0
🧭 Basis: +$0.6 (+0.33%)
💸 Funding: -0.0041% 🔴 in 42m
🏦 Margin: 5.110% APR
```
### C) Alpha only
```text
🪙 ABC
🅰️ Alpha: $0.1234
📊 Alpha 24h: 🟢 +12.40%
```
### D) Future only
```text
🪙 TOKEN
📈 Future: $1,234
💸 Funding: +0.0081% 🟢 in 17m
🏦 Margin: 6.200% APR
```
### E) Spot only
```text
🪙 TOKEN
💵 Spot: $1,228
🏦 Margin: 6.200% APR
```
## Verification checklist
- Token with all 3 sources -> all expected rows appear, ordered correctly.
- Token with Spot+Future but no Alpha -> no Alpha rows.
- Token with Alpha only -> no Spot/Future/Basis/Funding rows.
- Token with Future only -> no Spot/Basis/Alpha rows.
- Token with Spot only -> no Future/Basis/Funding/Alpha rows.
- Token with no data -> no reply.
- Basis not shown unless both Spot and Future exist.
- No trend arrows anywhere.
+14 -15
View File
@@ -1,29 +1,28 @@
module me.thuanle/bbot
go 1.22
go 1.25.0
require (
github.com/adshao/go-binance/v2 v2.8.5
github.com/go-resty/resty/v2 v2.16.5
github.com/jedib0t/go-pretty/v6 v6.5.5
github.com/adshao/go-binance/v2 v2.8.11
github.com/go-resty/resty/v2 v2.17.2
github.com/jedib0t/go-pretty/v6 v6.7.10
github.com/joho/godotenv v1.5.1
github.com/rs/zerolog v1.32.0
github.com/samber/lo v1.53.0
golang.org/x/text v0.16.0
gopkg.in/telebot.v3 v3.2.1
golang.org/x/text v0.36.0
gopkg.in/telebot.v3 v3.3.8
)
require (
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
)
+32 -30
View File
@@ -58,8 +58,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/adshao/go-binance/v2 v2.5.0 h1:mk8ylSjIzDYVBF9Wf2KXu6GWD/Ws4LLzD9q2R2mqZB0=
github.com/adshao/go-binance/v2 v2.5.0/go.mod h1:41Up2dG4NfMXpCldrDPETEtiOq+pHoGsFZ73xGgaumo=
github.com/adshao/go-binance/v2 v2.8.11 h1:uE9bERWjrUMykankotJrEdSmweYL65Zv/auQHzutEMM=
github.com/adshao/go-binance/v2 v2.8.11/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -135,8 +135,8 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -155,6 +155,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -212,6 +213,8 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -220,8 +223,8 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
@@ -254,16 +257,16 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jedib0t/go-pretty/v6 v6.5.5 h1:PpIU8lOjxvVYGGKule0QxxJfNysUSbC9lggQU2cpZJc=
github.com/jedib0t/go-pretty/v6 v6.5.5/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg=
github.com/jedib0t/go-pretty/v6 v6.7.10 h1:B/2qW2Bkv2L6n14PP8o1kx75kWzHOQ3YTluWzg9icac=
github.com/jedib0t/go-pretty/v6 v6.7.10/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -303,8 +306,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
@@ -316,11 +319,9 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@@ -354,8 +355,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
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/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
@@ -365,9 +367,11 @@ github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@@ -391,8 +395,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -435,8 +439,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -509,8 +511,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -624,8 +626,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -637,13 +639,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -880,8 +882,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs=
gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko=
gopkg.in/telebot.v3 v3.3.8 h1:uVDGjak9l824FN9YARWUHMsiNZnlohAVwUycw21k6t8=
gopkg.in/telebot.v3 v3.3.8/go.mod h1:1mlbqcLTVSfK9dx7fdp+Nb5HZsy4LLPtpZTKmwhwtzM=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+1
View File
@@ -4,4 +4,5 @@ const (
LogEnv = "LOG_ENV"
TelegramToken = "TELEGRAM_TOKEN"
AdminChatID = "ADMIN_CHAT_ID"
)
+13
View File
@@ -1,9 +1,22 @@
package data
import "me.thuanle/bbot/internal/data/market"
type IMarket interface {
GetFuturePrice(symbol string) (float64, float64, int64, bool)
GetAllPremiumIndex() (map[string]market.PremiumIndex, error)
GetAllFundRate() (map[string]float64, map[string]int64)
GetSpotPrice(symbol string) (float64, bool)
GetMarginInterestRates() map[string]float64
// Alpha token methods
IsAlphaToken(symbol string) bool
GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool)
GetAlphaPrice(symbol string) (float64, bool)
// Trading pair methods
IsSpotPair(symbol string) bool
IsFuturesPair(symbol string) bool
RefreshTradingPairCache() error
}
+198
View File
@@ -0,0 +1,198 @@
package market
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"github.com/rs/zerolog/log"
)
// AlphaTokenInfo represents the response from Binance Alpha token API
type AlphaTokenInfo struct {
TokenID string `json:"tokenId"`
ChainID string `json:"chainId"`
ChainIconURL string `json:"chainIconUrl"`
ChainName string `json:"chainName"`
ContractAddress string `json:"contractAddress"`
Name string `json:"name"`
Symbol string `json:"symbol"`
IconURL string `json:"iconUrl"`
Price string `json:"price"`
PercentChange24h string `json:"percentChange24h"`
Volume24h string `json:"volume24h"`
MarketCap string `json:"marketCap"`
FDV string `json:"fdv"`
Liquidity string `json:"liquidity"`
TotalSupply string `json:"totalSupply"`
CirculatingSupply string `json:"circulatingSupply"`
Holders string `json:"holders"`
Decimals int `json:"decimals"`
ListingCex bool `json:"listingCex"`
HotTag bool `json:"hotTag"`
CexCoinName string `json:"cexCoinName"`
CanTransfer bool `json:"canTransfer"`
Denomination int `json:"denomination"`
Offline bool `json:"offline"`
TradeDecimal int `json:"tradeDecimal"`
AlphaID string `json:"alphaId"`
Offsell bool `json:"offsell"`
PriceHigh24h string `json:"priceHigh24h"`
PriceLow24h string `json:"priceLow24h"`
Count24h string `json:"count24h"`
OnlineTge bool `json:"onlineTge"`
OnlineAirdrop bool `json:"onlineAirdrop"`
Score int `json:"score"`
CexOffDisplay bool `json:"cexOffDisplay"`
StockState bool `json:"stockState"`
ListingTime int64 `json:"listingTime"`
MulPoint int `json:"mulPoint"`
BnExclusiveState bool `json:"bnExclusiveState"`
}
// AlphaTokenResponse represents the API response structure
type AlphaTokenResponse struct {
Code string `json:"code"`
Message *string `json:"message"`
MessageDetail *string `json:"messageDetail"`
Data []AlphaTokenInfo `json:"data"`
}
type AlphaTickerData struct {
Price string `json:"price"`
}
type AlphaTickerResponse struct {
Code string `json:"code"`
Message *string `json:"message"`
MessageDetail *string `json:"messageDetail"`
Data AlphaTickerData `json:"data"`
}
// GetPrice returns the price as float64
func (a *AlphaTokenInfo) GetPrice() float64 {
price, err := strconv.ParseFloat(a.Price, 64)
if err != nil {
log.Error().Err(err).Str("symbol", a.Symbol).Msg("Failed to parse Alpha token price")
return 0
}
return price
}
// GetPercentChange24h returns the 24h percentage change as float64
func (a *AlphaTokenInfo) GetPercentChange24h() float64 {
change, err := strconv.ParseFloat(a.PercentChange24h, 64)
if err != nil {
log.Error().Err(err).Str("symbol", a.Symbol).Msg("Failed to parse Alpha token 24h change")
return 0
}
return change
}
// fetchAlphaTokens fetches Alpha tokens from Binance API
func (ms *MarketData) fetchAlphaTokens() ([]AlphaTokenInfo, error) {
const alphaTokenURL = "https://www.binance.com/bapi/defi/v1/public/wallet-direct/buw/wallet/cex/alpha/all/token/list"
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(alphaTokenURL)
if err != nil {
return nil, fmt.Errorf("failed to fetch Alpha tokens: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Alpha token API returned status: %d", resp.StatusCode)
}
var tokenResponse AlphaTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
return nil, fmt.Errorf("failed to decode Alpha token response: %w", err)
}
if tokenResponse.Code != "000000" {
return nil, fmt.Errorf("Alpha token API returned error code: %s", tokenResponse.Code)
}
return tokenResponse.Data, nil
}
// refreshAlphaTokenCache refreshes the Alpha token cache
func (ms *MarketData) refreshAlphaTokenCache() {
ms.alphaCacheMutex.Lock()
defer ms.alphaCacheMutex.Unlock()
log.Info().Msg("Refreshing Alpha token cache")
tokens, err := ms.fetchAlphaTokens()
if err != nil {
log.Error().Err(err).Msg("Failed to refresh Alpha token cache")
return
}
// Clear existing cache
ms.alphaTokens = make(map[string]AlphaTokenInfo)
// Populate cache with new data
for _, token := range tokens {
symbol := token.Symbol // Already uppercase from API
ms.alphaTokens[symbol] = token
}
ms.lastAlphaCacheUpdate = time.Now()
log.Info().Int("count", len(tokens)).Msg("Alpha token cache refreshed successfully")
}
// GetAlphaToken returns Alpha token info by symbol
func (ms *MarketData) GetAlphaToken(symbol string) (AlphaTokenInfo, bool) {
ms.alphaCacheMutex.RLock()
defer ms.alphaCacheMutex.RUnlock()
token, exists := ms.alphaTokens[symbol]
return token, exists
}
// IsAlphaToken checks if a symbol is an Alpha token
func (ms *MarketData) IsAlphaToken(symbol string) bool {
_, exists := ms.GetAlphaToken(symbol)
return exists
}
func (ms *MarketData) GetAlphaPrice(symbol string) (float64, bool) {
const alphaTickerURL = "https://www.binance.com/bapi/defi/v1/public/alpha-trade/ticker"
client := &http.Client{Timeout: 5 * time.Second}
endpoint := alphaTickerURL + "?" + url.Values{"symbol": []string{symbol}}.Encode()
resp, err := client.Get(endpoint)
if err != nil {
log.Error().Err(err).Str("symbol", symbol).Msg("Failed to fetch Alpha ticker")
return 0, false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, false
}
var tickerResp AlphaTickerResponse
if err := json.NewDecoder(resp.Body).Decode(&tickerResp); err != nil {
return 0, false
}
if tickerResp.Code != "000000" {
return 0, false
}
price, err := strconv.ParseFloat(tickerResp.Data.Price, 64)
if err != nil || price <= 0 {
return 0, false
}
return price, true
}
+61 -26
View File
@@ -1,50 +1,85 @@
package market
import (
"github.com/adshao/go-binance/v2/futures"
"github.com/rs/zerolog/log"
"context"
"strconv"
"time"
"github.com/rs/zerolog/log"
)
func (ms *MarketData) GetFuturePrice(symbol string) (float64, float64, int64, bool) {
p, ok := ms.futureMarkPrice[symbol]
if !ok {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
premiums, err := ms.futuresClient.NewPremiumIndexService().Symbol(symbol).Do(ctx)
if err != nil {
log.Error().Err(err).Str("symbol", symbol).Msg("Failed to fetch futures premium index")
return 0, 0, 0, false
}
return p, ms.futureFundingRate[symbol], ms.futureNextFundingTime[symbol], true
if len(premiums) == 0 {
return 0, 0, 0, false
}
func (ms *MarketData) StartFutureWsMarkPrice() error {
_, _, err := futures.WsAllMarkPriceServe(ms.futureWsMarkPriceHandler, ms.futureWsErrHandler)
p := premiums[0]
markPrice, err := strconv.ParseFloat(p.MarkPrice, 64)
if err != nil {
return err
}
return nil
return 0, 0, 0, false
}
func (ms *MarketData) futureWsMarkPriceHandler(event futures.WsAllMarkPriceEvent) {
for _, priceEvent := range event {
price, err := strconv.ParseFloat(priceEvent.MarkPrice, 64)
fundingRate, err := strconv.ParseFloat(p.LastFundingRate, 64)
if err != nil {
fundingRate = 0
}
return markPrice, fundingRate, p.NextFundingTime, true
}
type PremiumIndex struct {
MarkPrice float64
FundingRate float64
NextFundingTime int64
}
func (ms *MarketData) GetAllPremiumIndex() (map[string]PremiumIndex, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
premiums, err := ms.futuresClient.NewPremiumIndexService().Do(ctx)
if err != nil {
log.Error().Err(err).Msg("Failed to fetch all futures premium index")
return nil, err
}
result := make(map[string]PremiumIndex, len(premiums))
for _, p := range premiums {
markPrice, err := strconv.ParseFloat(p.MarkPrice, 64)
if err != nil {
continue
}
fundingRate, err := strconv.ParseFloat(priceEvent.FundingRate, 64)
if err != nil {
continue
}
ms.futureMarkPrice[priceEvent.Symbol] = price
ms.futureFundingRate[priceEvent.Symbol] = fundingRate
ms.futureNextFundingTime[priceEvent.Symbol] = priceEvent.NextFundingTime
rate, _ := strconv.ParseFloat(p.LastFundingRate, 64)
result[p.Symbol] = PremiumIndex{
MarkPrice: markPrice,
FundingRate: rate,
NextFundingTime: p.NextFundingTime,
}
}
func (ms *MarketData) futureWsErrHandler(err error) {
log.Debug().Err(err).Msg("Ws Error. Restart socket")
_ = ms.StartFutureWsMarkPrice()
return result, nil
}
func (ms *MarketData) GetAllFundRate() (map[string]float64, map[string]int64) {
return ms.futureFundingRate, ms.futureNextFundingTime
all, err := ms.GetAllPremiumIndex()
if err != nil {
return make(map[string]float64), make(map[string]int64)
}
rates := make(map[string]float64, len(all))
times := make(map[string]int64, len(all))
for sym, p := range all {
rates[sym] = p.FundingRate
times[sym] = p.NextFundingTime
}
return rates, times
}
+42 -12
View File
@@ -1,25 +1,55 @@
package market
import "github.com/rs/zerolog/log"
import (
"sync"
"time"
"github.com/adshao/go-binance/v2"
"github.com/adshao/go-binance/v2/futures"
"github.com/rs/zerolog/log"
)
type MarketData struct {
futureMarkPrice map[string]float64
futureFundingRate map[string]float64
futureNextFundingTime map[string]int64
// Trading pair caches
spotPairs map[string]bool
futuresPairs map[string]bool
pairCacheMutex sync.RWMutex
lastPairCacheUpdate time.Time
spotPrice map[string]float64
// Alpha token cache
alphaTokens map[string]AlphaTokenInfo
alphaCacheMutex sync.RWMutex
lastAlphaCacheUpdate time.Time
// Binance REST clients
spotClient *binance.Client
futuresClient *futures.Client
}
func NewMarketData() *MarketData {
log.Info().Msg("Start market service")
ms := &MarketData{
futureMarkPrice: make(map[string]float64),
futureFundingRate: make(map[string]float64),
futureNextFundingTime: make(map[string]int64),
spotPrice: make(map[string]float64),
spotPairs: make(map[string]bool),
futuresPairs: make(map[string]bool),
alphaTokens: make(map[string]AlphaTokenInfo),
spotClient: binance.NewClient("", ""),
futuresClient: futures.NewClient("", ""),
}
_ = ms.StartFutureWsMarkPrice()
_ = ms.StartSpotWsMarkPrice()
if err := ms.refreshTradingPairCache(); err != nil {
log.Error().Err(err).Msg("Failed initial trading pair cache load")
}
go ms.pairCacheRefreshLoop()
go ms.alphaCacheRefreshLoop()
return ms
}
func (ms *MarketData) alphaCacheRefreshLoop() {
ms.refreshAlphaTokenCache()
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for range ticker.C {
ms.refreshAlphaTokenCache()
}
}
+28 -22
View File
@@ -1,36 +1,42 @@
package market
import (
"github.com/adshao/go-binance/v2"
"github.com/rs/zerolog/log"
"context"
"strconv"
"time"
"github.com/rs/zerolog/log"
)
func (ms *MarketData) GetSpotPrice(symbol string) (float64, bool) {
p, ok := ms.spotPrice[symbol]
return p, ok
}
func (ms *MarketData) StartSpotWsMarkPrice() error {
_, _, err := binance.WsAllMarketsStatServe(ms.spotWsAllMarketsStatHandler, ms.spotWsErrHandler) //.WsAllMarkPriceServe(ms.futureWsMarkPriceHandler, ms.futureWsErrHandler)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
prices, err := ms.spotClient.NewListPricesService().Symbol(symbol).Do(ctx)
if err != nil {
return err
}
return nil
log.Error().Err(err).Str("symbol", symbol).Msg("Failed to fetch spot price")
return ms.getAlphaPrice(symbol)
}
func (ms *MarketData) spotWsAllMarketsStatHandler(event binance.WsAllMarketsStatEvent) {
for _, priceEvent := range event {
price, err := strconv.ParseFloat(priceEvent.LastPrice, 64)
if err != nil {
continue
}
ms.spotPrice[priceEvent.Symbol] = price
}
if len(prices) == 0 {
return ms.getAlphaPrice(symbol)
}
func (ms *MarketData) spotWsErrHandler(err error) {
log.Debug().Err(err).Msg("Spot Ws Error. Restart socket")
_ = ms.StartSpotWsMarkPrice()
price, err := strconv.ParseFloat(prices[0].Price, 64)
if err != nil || price == 0 {
return ms.getAlphaPrice(symbol)
}
return price, true
}
func (ms *MarketData) getAlphaPrice(symbol string) (float64, bool) {
if ms.IsAlphaToken(symbol) {
if alphaToken, exists := ms.GetAlphaToken(symbol); exists {
if price := alphaToken.GetPrice(); price > 0 {
return price, true
}
}
}
return 0, false
}
+74
View File
@@ -0,0 +1,74 @@
package market
import (
"context"
"time"
"github.com/rs/zerolog/log"
)
func (ms *MarketData) refreshTradingPairCache() error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
spotInfo, err := ms.spotClient.NewExchangeInfoService().Do(ctx)
if err != nil {
log.Error().Err(err).Msg("Failed to fetch spot exchange info")
return err
}
futuresInfo, err := ms.futuresClient.NewExchangeInfoService().Do(ctx)
if err != nil {
log.Error().Err(err).Msg("Failed to fetch futures exchange info")
return err
}
ms.pairCacheMutex.Lock()
defer ms.pairCacheMutex.Unlock()
ms.spotPairs = make(map[string]bool, len(spotInfo.Symbols))
for _, s := range spotInfo.Symbols {
if s.Status == "TRADING" {
ms.spotPairs[s.Symbol] = true
}
}
ms.futuresPairs = make(map[string]bool, len(futuresInfo.Symbols))
for _, s := range futuresInfo.Symbols {
if s.Status == "TRADING" {
ms.futuresPairs[s.Symbol] = true
}
}
ms.lastPairCacheUpdate = time.Now()
log.Info().
Int("spot", len(ms.spotPairs)).
Int("futures", len(ms.futuresPairs)).
Msg("Trading pair cache refreshed")
return nil
}
func (ms *MarketData) pairCacheRefreshLoop() {
ms.refreshTradingPairCache()
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for range ticker.C {
ms.refreshTradingPairCache()
}
}
func (ms *MarketData) IsSpotPair(symbol string) bool {
ms.pairCacheMutex.RLock()
defer ms.pairCacheMutex.RUnlock()
return ms.spotPairs[symbol]
}
func (ms *MarketData) IsFuturesPair(symbol string) bool {
ms.pairCacheMutex.RLock()
defer ms.pairCacheMutex.RUnlock()
return ms.futuresPairs[symbol]
}
func (ms *MarketData) RefreshTradingPairCache() error {
return ms.refreshTradingPairCache()
}
+11 -4
View File
@@ -1,10 +1,11 @@
package binancex
import (
"strings"
"me.thuanle/bbot/internal/configs/binance"
"me.thuanle/bbot/internal/data"
"me.thuanle/bbot/internal/utils/stringx"
"strings"
)
func Symbol2Token(sym string) string {
@@ -28,8 +29,7 @@ var (
)
func testSym(sym string) bool {
_, _, _, test := data.Market.GetFuturePrice(sym)
return test
return data.Market.IsFuturesPair(sym)
}
func Token2Symbols(token string) []string {
@@ -74,7 +74,14 @@ func Token2Symbols(token string) []string {
}
func IsToken(s string) bool {
return len(Token2Symbols(s)) > 0
// First check regular symbols
if len(Token2Symbols(s)) > 0 {
return true
}
// Then check Alpha tokens
s = strings.ToUpper(s)
return data.Market.IsAlphaToken(s)
}
// Future2SpotSymbol convert future symbol to spot symbol
+8 -4
View File
@@ -13,15 +13,19 @@ func GetTopPrices() ([]string, []float64, []float64) {
topPrice := make([]float64, n)
topRate := make([]float64, n)
all, err := data.Market.GetAllPremiumIndex()
if err != nil {
return topSym, topPrice, topRate
}
for i, sym := range strategy.TopPriceSymbols {
price, rate, _, ok := data.Market.GetFuturePrice(sym)
p, ok := all[sym]
if !ok {
continue
}
topSym[i] = sym
topPrice[i] = price
topRate[i] = rate
topPrice[i] = p.MarkPrice
topRate[i] = p.FundingRate
}
return topSym, topPrice, topRate
}
+5
View File
@@ -16,6 +16,10 @@ var commandList = []telebot.Command{
Text: "fee",
Description: "(f) - show top funding fee",
},
{
Text: "refresh",
Description: "Refresh trading pair cache",
},
}
func setupCommands(b *telebot.Bot) error {
@@ -36,6 +40,7 @@ func setupCommands(b *telebot.Bot) error {
//info
b.Handle("/p", commands.OnGetTopPrices)
b.Handle("/fee", commands.OnGetTopFundingFee)
b.Handle("/refresh", commands.OnRefreshPairCache)
//any text
b.Handle(telebot.OnText, commands.OnChatHandler)
+16
View File
@@ -1,7 +1,12 @@
package commands
import (
"os"
"strconv"
"gopkg.in/telebot.v3"
"me.thuanle/bbot/internal/configs/key"
"me.thuanle/bbot/internal/data"
"me.thuanle/bbot/internal/services/controllers"
"me.thuanle/bbot/internal/services/tele/chat"
"me.thuanle/bbot/internal/services/tele/view"
@@ -16,3 +21,14 @@ func OnGetTopFundingFee(context telebot.Context) error {
fee, float64s, cds := controllers.GetTopFundingFee()
return chat.ReplyMessagePre(context, view.RenderOnGetTopFundingFeeMessage(fee, float64s, cds))
}
func OnRefreshPairCache(context telebot.Context) error {
adminID, err := strconv.ParseInt(os.Getenv(key.AdminChatID), 10, 64)
if err != nil || adminID == 0 || context.Sender().ID != adminID {
return nil
}
if err := data.Market.RefreshTradingPairCache(); err != nil {
return chat.ReplyMessage(context, "Failed to refresh trading pair cache")
}
return chat.ReplyMessage(context, "Trading pair cache refreshed")
}
+107 -62
View File
@@ -1,18 +1,54 @@
package commands
import (
"golang.org/x/text/language"
"golang.org/x/text/message"
"strings"
"sync"
"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"
)
var lastEthPrice float64
type buildRichTokenMessageArgs struct {
Token string
HasSpot bool
SpotPrice float64
HasFuture bool
FuturePrice float64
FundingRate float64
FundingTimeMs int64
HasAlpha bool
AlphaPrice float64
HasAlpha24h bool
Alpha24h float64
HasMarginAPR bool
MarginAPRPercent float64
}
func buildRichTokenMessageInput(a buildRichTokenMessageArgs) view.RichTokenMessageInput {
return view.RichTokenMessageInput{
Token: a.Token,
HasSpot: a.HasSpot,
SpotPrice: a.SpotPrice,
HasFuture: a.HasFuture,
FuturePrice: a.FuturePrice,
FundingRate: a.FundingRate,
FundingTimeMs: a.FundingTimeMs,
HasAlpha: a.HasAlpha,
AlphaPrice: a.AlphaPrice,
HasAlpha24h: a.HasAlpha24h,
Alpha24hChange: a.Alpha24h,
HasMarginAPR: a.HasMarginAPR,
MarginAPRPercent: a.MarginAPRPercent,
}
}
func showStickerMode(context telebot.Context, token string) {
token = strings.ToUpper(token)
@@ -26,71 +62,80 @@ func showStickerMode(context telebot.Context, token string) {
}
}
func OnTokenInfoByToken(context telebot.Context, token string) error {
symbols := binancex.Token2Symbols(token)
if len(symbols) == 0 {
return nil
func collectRichTokenData(token string) buildRichTokenMessageArgs {
a := buildRichTokenMessageArgs{Token: token}
marginRates := data.Market.GetMarginInterestRates()
a.MarginAPRPercent = marginRates[token] * 365 * 100
a.HasMarginAPR = marginRates[token] != 0
futureSymbol := token + "USDT"
spotSymbol := token + "USDT"
var (
wg sync.WaitGroup
mu sync.Mutex
)
wg.Add(3)
go func() {
defer wg.Done()
if alphaToken, ok := data.Market.GetAlphaToken(token); ok {
alpha24h := alphaToken.GetPercentChange24h()
alphaPrice := alphaToken.GetPrice()
if p, ok := data.Market.GetAlphaPrice(token + "USDT"); ok {
alphaPrice = p
}
mu.Lock()
a.HasAlpha = true
a.HasAlpha24h = true
a.Alpha24h = alpha24h
a.AlphaPrice = alphaPrice
mu.Unlock()
}
}()
go func() {
defer wg.Done()
if data.Market.IsFuturesPair(futureSymbol) {
if fp, fr, ft, ok := data.Market.GetFuturePrice(futureSymbol); ok {
mu.Lock()
a.HasFuture = true
a.FuturePrice = fp
a.FundingRate = fr
a.FundingTimeMs = ft
mu.Unlock()
}
}
}()
go func() {
defer wg.Done()
if data.Market.IsSpotPair(spotSymbol) {
if sp, ok := data.Market.GetSpotPrice(spotSymbol); ok {
mu.Lock()
a.HasSpot = true
a.SpotPrice = sp
mu.Unlock()
}
}
}()
wg.Wait()
return a
}
func OnTokenInfoByToken(context telebot.Context, token string) error {
token = strings.ToUpper(token)
showStickerMode(context, token)
fp, fundRate, fundTime, ok := data.Market.GetFuturePrice(symbols[0])
marginRates := data.Market.GetMarginInterestRates()
tokenInterestRate := marginRates[strings.ToUpper(token)]
if !ok {
args := collectRichTokenData(token)
if !args.HasSpot && !args.HasFuture && !args.HasAlpha {
return nil
}
sSymbol := binancex.Future2SpotSymbol(symbols[0])
sp, _ := data.Market.GetSpotPrice(sSymbol)
_ = chat.ReplyMessage(context, view.RenderOnPriceMessage(symbols[0], sp, fp, fundRate, fundTime, tokenInterestRate))
if strings.ToUpper(token) == "ETH" {
mFmt := message.NewPrinter(language.AmericanEnglish)
realAmount := 35.
trangBucAmount := 14.
basePrice := 2500.0
baseTotal := realAmount * basePrice
trangBucTotal := trangBucAmount * basePrice
realCurTotal := realAmount * sp
trangBucCurTotal := trangBucAmount * sp
lastDelta := ""
if lastEthPrice == 0 {
lastEthPrice = sp
} else {
lastDelta = mFmt.Sprintf(
"Δ price: $%+.0f\n"+
"Δ Usdt: $%+.0f\n",
sp-lastEthPrice,
(sp-lastEthPrice)*realAmount,
)
lastEthPrice = sp
}
msg := mFmt.Sprintf(
"🎉🎊🎊🦈🦈🦈 @th13vn Real 🦈🦈🦈🎊🎊🎉\n"+
"∑ USDT: $%.0f\n"+
"Lợi nhuận: $%.0f\n"+
"%s\n"+
"\n"+
"🚀🚀🚀🚀🚀 Road to 5k 🚀🚀🚀🚀🚀: \n"+
"- Δ Price: $%0.0f\n"+
"- Δ Vol: $%0.0f\n"+
"\n"+
"💸💸💸💸💸 Trang Bức balance 💸💸💸💸💸\n"+
"∑ USDT: $%.0f\n"+
"Lợi nhuận: $%.0f\n",
realCurTotal,
realCurTotal-baseTotal,
lastDelta,
5000-sp,
5000*realAmount-realCurTotal,
trangBucCurTotal,
trangBucCurTotal-trangBucTotal,
)
msg := view.RenderRichTokenMessage(buildRichTokenMessageInput(args))
_ = chat.ReplyMessage(context, msg)
}
return nil
}
@@ -0,0 +1,230 @@
package commands
import (
"sync"
"testing"
"time"
"me.thuanle/bbot/internal/data"
"me.thuanle/bbot/internal/data/market"
)
func TestBuildRichTokenMessageInput_AllSources(t *testing.T) {
in := buildRichTokenMessageInput(buildRichTokenMessageArgs{
Token: "ETH",
SpotPrice: 3245,
HasSpot: true,
FuturePrice: 3251,
FundingRate: 0.000123,
FundingTimeMs: 1740000000000,
HasFuture: true,
AlphaPrice: 3248,
HasAlpha: true,
Alpha24h: -3.21,
HasAlpha24h: true,
MarginAPRPercent: 7.665,
HasMarginAPR: true,
})
if !in.HasSpot || !in.HasFuture || !in.HasAlpha || !in.HasAlpha24h || !in.HasMarginAPR {
t.Fatalf("expected all source flags true: %+v", in)
}
}
func TestBuildRichTokenMessageInput_OnlyAlpha(t *testing.T) {
in := buildRichTokenMessageInput(buildRichTokenMessageArgs{
Token: "ABC",
HasAlpha: true,
AlphaPrice: 0.1234,
HasAlpha24h: true,
Alpha24h: 12.4,
})
if !in.HasAlpha || !in.HasAlpha24h {
t.Fatalf("expected alpha flags true: %+v", in)
}
if in.HasSpot || in.HasFuture {
t.Fatalf("did not expect spot/future flags true: %+v", in)
}
}
type marketStub struct {
spotPairs map[string]bool
futuresPairs map[string]bool
spotPrices map[string]float64
futurePrices map[string]struct {
price float64
rate float64
time int64
}
alphaTokens map[string]market.AlphaTokenInfo
alphaPrices map[string]float64
marginRates map[string]float64
gate *sync.WaitGroup
ready chan struct{}
done chan struct{}
mu sync.Mutex
starts int
}
func (m *marketStub) waitConcurrentGate() {
if m.gate == nil {
return
}
m.mu.Lock()
m.starts++
m.mu.Unlock()
m.gate.Done()
<-m.ready
}
func (m *marketStub) GetFuturePrice(symbol string) (float64, float64, int64, bool) {
m.waitConcurrentGate()
v, ok := m.futurePrices[symbol]
if !ok {
return 0, 0, 0, false
}
return v.price, v.rate, v.time, true
}
func (m *marketStub) GetAllPremiumIndex() (map[string]market.PremiumIndex, error) { return nil, nil }
func (m *marketStub) GetAllFundRate() (map[string]float64, map[string]int64) { return nil, nil }
func (m *marketStub) GetSpotPrice(symbol string) (float64, bool) {
m.waitConcurrentGate()
v, ok := m.spotPrices[symbol]
return v, ok
}
func (m *marketStub) GetMarginInterestRates() map[string]float64 { return m.marginRates }
func (m *marketStub) IsAlphaToken(symbol string) bool {
_, ok := m.alphaTokens[symbol]
return ok
}
func (m *marketStub) GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool) {
v, ok := m.alphaTokens[symbol]
return v, ok
}
func (m *marketStub) GetAlphaPrice(symbol string) (float64, bool) {
m.waitConcurrentGate()
v, ok := m.alphaPrices[symbol]
return v, ok
}
func (m *marketStub) IsSpotPair(symbol string) bool { return m.spotPairs[symbol] }
func (m *marketStub) IsFuturesPair(symbol string) bool { return m.futuresPairs[symbol] }
func (m *marketStub) RefreshTradingPairCache() error { return nil }
func TestCollectRichTokenData_SpotOnlyReachable(t *testing.T) {
orig := data.Market
defer func() { data.Market = orig }()
data.Market = &marketStub{
spotPairs: map[string]bool{"ABCUSDT": true},
spotPrices: map[string]float64{"ABCUSDT": 1.23},
}
args := collectRichTokenData("ABC")
if !args.HasSpot {
t.Fatalf("expected spot to be reachable for spot-only token: %+v", args)
}
if args.HasFuture || args.HasAlpha {
t.Fatalf("did not expect future/alpha for spot-only token: %+v", args)
}
}
func TestCollectRichTokenData_FutureFailureStillKeepsSpot(t *testing.T) {
orig := data.Market
defer func() { data.Market = orig }()
data.Market = &marketStub{
spotPairs: map[string]bool{"ETHUSDT": true},
futuresPairs: map[string]bool{"ETHUSDT": true},
spotPrices: map[string]float64{"ETHUSDT": 3245},
futurePrices: map[string]struct {
price float64
rate float64
time int64
}{},
}
args := collectRichTokenData("ETH")
if !args.HasSpot {
t.Fatalf("expected spot to remain available when future lookup fails: %+v", args)
}
if args.HasFuture {
t.Fatalf("did not expect future when future lookup fails: %+v", args)
}
}
func TestCollectRichTokenData_AlphaUsesSymbolPrice(t *testing.T) {
orig := data.Market
defer func() { data.Market = orig }()
data.Market = &marketStub{
alphaTokens: map[string]market.AlphaTokenInfo{
"ABC": {Symbol: "ABC", PercentChange24h: "12.4", Price: "0.1"},
},
alphaPrices: map[string]float64{"ABCUSDT": 0.1234},
}
args := collectRichTokenData("ABC")
if !args.HasAlpha {
t.Fatalf("expected alpha to be available: %+v", args)
}
if !args.HasAlpha24h {
t.Fatalf("expected alpha 24h to be available: %+v", args)
}
if args.AlphaPrice != 0.1234 {
t.Fatalf("expected alpha price from symbol lookup, got %.4f", args.AlphaPrice)
}
}
func TestCollectRichTokenData_RunsLookupsConcurrently(t *testing.T) {
orig := data.Market
defer func() { data.Market = orig }()
gate := &sync.WaitGroup{}
gate.Add(3)
st := &marketStub{
spotPairs: map[string]bool{"ABCUSDT": true},
futuresPairs: map[string]bool{"ABCUSDT": true},
spotPrices: map[string]float64{"ABCUSDT": 1.23},
futurePrices: map[string]struct {
price float64
rate float64
time int64
}{"ABCUSDT": {price: 1.24, rate: 0.0001, time: 1740000000000}},
alphaTokens: map[string]market.AlphaTokenInfo{
"ABC": {Symbol: "ABC", PercentChange24h: "2.5", Price: "1.22"},
},
alphaPrices: map[string]float64{"ABCUSDT": 1.22},
gate: gate,
ready: make(chan struct{}),
}
data.Market = st
finished := make(chan buildRichTokenMessageArgs, 1)
go func() {
finished <- collectRichTokenData("ABC")
}()
waitDone := make(chan struct{})
go func() {
gate.Wait()
close(waitDone)
}()
select {
case <-waitDone:
close(st.ready)
case <-time.After(200 * time.Millisecond):
t.Fatalf("expected price lookups to run concurrently")
}
select {
case args := <-finished:
if !args.HasSpot || !args.HasFuture || !args.HasAlpha {
t.Fatalf("expected all lookups present: %+v", args)
}
case <-time.After(200 * time.Millisecond):
t.Fatalf("collectRichTokenData did not finish")
}
}
+86 -1
View File
@@ -2,13 +2,16 @@ package view
import (
"fmt"
"math"
"strings"
"time"
"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 {
@@ -48,6 +51,88 @@ func RenderOnPriceMessage(symbol string, spotPrice float64, futurePrice float64,
)
}
func RenderOnAlphaPriceMessage(symbol string, price float64, change24h float64) string {
icon := "🟢"
if change24h < 0 {
icon = "🔴"
}
return fmt.Sprintf(
"🅰️ %s\n"+
" Price: $%s\n"+
" 24h: %s%.2f%%",
symbol,
RenderPrice(price),
icon, change24h,
)
}
type RichTokenMessageInput struct {
Token string
HasSpot bool
SpotPrice float64
HasFuture bool
FuturePrice float64
FundingRate float64
FundingTimeMs int64
HasAlpha bool
AlphaPrice float64
HasAlpha24h bool
Alpha24hChange float64
HasMarginAPR bool
MarginAPRPercent float64
}
func RenderRichTokenMessage(in RichTokenMessageInput) string {
rows := []string{fmt.Sprintf("🪙 %s", strings.ToUpper(in.Token))}
if in.HasSpot {
rows = append(rows, fmt.Sprintf("💵 Spot: $%s", RenderPrice(in.SpotPrice)))
}
if in.HasFuture {
rows = append(rows, fmt.Sprintf("📈 Future: $%s", RenderPrice(in.FuturePrice)))
}
if in.HasAlpha {
rows = append(rows, fmt.Sprintf("🅰️ Alpha: $%s", RenderPrice(in.AlphaPrice)))
}
if in.HasSpot && in.HasFuture && in.SpotPrice > 0 {
delta := in.FuturePrice - in.SpotPrice
deltaPct := delta / in.SpotPrice * 100
sign := "+"
if delta < 0 {
sign = "-"
}
rows = append(rows, fmt.Sprintf("🧭 Basis: %s$%s (%+.2f%%)", sign, RenderPrice(math.Abs(delta)), deltaPct))
}
if in.HasFuture {
rows = append(rows, fmt.Sprintf("💸 Funding: %.4f%% %s in %s",
in.FundingRate*100,
IconOfFundingFeeDirection(in.FundingRate),
timex.CdMinuteStringTime(time.UnixMilli(in.FundingTimeMs)),
))
}
if in.HasMarginAPR {
rows = append(rows, fmt.Sprintf("🏦 Margin: %.3f%% APR", in.MarginAPRPercent))
}
if in.HasAlpha24h {
icon := "🟢"
if in.Alpha24hChange < 0 {
icon = "🔴"
}
rows = append(rows, fmt.Sprintf("📊 Alpha 24h: %s%+.2f%%", icon, in.Alpha24hChange))
}
return strings.Join(rows, "\n")
}
func RenderOnGetTopPricesMessage(symbols []string, price []float64, fundRate []float64) string {
t := helper.NewNoBorderTableWriter("", "Price", "Fund")
mFmt := message.NewPrinter(language.AmericanEnglish)
+137
View File
@@ -0,0 +1,137 @@
package view
import (
"strings"
"testing"
)
func assertContainsAll(t *testing.T, msg string, rows ...string) {
t.Helper()
for _, r := range rows {
if !strings.Contains(msg, r) {
t.Fatalf("expected row %q in message:\n%s", r, msg)
}
}
}
func assertContainsNone(t *testing.T, msg string, rows ...string) {
t.Helper()
for _, r := range rows {
if strings.Contains(msg, r) {
t.Fatalf("unexpected row %q in message:\n%s", r, msg)
}
}
}
func TestRenderRichTokenMessage_AllSources(t *testing.T) {
msg := RenderRichTokenMessage(RichTokenMessageInput{
Token: "ETH",
HasSpot: true,
SpotPrice: 3245,
HasFuture: true,
FuturePrice: 3251,
FundingRate: 0.000123,
FundingTimeMs: 1740000000000,
HasAlpha: true,
AlphaPrice: 3248,
HasAlpha24h: true,
Alpha24hChange: -3.21,
HasMarginAPR: true,
MarginAPRPercent: 7.665,
})
expectedOrder := []string{
"🪙 ETH",
"💵 Spot:",
"📈 Future:",
"🅰️ Alpha:",
"🧭 Basis:",
"💸 Funding:",
"🏦 Margin:",
"📊 Alpha 24h:",
}
last := -1
for _, part := range expectedOrder {
i := strings.Index(msg, part)
if i == -1 {
t.Fatalf("missing row %q in message:\n%s", part, msg)
}
if i < last {
t.Fatalf("row %q appears out of order in message:\n%s", part, msg)
}
last = i
}
}
func TestRenderRichTokenMessage_FutureOnly(t *testing.T) {
msg := RenderRichTokenMessage(RichTokenMessageInput{
Token: "ABC",
HasFuture: true,
FuturePrice: 1234,
FundingRate: 0.000081,
FundingTimeMs: 1740000000000,
HasMarginAPR: true,
MarginAPRPercent: 6.2,
})
assertContainsAll(t, msg, "🪙 ABC", "📈 Future:", "💸 Funding:", "🏦 Margin:")
assertContainsNone(t, msg, "💵 Spot:", "🅰️ Alpha:", "🧭 Basis:", "📊 Alpha 24h:")
}
func TestRenderRichTokenMessage_SpotOnly(t *testing.T) {
msg := RenderRichTokenMessage(RichTokenMessageInput{
Token: "XRP",
HasSpot: true,
SpotPrice: 2.1,
HasMarginAPR: true,
MarginAPRPercent: 5.11,
})
assertContainsAll(t, msg, "🪙 XRP", "💵 Spot:", "🏦 Margin:")
assertContainsNone(t, msg, "📈 Future:", "🅰️ Alpha:", "🧭 Basis:", "💸 Funding:", "📊 Alpha 24h:")
}
func TestRenderRichTokenMessage_AlphaOnly(t *testing.T) {
msg := RenderRichTokenMessage(RichTokenMessageInput{
Token: "ABC",
HasAlpha: true,
AlphaPrice: 0.1234,
HasAlpha24h: true,
Alpha24hChange: 12.4,
})
assertContainsAll(t, msg, "🪙 ABC", "🅰️ Alpha:", "📊 Alpha 24h:")
assertContainsNone(t, msg, "💵 Spot:", "📈 Future:", "🧭 Basis:", "💸 Funding:", "🏦 Margin:")
}
func TestRenderRichTokenMessage_SpotAndFuture(t *testing.T) {
msg := RenderRichTokenMessage(RichTokenMessageInput{
Token: "SOL",
HasSpot: true,
SpotPrice: 182.4,
HasFuture: true,
FuturePrice: 183.0,
FundingRate: -0.000041,
FundingTimeMs: 1740000000000,
HasMarginAPR: true,
MarginAPRPercent: 5.11,
})
assertContainsAll(t, msg, "🪙 SOL", "💵 Spot:", "📈 Future:", "🧭 Basis: +$0.6000", "💸 Funding:", "🏦 Margin:")
assertContainsNone(t, msg, "🅰️ Alpha:", "📊 Alpha 24h:")
}
func TestRenderRichTokenMessage_SpotAndFutureNegativeBasis(t *testing.T) {
msg := RenderRichTokenMessage(RichTokenMessageInput{
Token: "ADA",
HasSpot: true,
SpotPrice: 1.2,
HasFuture: true,
FuturePrice: 1.1,
FundingRate: 0.000011,
FundingTimeMs: 1740000000000,
})
assertContainsAll(t, msg, "🧭 Basis: -$0.10000 (-8.33%)")
}