/* * 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 #include #include #include #include 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; }