/**
* monitor_polymarket.cpp
* ----------------------
* Monitor completo con:
* - Keystore encriptado (AES-256-CBC + PBKDF2 via OpenSSL)
* - Autenticación L1 (EIP-712) para obtener API credentials
* - Autenticación L2 (HMAC-SHA256) para firmar cada orden
* - Ejecución automática de cobertura al detectar mercado
* - Cursor createdAfter, histórico persistente, percentil adaptativo
*
* Dependencias:
* - nlohmann/json [Link]
* - cpr [Link]
* - OpenSSL (libssl-dev en Ubuntu / brew install openssl en macOS)
*
* Compilar:
* g++ -std=c++20 -O2 monitor_polymarket.cpp \
* -lcpr -lcurl -lssl -lcrypto \
* -I/path/to/nlohmann \
* -o monitor
*
* Primer uso (crear keystore):
* ./monitor --create-keystore
*
* Uso normal:
* ./monitor
*/
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <deque>
#include <set>
#include <string>
#include <chrono>
#include <thread>
#include <cmath>
#include <numeric>
#include <algorithm>
#include <stdexcept>
#include <ctime>
#include <iomanip>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include <nlohmann/[Link]>
#include <cpr/cpr.h>
using json = nlohmann::json;
// ==================== Configuración ====================
constexpr int POLLING_SEC = 30;
constexpr double PAGO_BASE = 100.0; // USDC por mercado — ajustar con
cuidado
constexpr double SPREAD_MIN = 0.03;
constexpr double ARBITRAJE_UMBRAL = 0.98; // suma_precios < esto → arbitraje
(fees 2%)
constexpr int MAX_DIAS_RESOLUCION = 7;
constexpr int RESUMEN_CADA = 10;
constexpr int COLA_ROI_MAX = 100;
constexpr int MIN_MUESTRAS_HIST = 50;
constexpr int PERCENTIL_VOLUMEN = 30;
constexpr double VOLUMEN_MAX_INICIAL = 10'000.0;
constexpr int API_LIMIT = 20;
constexpr double PRICE_SLIPPAGE_MAX = 0.02; // precio live puede subir máx 2% vs
Gamma antes de abortar
const std::string KEYSTORE_FILE = "[Link]";
const std::string CREDS_FILE = "api_creds.json";
const std::string HISTORICO_FILE = "mercados_historial.json";
const std::string LOG_FILE = "[Link]";
const std::string GAMMA_API = "[Link]
const std::string CLOB_API = "[Link]
// ==================== Logging ====================
std::ofstream g_logfile;
std::string timestamp_str() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::ostringstream ss;
ss << std::put_time(std::gmtime(&t), "%Y-%m-%d %H:%M:%S UTC");
return [Link]();
}
enum class LogLevel { INFO, WARNING, ERR };
void log_msg(LogLevel level, const std::string& msg) {
const char* tag = (level == LogLevel::WARNING) ? "[WARNING]" :
(level == LogLevel::ERR) ? "[ERROR] " : "[INFO] ";
std::string line = timestamp_str() + " " + tag + " " + msg;
std::cout << line << "\n";
if (g_logfile.is_open()) g_logfile << line << "\n" << std::flush;
}
// ==================== Utilidades de bytes ====================
std::string bytes_to_hex(const std::vector<uint8_t>& b) {
std::ostringstream ss;
ss << std::hex << std::setfill('0');
for (auto c : b) ss << std::setw(2) << (int)c;
return [Link]();
}
std::vector<uint8_t> hex_to_bytes(const std::string& hex) {
std::vector<uint8_t> out;
for (size_t i = 0; i + 1 < [Link](); i += 2)
out.push_back((uint8_t)std::stoi([Link](i, 2), nullptr, 16));
return out;
}
std::string base64_encode(const std::vector<uint8_t>& data) {
static const char* T =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
int val = 0, valb = -6;
for (uint8_t c : data) {
val = (val << 8) + c; valb += 8;
while (valb >= 0) { out.push_back(T[(val >> valb) & 0x3F]); valb -= 6; }
}
if (valb > -6) out.push_back(T[((val << 8) >> (valb + 8)) & 0x3F]);
while ([Link]() % 4) out.push_back('=');
return out;
}
// ==================== Keystore (AES-256-CBC + PBKDF2-SHA256)
====================
json crear_keystore(const std::string& pk_hex, const std::string& passphrase) {
std::vector<uint8_t> salt(32), iv(16);
RAND_bytes([Link](), (int)[Link]());
RAND_bytes([Link](), (int)[Link]());
// Derivar clave
std::vector<uint8_t> dk(32);
PKCS5_PBKDF2_HMAC(passphrase.c_str(), (int)[Link](),
[Link](), (int)[Link](), 262144,
EVP_sha256(), (int)[Link](), [Link]());
// Cifrar
auto pk = hex_to_bytes(pk_hex);
std::vector<uint8_t> ct([Link]() + 16);
int l1 = 0, l2 = 0;
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, [Link](), [Link]());
EVP_EncryptUpdate(ctx, [Link](), &l1, [Link](), (int)[Link]());
EVP_EncryptFinal_ex(ctx, [Link]() + l1, &l2);
EVP_CIPHER_CTX_free(ctx);
[Link](l1 + l2);
// MAC para verificar integridad
std::vector<uint8_t> mac_key([Link]() + 16, [Link]());
unsigned char mac[32]; unsigned int mac_len = 32;
HMAC(EVP_sha256(), mac_key.data(), (int)mac_key.size(),
[Link](), (int)[Link](), mac, &mac_len);
return {
{"version", 1},
{"crypto", {
{"cipher", "aes-256-cbc"},
{"ciphertext", bytes_to_hex(ct)},
{"iv", bytes_to_hex(iv)},
{"kdf", "pbkdf2"},
{"kdfparams", {{"salt", bytes_to_hex(salt)}, {"iterations", 262144}}},
{"mac", bytes_to_hex(std::vector<uint8_t>(mac, mac+32))}
}}
};
}
std::string descifrar_keystore(const json& ks, const std::string& passphrase) {
auto& cr = ks["crypto"];
auto salt = hex_to_bytes(cr["kdfparams"]["salt"].get<std::string>());
auto iv = hex_to_bytes(cr["iv"].get<std::string>());
auto ct = hex_to_bytes(cr["ciphertext"].get<std::string>());
auto mac_ok = hex_to_bytes(cr["mac"].get<std::string>());
int iters = cr["kdfparams"]["iterations"].get<int>();
std::vector<uint8_t> dk(32);
PKCS5_PBKDF2_HMAC(passphrase.c_str(), (int)[Link](),
[Link](), (int)[Link](), iters,
EVP_sha256(), (int)[Link](), [Link]());
// Verificar MAC
std::vector<uint8_t> mac_key([Link]() + 16, [Link]());
unsigned char mac[32]; unsigned int mac_len = 32;
HMAC(EVP_sha256(), mac_key.data(), (int)mac_key.size(),
[Link](), (int)[Link](), mac, &mac_len);
if (std::vector<uint8_t>(mac, mac+32) != mac_ok)
throw std::runtime_error("Passphrase incorrecta o keystore corrupto");
// Descifrar
std::vector<uint8_t> pt([Link]());
int l1 = 0, l2 = 0;
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, [Link](), [Link]());
EVP_DecryptUpdate(ctx, [Link](), &l1, [Link](), (int)[Link]());
EVP_DecryptFinal_ex(ctx, [Link]() + l1, &l2);
EVP_CIPHER_CTX_free(ctx);
[Link](l1 + l2);
return bytes_to_hex(pt);
}
// ==================== HMAC-SHA256 (L2) ====================
std::string hmac_sha256_b64(const std::string& key, const std::string& msg) {
unsigned char d[32]; unsigned int dlen = 32;
HMAC(EVP_sha256(),
[Link](), (int)[Link](),
reinterpret_cast<const unsigned char*>([Link]()), (int)[Link](),
d, &dlen);
return base64_encode(std::vector<uint8_t>(d, d+32));
}
// ==================== API Credentials ====================
struct ApiCreds {
std::string api_key;
std::string secret;
std::string passphrase;
std::string address;
};
// Headers L2 para cada request
cpr::Header l2_headers(const ApiCreds& c,
const std::string& method,
const std::string& path,
const std::string& body = "") {
auto now = std::chrono::system_clock::now();
std::string ts = std::to_string(
std::chrono::duration_cast<std::chrono::seconds>(
now.time_since_epoch()).count());
std::string sig = hmac_sha256_b64([Link], ts + method + path + body);
return cpr::Header{
{"POLY-API-KEY", c.api_key},
{"POLY-PASSPHRASE", [Link]},
{"POLY-TIMESTAMP", ts},
{"POLY-SIGNATURE", sig},
{"Content-Type", "application/json"}
};
}
// Carga creds desde archivo (generadas una sola vez con py-clob-client)
ApiCreds cargar_creds(const std::string& address) {
std::ifstream f(CREDS_FILE);
if (![Link]()) {
// Instrucciones para generar creds con Python (una sola vez):
// pip install py-clob-client
// python3 -c "
// from py_clob_client.client import ClobClient
// import os, json
// c = ClobClient('[Link] chain_id=137,
// key=[Link]['PRIVATE_KEY'])
// creds = c.create_or_derive_api_creds()
// print([Link]({'apiKey': creds.api_key,
// 'secret': creds.api_secret,
// 'passphrase': creds.api_passphrase}))
// " > api_creds.json
throw std::runtime_error(
"No se encontró api_creds.json.\n"
"Genera las credenciales L2 con py-clob-client (ver comentario en código)\n"
"y guárdalas como: {\"apiKey\":\"...\",\"secret\":\"...\",\"passphrase\":\"...\"}");
}
json j; f >> j;
log_msg(LogLevel::INFO, "API credentials L2 cargadas.");
return {
j["apiKey"].get<std::string>(),
j["secret"].get<std::string>(),
j["passphrase"].get<std::string>(),
address
};
}
// ==================== Estructuras ====================
struct Cobertura {
std::vector<double> precios;
std::vector<double> apuestas;
double inversion_total = 0;
double retorno_minimo = 0;
double ganancia_neta = 0;
double roi = 0;
};
struct Mercado {
std::string slug, question, createdAt, endDate;
std::vector<std::string> token_ids; // uno por outcome
double volumeNum = 0;
std::vector<double> precios;
double suma_precios = 0;
bool hay_arbitraje = false;
Cobertura cobertura;
};
// ==================== Cobertura ====================
Cobertura calcular_cobertura(const std::vector<double>& precios, double pago) {
Cobertura c;
[Link] = precios;
std::vector<double> pesos([Link]());
double sp = 0;
for (size_t i = 0; i < [Link](); ++i) { pesos[i] = 1.0/precios[i]; sp += pesos[i]; }
[Link]([Link]());
c.inversion_total = 0;
std::vector<double> ret([Link]());
for (size_t i = 0; i < [Link](); ++i) {
[Link][i] = (pesos[i]/sp)*pago;
c.inversion_total += [Link][i];
ret[i] = [Link][i]/precios[i];
}
c.retorno_minimo = *std::min_element([Link](), [Link]());
c.ganancia_neta = c.retorno_minimo - c.inversion_total;
[Link] = (c.inversion_total > 0) ? (c.ganancia_neta/c.inversion_total)*100.0 : 0;
return c;
}
// ==================== Verificación live de precios (/price) ====================
// GET /price?token_id=X&side=BUY → precio real en el libro en este momento
// No usa /book (bug conocido con datos stale) — usa /price que es el endpoint correcto
double precio_live(const std::string& token_id) {
auto r = cpr::Get(
cpr::Url{CLOB_API + "/price"},
cpr::Parameters{{"token_id", token_id}, {"side", "BUY"}},
cpr::Timeout{5'000}
);
if (r.status_code != 200) {
log_msg(LogLevel::ERR, "GET /price fallido token=" +
token_id.substr(0,10) + " HTTP " + std::to_string(r.status_code));
return -1.0;
}
try {
auto j = json::parse([Link]);
std::string ps = [Link]("price", "");
if (![Link]()) return std::stod(ps);
} catch (...) {}
return -1.0;
}
// Verifica precios en vivo para todos los outcomes ANTES de ejecutar.
// Devuelve los precios reales si todo está OK, vector vacío si alguno falla.
// Condiciones de fallo:
// 1. /price no responde para algún outcome
// 2. precio live > precio_gamma * (1 + PRICE_SLIPPAGE_MAX) → demasiado slippage
// 3. suma de precios live >= ARBITRAJE_UMBRAL → arbitraje ya desapareció
std::vector<double> verificar_precios_live(
const std::vector<std::string>& token_ids,
const std::vector<double>& precios_gamma) {
std::vector<double> precios_live;
precios_live.reserve(token_ids.size());
double suma_live = 0.0;
for (size_t i = 0; i < token_ids.size(); ++i) {
double p = precio_live(token_ids[i]);
if (p < 0) {
log_msg(LogLevel::WARNING,
"Outcome " + std::to_string(i) + ": /price no respondió — abortando");
return {};
}
double limite = precios_gamma[i] * (1.0 + PRICE_SLIPPAGE_MAX);
if (p > limite) {
log_msg(LogLevel::WARNING,
"Outcome " + std::to_string(i) + ": slippage excesivo "
"gamma=" + std::to_string(precios_gamma[i]) +
" live=" + std::to_string(p) +
" limite=" + std::to_string(limite) + " — abortando");
return {};
}
precios_live.push_back(p);
suma_live += p;
log_msg(LogLevel::INFO,
"Outcome " + std::to_string(i) +
": gamma=" + std::to_string(precios_gamma[i]) +
" live=" + std::to_string(p) + " ✓");
}
// Verificar que el arbitraje sigue siendo válido con los precios reales
if (suma_live >= ARBITRAJE_UMBRAL) {
log_msg(LogLevel::WARNING,
"Arbitraje desapareció: suma_live=" + std::to_string(suma_live) +
" >= umbral=" + std::to_string(ARBITRAJE_UMBRAL) + " — abortando");
return {};
}
log_msg(LogLevel::INFO,
"Precios live verificados ✓ | suma_live=" + std::to_string(suma_live) +
" | arbitraje sigue vigente");
return precios_live;
}
// ==================== Ejecutar órdenes (CLOB L2) ====================
// Envía una sola orden BUY FOK. Devuelve order_id si OK, "" si falla.
std::string enviar_orden(const std::string& token_id, double precio,
double monto, const ApiCreds& creds) {
if (precio <= 0) {
log_msg(LogLevel::WARNING, "Precio inválido, abortando orden");
return "";
}
double shares = monto / precio;
shares = std::floor(shares * 1e6) / 1e6; // truncar a 6 decimales (no redondear)
if (shares <= 0) {
log_msg(LogLevel::WARNING, "Shares <= 0, abortando orden");
return "";
}
if (shares > 1e6) {
log_msg(LogLevel::WARNING, "Shares absurdas (" + std::to_string(shares) + "),
abortando orden");
return "";
}
json order = {
{"tokenID", token_id},
{"price", precio},
{"size", shares},
{"side", "BUY"},
{"orderType", "FOK"}
};
std::string body = [Link]();
std::string path = "/order";
auto r = cpr::Post(cpr::Url{CLOB_API + path},
l2_headers(creds, "POST", path, body),
cpr::Body{body}, cpr::Timeout{10'000});
if (r.status_code == 200 || r.status_code == 201) {
try {
auto resp = json::parse([Link]);
return [Link]("orderID", [Link]("id", "OK"));
} catch (...) { return "OK"; }
}
log_msg(LogLevel::ERR, "Orden fallida token=" + token_id.substr(0,10) +
"... HTTP " + std::to_string(r.status_code) + " " + [Link]);
return "";
}
// Ejecución todo-o-nada para N outcomes categóricos
// Flujo: verificar precios live → recalcular cobertura → ejecutar todos
// Si la verificación previa falla → cero órdenes enviadas, sin necesidad de rollback
bool ejecutar_cobertura(const Mercado& m, const ApiCreds& creds) {
size_t n = m.token_ids.size();
if (n < 2 || n != [Link]() || n != [Link]()) {
log_msg(LogLevel::WARNING, "Datos incompletos para: " + [Link]);
return false;
}
// — Fase 1: verificar precios live ANTES de ejecutar cualquier orden —
auto precios_live = verificar_precios_live(m.token_ids, [Link]);
if (precios_live.empty()) return false; // abortado limpiamente, sin rollback
// — Fase 2: recalcular cobertura con precios reales del libro —
Cobertura cob_live = calcular_cobertura(precios_live, PAGO_BASE);
log_msg(LogLevel::INFO,
"Cobertura recalculada con precios live | ROI=" +
std::to_string(cob_live.roi) + "% | Inversión=$" +
std::to_string(cob_live.inversion_total));
// — Fase 3: ejecutar todas las órdenes con precios live —
for (size_t i = 0; i < n; ++i) {
std::string order_id = enviar_orden(
m.token_ids[i], precios_live[i], cob_live.apuestas[i], creds);
if (order_id.empty()) {
// Caso extremadamente raro: precio se movió entre verificación y ejecución
// No hay rollback posible aquí de forma limpia — logear y alertar
log_msg(LogLevel::ERR,
"CRÍTICO: outcome " + std::to_string(i) +
" falló DESPUÉS de verificación. Revisar posición manualmente. "
"Outcomes ejecutados antes: " + std::to_string(i));
return false;
}
double shares_log = std::floor((cob_live.apuestas[i] / precios_live[i]) * 1e6) / 1e6;
log_msg(LogLevel::INFO,
"Outcome " + std::to_string(i) + "/" + std::to_string(n-1) +
" ejecutado | token=" + m.token_ids[i].substr(0,10) +
"... shares=" + std::to_string(shares_log) +
" @ " + std::to_string(precios_live[i]) +
" (USD $" + std::to_string(cob_live.apuestas[i]) + ")");
}
log_msg(LogLevel::INFO,
"✓ Cobertura completa: " + std::to_string(n) + " outcomes | " +
[Link](0, 55) +
" | ROI live: " + std::to_string(cob_live.roi) + "%" +
" | Ganancia esperada: $" + std::to_string(cob_live.ganancia_neta));
return true;
}
// ==================== Histórico / Cursor / Percentil ====================
json cargar_historico() {
if (std::ifstream f(HISTORICO_FILE); [Link]()) {
try { json j; f >> j; if (j.is_array()) return j; } catch (...) {}
}
return json::array();
}
void guardar_historico(const json& d) {
std::ofstream f(HISTORICO_FILE); f << [Link](2);
}
double umbral_volumen(const json& hist) {
std::vector<double> v;
for (auto& m : hist) { double x = [Link]("volumeNum",0.0); if (x>0) v.push_back(x); }
if ((int)[Link]() < MIN_MUESTRAS_HIST) return VOLUMEN_MAX_INICIAL;
std::sort([Link](), [Link]());
return v[static_cast<size_t>(std::floor(PERCENTIL_VOLUMEN/100.0*([Link]()-1)))];
}
std::string ultimo_created_at(const json& hist) {
std::string mx;
for (auto& m : hist) { std::string ts = [Link]("createdAt",""); if (ts>mx) mx=ts; }
if (![Link]()) return mx;
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::ostringstream ss;
ss << std::put_time(std::gmtime(&t), "%Y-%m-%dT%H:%M:%SZ");
return [Link]();
}
// ==================== Filtros ====================
json parse_campo(const json& m, const std::string& k) {
if () return json::array();
auto& v = m[k];
if (v.is_array()) return v;
if (v.is_string()) try { return json::parse([Link]<std::string>()); } catch (...) {}
return json::array();
}
double dias_hasta_resolucion(const json& m) {
for (auto& campo : {"endDate","resolutionDate","resolvedAt","end_date"}) {
if ( || m[campo].is_null()) continue;
std::string raw = m[campo].get<std::string>();
std::tm tm = {}; std::istringstream ss(raw);
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
if ([Link]()) continue;
return std::difftime(std::mktime(&tm), std::time(nullptr)) / 86400.0;
}
return -1.0;
}
bool procesar_mercado(const json& m, double umbral_vol, Mercado& out) {
double vol = 0;
try { vol = [Link]("volumeNum", 0.0); } catch (...) { return false; }
if (vol > umbral_vol) return false;
double dias = dias_hasta_resolucion(m);
if (dias <= 0 || dias > MAX_DIAS_RESOLUCION) return false;
// Categórico: requiere 2 o más outcomes mutuamente exclusivos
json outcomes = parse_campo(m, "outcomes");
json prices_r = parse_campo(m, "outcomePrices");
if ([Link]() < 2 || prices_r.size() != [Link]()) return false;
std::vector<double> precios;
try {
for (auto& p : prices_r)
precios.push_back(p.is_string()
? std::stod([Link]<std::string>()) : [Link]<double>());
} catch (...) { return false; }
// Todos los outcomes deben tener liquidez mínima
for (double p : precios)
if (p < SPREAD_MIN) return false;
// Arbitraje estructural: suma < 0.98 (margen neto después de fees del 2%)
double suma = 0; for (double p : precios) suma += p;
if (suma >= ARBITRAJE_UMBRAL) return false; // sin arbitraje, no ejecutar
// Token IDs para cada outcome
json tids = parse_campo(m, "clob_token_ids");
if ([Link]() != [Link]()) return false; // sin token_ids no podemos operar
std::vector<std::string> token_ids;
try {
for (auto& t : tids) token_ids.push_back([Link]<std::string>());
} catch (...) { return false; }
[Link] = [Link]("slug", "");
[Link] = [Link]("question", "Sin título");
[Link] = [Link]("createdAt", "");
[Link] = vol;
[Link] = precios;
out.suma_precios = suma;
out.hay_arbitraje = true; // si llegamos aquí, siempre hay arbitraje
out.token_ids = token_ids;
[Link] = calcular_cobertura(precios, PAGO_BASE);
for (auto& c : {"endDate","resolutionDate","resolvedAt","end_date"})
if ([Link](c) && !m[c].is_null()) { [Link] = m[c].get<std::string>(); break; }
return true;
}
// ==================== Modo: crear keystore ====================
void modo_crear_keystore() {
std::cout << "=== Crear keystore encriptado ===\n";
std::cout << "Private key (hex sin 0x): "; std::string pk; std::cin >> pk;
std::cout << "Passphrase de cifrado: "; std::string pass; std::cin >> pass;
std::cout << "Wallet address (EOA): "; std::string addr; std::cin >> addr;
json ks = crear_keystore(pk, pass);
ks["address"] = addr;
std::ofstream f(KEYSTORE_FILE); f << [Link](2);
std::cout << "✓ Keystore guardado en " << KEYSTORE_FILE << "\n";
std::cout << " Ejecuta: history -c (para borrar la clave del historial)\n";
}
// ==================== Main ====================
int main(int argc, char* argv[]) {
if (argc > 1 && std::string(argv[1]) == "--create-keystore") {
modo_crear_keystore();
return 0;
}
g_logfile.open(LOG_FILE, std::ios::app);
// Cargar y descifrar keystore
json keystore;
{ std::ifstream f(KEYSTORE_FILE); if (![Link]())
throw std::runtime_error("No se encontró " + KEYSTORE_FILE +
"\nEjecuta: ./monitor --create-keystore"); f >> keystore; }
std::cout << "Passphrase del keystore: ";
std::string passphrase; std::cin >> passphrase;
std::string private_key = descifrar_keystore(keystore, passphrase);
std::string address = [Link]("address", "");
log_msg(LogLevel::INFO, "Keystore OK. Address: " + address);
std::fill([Link](), [Link](), '\0');
// Cargar credenciales L2
ApiCreds creds = cargar_creds(address);
std::fill(private_key.begin(), private_key.end(), '\0');
// Estado del monitor
json historico = cargar_historico();
std::set<std::string> vistos;
for (auto& m : historico) [Link]([Link]("slug",""));
std::string ultimo_ts = ultimo_created_at(historico);
std::deque<double> cola_roi;
int ciclo=0, total_nuevos=0, total_arb=0, total_ordenes=0;
log_msg(LogLevel::INFO, "Monitor iniciado. createdAfter=" + ultimo_ts);
while (true) {
++ciclo;
double umbral = umbral_volumen(historico);
std::string max_ts = ultimo_ts;
// Consulta API Gamma
auto r = cpr::Get(
cpr::Url{GAMMA_API},
cpr::Parameters{
{"active","true"},{"closed","false"},
{"order","createdAt"},{"ascending","true"},
{"limit", std::to_string(API_LIMIT)},
{"createdAfter", ultimo_ts}
},
cpr::Timeout{10'000}
);
if (r.status_code != 200) {
log_msg(LogLevel::ERR, "API HTTP " + std::to_string(r.status_code));
std::this_thread::sleep_for(std::chrono::seconds(POLLING_SEC));
continue;
}
json mercados;
try { mercados = json::parse([Link]); }
catch (const std::exception& e) {
log_msg(LogLevel::ERR, std::string("JSON: ") + [Link]());
std::this_thread::sleep_for(std::chrono::seconds(POLLING_SEC));
continue;
}
for (auto& m : mercados) {
std::string slug = [Link]("slug","");
std::string cat = [Link]("createdAt","");
if ([Link]() || [Link](slug)) continue;
[Link](slug);
if (cat > max_ts) max_ts = cat; // cursor avanza aunque no pase filtros
Mercado res;
if (!procesar_mercado(m, umbral, res)) continue;
historico.push_back({
{"slug",[Link]},{"question",[Link]},
{"createdAt",[Link]},{"endDate",[Link]},
{"volumeNum",[Link]},{"precios",[Link]},
{"suma_precios",res.suma_precios},{"hay_arbitraje",res.hay_arbitraje},
{"cobertura",{
{"apuestas",[Link]},
{"inversion_total",[Link].inversion_total},
{"retorno_minimo",[Link].retorno_minimo},
{"ganancia_neta",[Link].ganancia_neta},
{"roi_%",[Link]}
}}
});
++total_nuevos;
if ((int)cola_roi.size() >= COLA_ROI_MAX) cola_roi.pop_front();
cola_roi.push_back([Link]);
// Solo llegamos aquí si hay arbitraje (procesar_mercado lo garantiza)
++total_arb;
std::ostringstream oss;
oss << "[ARBITRAJE x" << [Link]() << "] "
<< [Link](0,55)
<< "\n Outcomes: " << [Link]()
<< " | Suma precios: " << res.suma_precios
<< " | ROI: " << [Link] << "%"
<< " | Inversion: $" << [Link].inversion_total
<< " | Ganancia: $" << [Link].ganancia_neta;
oss << "\n Apuestas: [";
for (size_t i = 0; i < [Link](); ++i)
oss << "$" << [Link][i]
<< (i+1 < [Link]() ? ", " : "]");
oss << "\n URL: [Link] << slug;
log_msg(LogLevel::WARNING, [Link]());
// Ejecutar cobertura automáticamente
if (ejecutar_cobertura(res, creds)) ++total_ordenes;
}
guardar_historico(historico);
ultimo_ts = max_ts;
if (ciclo % RESUMEN_CADA == 0 && !cola_roi.empty()) {
double roi_med = std::accumulate(cola_roi.begin(),cola_roi.end(),0.0)
/ (double)cola_roi.size();
std::ostringstream oss;
oss << "\n" << std::string(50,'-') << "\n"
<< " Ciclo #" << ciclo << " | Umbral: $" << umbral << "\n"
<< " Cursor: " << ultimo_ts << "\n"
<< " Mercados: " << total_nuevos
<< " | Arbitrajes: "<< total_arb
<< " | Ordenes: " << total_ordenes << "\n"
<< " ROI medio: " << roi_med << "%\n"
<< std::string(50,'-');
log_msg(LogLevel::INFO, [Link]());
}
std::this_thread::sleep_for(std::chrono::seconds(POLLING_SEC));
}
}