96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
"""
|
|
KuCoin API client for the opportunity engine.
|
|
|
|
Fetches trading pair metadata (symbol, base, quote, fees, feeCurrency)
|
|
and builds an in-memory fee table keyed by base currency. This data is
|
|
used to construct the triangle index and to populate fee_currency in
|
|
signal payloads.
|
|
"""
|
|
import aiohttp
|
|
import structlog
|
|
|
|
logger = structlog.get_logger().bind(component="kucoin_api")
|
|
|
|
KUCoin_SYMBOLs_URL = "https://api.kucoin.com/api/v1/symbols"
|
|
|
|
DEFAULT_FEES = {
|
|
"BTC": {"maker": 0.0010, "taker": 0.0010},
|
|
"ETH": {"maker": 0.0010, "taker": 0.0010},
|
|
"USDT": {"maker": 0.0010, "taker": 0.0010},
|
|
"USDC": {"maker": 0.0010, "taker": 0.0010},
|
|
}
|
|
|
|
|
|
class KuCoinAPI:
|
|
"""
|
|
Fetch and cache KuCoin pair metadata and per-currency fee rates.
|
|
|
|
Used at startup to build the fee table required by triangle enumeration.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._fee_table: dict[str, dict[str, float]] = {}
|
|
self._pairs: dict[str, dict] = {}
|
|
self._log = logger
|
|
|
|
async def fetch_pairs_and_fees(self) -> None:
|
|
"""
|
|
Fetch all symbols from KuCoin, populate _pairs and _fee_table.
|
|
|
|
Logs warnings for any symbol that cannot be parsed and skips it.
|
|
Sets feeCurrency to the empty string when absent in the API response.
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(KUCoin_SYMBOLs_URL) as resp:
|
|
resp.raise_for_status()
|
|
payload = await resp.json()
|
|
|
|
for item in payload.get("data", []):
|
|
symbol = item.get("symbol", "")
|
|
base = item.get("baseCurrency", "")
|
|
quote = item.get("quoteCurrency", "")
|
|
maker_fee = float(item.get("makerFeeRate", 0))
|
|
taker_fee = float(item.get("takerFeeRate", 0))
|
|
enable_trading = item.get("enableTrading", False)
|
|
|
|
if not all([symbol, base, quote]):
|
|
continue
|
|
|
|
fee_currency = item.get("feeCurrency") or ""
|
|
self._pairs[symbol] = {
|
|
"symbol": symbol,
|
|
"base": base,
|
|
"quote": quote,
|
|
"maker_fee": maker_fee,
|
|
"taker_fee": taker_fee,
|
|
"enable_trading": enable_trading,
|
|
"fee_currency": fee_currency,
|
|
}
|
|
|
|
if base not in self._fee_table:
|
|
self._fee_table[base] = {
|
|
"maker": maker_fee if maker_fee > 0 else DEFAULT_FEES.get(base, {}).get("maker", 0.0010),
|
|
"taker": taker_fee if taker_fee > 0 else DEFAULT_FEES.get(base, {}).get("taker", 0.0010),
|
|
}
|
|
|
|
self._log.info("fee_table_loaded", bases=len(self._fee_table), pairs=len(self._pairs))
|
|
|
|
def get_fee(self, symbol: str, side: str) -> float:
|
|
"""
|
|
Return the taker fee rate for the base currency of a given symbol.
|
|
|
|
Falls back to DEFAULT_FEES when the base is not in the fee table.
|
|
"""
|
|
if symbol not in self._pairs:
|
|
return 0.0010
|
|
base = self._pairs[symbol]["base"]
|
|
fee_data = self._fee_table.get(base, DEFAULT_FEES.get(base, {"maker": 0.0010, "taker": 0.0010}))
|
|
return fee_data.get(side, 0.0010)
|
|
|
|
def get_pair_info(self, symbol: str) -> dict | None:
|
|
"""Return the full info dict for a symbol, or None if not loaded."""
|
|
return self._pairs.get(symbol)
|
|
|
|
def get_all_pairs(self) -> list[dict]:
|
|
"""Return all pairs where enable_trading is True."""
|
|
return [p for p in self._pairs.values() if p["enable_trading"]] |