fix(market): route Hyperliquid USDC perps correctly + symbol fuzzy match

A single-symbol QNT-USDC trader produced 0 candidate coins, 500 errors
from Hyperliquid, and "🚫 Dropped AI decision" warnings — the agent had
no market data to reason about, so it sat in `wait` forever. Three
chained bugs:

1. provider/hyperliquid/kline.go (IsXYZAsset / FormatCoinForAPI):
   asset detection required the base symbol to appear in the hardcoded
   StockPerpsSymbols / XYZOtherSymbols / display-alias lists. QNT, ARM,
   and every other newly-listed Hyperliquid USDC perp wasn't in the
   list, so the code routed them to the crypto path (CoinAnk) which
   doesn't have them. Now the `-USDC` suffix and `xyz:` prefix are
   trusted as definitive Hyperliquid signals — these tokens are
   Hyperliquid-specific and new listings don't require a code change.
   The hardcoded lists are kept as fallbacks for bare base symbols.

2. market/data_klines.go (getKlinesFromHyperliquid): the function
   stripped the `xyz:` prefix before calling GetCandles, defeating
   GetCandles's own FormatCoinForAPI logic. With the hardcoded list
   missing the new ticker, FormatCoinForAPI couldn't re-add the prefix
   and the request hit Hyperliquid's crypto perp endpoint — which
   returns 500 for stock-only tickers. Pass the symbol through as-is.

3. trader/auto_trader_loop.go (filterDecisionsToStrategyUniverse): the
   AI sometimes echoes a candidate as "QNTUSDC" / "QNT-USDC" / "QNTUSDT"
   / bare "QNT" instead of the canonical "xyz:QNT" we supplied. Strict
   exact-match was dropping all of them. Added a base-level key
   (strips xyz:, -USDC, -USDT, USDC, USDT, USD; normalizes display
   aliases like ROBINHOOD → HOOD) and rewrites the matched decision's
   symbol to the canonical form so the order pipeline downstream sees
   the format it expects.

After this, a single-symbol stock trader fetches real K-line data from
Hyperliquid, the AI sees real candidates, and decisions get executed
on-chain instead of silently filtered.
This commit is contained in:
tinkle-community
2026-05-29 22:14:41 +08:00
parent e4adafa364
commit d008ccc6ab
3 changed files with 99 additions and 13 deletions

View File

@@ -114,18 +114,17 @@ func getKlinesFromCoinAnk(symbol, interval, exchange string, limit int) ([]Kline
// getKlinesFromHyperliquid fetches kline data from Hyperliquid API for xyz dex assets
func getKlinesFromHyperliquid(symbol, interval string, limit int) ([]Kline, error) {
// Remove xyz: prefix if present for the API call
baseCoin := strings.TrimPrefix(symbol, "xyz:")
// Map interval to Hyperliquid format
// Pass the symbol AS-IS to GetCandles. It internally calls FormatCoinForAPI
// which handles the xyz: prefix correctly. Stripping the prefix here was a
// bug: if the base symbol (e.g. "QNT") was not in our hardcoded
// StockPerpsSymbols list, FormatCoinForAPI couldn't tell it was an xyz
// asset and the request hit the crypto perp endpoint instead — which
// returns 500 for stock symbols that have no crypto perp on Hyperliquid.
hlInterval := hyperliquid.MapTimeframe(interval)
// Create Hyperliquid client
client := hyperliquid.NewClient()
// Fetch candles
ctx := context.Background()
candles, err := client.GetCandles(ctx, baseCoin, hlInterval, limit)
candles, err := client.GetCandles(ctx, symbol, hlInterval, limit)
if err != nil {
return nil, fmt.Errorf("Hyperliquid API error: %w", err)
}