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
This commit is contained in:
parent
2a82086683
commit
71ed25fe56
|
|
@ -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)
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue