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>
This commit is contained in:
@@ -2,13 +2,15 @@ package view
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jedib0t/go-pretty/v6/table"
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"golang.org/x/text/message"
|
"golang.org/x/text/message"
|
||||||
"me.thuanle/bbot/internal/helper/binancex"
|
"me.thuanle/bbot/internal/helper/binancex"
|
||||||
"me.thuanle/bbot/internal/services/tele/view/helper"
|
"me.thuanle/bbot/internal/services/tele/view/helper"
|
||||||
"me.thuanle/bbot/internal/utils/timex"
|
"me.thuanle/bbot/internal/utils/timex"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RenderPrice(price float64) string {
|
func RenderPrice(price float64) string {
|
||||||
@@ -63,6 +65,69 @@ func RenderOnAlphaPriceMessage(symbol string, price float64, change24h float64)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
rows = append(rows, fmt.Sprintf("🧭 Basis: $%+s (%+.2f%%)", RenderPrice(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 {
|
func RenderOnGetTopPricesMessage(symbols []string, price []float64, fundRate []float64) string {
|
||||||
t := helper.NewNoBorderTableWriter("", "Price", "Fund")
|
t := helper.NewNoBorderTableWriter("", "Price", "Fund")
|
||||||
mFmt := message.NewPrinter(language.AmericanEnglish)
|
mFmt := message.NewPrinter(language.AmericanEnglish)
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
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:", "💸 Funding:", "🏦 Margin:")
|
||||||
|
assertContainsNone(t, msg, "🅰️ Alpha:", "📊 Alpha 24h:")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user