From 71ed25fe567706f5537a136f132f9d0e355d3c7b Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 24 May 2026 17:49:09 -0300 Subject: [PATCH] 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 --- executor/executor.py | 57 ++------------------------------------------ src/evaluate.c | 46 ++++++++++++++++++++++++++--------- src/symbols_api.c | 5 ++++ src/symbols_api.h | 1 + src/triangle.h | 1 + 5 files changed, 44 insertions(+), 66 deletions(-) diff --git a/executor/executor.py b/executor/executor.py index faa83aa..2bb7e71 100644 --- a/executor/executor.py +++ b/executor/executor.py @@ -351,17 +351,6 @@ class Executor: self._log.debug("signal_discarded_stale", correlation_id=correlation_id, book_ts_ms=book_ts_ms, stale_ts=stale_ts) return - precheck_ok, min_required, leg_minima = self._precheck_volume(signal) - if not precheck_ok: - self._log.debug( - "signal_discarded_vol_too_small", - correlation_id=correlation_id, - starting_volume=str(signal.get("starting_volume", "")), - min_required=str(min_required), - leg_minima=leg_minima, - ) - return - async with self._isolation_lock: if self._session is None or self._session.closed: self._session = self._create_session() @@ -390,38 +379,6 @@ class Executor: async with self._isolation_lock: self._in_flight.pop(correlation_id, None) - def _precheck_volume(self, signal: dict) -> tuple[bool, Decimal, dict[str, str]]: - """ - Verify the signal's starting volume can clear each leg's base_min_size. - - Uses the top-of-book price to convert quote-currency order_param to - base-currency for buy legs. This is a cheap rejection filter before - we commit to execution — avoids hitting the exchange with an order - that would be rejected for being below the minimum size. - """ - legs = signal.get("legs", []) - starting_volume = Decimal(str(signal.get("starting_volume", "0"))) - if not legs or starting_volume <= 0: - return (False, _D0_1, {}) - - books = signal.get("books", []) - for i, leg in enumerate(legs): - order_param = Decimal(str(leg.get("order_param", "0"))) - base_min = Decimal(str(leg.get("base_min_size", "0"))) - if order_param <= 0 or base_min <= 0: - continue - side = leg.get("side", "") - book = books[i] if i < len(books) else {} - if side == "buy": - asks = book.get("asks", []) - price = Decimal(str(asks[0]["price"])) if asks else _D0 - if price > 0 and order_param / price < base_min: - return (False, base_min, {leg.get("pair", ""): str(base_min)}) - else: - if order_param < base_min: - return (False, base_min, {leg.get("pair", ""): str(base_min)}) - return (True, _D0_1, {}) - def _is_blocked(self, triangle_key: frozenset, pair_symbols: frozenset, primary_quote: str) -> bool: """ Return True if the given triangle_key, pair_symbols, or primary_quote @@ -553,12 +510,7 @@ class Executor: input_vol = order_param else: prev = fills[i - 1] - if prev.side == "buy": - # Buy output is base asset: use filled_volume directly. - input_vol = prev.filled_volume - else: - # Sell output is quote: deduct fee before passing as next input. - input_vol = prev.filled_volume * (_D1 - fee_rate) + input_vol = prev.filled_volume if base_min_size > 0 and side == "sell" and input_vol < base_min_size: self._log.info("execution_aborted_below_min", @@ -695,12 +647,7 @@ class Executor: weighted_avg_price = Decimal(str(bids[0]["price"])) if bids else _D0 avg_price = weighted_avg_price if weighted_avg_price > 0 else _D0 - # Estimate fee: KuCoin charges fee in the currency being received. - fee = ( - total_size * fee_rate - if fee_ccy == base_ccy - else total_funds * fee_rate - ) + fee = _D0 fills.append(LegFill( leg=i, diff --git a/src/evaluate.c b/src/evaluate.c index 2034339..d89a812 100644 --- a/src/evaluate.c +++ b/src/evaluate.c @@ -201,6 +201,41 @@ bool evaluate_symbol(evaluator_t *ev, uint16_t symbol_idx, int64_t t_sock_arrive } } + /* + * Minimum starting-quote volume required to clear every leg's + * exchange minimum order size. Each leg's minimum is expressed + * in its quote currency and then converted back to starting-quote + * units via the cumulative pure-rate product (no fees). + */ + double cumulative_rate = 1.0; + double leg_min_starting[3]; + for (int leg = 0; leg < 3; leg++) { + const order_book_t *bk = books_arr[leg]; + bool is_buy = !tri->use_bid[leg]; + double price = is_buy ? bk->asks[0][0] : bk->bids[0][0]; + double qi = tri->quote_increment[leg]; + double bms = tri->base_min_size[leg]; + double qms = tri->quote_min_size[leg]; + + double min_quote = fmax(bms * price, qms); + if (qi > 0) min_quote = ceil(min_quote / qi - 1e-12) * qi; + + leg_min_starting[leg] = min_quote / cumulative_rate; + cumulative_rate *= rates[leg]; + } + + double min_volume = leg_min_starting[0]; + for (int leg = 1; leg < 3; leg++) { + if (leg_min_starting[leg] > min_volume) + min_volume = leg_min_starting[leg]; + } + + /* Viability gate: must be able to clear every leg's minimum */ + if (max_volume < min_volume) continue; + + /* Floor at the strictest leg's minimum */ + max_volume = fmax(max_volume, min_volume); + int64_t cooldown_ms = (int64_t)(cfg->cooldown_seconds * 1000); if (now - ev->last_signal_ts_ms[i] < cooldown_ms) continue; ev->last_signal_ts_ms[i] = now; @@ -265,17 +300,6 @@ bool evaluate_symbol(evaluator_t *ev, uint16_t symbol_idx, int64_t t_sock_arrive sig.starting_volume = leg_quote_vol[0]; sig.live = cfg->live_mode; - if (sig.live) { - bool below_min = false; - for (int leg = 0; leg < 3; leg++) { - if (leg_base_size[leg] <= 0 || leg_base_size[leg] < tri->base_min_size[leg]) { - below_min = true; - break; - } - } - if (below_min) continue; - } - snprintf(sig.triangle_key, sizeof(sig.triangle_key), "%s/%s/%s", tri->base, tri->mid, tri->quote); strncpy(sig.primary_quote, tri->base, CURRENCY_NAME_LEN); diff --git a/src/symbols_api.c b/src/symbols_api.c index 55a2957..d3cf675 100644 --- a/src/symbols_api.c +++ b/src/symbols_api.c @@ -148,6 +148,7 @@ int fetch_trading_pairs(pair_list_t *out) { cJSON *base_inc = cJSON_GetObjectItem(item, "baseIncrement"); cJSON *quote_inc = cJSON_GetObjectItem(item, "quoteIncrement"); cJSON *base_min = cJSON_GetObjectItem(item, "baseMinSize"); + cJSON *quote_min = cJSON_GetObjectItem(item, "quoteMinSize"); if (!cJSON_IsString(sym) || !cJSON_IsString(base) || !cJSON_IsString(quote)) continue; if (!cJSON_IsBool(enable) || !cJSON_IsTrue(enable)) continue; @@ -165,6 +166,7 @@ int fetch_trading_pairs(pair_list_t *out) { pair->base_increment = cJSON_IsString(base_inc) ? atof(base_inc->valuestring) : 0.0; pair->quote_increment = cJSON_IsString(quote_inc) ? atof(quote_inc->valuestring) : 0.0; pair->base_min_size = cJSON_IsString(base_min) ? atof(base_min->valuestring) : 0.0; + pair->quote_min_size = cJSON_IsString(quote_min) ? atof(quote_min->valuestring) : 0.0; } cJSON_Delete(root); @@ -349,6 +351,9 @@ int discover_symbols(symbol_table_t *symbols, triangle_set_t *triangles, t->base_min_size[0] = pairs.pairs[i1].base_min_size; t->base_min_size[1] = pairs.pairs[i2].base_min_size; t->base_min_size[2] = pairs.pairs[i3].base_min_size; + t->quote_min_size[0] = pairs.pairs[i1].quote_min_size; + t->quote_min_size[1] = pairs.pairs[i2].quote_min_size; + t->quote_min_size[2] = pairs.pairs[i3].quote_min_size; tri_count++; } diff --git a/src/symbols_api.h b/src/symbols_api.h index dff8908..3c1e7ec 100644 --- a/src/symbols_api.h +++ b/src/symbols_api.h @@ -19,6 +19,7 @@ typedef struct { double base_increment; /* base lot size step */ double quote_increment; /* quote lot size step */ double base_min_size; /* minimum base order size */ + double quote_min_size; /* minimum quote order size */ } trading_pair_t; /* Growable list of trading pairs */ diff --git a/src/triangle.h b/src/triangle.h index 61b6cde..3b52669 100644 --- a/src/triangle.h +++ b/src/triangle.h @@ -18,6 +18,7 @@ typedef struct { double base_increment[3]; /* base asset lot step for each leg */ double quote_increment[3]; /* quote asset lot step for each leg */ double base_min_size[3]; /* min base order size for each leg */ + double quote_min_size[3]; /* min quote order size for each leg */ uint16_t id; /* unique triangle ID */ char symbol_names[3][SYMBOL_NAME_LEN]; /* trading pair names for each leg */ char fee_currency[3][CURRENCY_NAME_LEN]; /* fee currency per leg */