From affe18cbacc7d31b327c3917560593b08539b559 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 25 May 2026 22:34:24 -0300 Subject: [PATCH] fix: authenticated session warmup, balance-aware cascade, books always in signal executor/executor.py: - Replace unauthenticated /api/v1/time warmup with authenticated /api/v1/accounts - Keepalive interval 15s -> 30s, uses authenticated warmup_session - After sell leg, override filled_volume with latest balance from WS (net of fee) executor/kucoin_api.py: - Add warmup_session() method for GET /api/v1/accounts (authenticated) - Pre-heats TCP/TLS connection pool to reduce first-order latency executor/ws_client.py: - Add latest_balance() method to expose WS balance cache src/events.c: - Always include book tops in signal (remove !sig->live gate) - Only serialize top bid/ask level (not all 5) --- executor/executor.py | 19 ++++++++----------- executor/kucoin_api.py | 23 +++++++++++++++++++++++ executor/ws_client.py | 4 ++++ src/events.c | 24 ++++++++++++------------ 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/executor/executor.py b/executor/executor.py index dea2323..6d651ac 100644 --- a/executor/executor.py +++ b/executor/executor.py @@ -242,17 +242,11 @@ class Executor: async def start(self) -> None: """Pre-initialize and warm up the aiohttp session so the first trade is not slowed.""" self._session = self._create_session() - async def _warm_up(): - try: - async with self._session.get("https://api.kucoin.com/api/v1/time") as resp: - await resp.text() - except Exception: - pass - await _warm_up() + await self._api.warmup_session(self._session) self._keepalive_task = asyncio.create_task(self._keepalive_loop()) - _KEEPALIVE_INTERVAL = 15.0 + _KEEPALIVE_INTERVAL = 30.0 def _create_session(self) -> aiohttp.ClientSession: connector = TCPConnector( @@ -269,13 +263,12 @@ class Executor: ) async def _keepalive_loop(self) -> None: - """Ping KuCoin periodically to keep the authenticated POST path warm.""" + """Ping KuCoin periodically to keep the authenticated connection pool warm.""" while True: try: await asyncio.sleep(self._KEEPALIVE_INTERVAL) if self._live_mode: - async with self._session.get("https://api.kucoin.com/api/v1/time") as resp: - await resp.text() + await self._api.warmup_session(self._session) else: await self._api.order_test( session=self._session, @@ -703,6 +696,10 @@ class Executor: await self._ws_client.await_balance( output_ccy, fills[-1].filled_volume, 2000 ) + if side == "sell": + bal = self._ws_client.latest_balance(output_ccy) + if bal > 0: + fills[-1].filled_volume = bal in_flight.last_trade_ts_ms = int(time.time() * 1000) continue diff --git a/executor/kucoin_api.py b/executor/kucoin_api.py index 6b4612c..83c16bd 100644 --- a/executor/kucoin_api.py +++ b/executor/kucoin_api.py @@ -432,3 +432,26 @@ class KuCoinAPI: except (aiohttp.ClientError, OSError) as e: self._log.error("private_token_http_error", error=str(e)) return None + + async def warmup_session(self, session: aiohttp.ClientSession) -> None: + """Warm up the authenticated HTTP connection pool with a minimal GET.""" + timestamp = str(int(time.time() * 1000)) + path = "/api/v1/accounts" + sign = _signRequest(timestamp, "GET", path, self._api_secret, "") + headers = { + "KC-API-TIMESTAMP": timestamp, + "KC-API-SIGN": sign, + "KC-API-KEY": self._api_key, + "KC-API-PASSPHRASE": self._encrypted_passphrase, + "KC-API-SIGN-TYPE": "2", + "KC-API-KEY-VERSION": "3", + } + try: + async with session.get( + f"https://api.kucoin.com{path}", + headers=headers, + ) as resp: + await resp.read() + self._log.info("session_warmed", status=resp.status) + except Exception as e: + self._log.warning("session_warmup_failed", error=str(e)) diff --git a/executor/ws_client.py b/executor/ws_client.py index ac03e1a..c71e2a7 100644 --- a/executor/ws_client.py +++ b/executor/ws_client.py @@ -239,6 +239,10 @@ class KuCoinWSClient: if self._balance_futures.get(key) is future: del self._balance_futures[key] + def latest_balance(self, currency: str) -> Decimal: + """Return the latest known available balance for *currency*, or 0.""" + return self._latest_balance.get(currency.upper(), _D0) + async def _connection_worker(self) -> None: """Main connection loop with exponential backoff reconnection.""" while self._running: diff --git a/src/events.c b/src/events.c index f249809..ef58e68 100644 --- a/src/events.c +++ b/src/events.c @@ -257,24 +257,24 @@ static void send_signal_to_executor(event_loops_t *loops, signal_entry_t *sig) { } } - // Full book snapshot included when !live (paper trading mode) + // Book tops included for display and drift analysis char books_json_str[2048] = ""; - if (!sig->live && sig->book_count > 0) { + if (sig->book_count > 0) { char *bp = books_json_str; size_t rem = sizeof(books_json_str); for (uint8_t b = 0; b < sig->book_count; b++) { const signal_book_t *sb = &sig->books[b]; char bid_arr[256] = {0}, ask_arr[256] = {0}; - for (uint8_t lev = 0; lev < sb->bid_count; lev++) { - char tmp[64]; - snprintf(tmp, sizeof(tmp), "%s{\"price\":\"%.6g\",\"size\":\"%.8g\"}", - lev ? "," : "", sb->bids[lev].price, sb->bids[lev].size); + // Only top-of-book level included for display / drift analysis + char tmp[64]; + if (sb->bid_count > 0) { + snprintf(tmp, sizeof(tmp), "{\"price\":\"%.6g\",\"size\":\"%.8g\"}", + sb->bids[0].price, sb->bids[0].size); strncat(bid_arr, tmp, sizeof(bid_arr) - 1); } - for (uint8_t lev = 0; lev < sb->ask_count; lev++) { - char tmp[64]; - snprintf(tmp, sizeof(tmp), "%s{\"price\":\"%.6g\",\"size\":\"%.8g\"}", - lev ? "," : "", sb->asks[lev].price, sb->asks[lev].size); + if (sb->ask_count > 0) { + snprintf(tmp, sizeof(tmp), "{\"price\":\"%.6g\",\"size\":\"%.8g\"}", + sb->asks[0].price, sb->asks[0].size); strncat(ask_arr, tmp, sizeof(ask_arr) - 1); } int n = snprintf(bp, rem, @@ -298,9 +298,9 @@ static void send_signal_to_executor(event_loops_t *loops, signal_entry_t *sig) { (long long)sig->ts_ms, (long long)sig->book_ts_ms, (long long)sig->t_sock_arrive_ms, (long long)sig->t_arrive_ms, (long long)sig->t_eval_ms, - (sig->live || sig->book_count == 0) ? "" : ",\"books\":[", + sig->book_count == 0 ? "" : ",\"books\":[", books_json_str[0] ? books_json_str : "", - (sig->live || sig->book_count == 0) ? "" : "]"); + sig->book_count == 0 ? "" : "]"); size_t to_send = strlen(json_buf); size_t sent = 0;