Commit Graph

40 Commits

Author SHA1 Message Date
nicolas 3c4bbfde8b cleanup: remove stale files, add session to .gitignore 2026-06-04 10:35:34 -03:00
nicolas e6ca8ac624 chore: remove MAX_DIRTY_BATCH cap, use MAX_SYMBOLS for dirty symbol set 2026-06-03 09:42:06 -03:00
nicolas ccf6ff8670 fix: dedicate a separate WS connection for private channels (fills + balance)
Book connections no longer subscribe to tradeOrdersV2 or /account/balance.
A dedicated private WS connection is created as the last connection
(index = connection_count - 1) with zero book subscriptions, only private
channels. Decouples fill delivery from book connection health.
2026-06-02 23:42:18 -03:00
nicolas b0056f4b6b fix: per-slot fill channels with orderId fallback routing; fix reconnect double-subscribe
Architecture: each executor slot owns its own fill_channel_t. Executor encodes
slot_index in client_oid bytes 1-2 ("c{slot:02x}{ts:08x}{leg:04x}"). Hot thread
decodes slot from client_oid to route match events. For terminal events without
clientOid, falls back to orderId->slot_index mapping registered after each
successful rest_order_place.

Reconnect fixes:
- Remove redundant ws_client_subscribe after reconnect (already done in
  ws_client_connect) - caused "exceed max subscription count" 509 errors
- Only advance last_activity_ms on successful reconnect, so failed attempts
  retry every 100ms instead of every 5s
- Private channel subscriptions gated to connection 0 only to prevent
  duplicate fill events from KuCoin broadcasting to all connections
2026-06-02 18:29:30 -03:00
nicolas d569063c75 fix: register reconnected WS socket in epoll, arm ping timer after connect
- events.c: EPOLL_CTL_MOD failure falls through to EPOLL_CTL_ADD (handles
  stale fd reuse after close)
- events.c: defer timer_fd registration until ping_interval_ms is known
  (hot thread starts before WS connection is established)
- events.h: replace unused next_ping_ms with ping_timer_armed flag
- main.c: remove stale epoll entry before WS reconnect, re-register new
  socket with O_NONBLOCK
- evaluate.c: fix order_param assignment — only overwrite with fills[leg][4]
  on buy legs (leg 0) and non-leg-0 entries; sell leg 0 preserves
  original quote_volume
2026-06-02 09:42:13 -03:00
nicolas 930365f072 feat: add allow_same_quote config, fix overlapping pair isolation to check all 3 legs, document config example 2026-05-30 11:22:16 -03:00
nicolas 60257f068f fix: use double-precision timestamps for sub-millisecond signal timing 2026-05-29 15:24:38 -03:00
nicolas 17aac4b4ef cleanup: remove unused constants MAX_CURRENCIES and _D1 2026-05-29 12:05:30 -03:00
nicolas 2c8cfc74b5 cleanup: remove dead config fields (executor_socket_path, cooldown_seconds, stats_interval_seconds), add concurrent_slots to example 2026-05-29 10:31:49 -03:00
nicolas 405fc557dc remove fee table path: fee rate now computed solely from formula fee_category * 0.001 * taker_fee_coeff 2026-05-29 09:49:37 -03:00
nicolas faa88070e7 fix: check slot before keepalive to prevent blocking REST call delaying signal pickup 2026-05-29 00:00:23 -03:00
nicolas e461dfb7a7 fix: keep slot IN_FLIGHT during execution so evaluator sees busy and drops instead of queuing 2026-05-28 22:27:33 -03:00
nicolas b714ac132e perf: replace SPSC signal queue with per-executor atomic slot delivery 2026-05-28 22:15:59 -03:00
nicolas f2c50b37ea docs: update README to reflect single-binary architecture and current config 2026-05-28 20:50:03 -03:00
nicolas 6005b5ca88 fix: tear down SSL connection on read failure to force reconnect 2026-05-28 20:39:02 -03:00
nicolas 7253d34983 feat: make balance wait between live legs configurable, default off 2026-05-28 16:37:32 -03:00
nicolas 2c3796005b fix: drain signal eventfd after pop to prevent CPU spin; fix all warnings
- Drain signal queue eventfd (fds[0]) after each pop attempt.
  Without this, the eventfd counter stays >0 forever after the first
  signal, causing poll() to return immediately every iteration (100% CPU).
- Fix strncpy truncation warnings in fill_handler.c (use memcpy+nullterm)
- Fix write/read return value warnings in fill_handler.c, log.c
- Fix misleading indentation in http_client.c (add line breaks after if)
2026-05-28 15:43:43 -03:00
nicolas b8d6499c33 fix: compute net_bps and update best/worst before STATUS log
Reorder so the per-triangle STATUS log shows the current best value
instead of the previous iteration's stale init value (-1e18).
Also remove executor idle diagnostic line and restore pre-simulation
best_net_bps tracking for ALL evaluated triangles, not just signal candidates.
2026-05-28 10:07:26 -03:00
nicolas 2f518d1a2d fix: cascade-based threshold gate, status log, best_net_bps tracking
- Restore post-simulation threshold gate using cascade net_bps (catches
  edge cases where cumulative formula sign differs from cascade result)
- Restore STATUS line with eval count (pre-simulation, every 30s)
- Move best_net_bps/worst tracking before post-simulation gate so all
  cascade-evaluated triangles contribute
- Remove executor idle diagnostic line
- Remove cooldown from evaluator (last_signal_ts_ms field removed)
- Move cooldown check before max_volume computation to save CPU
2026-05-28 10:01:17 -03:00
nicolas 9e0866c9e7 fix: remove double fee hold on legs 1-2; fix sell order_param dimension
- Remove apply_fee_hold from input path (legs 1-2) — fee hold
  is already applied at end-of-leg cascade. Both evaluate.c and executor.c.
- Fix sell order_param: set from base amount instead of quote_volume
  (A5/A8). Sell order size was incorrectly computed as a quote-equivalent.
2026-05-28 09:19:48 -03:00
nicolas 728f41679a fix: replace evaluate.c cascade with paper-trade simulation from executor
The evaluation now runs the exact same paper-mode simulation code that
the executor uses, instead of a separate cascade with different formulas.
predicted_bps comes from the simulation PnL, matching effective_bps.
2026-05-27 23:44:14 -03:00
nicolas 174b7570fa feat: concurrent executor slots; fix: fundsIncrement for market buys, remove double-counted leg0 fee hold
- Add concurrent_slots config (fused_engine section, default 1)
- Create executor_shared_t with shared in_flight table + queue mutex for multi-thread
- Move in_flight state from executor_thread_t to executor_shared_t (cross-thread isolation)
- event_executor_thread: per-thread entry point, N threads created in main.c
- Add fundsIncrement to trading_pair_t, triangle_t, signal_leg_t (fetch from KuCoin API)
- Use funds_increment for rounding market buy quote_cost (evaluate.c) and increment floor (executor.c)
- Fix leg 0: remove double-counted apply_fee_hold (evaluate already accounts via ff)
2026-05-27 13:18:53 -03:00
nicolas 562fddf124 cleanup: remove Python executor, dead config/HTTP server; add balance wait; fix fee hold, PnL, warnings
- Remove executor/ and common/ Python directories (dead code after C migration)
- Remove src/http_server.c/.h (was for Python executor, generates warnings)
- Remove dead config keys: socket_path, executor_socket_path, send_signals, rest_host, rest_port
- Remove dead UDS code in events.c/h (send_signal_to_executor, unix_* functions)
- Fix fee hold on leg 0 buys (apply_fee_hold to prevent Balance insufficient)
- Fix PnL leg0_in to use fills[0][4] instead of wrong currency field
- Fix REST keepalive warmup currency (use initial_capital[0] instead of hardcoded USDT)
- Add balance wait between legs via /account/balance WS + eventfd wake
- Fix all strncpy truncation warnings in config.c, symbols_api.c, ws_client.c
2026-05-27 12:14:10 -03:00
nicolas 06706ca479 fix: timing legends, log timestamp cache, mask generation, fill drop warning, remove fast parser
- Remove zero-alloc JSON fast parser (caused CPU increase, reverted to cJSON)
- Add descriptive legends to timing fields (t-1_snapshot, t0_arrival, t1_signal, etc.)
- Fix t2 missing when order fires at exec_start (guard on fills[l][0] not timing value)
- Cache log_write timestamp to avoid time()+localtime_r() per call
- Collapse 4 now_ms_impl() calls to 1 for WS mask generation
- Add fill_drop_warn counter for lost fill events (rate-limited warning)
- Add fill_drop_warn field to ws_client_t
2026-05-27 11:40:03 -03:00
nicolas 0420d548d5 fix: timings use correct fields and clocks, match Python format
- t-2_book_snapshot uses sig->book_ts_ms (not sig->ts_ms)
- All pipeline timings relative to exec_start_rt (realtime clock)
- Leg timings use monotonic clock (leg_timings array)
- execution_complete uses now_mono_ms() - exec_start_mono (not (now - now))
- Only include timing entries for fields that are non-zero
2026-05-27 09:39:14 -03:00
nicolas 8739c871d5 fix: separate screen-only logging from file logging
- Add log_write_screen() for status/stats output (stderr only)
- Change STATUS line to use log_write_screen (not in log file)
- SIGNAL, ORDER, FILL, FILLED/FAILED still go to both screen and log file
- Log file at /tmp/engine.log
2026-05-27 09:31:49 -03:00
nicolas 46084de4b2 fix: keepalive, connection health check, log file, single-line reports, timings fix
- Move REST keepalive to poll loop (async, not blocking signal execution)
- Fix ensure_connected to detect RST connections (recv < 0, not just == 0)
- Add log_set_file() + log file /tmp/engine.log (background thread writes)
- Single-line FILLED/FAILED reports (no multi-line fills inside brackets)
- Fix timing clock (use CLOCK_REALTIME consistently, not mixing with MONOTONIC)
- Add ORDER/FILL/REJECTED intermediate output lines
- Add session warmup at executor_thread_create
- Fix FILL latency (use order-fire time, not signal-received time)
- Paper mode: add clientOid to test endpoint, fix fee simulation
- Concurrency: add primary_quote isolation
2026-05-27 00:15:08 -03:00
nicolas 03b5daa003 migrate: absorb all executor functions into fused_engine (C)
Replace the Python executor with direct C execution in a dedicated
executor thread. Removes UDS JSON serialization, Python async
overhead, and the 2+ms pipeline gap between signal creation and
order fire.

New components:
- src/rest_client.c/h: Keepalive HTTPS, HMAC-SHA256 signing,
  order_place, order_test, Content-Length response parsing
- src/fill_handler.c/h: SPSC ring buffer for WS match events,
  hot thread -> executor thread fill dispatch
- src/executor.c/h: execute_triangle() cascade, fee hold
  reduction, increment floor, paper mode simulation, PnL,
  concurrency isolation, reporting

Modified:
- src/ws_client.c: Subscribe to tradeOrdersV2 + account.balance,
  dispatch orderChange match events to fill SPSC, private token
  fetch via bullet-private, token cleared on reconnect
- src/http_client.c: Added https_post_auth() for signed POST
- src/events.c: Cold thread replaced with executor thread
  (poll on wake_fd + fill_fd, direct execution)
- config.yaml.example: initial_capital moved to fused_engine,
  added cooldown_seconds, kcs_discount_active

Removed:
- src/kucoin_sign.c/h (redundant with http_client.c helpers)
2026-05-26 19:54:41 -03:00
nicolas 60c21bed36 chore: add microsecond resolution to structlog timestamps 2026-05-25 23:12:32 -03:00
nicolas e9727003d7 chore: remove session_warmed info log from warmup 2026-05-25 22:49:38 -03:00
nicolas affe18cbac 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)
2026-05-25 22:34:24 -03:00
nicolas 0d3acc62cb fix: add kcs_discount_active to Python config known keys 2026-05-25 20:39:40 -03:00
nicolas 3828e2b104 fix: cross-leg increment floor, ceiling-to-floor rounding, balance WS subscription, order-level logging
src/evaluate.c:
- Add cross-leg increment floor after each leg's output
- Fix sell-leg min_volume conversion (was understated by rates[leg])
- Change ceil to floor for all leg rounding (round input down, then compute)

executor/ws_client.py:
- Subscribe to /account/balance via Classic WS (subject: account.balance)
- Add await_balance() with ack tracking and per-currency futures
- Handle balance events and store latest available per currency

executor/executor.py:
- Reject order detail included in fills list with real attempted volume/latency
- Screen/log output shows fills, book tops, profit for all statuses
- side field in order_placed/order_rejected logs
- predicted_bps read early from signal (no more hardcoded 0.0)
- timings in failed/aborted reports
- Paper mode rounding: buy funds/base floored to qi/bi
2026-05-25 20:21:19 -03:00
nicolas c1c4aa4be8 docs: add docstrings to ~50 undocumented functions across C source files 2026-05-24 23:32:33 -03:00
nicolas 97b341fec9 cleanup: remove dead fh_ob/oe_em Python modules, add book_ts_ms to screen output 2026-05-24 21:36:48 -03:00
nicolas 43333984a3 feat: print execution reports to stdout for all statuses
Screen-print every completed trade (filled/failed/aborted) with
correlation_id, triangle, predicted_bps, effective_bps, profit, and
error (if any).  Flushes immediately for real-time visibility.
2026-05-24 20:50:16 -03:00
nicolas 7afd4977ca fix: revert jsmn parser to cJSON, keep coalescing
The jsmn zero-alloc parser had token-navigation bugs that caused
all book updates to fail silently. Restore cJSON-based parsing
while preserving the coalescing architecture (accumulate dirty
symbols, evaluate once per burst).
2026-05-24 19:34:43 -03:00
nicolas 7c9b7f7ae6 perf: jsmn zero-alloc parser, coalesce evaluations, fix fill race
Engine (ws_client.c/h):
- Replace cJSON with jsmn (stack tokens, zero malloc) for book updates
- Quick-route message frames ("type":"message") to jsmn, bypass cJSON
- Coalesce same-symbol updates within one SSL_read burst: evaluate once
- ws_client_process_frame returns symbol_idx for batch tracking
- Restore book->sequence field update from sequence/sequenceNum

Executor (ws_client.py):
- Fix race: fill event arriving before await_fill registers future
  is now detected via FillAccumulator._done flag, resolved immediately
2026-05-24 18:17:14 -03:00
nicolas 71ed25fe56 refactor: move order sizing to engine, simplify executor
Engine (evaluate.c):
- Compute per-leg minimum order size from quoteMinSize
  (max(baseMinSize * price, quoteMinSize), rounded to quoteIncrement)
- Convert each leg's minimum to starting-quote via pure-rate product (no fees)
- Viability gate: skip triangle if candidate < min_volume (strictest leg)
- Floor starting_volume at min_volume; supersedes old base_min_size guard

Data (symbols_api.h, triangle.h, symbols_api.c):
- Parse quoteMinSize from KuCoin /api/v2/symbols; propagate to triangle struct

Executor (executor.py):
- Remove _precheck_volume: sizing is the engine's responsibility
- Live mode: don't deduct estimated fee from filled_volume (exchange nets fees)
- Live mode: LegFill.fee always zero
2026-05-24 17:49:09 -03:00
nicolas 2a82086683 Add initial triangular arbitrage bot
Two-process architecture: a C17 fused engine (WebSocket order book
mirror, triangle enumeration, real-time profitability evaluation)
communicating via Unix domain socket to a Python 3 executor (order
placement with paper/live trading modes, REST control API).
Targets KuCoin spot market.
2026-05-24 16:12:04 -03:00