Files
nofx/provider/vergex/client_test.go
2026-06-27 00:37:59 +08:00

221 lines
7.0 KiB
Go

package vergex
import (
"fmt"
"net/url"
"strings"
"testing"
)
func TestParseSignalRankingAndFilterTradFiItems(t *testing.T) {
body := []byte(`{
"data": {
"rankings": [
{"marketType":"hip3-perp","symbol":"AAPL","bias":"long","confidence":0.88,"compositeZ":1.75},
{"marketType":"stock","symbol":"NVDA","bias":"long","confidence":0.81,"compositeZ":1.25},
{"market_type":"core_perp","symbol":"BTC","bias":"short","score":0.91}
]
}
}`)
ranking, err := ParseSignalRanking(body)
if err != nil {
t.Fatalf("ParseSignalRanking returned error: %v", err)
}
if len(ranking.Items) != 3 {
t.Fatalf("items len = %d, want 3", len(ranking.Items))
}
if ranking.Items[0].Symbol != "AAPL" || ranking.Items[0].MarketType != "hip3-perp" || ranking.Items[0].Bias != "long" {
t.Fatalf("unexpected first item: %+v", ranking.Items[0])
}
items := FilterTradFiItems(ranking.Items, "hip3_perp", 5)
if len(items) != 2 {
t.Fatalf("filtered len = %d, want 2", len(items))
}
if got := TradableSymbol(items[0].Symbol); got != "xyz:AAPL" {
t.Fatalf("TradableSymbol = %q, want xyz:AAPL", got)
}
if got := TradableSymbol(items[1].Symbol); got != "xyz:NVDA" {
t.Fatalf("TradableSymbol = %q, want xyz:NVDA", got)
}
}
func TestFilterTradFiItemsAllowsFullClaw402Board(t *testing.T) {
items := make([]SignalRankItem, 0, 35)
for i := 1; i <= 35; i++ {
items = append(items, SignalRankItem{
Rank: i,
Symbol: fmt.Sprintf("xyz:STK%02d", i),
MarketType: "hip3_perp",
Bias: "bullish",
})
}
filtered := FilterTradFiItems(items, "hip3_perp", 30)
if len(filtered) != 30 {
t.Fatalf("filtered len = %d, want 30", len(filtered))
}
if filtered[0].Symbol != "STK01" || filtered[29].Symbol != "STK30" {
t.Fatalf("unexpected filtered bounds: first=%q last=%q", filtered[0].Symbol, filtered[29].Symbol)
}
capped := FilterTradFiItems(items, "hip3_perp", 35)
if len(capped) != MaxSignalRankingItems {
t.Fatalf("capped len = %d, want %d", len(capped), MaxSignalRankingItems)
}
}
func TestParseSignalRankingReadsNestedMarketShape(t *testing.T) {
body := []byte(`{
"data": {
"items": [
{"market":{"marketType":"hip3_perp","symbol":"xyz:NBIS"},"symbol":"xyz:NBIS","bias":"bullish","compositeZ":1.05,"rank":5},
{"market":{"marketType":"hip3_perp","symbol":"xyz:DRAM"},"symbol":"xyz:DRAM","bias":"bullish","compositeZ":0.47,"rank":10},
{"market":{"marketType":"core_perp","symbol":"BTC"},"symbol":"BTC","bias":"bearish","compositeZ":-0.08,"rank":12}
]
}
}`)
ranking, err := ParseSignalRanking(body)
if err != nil {
t.Fatalf("ParseSignalRanking returned error: %v", err)
}
allItems := FilterSignalRankingItems(ranking.Items, "all", 30)
if len(allItems) != 3 {
t.Fatalf("all filtered len = %d, want 3: %+v", len(allItems), allItems)
}
if allItems[2].Symbol != "BTC" || allItems[2].MarketType != "core_perp" || allItems[2].Category != "crypto" {
t.Fatalf("unexpected crypto item: %+v", allItems[2])
}
items := FilterTradFiItems(ranking.Items, "hip3_perp", 30)
if len(items) != 2 {
t.Fatalf("filtered len = %d, want 2: %+v", len(items), items)
}
if items[0].Symbol != "NBIS" || items[0].MarketType != "hip3_perp" {
t.Fatalf("unexpected first item: %+v", items[0])
}
if items[1].Symbol != "DRAM" || items[1].MarketType != "hip3_perp" {
t.Fatalf("unexpected second item: %+v", items[1])
}
}
func TestMarketSymbolPreservesHIP3XYZPrefix(t *testing.T) {
if got := MarketSymbol("hip3_perp", "INTC"); got != "xyz:INTC" {
t.Fatalf("MarketSymbol hip3_perp/INTC = %q, want xyz:INTC", got)
}
if got := MarketSymbol("hip3_perp", "xyz:skhx"); got != "xyz:SKHX" {
t.Fatalf("MarketSymbol hip3_perp/xyz:skhx = %q, want xyz:SKHX", got)
}
if got := MarketSymbol("core_perp", "BTC"); got != "BTC" {
t.Fatalf("MarketSymbol core_perp/BTC = %q, want BTC", got)
}
if got := TradableSymbolForMarket("core_perp", "BTC"); got != "BTC" {
t.Fatalf("TradableSymbolForMarket core_perp/BTC = %q, want BTC", got)
}
if got := TradableSymbolForMarket("hip3_perp", "INTC"); got != "xyz:INTC" {
t.Fatalf("TradableSymbolForMarket hip3_perp/INTC = %q, want xyz:INTC", got)
}
}
func TestAddQueryDefaultsUsesClaw402GatewayParams(t *testing.T) {
params := url.Values{}
addQueryDefaults(params, Query{
MarketType: "hip3_perp",
Symbol: "INTC",
Chain: "hyperliquid",
LiqBand: "15",
}, true)
if got := params.Get("marketType"); got != "hip3_perp" {
t.Fatalf("marketType = %q", got)
}
if got := params.Get("symbol"); got != "xyz:INTC" {
t.Fatalf("symbol = %q, want xyz:INTC", got)
}
if got := params.Get("chain"); got != "mainnet" {
t.Fatalf("chain = %q, want mainnet", got)
}
if got := params.Get("liqBand"); got != "15" {
t.Fatalf("liqBand = %q, want 15", got)
}
}
func TestQueryChainMapsHyperliquidToVergexMainnet(t *testing.T) {
if got := QueryChain("hyperliquid"); got != "mainnet" {
t.Fatalf("QueryChain hyperliquid = %q, want mainnet", got)
}
}
func TestFormatAnalysisForAIIncludesDetailErrors(t *testing.T) {
text := FormatAnalysisForAI(&MarketAnalysis{
Symbol: "xyz:NVDA",
QuerySymbol: "NVDA",
MarketType: "stock",
SignalLabError: "upstream returned status 502",
HeatmapError: "market not found",
})
if !containsAll(text, "Signal Lab: unavailable", "upstream returned status 502", "Cost/Liquidation Heatmap: unavailable", "market not found") {
t.Fatalf("formatted analysis did not include detail errors:\n%s", text)
}
}
func TestFormatAnalysisForAIFormatsVergexDetailsAsMarkdown(t *testing.T) {
text := FormatAnalysisForAI(&MarketAnalysis{
Symbol: "xyz:DRAM",
QuerySymbol: "DRAM",
MarketType: "hip3_perp",
SignalLab: []byte(`{
"data": {
"band": "15",
"bias": "bullish",
"compositeZ": 1.41,
"confidence": "Medium",
"dimensions": [
{
"family": "I Cost & Positioning",
"label": "Capital-gains overhang",
"direction": "bullish",
"strength": "medium",
"percentile": 80,
"detail": "price is above aggregate cost"
}
]
}
}`),
Heatmap: []byte(`{
"data": {
"binStep": 3.2,
"bins": [
{"bucketStartPrice": 100, "bucketEndPrice": 103.2, "longCost": 1200000, "shortCost": 1000, "longLiq": 5000, "shortLiq": 700000},
{"bucketStartPrice": 103.2, "bucketEndPrice": 106.4, "longCost": 1000, "shortCost": 2000, "longLiq": 900000, "shortLiq": 4000}
]
}
}`),
})
if !containsAll(text,
"#### Signal Lab",
"| Family | Signal | Direction | Strength | Percentile | Detail |",
"Capital-gains overhang",
"#### Cost/Liquidation Heatmap",
"| Price zone | Long cost | Short cost | Long liq | Short liq | Main cluster |",
"$1.20M",
) {
t.Fatalf("formatted analysis is not markdown enough:\n%s", text)
}
if strings.Contains(text, "Signal Lab: {") || strings.Contains(text, "Cost/Liquidation Heatmap: {") {
t.Fatalf("formatted analysis still includes raw inline JSON:\n%s", text)
}
}
func containsAll(text string, needles ...string) bool {
for _, needle := range needles {
if !strings.Contains(text, needle) {
return false
}
}
return true
}