feat: use coinank free API for kline data and show exchange badge

- Switch to coinank free/open kline API (no authentication required)
- Add exchange badge display with brand colors (Binance/Bybit/OKX/Hyperliquid/Aster)
- Auto-convert OKX symbol format (BTCUSDT -> BTC-USDT-SWAP)
- Fallback to Binance data for unsupported exchanges (Bitget/Lighter)
- Pass exchange prop from trader account to chart component
This commit is contained in:
tinkle-community
2025-12-27 23:35:15 +08:00
parent 9c66afd7a0
commit 0f3ba1382a
5 changed files with 75 additions and 22 deletions
+27 -8
View File
@@ -13,7 +13,7 @@ import (
"nofx/logger"
"nofx/manager"
"nofx/market"
"nofx/provider/coinank"
"nofx/provider/coinank/coinank_api"
"nofx/provider/coinank/coinank_enum"
"nofx/store"
"nofx/trader"
@@ -2321,11 +2321,8 @@ func (s *Server) handleKlines(c *gin.Context) {
c.JSON(http.StatusOK, klines)
}
// getKlinesFromCoinank fetches kline data from coinank API for multiple exchanges
// getKlinesFromCoinank fetches kline data from coinank free/open API for multiple exchanges
func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit int) ([]market.Kline, error) {
// Import coinank packages
coinankClient := coinank.NewCoinankClient(coinank_enum.MainUrl, "0cccbd7992754b67b1848c6746c0fce0")
// Map exchange string to coinank enum
var coinankExchange coinank_enum.Exchange
switch strings.ToLower(exchange) {
@@ -2397,13 +2394,35 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i
return nil, fmt.Errorf("unsupported interval for coinank: %s", interval)
}
// Call coinank API
// Convert symbol format for different exchanges
// OKX uses "BTC-USDT-SWAP" format instead of "BTCUSDT"
apiSymbol := symbol
if coinankExchange == coinank_enum.Okex {
// Convert BTCUSDT -> BTC-USDT-SWAP
if strings.HasSuffix(symbol, "USDT") {
base := strings.TrimSuffix(symbol, "USDT")
apiSymbol = fmt.Sprintf("%s-USDT-SWAP", base)
}
}
// Call coinank free/open API (no authentication required)
ctx := context.Background()
endTime := time.Now().UnixMilli()
coinankKlines, err := coinankClient.Kline(ctx, symbol, coinankExchange, 0, endTime, limit, coinankInterval)
ts := time.Now().UnixMilli()
// Use "To" side to search backward from current time (get historical klines)
coinankKlines, err := coinank_api.Kline(ctx, apiSymbol, coinankExchange, ts, coinank_enum.To, limit, coinankInterval)
if err != nil {
// Free API doesn't support all exchanges (e.g., OKX, Bitget)
// Fallback to Binance data as reference
if coinankExchange != coinank_enum.Binance {
logger.Warnf("⚠️ CoinAnk free API doesn't support %s, falling back to Binance data", coinankExchange)
coinankKlines, err = coinank_api.Kline(ctx, symbol, coinank_enum.Binance, ts, coinank_enum.To, limit, coinankInterval)
if err != nil {
return nil, fmt.Errorf("coinank API error (fallback): %w", err)
}
} else {
return nil, fmt.Errorf("coinank API error: %w", err)
}
}
// Convert coinank kline format to market.Kline format
klines := make([]market.Kline, len(coinankKlines))
+6 -9
View File
@@ -6,7 +6,7 @@ import (
"fmt"
"io"
"nofx/logger"
"nofx/provider/coinank"
"nofx/provider/coinank/coinank_api"
"nofx/provider/coinank/coinank_enum"
"math"
"strconv"
@@ -25,13 +25,9 @@ type FundingRateCache struct {
var (
fundingRateMap sync.Map // map[string]*FundingRateCache
frCacheTTL = 1 * time.Hour
coinankClient *coinank.CoinankClient // Global CoinAnk client for kline data
)
// Initialize CoinAnk client
func init() {
coinankClient = coinank.NewCoinankClient(coinank_enum.MainUrl, "0cccbd7992754b67b1848c6746c0fce0")
}
// Note: Kline data now uses free/open API (coinank_api.Kline) which doesn't require authentication
// getKlinesFromCoinAnk fetches kline data from CoinAnk API (replacement for WSMonitorCli)
func getKlinesFromCoinAnk(symbol, interval string, limit int) ([]Kline, error) {
@@ -70,10 +66,11 @@ func getKlinesFromCoinAnk(symbol, interval string, limit int) ([]Kline, error) {
return nil, fmt.Errorf("unsupported interval: %s", interval)
}
// Call CoinAnk API (default to Binance exchange for compatibility)
// Call CoinAnk free/open API (no authentication required)
ctx := context.Background()
endTime := time.Now().UnixMilli()
coinankKlines, err := coinankClient.Kline(ctx, symbol, coinank_enum.Binance, 0, endTime, limit, coinankInterval)
ts := time.Now().UnixMilli()
// Use "To" side to search backward from current time (get historical klines)
coinankKlines, err := coinank_api.Kline(ctx, symbol, coinank_enum.Binance, ts, coinank_enum.To, limit, coinankInterval)
if err != nil {
return nil, fmt.Errorf("CoinAnk API error: %w", err)
}
+4 -4
View File
@@ -72,15 +72,15 @@ function getExchangeDisplayNameFromList(
: typeName
}
// Helper function to get exchange type from exchange ID (UUID) - for TradingView charts
// Helper function to get exchange type from exchange ID (UUID) - for kline charts
function getExchangeTypeFromList(
exchangeId: string | undefined,
exchanges: Exchange[] | undefined
): string {
if (!exchangeId) return 'BINANCE'
if (!exchangeId) return 'binance'
const exchange = exchanges?.find((e) => e.id === exchangeId)
if (!exchange) return 'BINANCE' // Default to BINANCE for charts
return exchange.exchange_type?.toUpperCase() || 'BINANCE'
if (!exchange) return 'binance' // Default to binance for charts
return exchange.exchange_type?.toLowerCase() || 'binance'
}
// Helper function to check if exchange is a perp-dex type (wallet-based)
+36
View File
@@ -678,6 +678,42 @@ export function AdvancedChart({
<span className="text-xs px-2 py-0.5 rounded" style={{ background: '#2B3139', color: '#848E9C' }}>
{interval}
</span>
{/* 交易所标识 */}
<span
className="text-xs px-2 py-0.5 rounded font-medium uppercase"
style={{
background: exchange === 'binance' ? 'rgba(243, 186, 47, 0.15)' :
exchange === 'bybit' ? 'rgba(247, 147, 26, 0.15)' :
exchange === 'okx' ? 'rgba(0, 180, 255, 0.15)' :
exchange === 'bitget' ? 'rgba(0, 212, 170, 0.15)' :
exchange === 'hyperliquid' ? 'rgba(80, 227, 194, 0.15)' :
exchange === 'aster' ? 'rgba(138, 43, 226, 0.15)' :
'rgba(255, 255, 255, 0.1)',
color: exchange === 'binance' ? '#F3BA2F' :
exchange === 'bybit' ? '#F7931A' :
exchange === 'okx' ? '#00B4FF' :
exchange === 'bitget' ? '#00D4AA' :
exchange === 'hyperliquid' ? '#50E3C2' :
exchange === 'aster' ? '#8A2BE2' :
'#848E9C',
border: `1px solid ${
exchange === 'binance' ? 'rgba(243, 186, 47, 0.3)' :
exchange === 'bybit' ? 'rgba(247, 147, 26, 0.3)' :
exchange === 'okx' ? 'rgba(0, 180, 255, 0.3)' :
exchange === 'bitget' ? 'rgba(0, 212, 170, 0.3)' :
exchange === 'hyperliquid' ? 'rgba(80, 227, 194, 0.3)' :
exchange === 'aster' ? 'rgba(138, 43, 226, 0.3)' :
'rgba(255, 255, 255, 0.2)'
}`
}}
title={['bitget', 'lighter'].includes(exchange?.toLowerCase() || '')
? 'Data source: Binance (fallback)' : undefined}
>
{exchange}
{['bitget', 'lighter'].includes(exchange?.toLowerCase() || '') && (
<span className="ml-1 text-[9px] opacity-60">*</span>
)}
</span>
</div>
<div className="flex items-center gap-2">
+1
View File
@@ -171,6 +171,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
interval={interval}
traderID={traderId}
height={550}
exchange={exchangeId || 'binance'}
onSymbolChange={setChartSymbol}
/>
</motion.div>