mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 17:12:25 +08:00
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:
@@ -13,7 +13,7 @@ import (
|
|||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
"nofx/manager"
|
"nofx/manager"
|
||||||
"nofx/market"
|
"nofx/market"
|
||||||
"nofx/provider/coinank"
|
"nofx/provider/coinank/coinank_api"
|
||||||
"nofx/provider/coinank/coinank_enum"
|
"nofx/provider/coinank/coinank_enum"
|
||||||
"nofx/store"
|
"nofx/store"
|
||||||
"nofx/trader"
|
"nofx/trader"
|
||||||
@@ -2321,11 +2321,8 @@ func (s *Server) handleKlines(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, klines)
|
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) {
|
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
|
// Map exchange string to coinank enum
|
||||||
var coinankExchange coinank_enum.Exchange
|
var coinankExchange coinank_enum.Exchange
|
||||||
switch strings.ToLower(exchange) {
|
switch strings.ToLower(exchange) {
|
||||||
@@ -2397,12 +2394,34 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i
|
|||||||
return nil, fmt.Errorf("unsupported interval for coinank: %s", interval)
|
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()
|
ctx := context.Background()
|
||||||
endTime := time.Now().UnixMilli()
|
ts := time.Now().UnixMilli()
|
||||||
coinankKlines, err := coinankClient.Kline(ctx, symbol, coinankExchange, 0, endTime, limit, coinankInterval)
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("coinank API error: %w", err)
|
// 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
|
// Convert coinank kline format to market.Kline format
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
"nofx/provider/coinank"
|
"nofx/provider/coinank/coinank_api"
|
||||||
"nofx/provider/coinank/coinank_enum"
|
"nofx/provider/coinank/coinank_enum"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -25,13 +25,9 @@ type FundingRateCache struct {
|
|||||||
var (
|
var (
|
||||||
fundingRateMap sync.Map // map[string]*FundingRateCache
|
fundingRateMap sync.Map // map[string]*FundingRateCache
|
||||||
frCacheTTL = 1 * time.Hour
|
frCacheTTL = 1 * time.Hour
|
||||||
coinankClient *coinank.CoinankClient // Global CoinAnk client for kline data
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize CoinAnk client
|
// Note: Kline data now uses free/open API (coinank_api.Kline) which doesn't require authentication
|
||||||
func init() {
|
|
||||||
coinankClient = coinank.NewCoinankClient(coinank_enum.MainUrl, "0cccbd7992754b67b1848c6746c0fce0")
|
|
||||||
}
|
|
||||||
|
|
||||||
// getKlinesFromCoinAnk fetches kline data from CoinAnk API (replacement for WSMonitorCli)
|
// getKlinesFromCoinAnk fetches kline data from CoinAnk API (replacement for WSMonitorCli)
|
||||||
func getKlinesFromCoinAnk(symbol, interval string, limit int) ([]Kline, error) {
|
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)
|
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()
|
ctx := context.Background()
|
||||||
endTime := time.Now().UnixMilli()
|
ts := time.Now().UnixMilli()
|
||||||
coinankKlines, err := coinankClient.Kline(ctx, symbol, coinank_enum.Binance, 0, endTime, limit, coinankInterval)
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("CoinAnk API error: %w", err)
|
return nil, fmt.Errorf("CoinAnk API error: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,15 +72,15 @@ function getExchangeDisplayNameFromList(
|
|||||||
: typeName
|
: 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(
|
function getExchangeTypeFromList(
|
||||||
exchangeId: string | undefined,
|
exchangeId: string | undefined,
|
||||||
exchanges: Exchange[] | undefined
|
exchanges: Exchange[] | undefined
|
||||||
): string {
|
): string {
|
||||||
if (!exchangeId) return 'BINANCE'
|
if (!exchangeId) return 'binance'
|
||||||
const exchange = exchanges?.find((e) => e.id === exchangeId)
|
const exchange = exchanges?.find((e) => e.id === exchangeId)
|
||||||
if (!exchange) return 'BINANCE' // Default to BINANCE for charts
|
if (!exchange) return 'binance' // Default to binance for charts
|
||||||
return exchange.exchange_type?.toUpperCase() || 'BINANCE'
|
return exchange.exchange_type?.toLowerCase() || 'binance'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if exchange is a perp-dex type (wallet-based)
|
// Helper function to check if exchange is a perp-dex type (wallet-based)
|
||||||
|
|||||||
@@ -678,6 +678,42 @@ export function AdvancedChart({
|
|||||||
<span className="text-xs px-2 py-0.5 rounded" style={{ background: '#2B3139', color: '#848E9C' }}>
|
<span className="text-xs px-2 py-0.5 rounded" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||||
{interval}
|
{interval}
|
||||||
</span>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
|||||||
interval={interval}
|
interval={interval}
|
||||||
traderID={traderId}
|
traderID={traderId}
|
||||||
height={550}
|
height={550}
|
||||||
|
exchange={exchangeId || 'binance'}
|
||||||
onSymbolChange={setChartSymbol}
|
onSymbolChange={setChartSymbol}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
Reference in New Issue
Block a user