diff --git a/src/evaluate.c b/src/evaluate.c index bbb4171..c510e5e 100644 --- a/src/evaluate.c +++ b/src/evaluate.c @@ -18,6 +18,7 @@ #include #include #include +#include static inline int64_t now_ms(void) { struct timespec ts; @@ -25,6 +26,18 @@ static inline int64_t now_ms(void) { return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } +/* ── Copied from executor.c for paper-trade simulation ── */ +static double apply_fee_hold(double vol, double fee_rate, bool next_is_buy) { + if (next_is_buy && fee_rate > 0) + vol /= (1.0 + fee_rate); + return vol; +} +static double apply_increment_floor(double vol, double inc) { + if (inc > 0) + vol = floor(vol / inc - 1e-12) * inc; + return vol; +} + void evaluator_init(evaluator_t *ev, const triangle_set_t *triangles, const order_book_t *books, const config_t *cfg, spsc_queue_t *queue, bool kcs_discount) { @@ -37,7 +50,6 @@ void evaluator_init(evaluator_t *ev, const triangle_set_t *triangles, ev->stats.best_net_bps = -1e18; ev->stats.worst_net_bps = 1e18; ev->stats.best_triangle_key[0] = '\0'; - memset(ev->last_signal_ts_ms, 0, sizeof(ev->last_signal_ts_ms)); } /* @@ -54,8 +66,10 @@ void evaluator_init(evaluator_t *ev, const triangle_set_t *triangles, * (converted back to starting quote via inverse cumulative product) * 6. Apply exchange precision rounding: * - floor() for quantities (base size) - * - ceil() for quote costs (must cover the order) + * - ceil() for quote costs (must cover the full cost) * - Adjust by 1e-12 epsilon to avoid floating-point boundary errors + * e.g. ceil(value / qi - 1e-12) ensures that 0.10000000000000001 doesn't + * round up to 0.10000001 when qi = 0.01 * 7. Check base_min_size constraints in live mode * 8. Push signal to queue with full leg/order params * @@ -122,12 +136,10 @@ bool evaluate_symbol(evaluator_t *ev, uint16_t symbol_idx, int64_t t_sock_arrive double rate; double max_input; if (use_bid) { - // Sell: we receive base, sell at bid -> output = bid_price if (bk->bid_count == 0) { valid = false; break; } rate = bk->bids[0][0]; max_input = bk->bids[0][1]; } else { - // Buy: we input quote, buy at ask -> rate = 1/ask_price (quote-to-base conversion) if (bk->ask_count == 0) { valid = false; break; } double ask_price = bk->asks[0][0]; if (ask_price <= 0.0) { valid = false; break; } @@ -141,9 +153,6 @@ bool evaluate_symbol(evaluator_t *ev, uint16_t symbol_idx, int64_t t_sock_arrive double leg_mult = rate * ff; cumulative *= leg_mult; - // max_v0_list[leg]: how much starting quote can pass through this leg - // Divide by cumulative_mult (product of prior leg multipliers) to - // convert this leg's max_input back to starting-quote-equivalent volume if (cumulative_mult > 0) { max_v0_list[leg] = max_input / cumulative_mult; } @@ -250,81 +259,177 @@ bool evaluate_symbol(evaluator_t *ev, uint16_t symbol_idx, int64_t t_sock_arrive signal_entry_t sig; memset(&sig, 0, sizeof(sig)); - /* - * Compute per-leg order parameters at max_volume scale with exchange precision. - * - * Precision rounding strategy: - * - Base size: floor(base_increment) — we cannot trade a fraction of a step - * - Quote cost: ceil(quote_increment) — must cover the full cost - * - 1e-12 epsilon guards against floating-point truncation at increment boundaries - * e.g. ceil(value / qi - 1e-12) ensures that 0.10000000000000001 doesn't - * round up to 0.10000001 when qi = 0.01 - * - * Buy leg: input = quote, output = base - * quote_cost = ceil(leg_input / qi - eps) * qi - * net = quote_cost * ff - * base = floor(net / price / bi + eps) * bi - * final_quote = ceil(base * price / qi - eps) * qi (re-check) - * - * Sell leg: input = base, output = quote - * base = floor(leg_input / bi + eps) * bi - * gross = ceil(base * price / qi - eps) * qi - * net = gross * ff - */ - double leg_input = max_volume; - double leg_quote_vol[3] = {0}; - double leg_base_size[3] = {0}; - for (int leg = 0; leg < 3; leg++) { - const order_book_t *bk = books_arr[leg]; - double bi = tri->base_increment[leg]; - double qi = tri->quote_increment[leg]; - double ff = tri->fee_factor[leg]; - bool is_buy = !tri->use_bid[leg]; - double price = is_buy ? bk->asks[0][0] : bk->bids[0][0]; - double leg_output; + /* Set leg 0 order_param from max_volume (used by the paper simulation) */ + { + double fi0 = tri->funds_increment[0]; + double qi0 = tri->quote_increment[0]; + double bi0 = tri->base_increment[0]; + double ff0 = tri->fee_factor[0]; + if (fi0 <= 0) fi0 = qi0; + bool is_buy0 = !tri->use_bid[0]; + double price0 = is_buy0 ? books_arr[0]->asks[0][0] : books_arr[0]->bids[0][0]; - if (is_buy) { - double fi = tri->funds_increment[leg]; - if (fi <= 0) fi = qi; - double quote_input = (qi > 0) ? floor(leg_input / qi - 1e-12) * qi : leg_input; - double net = quote_input * ff; - double base = (bi > 0) ? floor(net / price / bi + 1e-12) * bi : (net / price); - double quote_cost = (fi > 0) ? floor(base * price / fi - 1e-12) * fi : (base * price); - leg_quote_vol[leg] = quote_cost; - sig.legs.legs[leg].quote_volume = quote_cost; - leg_base_size[leg] = base; - leg_output = base; + if (is_buy0) { + double quote_input = floor(max_volume / qi0 - 1e-12) * qi0; + double base = floor(quote_input * ff0 / price0 / bi0 - 1e-12) * bi0; + double quote_cost = floor(base * price0 / fi0 - 1e-12) * fi0; + sig.legs.legs[0].quote_volume = quote_cost; } else { - double base = (bi > 0) ? floor(leg_input / bi + 1e-12) * bi : leg_input; - double gross = (qi > 0) ? floor(base * price / qi - 1e-12) * qi : (base * price); - leg_quote_vol[leg] = gross; - sig.legs.legs[leg].quote_volume = gross; - leg_base_size[leg] = base; - leg_output = gross * ff; + double base = floor(max_volume / bi0 - 1e-12) * bi0; + sig.legs.legs[0].quote_volume = floor(base * price0 / qi0 - 1e-12) * qi0; } + sig.legs.legs[0].base_increment = bi0; + sig.legs.legs[0].quote_increment = qi0; + sig.legs.legs[0].funds_increment = fi0; + sig.legs.legs[0].fee_rate = 1.0 - ff0; + strncpy(sig.legs.legs[0].symbol, tri->symbol_names[0], SYMBOL_NAME_LEN); + strncpy(sig.legs.legs[0].fee_currency, tri->fee_currency[0], CURRENCY_NAME_LEN); + snprintf(sig.legs.legs[0].order_param, sizeof(sig.legs.legs[0].order_param), "%.8g", + sig.legs.legs[0].quote_volume); + if (!tri->use_bid[0]) strncpy(sig.legs.legs[0].side, "buy", 5); + else strncpy(sig.legs.legs[0].side, "sell", 5); + } + /* Set legs 1-2 data needed for the simulation */ + for (int l = 1; l < 3; l++) { + strncpy(sig.legs.legs[l].symbol, tri->symbol_names[l], SYMBOL_NAME_LEN); + strncpy(sig.legs.legs[l].fee_currency, tri->fee_currency[l], CURRENCY_NAME_LEN); + sig.legs.legs[l].base_increment = tri->base_increment[l]; + sig.legs.legs[l].quote_increment = tri->quote_increment[l]; + sig.legs.legs[l].funds_increment = tri->funds_increment[l]; + sig.legs.legs[l].fee_rate = 1.0 - tri->fee_factor[l]; + if (!tri->use_bid[l]) strncpy(sig.legs.legs[l].side, "buy", 5); + else strncpy(sig.legs.legs[l].side, "sell", 5); + } - /* Floor leg_output to the next leg's input increment so the - cascade always produces valid order parameters downstream. - For the last leg (leg 2) there is no next leg to constrain. */ - if (leg < 2) { - bool next_buy = !tri->use_bid[leg + 1]; - double next_incr = next_buy - ? tri->quote_increment[leg + 1] - : tri->base_increment[leg + 1]; - if (next_incr > 0) { - leg_output = floor(leg_output / next_incr - 1e-12) * next_incr; + /* Paper-trade simulation: exact copy of executor.c */ + double leg_output[3] = {0}; + double fills[3][6] = {{0}}; + bool sim_ok = true; + { + for (int leg = 0; leg < 3; leg++) { + signal_leg_t *sl = &sig.legs.legs[leg]; + bool is_buy = (strcmp(sl->side, "buy") == 0); + + double input_vol; + if (leg == 0) { + input_vol = atof(sl->order_param); + input_vol = apply_increment_floor(input_vol, + is_buy ? (sl->funds_increment > 0 ? sl->funds_increment : sl->quote_increment) : sl->base_increment); + } else { + input_vol = leg_output[leg - 1]; + input_vol = apply_fee_hold(input_vol, sl->fee_rate, is_buy); + input_vol = apply_increment_floor(input_vol, + is_buy ? (sl->funds_increment > 0 ? sl->funds_increment : sl->quote_increment) : sl->base_increment); + } + fills[leg][4] = input_vol; + + double total_size = 0, total_funds = 0, avg_price = 0, total_fee = 0; + { + const order_book_t *bk = books_arr[leg]; + if (is_buy && bk->ask_count > 0) { + double ask_price = bk->asks[0][0]; + total_size = input_vol / ask_price; + if (sl->base_increment > 0) + total_size = floor(total_size / sl->base_increment - 1e-12) * sl->base_increment; + if (sl->quote_increment > 0) + total_funds = floor(total_size * ask_price / sl->quote_increment - 1e-12) * sl->quote_increment; + else + total_funds = total_size * ask_price; + avg_price = ask_price; + } else if (!is_buy && bk->bid_count > 0) { + double bid_price = bk->bids[0][0]; + total_size = input_vol; + if (sl->base_increment > 0) + total_size = floor(total_size / sl->base_increment - 1e-12) * sl->base_increment; + if (sl->quote_increment > 0) + total_funds = floor(total_size * bid_price / sl->quote_increment - 1e-12) * sl->quote_increment; + else + total_funds = total_size * bid_price; + avg_price = bid_price; + } else { + ev->stats.triangles_skipped++; + sim_ok = false; + break; + } + if (sl->fee_rate > 0) { + double output_amt = is_buy ? total_size : total_funds; + total_fee = output_amt * sl->fee_rate; + } + } + + leg_output[leg] = is_buy ? total_size : total_funds; + fills[leg][0] = leg_output[leg]; + fills[leg][1] = avg_price; + fills[leg][2] = total_fee; + fills[leg][3] = total_funds; + + if (leg < 2) { + signal_leg_t *nsl = &sig.legs.legs[leg + 1]; + bool nxt_buy = (strcmp(nsl->side, "buy") == 0); + leg_output[leg] = apply_fee_hold(leg_output[leg], nsl->fee_rate, nxt_buy); + leg_output[leg] = apply_increment_floor(leg_output[leg], + nxt_buy ? (nsl->funds_increment > 0 ? nsl->funds_increment : nsl->quote_increment) : nsl->base_increment); } } - - leg_input = leg_output; } - sig.starting_volume = leg_quote_vol[0]; + if (!sim_ok) continue; + + /* PnL: exact copy of executor.c */ + double profit = 0; + net_bps = 0; + if (fills[2][0] > 0) { + double leg0_in = fills[0][4]; + double leg2_out = fills[2][0]; + { + const signal_leg_t *sl2 = &sig.legs.legs[2]; + bool leg2_buy = (strcmp(sl2->side, "buy") == 0); + const char *pair = sl2->symbol; + const char *dash = strchr(pair, '-'); + char base_ccy[16] = {0}, quote_ccy[16] = {0}; + if (dash) { + size_t blen = (size_t)(dash - pair); + if (blen > 15) blen = 15; + memcpy(base_ccy, pair, blen); + strncpy(quote_ccy, dash + 1, 15); + } + const char *out_ccy = leg2_buy ? base_ccy : quote_ccy; + if (out_ccy[0] && strcmp(sl2->fee_currency, out_ccy) == 0) { + leg2_out -= fills[2][2]; + } + } + profit = leg2_out - leg0_in; + if (leg0_in > 0) + net_bps = (profit / leg0_in) * 10000.0; + } + sig.predicted_bps = net_bps; + + if (net_bps > ev->stats.best_net_bps) { + ev->stats.best_net_bps = net_bps; + snprintf(ev->stats.best_triangle_key, sizeof(ev->stats.best_triangle_key), + "%s/%s/%s", tri->base, tri->mid, tri->quote); + } + if (net_bps < ev->stats.worst_net_bps) ev->stats.worst_net_bps = net_bps; + if (last_status_ms == 0 || now - last_status_ms >= 30000) { + last_status_ms = now; + log_write_screen("[STATUS] evals=%lu signals=%lu " + "best=%.2f bps (%s) | %u triangles\n", + (unsigned long)ev->stats.triangles_evaluated, + (unsigned long)ev->stats.signals_fired, + ev->stats.best_net_bps, ev->stats.best_triangle_key, + tris->triangle_count); + } + + if (net_bps <= cfg->signal_threshold_bps) { + ev->stats.triangles_skipped++; + continue; + } + + sig.starting_volume = fills[0][3]; sig.live = cfg->live_mode; snprintf(sig.triangle_key, sizeof(sig.triangle_key), "%s/%s/%s", tri->base, tri->mid, tri->quote); strncpy(sig.primary_quote, tri->base, CURRENCY_NAME_LEN); - sig.predicted_bps = net_bps; snprintf(sig.max_volume, sizeof(sig.max_volume), "%.8g", max_volume); sig.ts_ms = now; sig.book_ts_ms = book_ts_ms; @@ -367,12 +472,11 @@ bool evaluate_symbol(evaluator_t *ev, uint16_t symbol_idx, int64_t t_sock_arrive bool use_bid = tri->use_bid[leg]; bool is_buy = !use_bid; - // order_param: for buys the param is quote volume, for sells it's base size if (is_buy) { snprintf(sl->order_param, sizeof(sl->order_param), "%.8g", sig.legs.legs[leg].quote_volume); } else { - snprintf(sl->order_param, sizeof(sl->order_param), "%.8g", leg_base_size[leg]); + snprintf(sl->order_param, sizeof(sl->order_param), "%.8g", fills[leg][4]); } sl->base_increment = tri->base_increment[leg]; sl->quote_increment = tri->quote_increment[leg]; @@ -380,12 +484,10 @@ bool evaluate_symbol(evaluator_t *ev, uint16_t symbol_idx, int64_t t_sock_arrive sl->base_min_size = tri->base_min_size[leg]; if (use_bid) { - // Hit the bid: we sell base, receive quote strncpy(sl->input_currency, base_cur, CURRENCY_NAME_LEN); strncpy(sl->output_currency, quote_cur, CURRENCY_NAME_LEN); strncpy(sl->side, "sell", 5); } else { - // Hit the ask: we buy base, pay quote strncpy(sl->input_currency, quote_cur, CURRENCY_NAME_LEN); strncpy(sl->output_currency, base_cur, CURRENCY_NAME_LEN); strncpy(sl->side, "buy", 5); @@ -399,7 +501,7 @@ bool evaluate_symbol(evaluator_t *ev, uint16_t symbol_idx, int64_t t_sock_arrive ev->stats.signals_fired++; ev->stats.last_eval_ts_ms = now; log_write("[SIGNAL] %.4f bps vol=%s | %s (%s, %s, %s)\n", - net_bps, sig.max_volume, sig.triangle_key, + sig.predicted_bps, sig.max_volume, sig.triangle_key, sig.legs.legs[0].symbol, sig.legs.legs[1].symbol, sig.legs.legs[2].symbol); fired_any = true; } else {