465 lines
20 KiB
C
465 lines
20 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;
|
|
|
|
/* Hash an unordered currency pair (a, b) to a bucket index (djb2 variant). */
|
|
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;
|
|
}
|
|
|
|
/* Zero-initialise the pair hash table. */
|
|
static void ph_init(pair_hash_t *ph) {
|
|
memset(ph, 0, sizeof(*ph));
|
|
}
|
|
|
|
/* Insert an unordered currency pair -> pair_index mapping with open
|
|
addressing linear probing. Silently ignores duplicates (keeps first). */
|
|
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;
|
|
}
|
|
|
|
/* Look up an unordered currency pair, writing its index to *idx if found.
|
|
Returns true on hit, false on miss. */
|
|
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%.
|
|
Looks up the fee_currency in the fee table first; falls back to category formula. */
|
|
static double fee_rate_for_pair(const trading_pair_t *pair, bool kcs_discount,
|
|
const fee_entry_t *fees, uint32_t fee_count) {
|
|
if (fees && fee_count > 0) {
|
|
for (uint32_t i = 0; i < fee_count; i++) {
|
|
if (strcmp(fees[i].currency, "ALL") == 0 ||
|
|
strcmp(fees[i].currency, pair->fee_currency) == 0) {
|
|
double rate = fees[i].taker_fee;
|
|
return kcs_discount ? rate * 0.8 : rate;
|
|
}
|
|
}
|
|
}
|
|
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");
|
|
cJSON *quote_min = cJSON_GetObjectItem(item, "quoteMinSize");
|
|
cJSON *funds_inc = cJSON_GetObjectItem(item, "fundsIncrement");
|
|
|
|
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;
|
|
pair->quote_min_size = cJSON_IsString(quote_min) ? atof(quote_min->valuestring) : 0.0;
|
|
pair->funds_increment = cJSON_IsString(funds_inc) ? atof(funds_inc->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;
|
|
{ size_t _l = strlen(pairs.pairs[i1].symbol); if (_l >= SYMBOL_NAME_LEN) _l = SYMBOL_NAME_LEN - 1; memcpy(t->symbol_names[0], pairs.pairs[i1].symbol, _l); t->symbol_names[0][_l] = '\0'; }
|
|
{ size_t _l = strlen(pairs.pairs[i2].symbol); if (_l >= SYMBOL_NAME_LEN) _l = SYMBOL_NAME_LEN - 1; memcpy(t->symbol_names[1], pairs.pairs[i2].symbol, _l); t->symbol_names[1][_l] = '\0'; }
|
|
{ size_t _l = strlen(pairs.pairs[i3].symbol); if (_l >= SYMBOL_NAME_LEN) _l = SYMBOL_NAME_LEN - 1; memcpy(t->symbol_names[2], pairs.pairs[i3].symbol, _l); t->symbol_names[2][_l] = '\0'; }
|
|
{ size_t _l = strlen(pairs.pairs[i1].fee_currency); if (_l >= CURRENCY_NAME_LEN) _l = CURRENCY_NAME_LEN - 1; memcpy(t->fee_currency[0], pairs.pairs[i1].fee_currency, _l); t->fee_currency[0][_l] = '\0'; }
|
|
{ size_t _l = strlen(pairs.pairs[i2].fee_currency); if (_l >= CURRENCY_NAME_LEN) _l = CURRENCY_NAME_LEN - 1; memcpy(t->fee_currency[1], pairs.pairs[i2].fee_currency, _l); t->fee_currency[1][_l] = '\0'; }
|
|
{ size_t _l = strlen(pairs.pairs[i3].fee_currency); if (_l >= CURRENCY_NAME_LEN) _l = CURRENCY_NAME_LEN - 1; memcpy(t->fee_currency[2], pairs.pairs[i3].fee_currency, _l); t->fee_currency[2][_l] = '\0'; }
|
|
{ size_t _l = strlen(hold); if (_l >= CURRENCY_NAME_LEN) _l = CURRENCY_NAME_LEN - 1; memcpy(t->base, hold, _l); t->base[_l] = '\0'; }
|
|
{ size_t _l = strlen(x); if (_l >= CURRENCY_NAME_LEN) _l = CURRENCY_NAME_LEN - 1; memcpy(t->mid, x, _l); t->mid[_l] = '\0'; }
|
|
{ size_t _l = strlen(y); if (_l >= CURRENCY_NAME_LEN) _l = CURRENCY_NAME_LEN - 1; memcpy(t->quote, y, _l); t->quote[_l] = '\0'; }
|
|
t->fee_factor[0] = 1.0 - fee_rate_for_pair(&pairs.pairs[i1], kcs, fees, fee_count);
|
|
t->fee_factor[1] = 1.0 - fee_rate_for_pair(&pairs.pairs[i2], kcs, fees, fee_count);
|
|
t->fee_factor[2] = 1.0 - fee_rate_for_pair(&pairs.pairs[i3], kcs, fees, fee_count);
|
|
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;
|
|
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;
|
|
t->funds_increment[0] = pairs.pairs[i1].funds_increment;
|
|
t->funds_increment[1] = pairs.pairs[i2].funds_increment;
|
|
t->funds_increment[2] = pairs.pairs[i3].funds_increment;
|
|
|
|
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++) {
|
|
bool skip = false;
|
|
for (int leg = 0; leg < 3 && !skip; leg++) {
|
|
uint32_t pair_idx = tris[i].symbol_idx[leg];
|
|
int16_t sym_idx = symbol_table_lookup(symbols, pairs.pairs[pair_idx].symbol);
|
|
if (sym_idx >= 0) {
|
|
tris[i].symbol_idx[leg] = (uint16_t)sym_idx;
|
|
} else {
|
|
log_write("[SYMBOLS] Symbol not found: %s (triangle %u leg %d)\n",
|
|
pairs.pairs[pair_idx].symbol, i, leg);
|
|
convert_miss++;
|
|
skip = true;
|
|
}
|
|
}
|
|
if (skip) {
|
|
/* Mark triangle invalid — evaluate_symbol will ignore it */
|
|
tris[i].symbol_idx[0] = UINT16_MAX;
|
|
}
|
|
}
|
|
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;
|
|
}
|