triangular_arbitrage_bot/src/symbols_api.c

427 lines
17 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#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;
}