258 lines
11 KiB
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;
|
|
}
|