/* * symbols_api.c - KuCoin symbol discovery and triangle enumeration * * Fetches all trading pairs from KuCoin /api/v2/symbols, filters by exclusion list, * enumerates all triangular arbitrage paths over the hold currencies. * * Triangle enumeration: for each hold currency H, find all pairs (H, A) and (H, B), * then for each unordered pair {A, B} check if pair (A, B) exists, forming triangle * H -> A -> B -> H in both directional orders. */ #include "log.h" #include "symbols_api.h" #include "http_client.h" #include "cJSON.h" #include #include #include #include #define MAX_PAIRS 4096 #define MAX_SYMBOLS 2048 #define SYMBOL_NAME_LEN 16 #define CURRENCY_NAME_LEN 8 #define PAIR_HASH_SIZE 4096 #define MAX_NEIGHBORS 2048 /* --- Pair hash table: unordered currency pair -> pair index --- */ typedef struct { char a[CURRENCY_NAME_LEN]; char b[CURRENCY_NAME_LEN]; uint32_t pair_idx; bool used; } ph_entry_t; typedef struct { ph_entry_t entries[PAIR_HASH_SIZE]; } pair_hash_t; static uint32_t ph_hash(const char *a, const char *b) { uint32_t h = 5381; const uint8_t *s = (const uint8_t *)a; while (*s) { h = (h << 5) + h + *s; s++; } s = (const uint8_t *)b; while (*s) { h = (h << 5) + h + *s; s++; } return h % PAIR_HASH_SIZE; } static void ph_init(pair_hash_t *ph) { memset(ph, 0, sizeof(*ph)); } static void ph_insert(pair_hash_t *ph, const char *c1, const char *c2, uint32_t idx) { // Normalize: store lexicographically smaller first for unordered lookup const char *a = c1, *b = c2; if (strcmp(a, b) > 0) { a = c2; b = c1; } uint32_t h = ph_hash(a, b); while (ph->entries[h].used) { if (strcmp(ph->entries[h].a, a) == 0 && strcmp(ph->entries[h].b, b) == 0) return; /* already inserted, keep first */ h = (h + 1) % PAIR_HASH_SIZE; } strncpy(ph->entries[h].a, a, CURRENCY_NAME_LEN - 1); strncpy(ph->entries[h].b, b, CURRENCY_NAME_LEN - 1); ph->entries[h].pair_idx = idx; ph->entries[h].used = true; } static bool ph_find(const pair_hash_t *ph, const char *c1, const char *c2, uint32_t *idx) { const char *a = c1, *b = c2; if (strcmp(a, b) > 0) { a = c2; b = c1; } uint32_t h = ph_hash(a, b); while (ph->entries[h].used) { if (strcmp(ph->entries[h].a, a) == 0 && strcmp(ph->entries[h].b, b) == 0) { *idx = ph->entries[h].pair_idx; return true; } h = (h + 1) % PAIR_HASH_SIZE; } return false; } /* --- Fee lookup helper --- */ /* Fee rate per category: cat1=0.1%, cat2=0.2%, cat3=0.3% */ static double fee_rate_for_pair(const trading_pair_t *pair, bool kcs_discount) { double base = pair->fee_category * 0.001; double rate = base * pair->taker_fee_coeff; return kcs_discount ? rate * 0.8 : rate; } int fetch_trading_pairs(pair_list_t *out) { memset(out, 0, sizeof(*out)); out->capacity = MAX_PAIRS; out->pairs = calloc(out->capacity, sizeof(trading_pair_t)); if (!out->pairs) return -1; int out_len = 0; char *json = https_get("api.kucoin.com", 443, "/api/v2/symbols", &out_len); if (!json || out_len <= 0) { log_write("[SYMBOLS] Failed to fetch trading pairs\n"); free(out->pairs); out->pairs = NULL; return -1; } cJSON *root = cJSON_Parse(json); free(json); if (!root) { log_write("[SYMBOLS] Failed to parse symbols JSON\n"); free(out->pairs); out->pairs = NULL; return -1; } cJSON *code = cJSON_GetObjectItem(root, "code"); if (!cJSON_IsString(code) || strcmp(code->valuestring, "200000") != 0) { log_write("[SYMBOLS] API error: %s\n", cJSON_PrintUnformatted(root)); cJSON_Delete(root); free(out->pairs); out->pairs = NULL; return -1; } cJSON *data = cJSON_GetObjectItem(root, "data"); if (!cJSON_IsArray(data)) { log_write("[SYMBOLS] 'data' is not an array\n"); cJSON_Delete(root); free(out->pairs); out->pairs = NULL; return -1; } uint32_t count = 0; cJSON *item; cJSON_ArrayForEach(item, data) { if (count >= out->capacity) break; if (!cJSON_IsObject(item)) continue; cJSON *sym = cJSON_GetObjectItem(item, "symbol"); cJSON *base = cJSON_GetObjectItem(item, "baseCurrency"); cJSON *quote = cJSON_GetObjectItem(item, "quoteCurrency"); cJSON *fee_cur = cJSON_GetObjectItem(item, "feeCurrency"); cJSON *enable = cJSON_GetObjectItem(item, "enableTrading"); cJSON *fee_cat = cJSON_GetObjectItem(item, "feeCategory"); cJSON *taker_coeff = cJSON_GetObjectItem(item, "takerFeeCoefficient"); cJSON *base_inc = cJSON_GetObjectItem(item, "baseIncrement"); cJSON *quote_inc = cJSON_GetObjectItem(item, "quoteIncrement"); cJSON *base_min = cJSON_GetObjectItem(item, "baseMinSize"); if (!cJSON_IsString(sym) || !cJSON_IsString(base) || !cJSON_IsString(quote)) continue; if (!cJSON_IsBool(enable) || !cJSON_IsTrue(enable)) continue; trading_pair_t *pair = &out->pairs[count++]; strncpy(pair->symbol, sym->valuestring, SYMBOL_NAME_LEN - 1); strncpy(pair->base, base->valuestring, CURRENCY_NAME_LEN - 1); strncpy(pair->quote, quote->valuestring, CURRENCY_NAME_LEN - 1); if (cJSON_IsString(fee_cur)) { strncpy(pair->fee_currency, fee_cur->valuestring, CURRENCY_NAME_LEN - 1); } pair->fee_category = cJSON_IsNumber(fee_cat) ? (uint8_t)fee_cat->valueint : 1; pair->taker_fee_coeff = cJSON_IsNumber(taker_coeff) ? taker_coeff->valuedouble : 1.0; pair->maker_fee_coeff = 1.0; 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; } cJSON_Delete(root); out->count = count; log_write("[SYMBOLS] Fetched %u trading pairs\n", count); return 0; } /* * Discover triangles from trading pairs. * * For each configured hold currency H: * 1. Find all neighbor currencies A such that pair(H, A) exists * 2. For each unordered pair (A, B) of neighbors, check if pair(A, B) exists * 3. If yes, two directed triangles are formed: * dir0: H -> A -> B -> H (H->A, A->B, B->H) * dir1: H -> B -> A -> H (H->B, B->A, A->H) * * use_bid logic (per leg): * For a leg trading pair X-Y: * We want output_currency = Y (the next currency in the cycle). * If Y equals pair.quote, we can buy Y (pay X) at ask (use_bid=0). * BUT the code uses use_bid=1 when output_currency == pair.quote * because selling X gets us quote currency (Y). * Actually: use_bid=1 means hit bid = sell base = receive quote. * use_bid=0 means hit ask = buy base = pay quote. * * t->use_bid[leg] = 1 when the output currency matches pair.quote: * We reach this leg with "next currency = quote" -> to get from base to quote, * we sell base at bid (use_bid=1). * t->use_bid[leg] = 0 when the output currency matches pair.base: * We need to go from quote to base -> buy base at ask (use_bid=0). * * After enumeration, converts pair indices to symbol table indices and builds * tri_index (symbol -> flat triangle list) for fast symbol-based lookup. */ int discover_symbols(symbol_table_t *symbols, triangle_set_t *triangles, const config_t *cfg, const fee_entry_t *fees, uint32_t fee_count) { pair_list_t pairs; if (fetch_trading_pairs(&pairs) != 0) { log_write("[SYMBOLS] Failed to fetch pairs\n"); return -1; } /* Filter out excluded currencies */ if (cfg->excluded_currency_count > 0) { uint32_t w = 0; for (uint32_t i = 0; i < pairs.count; i++) { const trading_pair_t *p = &pairs.pairs[i]; bool skip = false; for (uint32_t j = 0; j < cfg->excluded_currency_count; j++) { if (strcmp(p->base, cfg->excluded_currencies[j]) == 0 || strcmp(p->quote, cfg->excluded_currencies[j]) == 0) { skip = true; break; } } if (!skip) { if (w != i) pairs.pairs[w] = pairs.pairs[i]; w++; } } log_write("[SYMBOLS] excluded %u/%u pairs (excluded_currencies=%u)\n", pairs.count - w, pairs.count, cfg->excluded_currency_count); pairs.count = w; } log_write("[SYMBOLS] %u pairs after filtering\n", pairs.count); /* Build pair hash table: unordered {c1,c2} -> pair index */ pair_hash_t ph; ph_init(&ph); for (uint32_t i = 0; i < pairs.count; i++) { ph_insert(&ph, pairs.pairs[i].base, pairs.pairs[i].quote, i); } /* Collect unique symbols */ uint32_t sym_count = 0; char discovered_symbols[MAX_SYMBOLS][SYMBOL_NAME_LEN]; for (uint32_t i = 0; i < pairs.count; i++) { const char *sym = pairs.pairs[i].symbol; bool dup = false; for (uint32_t j = 0; j < sym_count; j++) { if (strcmp(discovered_symbols[j], sym) == 0) { dup = true; break; } } if (!dup && sym_count < MAX_SYMBOLS) { strncpy(discovered_symbols[sym_count++], sym, SYMBOL_NAME_LEN - 1); } } /* Enumerate triangles for each hold currency */ uint32_t tri_count = 0; triangle_t *tris = calloc(MAX_TRIANGLES, sizeof(triangle_t)); if (!tris) { free(pairs.pairs); return -1; } bool kcs = cfg->kcs_discount_active; for (uint32_t hi = 0; hi < cfg->hold_currency_count; hi++) { const char *hold = cfg->hold_currencies[hi]; /* Collect neighbors: all currencies c where pair(hold, c) exists, excluding the hold itself and other hold currencies. */ char neighbors[MAX_NEIGHBORS][CURRENCY_NAME_LEN]; uint32_t nb_count = 0; for (uint32_t i = 0; i < pairs.count; i++) { const trading_pair_t *p = &pairs.pairs[i]; const char *other = NULL; if (strcmp(p->base, hold) == 0) other = p->quote; else if (strcmp(p->quote, hold) == 0) other = p->base; else continue; bool dup = false; for (uint32_t j = 0; j < nb_count; j++) { if (strcmp(neighbors[j], other) == 0) { dup = true; break; } } if (!dup && nb_count < MAX_NEIGHBORS) { strncpy(neighbors[nb_count++], other, CURRENCY_NAME_LEN - 1); } } /* Sort neighbors for deterministic ordering */ for (uint32_t i = 0; i < nb_count; i++) { for (uint32_t j = 0; j < nb_count - 1 - i; j++) { if (strcmp(neighbors[j], neighbors[j+1]) > 0) { char t[CURRENCY_NAME_LEN]; strcpy(t, neighbors[j]); strcpy(neighbors[j], neighbors[j+1]); strcpy(neighbors[j+1], t); } } } /* For each unique unordered pair {a,b} of neighbors, check if pair(a,b) exists */ for (uint32_t a = 0; a < nb_count; a++) { for (uint32_t b = a + 1; b < nb_count; b++) { uint32_t ab_idx; if (!ph_find(&ph, neighbors[a], neighbors[b], &ab_idx)) continue; /* pair(a,b) exists -> triangle (hold, neighbors[a], neighbors[b]) */ /* * Two directions: * dir0: hold -> a -> b -> hold (via pairs hold-a, a-b, b-hold) * dir1: hold -> b -> a -> hold (via pairs hold-b, b-a, a-hold) */ for (int dir = 0; dir < 2; dir++) { const char *x = (dir == 0) ? neighbors[a] : neighbors[b]; const char *y = (dir == 0) ? neighbors[b] : neighbors[a]; uint32_t i1, i2, i3; if (!ph_find(&ph, hold, x, &i1)) continue; if (!ph_find(&ph, x, y, &i2)) continue; if (!ph_find(&ph, y, hold, &i3)) continue; if (tri_count >= MAX_TRIANGLES) goto done; triangle_t *t = &tris[tri_count]; t->symbol_idx[0] = (uint16_t)i1; t->symbol_idx[1] = (uint16_t)i2; t->symbol_idx[2] = (uint16_t)i3; /* * use_bid[leg] = 1 if we want to receive pair.quote (sell base at bid), * = 0 if we want to receive pair.base (buy base at ask). * * For leg 0 (hold -> x): we hold 'hold' and want to end up with 'x'. * If pair.quote == x: we can sell 'hold' to get 'x' at bid -> use_bid=1 * If pair.base == x: we need to buy 'x' paying 'hold' at ask -> use_bid=0 */ t->use_bid[0] = (strcmp(pairs.pairs[i1].quote, x) == 0) ? 1 : 0; t->use_bid[1] = (strcmp(pairs.pairs[i2].quote, y) == 0) ? 1 : 0; t->use_bid[2] = (strcmp(pairs.pairs[i3].quote, hold) == 0) ? 1 : 0; t->id = (uint16_t)tri_count; strncpy(t->symbol_names[0], pairs.pairs[i1].symbol, SYMBOL_NAME_LEN - 1); strncpy(t->symbol_names[1], pairs.pairs[i2].symbol, SYMBOL_NAME_LEN - 1); strncpy(t->symbol_names[2], pairs.pairs[i3].symbol, SYMBOL_NAME_LEN - 1); strncpy(t->fee_currency[0], pairs.pairs[i1].fee_currency, CURRENCY_NAME_LEN - 1); strncpy(t->fee_currency[1], pairs.pairs[i2].fee_currency, CURRENCY_NAME_LEN - 1); strncpy(t->fee_currency[2], pairs.pairs[i3].fee_currency, CURRENCY_NAME_LEN - 1); strncpy(t->base, hold, CURRENCY_NAME_LEN - 1); strncpy(t->mid, x, CURRENCY_NAME_LEN - 1); strncpy(t->quote, y, CURRENCY_NAME_LEN - 1); t->fee_factor[0] = 1.0 - fee_rate_for_pair(&pairs.pairs[i1], kcs); t->fee_factor[1] = 1.0 - fee_rate_for_pair(&pairs.pairs[i2], kcs); t->fee_factor[2] = 1.0 - fee_rate_for_pair(&pairs.pairs[i3], kcs); t->base_increment[0] = pairs.pairs[i1].base_increment; t->base_increment[1] = pairs.pairs[i2].base_increment; t->base_increment[2] = pairs.pairs[i3].base_increment; t->quote_increment[0] = pairs.pairs[i1].quote_increment; t->quote_increment[1] = pairs.pairs[i2].quote_increment; t->quote_increment[2] = pairs.pairs[i3].quote_increment; 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; tri_count++; } } } } done: triangles->triangles = tris; triangles->triangle_count = tri_count; /* Populate symbol table first (so symbols->count is final) */ for (uint32_t i = 0; i < sym_count; i++) { symbol_table_add(symbols, discovered_symbols[i]); } symbol_table_sort(symbols); /* Convert triangle symbol_idx from pair index to symbol table index */ uint32_t convert_miss = 0; for (uint32_t i = 0; i < tri_count; i++) { for (int leg = 0; leg < 3; leg++) { uint32_t pair_idx = tris[i].symbol_idx[leg]; int16_t sym_idx = symbol_table_lookup(symbols, pairs.pairs[pair_idx].symbol); tris[i].symbol_idx[leg] = (uint16_t)(sym_idx >= 0 ? sym_idx : 0); if (sym_idx < 0) convert_miss++; } } log_write("[SYMBOLS] converted %u triangle legs, %u lookup failures\n", 3 * tri_count, convert_miss); /* Allocate tri_index based on final symbol count */ triangles->tri_index = calloc(symbols->count, sizeof(tri_index_entry_t)); /* * Build triangle flat index by symbol. * Phase 1: count triangles per symbol (all 3 legs) * Phase 2: compute offsets (prefix sum) * Phase 3: fill flat index array (tri_flat) */ for (uint32_t i = 0; i < tri_count; i++) { for (int leg = 0; leg < 3; leg++) { uint16_t si = tris[i].symbol_idx[leg]; if (si < symbols->count) triangles->tri_index[si].count++; } } uint32_t syms_with_tri = 0; for (uint32_t s = 0; s < symbols->count; s++) { if (triangles->tri_index[s].count > 0) syms_with_tri++; } log_write("[SYMBOLS] %u/%u symbols have triangles (total_refs=%u)\n", syms_with_tri, symbols->count, 3 * tri_count); uint32_t off = 0; uint32_t total_refs = 0; for (uint32_t s = 0; s < symbols->count; s++) { triangles->tri_index[s].offset = off; off += triangles->tri_index[s].count; total_refs += triangles->tri_index[s].count; triangles->tri_index[s].count = 0; // reset for phase 3 } uint32_t *tri_flat = calloc(total_refs, sizeof(uint32_t)); triangles->tri_flat = tri_flat; for (uint32_t i = 0; i < tri_count; i++) { for (int leg = 0; leg < 3; leg++) { uint16_t si = tris[i].symbol_idx[leg]; if (si < symbols->count) { tri_flat[triangles->tri_index[si].offset + triangles->tri_index[si].count++] = i; } } } log_write("[SYMBOLS] %u symbols, %u triangles\n", symbols->count, tri_count); free(pairs.pairs); return 0; }