mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 01:44:38 +08:00
feat: simplify Claw402 autopilot trading flow
This commit is contained in:
220
provider/vergex/client_test.go
Normal file
220
provider/vergex/client_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user