Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A low-latency C++20 research and execution engine for Polymarket's CLOB V2. It combines authenticated alpha ingress, an L2 WebSocket book, exact fixed-point order construction, EIP-712 signing, a consumable pre-signed ladder, bounded SPSC queues, risk gates, and asynchronous HTTPS submission.

> **Deployment status:** offline and mock paths are tested. This repository is **not approved for unattended live trading**: private-channel fill/order reconciliation and automatic inventory recovery are still missing, and signature type 3 intentionally fails closed until correct ERC-7739 wrapping is implemented. See [the deployment runbook](docs/DEPLOYMENT.md) and [remediation ledger](docs/REMEDIATION_STATUS.md).
> **Deployment status:** offline and mock paths are tested. The engine ships four layers over the hot path: private user-channel accounting with venue reconciliation (P1), an always-on risk manager with stop-loss/hedging/day-loss kill switch (P2), an adverse-selection volatility gate with dynamic pre-signed-ladder TTL and a >5%/100ms shock cooldown (P3), and a closed-form Bayesian signal brain (P4) whose cold-path evidence ingestion (polling/API NDJSON adapters, recalibrable source reliability) only touches the hot tick through the bounded SPSC drain — posterior update/read ≈ 41/39 ns p50, well under the 50 ns budget. `BOT_MODE=mock` is full paper trading: accepted mock orders synthesize venue fills so tracker, brakes, and brain observe real flow end to end. Signature type 3 intentionally fails closed until correct ERC-7739 wrapping is implemented. See [the deployment runbook](docs/DEPLOYMENT.md) and [remediation ledger](docs/REMEDIATION_STATUS.md).

## Safety model

Expand Down
10 changes: 10 additions & 0 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,19 @@ add_executable(l2_backtester ../tests/replay/l2_backtester.cpp)
crowdintel_target(l2_backtester)
set_target_properties(l2_backtester PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

add_executable(test_layers ../tests/unit/test_layers.cpp)
crowdintel_target(test_layers)
if(HAVE_NETWORK)
target_compile_definitions(test_layers PRIVATE CROWDINTEL_HAVE_NETWORK=1)
target_link_libraries(test_layers PRIVATE
CURL::libcurl OpenSSL::SSL OpenSSL::Crypto)
endif()
set_target_properties(test_layers PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

enable_testing()
add_test(NAME crypto_kat COMMAND $<TARGET_FILE:test_signer>)
add_test(NAME core_units COMMAND $<TARGET_FILE:test_core>)
add_test(NAME layer_units COMMAND $<TARGET_FILE:test_layers>)
add_test(NAME backtester_smoke COMMAND $<TARGET_FILE:l2_backtester>
${CMAKE_CURRENT_SOURCE_DIR}/../tests/replay/sample_ticks.csv)
# latency_bench is a benchmark, not a correctness test; CI invokes it explicitly.
48 changes: 48 additions & 0 deletions core/include/account_events.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#ifndef ACCOUNT_EVENTS_HPP
#define ACCOUNT_EVENTS_HPP

// ─────────────────────────────────────────────────────────────────────────────
// AccountEvent: normalized user-channel (private WebSocket) account fact.
//
// One cache-line message produced ONLY by the private-WS feed thread (or the
// simulation venue) and consumed ONLY by the hot loop, which applies each
// event to the PositionTracker. This keeps the tracker single-writer and
// satisfies the repository SPSC ownership invariant: nobody touches the
// tracker directly off the hot path; facts travel through the typed queue.
//
// All quantities are fixed-point x1e6. Venue string identifiers (order id,
// trade id) are hashed to u64 so no variable-length parsing survives into
// the hot path. A FILL whose order_hash matches a tracked open order also
// reduces that order's outstanding quantity.
// ─────────────────────────────────────────────────────────────────────────────

#include <cstdint>
#include <type_traits>

struct AccountEvent {
enum class Type : uint8_t {
FILL = 0, // trade matched (partial fills arrive as several FILLs)
FILL_MINED = 1, // on-chain mined/confirmed duplicate of a FILL
OPEN = 2, // order accepted/resting on the venue book
CANCEL = 3, // order cancelled (partial cancel keeps remaining)
REJECT = 4, // order rejected by the venue
FAILED = 5, // trade failed/rolled back after a prior MATCHED
};

Type type = Type::FILL;
uint8_t side = 0; // K_SIDE_BUY / K_SIDE_SELL (user perspective)
uint8_t asset = 0; // 0 = primary token, 1 = hedge/complement token
uint8_t reserved[5]{};
uint64_t price = 0; // fill/order price, x1e6
uint64_t size = 0; // quantity of THIS event, shares x1e6
uint64_t remaining = 0; // venue-reported outstanding qty (0 = unknown)
uint64_t timestamp_ns = 0; // venue event time, realtime epoch ns
uint64_t market_hash = 0; // FNV-1a of the configured market slug
uint64_t order_hash = 0; // FNV-1a of the venue order id (0 = unknown)
uint64_t event_id = 0; // FNV-1a of trade/event id for dedup (0 = n/a)
};

static_assert(std::is_trivially_copyable_v<AccountEvent>);
static_assert(sizeof(AccountEvent) == 64, "one cache line per account event");

#endif // ACCOUNT_EVENTS_HPP
123 changes: 123 additions & 0 deletions core/include/bayesian_engine.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#ifndef BAYESIAN_ENGINE_HPP
#define BAYESIAN_ENGINE_HPP

// ─────────────────────────────────────────────────────────────────────────────
// BayesianEngine: the closed-form signal prior/posterior (P4).
//
// One Dirichlet state over at most MAX_OUTCOMES slots — the binary Polymarket
// market uses slots 0 (YES) and 1 (NO), which is exactly the Beta-Binomial
// conjugate pair; neg-risk multi-outcome markets use additional slots. The
// prior (α, β) is built from the market mid × prior strength N0
// (BOT_BAYES_PRIOR_STRENGTH), anchoring the posterior to the order book.
//
// Evidence flattens into the conjugate state:
// COUNT (Beta-Binomial): α_j += k·w and the complement mass (n−k)·w
// renormalized over the other slots;
// LR (log-odds shift): s += w·lr, applied to the binary odds at
// read time via p = σ(ln(α₀/α₁) + s).
//
// Constraints honoured by construction: no heap allocation, no virtual
// calls, no IO. A COUNT update is a handful of adds/multiplies (~10-20 ns
// RDTSC); a posterior read is one divide (COUNT-only state) or a divide plus
// one exp() when LR mass is present. The hot loop calls update/posterior
// inline; the cold path never touches this object.
// ─────────────────────────────────────────────────────────────────────────────

#include <cmath>
#include <cstdint>

class BayesianEngine {
public:
static constexpr size_t MAX_OUTCOMES = 8;

// Hot: seed the prior from the current mid price. Idempotent by design:
// only the first call after construction/reset takes effect.
void ensure_prior(double mid, double prior_strength) noexcept {
if (has_prior_) return;
if (!(mid > 0.0 && mid < 1.0)) return; // book not ready yet
alpha_[0] = mid * prior_strength;
alpha_[1] = (1.0 - mid) * prior_strength;
for (size_t i = 2; i < MAX_OUTCOMES; ++i) alpha_[i] = 0.0;
log_odds_shift_ = 0.0;
has_prior_ = true;
}

void reset() noexcept {
for (size_t i = 0; i < MAX_OUTCOMES; ++i) alpha_[i] = 0.0;
log_odds_shift_ = 0.0;
has_prior_ = false;
events_ = 0;
count_events_ = 0;
lr_events_ = 0;
}

bool has_prior() const noexcept { return has_prior_; }

// Beta-Binomial conjugate update. `weight_x1e6` down-weights the whole
// batch by source reliability (0 = ignored at the engine gate).
void update_count(uint8_t outcome, uint32_t n, uint32_t k,
uint32_t weight_x1e6) noexcept {
if (!has_prior_ || outcome >= MAX_OUTCOMES || n == 0 || k > n ||
weight_x1e6 == 0)
return;
const double w = static_cast<double>(weight_x1e6) * 1e-6;
alpha_[outcome] += static_cast<double>(k) * w;
const double complement = static_cast<double>(n - k) * w;
if (complement <= 0.0) { ++events_; ++count_events_; return; }
// Distribute the complement mass over the OTHER slots, proportional
// to their current concentration (shape-preserving renormalization).
double others = 0.0;
for (size_t i = 0; i < MAX_OUTCOMES; ++i)
if (i != outcome) others += alpha_[i];
if (others <= 0.0) { ++events_; ++count_events_; return; }
for (size_t i = 0; i < MAX_OUTCOMES; ++i) {
if (i == outcome) continue;
alpha_[i] += complement * (alpha_[i] / others);
}
++events_;
++count_events_;
}

// Log-likelihood shift: posterior odds for outcome-vs-complement are
// multiplied by exp(w·lr). Cheap accumulate; exp happens at read time.
void update_lr(int32_t lr_x1e6, uint32_t weight_x1e6) noexcept {
if (!has_prior_ || weight_x1e6 == 0 || lr_x1e6 == 0) return;
log_odds_shift_ += static_cast<double>(lr_x1e6) * 1e-6 *
(static_cast<double>(weight_x1e6) * 1e-6);
++events_;
++lr_events_;
}

// Posterior probability of `outcome`. COUNT-only states read as a
// single divide; LR mass routes the binary state through the sigmoid.
double posterior(uint8_t outcome = 0) const noexcept {
if (!has_prior_ || outcome >= MAX_OUTCOMES) return -1.0;
double total = 0.0;
for (size_t i = 0; i < MAX_OUTCOMES; ++i) total += alpha_[i];
if (total <= 0.0) return -1.0;
double mean = alpha_[outcome] / total;
if (log_odds_shift_ != 0.0 &&
(outcome == 0 || outcome == 1) && alpha_[0] > 0.0 &&
alpha_[1] > 0.0) {
const double g = std::log(alpha_[0] / alpha_[1]) +
log_odds_shift_;
const double p = 1.0 / (1.0 + std::exp(-g));
mean = outcome == 0 ? p : 1.0 - p;
}
return mean;
}

uint64_t events() const noexcept { return events_; }
uint64_t count_events() const noexcept { return count_events_; }
uint64_t lr_events() const noexcept { return lr_events_; }

private:
double alpha_[MAX_OUTCOMES]{};
double log_odds_shift_ = 0.0;
bool has_prior_ = false;
uint64_t events_ = 0;
uint64_t count_events_ = 0;
uint64_t lr_events_ = 0;
};

#endif // BAYESIAN_ENGINE_HPP
42 changes: 42 additions & 0 deletions core/include/evidence.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#ifndef EVIDENCE_HPP
#define EVIDENCE_HPP

// ─────────────────────────────────────────────────────────────────────────────
// EvidenceEvent: one observation from an external information source feeding
// the Bayesian brain (P4). Produced ONLY by cold-path ingress (HTTP pollers,
// sports-data adapters, macro feeds, NDJSON replay) and consumed ONLY by the
// hot loop, which folds each event into the posterior. One cache-line POD —
// pushing never allocates and never blocks.
//
// Two flavors share the POD (kind):
// COUNT — Beta-Binomial evidence: `count_n` trials with `count_k` positive
// outcomes (e.g. poll: 32 respondents, 22 YES). Weighted by
// source reliability at application time.
// LR — log-likelihood-ratio evidence: posterior_odds *= exp(lr_x1e6 *
// weight / 1e6). Used by macro/sports adapters that emit a score.
//
// Fixed-point x1e6 everywhere; venue strings never survive into the hot path.
// ─────────────────────────────────────────────────────────────────────────────

#include <cstdint>
#include <type_traits>

struct EvidenceEvent {
enum class Kind : uint8_t { COUNT = 0, LR = 1 };

Kind kind = Kind::COUNT;
uint8_t outcome = 0; // Dirichlet slot for multi-outcome markets
uint8_t neg_risk = 0; // 1 when the evidence targets the complement
uint8_t reserved = 0;
uint32_t source_id = 0; // index into SourceReliability (producer-set)
uint64_t timestamp_ns = 0; // observation time (realtime epoch ns)
uint32_t event_hash = 0; // dedup hash of source-side event id
uint32_t count_n = 0; // COUNT: trials (x1, e.g. respondents polled)
uint32_t count_k = 0; // COUNT: positive outcomes (x1)
int32_t lr_x1e6 = 0; // LR: log-likelihood ratio, x1e6
};

static_assert(std::is_trivially_copyable_v<EvidenceEvent>);
static_assert(sizeof(EvidenceEvent) == 32, "half cache line per evidence event");

#endif // EVIDENCE_HPP
49 changes: 49 additions & 0 deletions core/include/journal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#ifndef JOURNAL_HPP
#define JOURNAL_HPP

// ─────────────────────────────────────────────────────────────────────────────
// JournalEvent: auditable record of every economically relevant hot-path
// decision. The hot loop is the only producer; a cold thread is the only
// consumer and renders NDJSON. Entries are pre-sized PODs: pushing an event
// never allocates and never blocks (drops are counted by the consumer).
// ─────────────────────────────────────────────────────────────────────────────

#include <cstdint>
#include <type_traits>

struct JournalEvent {
enum class Type : uint8_t {
ACCOUNT_FILL = 0,
ACCOUNT_REJECT = 1,
RESERVATION_STALE_RELEASE = 2,
STOP_LOSS_TRIGGERED = 3,
HEDGE_TRIGGERED = 4,
KILL_SWITCH = 5,
RECONCILE_DRIFT = 6,
VOLATILITY_ENTER = 7,
VOLATILITY_EXIT = 8,
STALE_PRICE_ABORT = 9,
POOL_STALE_DROP = 10,
BAYES_UPDATE = 11,
BAYES_SIGNAL = 12,
BAYES_LOW_RELIABILITY = 13,
ORDER_ACCEPTED = 14,
ORDER_FAILED = 15,
DAY_RESET = 16,
};

Type type = Type::ORDER_FAILED;
uint8_t reserved0[7]{};
int64_t pnl = 0; // signed x1e6 USD where applicable
uint64_t aux0 = 0; // per-type payload (e.g. price x1e6)
uint64_t aux1 = 0; // second payload (e.g. shares x1e6)
uint64_t aux2 = 0; // third payload (e.g. threshold / version)
uint64_t mono_ns = 0; // CLOCK_MONOTONIC event time
uint64_t aux3 = 0; // fourth payload (e.g. posterior x1e6)
uint64_t reserved1 = 0;
};

static_assert(std::is_trivially_copyable_v<JournalEvent>);
static_assert(sizeof(JournalEvent) == 64, "one cache line per journal event");

#endif // JOURNAL_HPP
Loading
Loading