triangular_arbitrage_bot/src/config.c

258 lines
11 KiB
C

/*
* config.c - libyaml-based config parser for fused_engine
*
* Parses config.yaml, supporting three levels:
* - Top level: api keys, live mode flag
* - fused_engine section: thresholds, symbols, hold currencies, connection params
* - executor section: ignored (consumed by separate executor binary)
*
* Uses a state machine (parse_state_t) tracking section, mapping depth,
* sequence membership (symbols/hold/excluded lists), and key/value alternation.
*/
#include "log.h"
#include "config.h"
#include <yaml.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
static void copy_string(const char *src, char *dst, size_t dst_len) {
if (!src) return;
strncpy(dst, src, dst_len - 1);
dst[dst_len - 1] = '\0';
}
typedef struct {
config_t *cfg;
char section[32]; // current YAML section name (e.g. "fused_engine")
char current_key[64]; // last seen scalar key
bool in_symbols; // inside fused_engine.symbols sequence
bool in_hold; // inside fused_engine.hold_currencies
bool in_excluded; // inside fused_engine.excluded_currencies
bool in_capital_map; // inside fused_engine.initial_capital mapping
char capital_currency[CURRENCY_NAME_LEN]; // current currency key in capital map
bool expect_key; // true = next scalar is a key, false = next is a value
int mapping_depth; // tracks nesting depth for section detection
} parse_state_t;
/*
* Dispatch a parsed key-value pair to the appropriate config field.
* Section determines which field group to look up.
*/
static void handle_value(parse_state_t *st, const char *val) {
const char *key = st->current_key;
if (strcmp(st->section, "fused_engine") == 0) {
if (st->in_symbols) {
if (st->cfg->symbol_count < MAX_SYMBOLS) {
copy_string(val, st->cfg->symbols[st->cfg->symbol_count], SYMBOL_NAME_LEN);
st->cfg->symbol_count++;
}
} else if (st->in_hold) {
if (st->cfg->hold_currency_count < MAX_HOLD_CURRENCIES) {
copy_string(val, st->cfg->hold_currencies[st->cfg->hold_currency_count], CURRENCY_NAME_LEN);
st->cfg->hold_currency_count++;
}
} else if (st->in_excluded) {
if (st->cfg->excluded_currency_count < MAX_EXCLUDED_CURRENCIES) {
copy_string(val, st->cfg->excluded_currencies[st->cfg->excluded_currency_count], CURRENCY_NAME_LEN);
st->cfg->excluded_currency_count++;
}
} else if (strcmp(key, "log_level") == 0) {
copy_string(val, st->cfg->log_level, sizeof(st->cfg->log_level));
} else if (strcmp(key, "socket_path") == 0) {
copy_string(val, st->cfg->socket_path, sizeof(st->cfg->socket_path));
} else if (strcmp(key, "rest_host") == 0) {
copy_string(val, st->cfg->rest_host, sizeof(st->cfg->rest_host));
} else if (strcmp(key, "rest_port") == 0) {
st->cfg->rest_port = atoi(val);
} else if (strcmp(key, "ws_url") == 0) {
copy_string(val, st->cfg->ws_url, sizeof(st->cfg->ws_url));
} else if (strcmp(key, "token_url") == 0) {
copy_string(val, st->cfg->token_url, sizeof(st->cfg->token_url));
} else if (strcmp(key, "reconnect_base_delay") == 0) {
st->cfg->reconnect_base_delay = atof(val);
} else if (strcmp(key, "reconnect_max_delay") == 0) {
st->cfg->reconnect_max_delay = atof(val);
} else if (strcmp(key, "heartbeat_interval") == 0) {
st->cfg->heartbeat_interval = atof(val);
} else if (strcmp(key, "signal_threshold_bps") == 0) {
st->cfg->signal_threshold_bps = atof(val);
} else if (strcmp(key, "kcs_discount_active") == 0) {
st->cfg->kcs_discount_active = (strcmp(val, "true") == 0 || strcmp(val, "yes") == 0);
} else if (strcmp(key, "executor_socket_path") == 0) {
copy_string(val, st->cfg->executor_socket_path, sizeof(st->cfg->executor_socket_path));
} else if (strcmp(key, "send_signals") == 0) {
st->cfg->send_signals = (strcmp(val, "true") == 0 || strcmp(val, "yes") == 0);
} else if (strcmp(key, "cooldown_seconds") == 0) {
st->cfg->cooldown_seconds = atof(val);
} else if (strcmp(key, "stats_interval_seconds") == 0) {
st->cfg->stats_interval_seconds = atof(val);
}
} else if (strcmp(st->section, "executor") == 0) {
return;
} else if (st->section[0] == '\0') {
// Top-level keys (outside any named section)
if (strcmp(key, "kucoin_api_key") == 0) {
copy_string(val, st->cfg->kucoin_api_key, sizeof(st->cfg->kucoin_api_key));
} else if (strcmp(key, "kucoin_api_secret") == 0) {
copy_string(val, st->cfg->kucoin_api_secret, sizeof(st->cfg->kucoin_api_secret));
} else if (strcmp(key, "kucoin_api_passphrase") == 0) {
copy_string(val, st->cfg->kucoin_api_passphrase, sizeof(st->cfg->kucoin_api_passphrase));
} else if (strcmp(key, "live_mode") == 0) {
st->cfg->live_mode = (strcmp(val, "true") == 0 || strcmp(val, "yes") == 0);
}
}
}
/*
* Load and parse a YAML config file into config_t.
* Uses the libyaml pull-parser API with a state machine to track:
* - Section nesting (mapping_depth)
* - Key/value alternation within mappings
* - Sequence membership for arrays (symbols, hold_currencies, excluded_currencies)
*
* Sets sensible defaults before parsing so the file only needs to override.
*/
int config_load(const char *path, config_t *cfg) {
if (!path || !cfg) return -1;
memset(cfg, 0, sizeof(config_t));
copy_string("INFO", cfg->log_level, sizeof(cfg->log_level));
copy_string("/tmp/fh_ob.sock", cfg->socket_path, sizeof(cfg->socket_path));
copy_string("0.0.0.0", cfg->rest_host, sizeof(cfg->rest_host));
cfg->rest_port = 8000;
copy_string("wss://ws-api-spot.kucoin.com", cfg->ws_url, sizeof(cfg->ws_url));
copy_string("https://api.kucoin.com/api/v1/bullet-public", cfg->token_url, sizeof(cfg->token_url));
cfg->reconnect_base_delay = 1.0;
cfg->reconnect_max_delay = 60.0;
cfg->heartbeat_interval = 18.0;
cfg->signal_threshold_bps = 0.2;
copy_string("USDT", cfg->hold_currencies[0], CURRENCY_NAME_LEN);
cfg->hold_currency_count = 1;
cfg->kcs_discount_active = false;
copy_string("/tmp/executor.sock", cfg->executor_socket_path, sizeof(cfg->executor_socket_path));
cfg->send_signals = false;
cfg->cooldown_seconds = 0.0;
cfg->stats_interval_seconds = 60.0;
cfg->live_mode = false;
FILE *f = fopen(path, "r");
if (!f) {
log_write("config: cannot open '%s'\n", path);
return -1;
}
yaml_parser_t parser;
if (!yaml_parser_initialize(&parser)) {
fclose(f);
return -1;
}
yaml_parser_set_input_file(&parser, f);
parse_state_t st = {0};
st.cfg = cfg;
st.expect_key = true; // YAML mapping starts with a key
while (1) {
yaml_event_t event;
if (!yaml_parser_parse(&parser, &event)) break;
switch (event.type) {
case YAML_STREAM_END_EVENT:
yaml_event_delete(&event);
goto done;
case YAML_SCALAR_EVENT: {
char *s = (char *)event.data.scalar.value;
if (st.in_capital_map) {
if (st.expect_key) {
strncpy(st.capital_currency, s, CURRENCY_NAME_LEN - 1);
st.expect_key = false;
} else {
if (st.cfg->initial_capital_count < MAX_CAPITAL_ENTRIES) {
strncpy(st.cfg->initial_capital[st.cfg->initial_capital_count].currency,
st.capital_currency, CURRENCY_NAME_LEN - 1);
st.cfg->initial_capital[st.cfg->initial_capital_count].amount = atof(s);
st.cfg->initial_capital_count++;
}
st.expect_key = true;
}
} else if (st.expect_key) {
strncpy(st.current_key, s, sizeof(st.current_key) - 1);
st.expect_key = false;
} else {
handle_value(&st, s);
if (!st.in_symbols && !st.in_hold && !st.in_excluded) {
st.expect_key = true;
}
}
break;
}
case YAML_MAPPING_START_EVENT:
// Root mapping is depth 0; depth 1 mappings are named sections
if (st.mapping_depth == 1 &&
(strcmp(st.current_key, "fused_engine") == 0 ||
strcmp(st.current_key, "executor") == 0)) {
strncpy(st.section, st.current_key, sizeof(st.section) - 1);
}
// Nested mapping inside fused_engine section (depth 2)
if (st.mapping_depth == 2 && strcmp(st.section, "fused_engine") == 0 &&
strcmp(st.current_key, "initial_capital") == 0) {
st.in_capital_map = true;
st.cfg->initial_capital_count = 0;
st.expect_key = true;
}
st.mapping_depth++;
st.expect_key = true;
break;
case YAML_MAPPING_END_EVENT:
st.mapping_depth--;
if (st.in_capital_map && st.mapping_depth <= 2) {
st.in_capital_map = false;
}
if (st.mapping_depth < 2) {
st.section[0] = '\0'; // exited a named section
}
st.current_key[0] = '\0';
break;
case YAML_SEQUENCE_START_EVENT:
if (strcmp(st.section, "fused_engine") == 0 && strcmp(st.current_key, "symbols") == 0) {
st.in_symbols = true;
} else if (strcmp(st.section, "fused_engine") == 0 && strcmp(st.current_key, "hold_currencies") == 0) {
st.in_hold = true;
st.cfg->hold_currency_count = 0; // override default
} else if (strcmp(st.section, "fused_engine") == 0 && strcmp(st.current_key, "excluded_currencies") == 0) {
st.in_excluded = true;
st.cfg->excluded_currency_count = 0;
}
st.expect_key = false; // sequence items are values
break;
case YAML_SEQUENCE_END_EVENT:
st.in_symbols = false;
st.in_hold = false;
st.in_excluded = false;
st.expect_key = true;
break;
default:
break;
}
yaml_event_delete(&event);
}
done:
yaml_parser_delete(&parser);
fclose(f);
return 0;
}