Skip to content

About

Bot de baja latencia para transacciones de polymarket

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bot CrowdIntel — Polymarket CLOB V2 trading engine

High-frequency deterministic execution engine for Polymarket CLOB V2 on Polygon (chainId 137), plus the three alpha strategies that were measured before any of them was funded.

The defining property of this repository is that every strategic claim has a number behind it, and the numbers are reproducible from scripts in scripts/. Two of the three strategies came back negative. They are documented, not deleted, because the negative results are what stopped $7,500 of planned capital from being deployed without evidence.


1. What was measured, and what it decided

Sleeve Gate Result Verdict
A · Crypto TWAP convergence scripts/experiment_twap_edge.py Replica 88% accurate at T-10s vs market 100%; market Brier 0.017. 12 trades, 9 wins, −1.001 u/c Do not fund
B · Insider mirroring scripts/experiment_copy_edge.py Wallet skill persists (+11 pts, n=19), but the copy loses 13–16¢ in 10 minutes; model right on disagreement 33–50% Inconclusive — only with crowdintel's own timestamps
C · Maker carry scripts/experiment_maker_edge.py +0.392¢ median net per fill, positive in 19 of 22 markets Build this one

Full write-ups: docs/EXPERIMENT_MOTOR_A.md, docs/EXPERIMENT_MOTOR_B.md, docs/EXPERIMENT_MOTOR_C.md.

Why C wins and A loses, with the same mechanism: the adverse-selection term. These markets resolve over weeks, so one-minute volatility is 0.05–0.6¢ against a 0.5–2.0¢ half-spread. A five-minute crypto round is the opposite: the venue's own probability is essentially settled in the final seconds, and the taker fee is 7% on top.


2. Venue facts established from primary sources

Not from blog posts — from the venue's own documentation, the live API, and DNS.

Fact Source
CLOB V2 primary servers: AWS eu-west-2 (London) Official docs; corroborated by mm.clob.polymarket.com CNAME → clob-mm-gateway-…elb.eu-west-2.amazonaws.com
GB is close-only on the API, so hosting in London gets orders rejected docs.polymarket.com/api-reference/geoblock, three-tier table
IE is "the API itself is not restricted" → eu-west-1 (Dublin) Same table; docs name it "Closest Non-Georestricted Region"
Crypto up/down markets settle on Chainlink TWAP-60s Live market object: cryptoMarketConfig.twapLookbackSeconds: 60, resolutionSource: …/btc-usd-twap-60s-streams
Fee = rate × p × (1−p), per category Live feeSchedule: {exponent:1, rate:0.07}
Makers pay nothing feeSchedule.takerOnly: true, verified per market
Rewards band is fractional cents (rewardsMaxSpread: 3.5 = 350 bps) Live clobRewards + rewardsMaxSpread
Binance WebSocket is AWS Tokyo, no EU endpoint Resolved IPs mapped against AWS ip-ranges.json

The practical consequence: Dublin, one region, no dual-colo. Running the strategy in Tokyo and shipping the order to London costs the same 214 ms as receiving the Tokyo feed in Dublin. That is physics, not engineering.


3. Safety — the parts that stop it losing money

Five layers, in order of activation. Each one closes a way to lose the bankroll that was reachable from a perfectly valid environment file.

Layer Behaviour
Run mode BOT_MODE unset or unrecognised aborts. An offline build refuses live. paper is accepted but reported as behaving as mock until a paper engine exists.
Startup invariants A live session is refused when daily_loss is 0 (disables the kill), stop_loss_pct is 0 (disables the stop), any cap exceeds its bankroll fraction, or the buy-volume cap cannot fund two max-order orders.
Durable daily state daily_buy_volume and the kill latch survive restarts. A corrupt state file refuses startup in live — not knowing today's exposure is worse than not trading.
Monetary aliases *_USDC and *_USD both accepted; a conflicting pair is a hard error, never a silent precedence choice. A startup banner prints the effective limits.
Reconciliation drift WS-vs-REST inventory drift latches the kill and stops trading.

Two defects found while building this, both fixed:

  • BOT_MODE was fail-open: in the production network build an unset BOT_MODE meant mock = false, i.e. real orders. Now every unknown path aborts.
  • BOT_MAX_DAILY_BUY_VOLUME_USD was validated but never read — the gross BUY cap was pinned at $50 and could not be raised without recompiling.
  • utc_day_number() returned era * 36524 + doe instead of the civil-from-days value, so it was neither the day count nor collision-free. A collision would make the daily loss cap miss its rollover.

4. Architecture

   Binance WS (Tokyo)          Polygon RPC              CLOB WS (London)
   12–214 ms                    on-chain                28–35 ms
        │                           │                        │
        ▼                           ▼                        ▼
   ┌──────────────────── SPSC rings, no allocation on the hot path ─────────┐
   │                                                                        │
   │  ExecutionEngine (core/src/execution_engine.hpp, 1000 lines)           │
   │    compliance → book freshness → volatility gate → Kelly sizing        │
   │    → fee-aware edge gate → risk authorization → wire body              │
   │                                                                        │
   │  Target: tick-to-decision p50 ≈ 754 ns, p99 ≈ 1.2 µs (measured)        │
   └────────────────────────────────────────────────────────────────────────┘
        │                    │                      │
        ▼                    ▼                      ▼
   OrderGateway       PositionTracker         Metrics  →  /metrics (Prometheus)
   (async egress)     (fills, inventory)      DailyState → disk (CRC, atomic)

Hot path is single-threaded and allocation free. Every other thread is cold: market/user WebSocket listeners, the order gateway worker, the journal renderer, the reconciler, the daily-state persister and the health server.

Execution and risk, wired

execution_engine.hpp · market_config.hpp · risk_manager.hpp · lightweight_client.hpp (libcurl, persistent handle, TCP_NODELAY) · order_gateway.hpp · presigned_pool.hpp · ws_market_listener.hpp · ws_user_listener.hpp · compliance_guard.hpp · position_tracker.hpp · volatility_gate.hpp · bayesian_engine.hpp · metrics.hpp · daily_state.hpp · market_metadata.hpp

Motor C maker path — running today in mock

BOT_MODE=mock BOT_ENTRY_ORDER_TYPE=GTC \
BOT_MK_QUOTE_SIZE=200000000 BOT_MK_REWARDS_MIN_SIZE=200000000 \
BOT_MK_REWARDS_BAND_BPS=350 BOT_MK_ADVERSE_BPS=25 \
./build/bin/crowdintel_bot
# -> [maker] ticks=1163 places=2 cancels=0 rejected=0 skipped=1 rests=2 fills=0

maker_engine.hpp is the Motor C tick path: drain venue events → kill guard → book → MakerLoop::on_tick → run_maker_decision. It is deliberately separate from ExecutionEngine: the maker sleeve has no AlphaSignal, no Kelly sizing and no entry signal, and adding a second personality to a 1000-line class would have been the wrong trade. It enforces the same brakes the taker path does — a latched kill withdraws both rests, and a BUY must pass RiskManager::authorize before a signature exists. A SELL is never blocked: it reduces inventory.

maker_loop.hpp (the cycle) composes maker_quote.hpp (where to quote) and quote_registry.hpp (when to pull); resting_mock.hpp / resting_mock_client.hpp provide a transport where orders actually rest; plus entry_order_type routing and DELETE /order in lightweight_client.hpp.

The loop cancels before it places: a replace that placed first could momentarily double the exposure on that side, which is what the inventory caps exist to prevent. It signs nothing and sends nothing — it emits a decision the engine executes, so signing stays in one place.

Entries may rest (GTC), exits are always FAK — a protective exit that merely rests is an exit that did not happen. Resting entries bypass the pre-signed pool because a pre-signed ladder assumes immediate consumption on a TTL, while a resting order's lifetime is governed by cancel/replace.

MarkoutTracker (markout_tracker.hpp) records every fill and settles it against the mid one BOT_MK_MARKOUT_HORIZON_MS later, publishing the conditional adverse move as crowdintel_maker_markout_cents (mean) plus …_samples and …_dropped. It is measured, not asserted — and it is the number §9.3 says can still kill the sleeve.

Motor A crypto path (built, gated negative)

chainlink_stream.hpp — strict Data Streams report parser plus a time-weighted 60-second TWAP window. Arithmetic mean would be wrong: the DON's cadence is uneven.


5. Fee model

One implementation, in market_config.hpp:

taker_fee_per_share(rate, price) = rate * price * (1 - price)
Category rate
Geopolitics / world events 0.00
Politics / Finance / Tech / Mentions 0.04
Sports / Economics / Weather / Culture 0.05
Crypto 0.07
Unknown 0.07 (fail-closed: assume the expensive one)

BOT_MARKET_CATEGORY maps to the rate; BOT_TAKER_FEE_RATE overrides it. Both consumers — the decision gate and the Kelly sizing — read the same field, so they cannot disagree.

fee_calculator.hpp and fee_calculator_v2.hpp still exist but carry a DEAD CODE banner: they implement a flat-tier table that does not match the venue, and no production translation unit includes them. This matters: that disconnected duplicate is what a reader reaches for first.


6. Build and test

# libsecp256k1 with the recovery module is a hard dependency
export SECP256K1_ROOT=/path/to/secp256k1
cmake -S core -B build -DCMAKE_BUILD_TYPE=Release -DCROWDINTEL_NETWORK=ON
cmake --build build -j
ctest --test-dir build --output-on-failure

Current state: 40 of 41 CTest targets pass. The single failure is pre-existing and unrelated: layer_units's "flow resumes after cooldown". Suppression has two sources — a time-based shock cooldown that expires, and a published regime pause that only recomputes on fresh book samples. That is a conservative failure (it stops trading, it never loses money), and changing it is a product decision, so it is left failing rather than silently "fixed".

Behaviour that was verified rather than assumed:

  • BOT_MODE unset → FATAL, exit 1
  • BOT_ENTRY_ORDER_TYPE=LIMIT → FATAL
  • BOT_MARKET_CATEGORY=geopolitics → fee=0.00000 source=category
  • restart → [state] restored buy_volume=49.99 USD for UTC day 20755
  • kill -9 then restart with a latched kill → kill latch restored from disk
  • BOT_ENTRY_ORDER_TYPE=GTC + real slug → [rewards] from venue: pool=$3000/day band=350bps min_size=200
  • SIGTERM → clean exit (this hung before the health server switched to poll())

Test suites added

Suite Assertions Covers
test_config_hardening 59 run mode, monetary aliases, fee categories, live invariants
test_daily_state 37 CRC-32 vectors, atomic write, corruption, symlink refusal
test_metrics 35 decision histogram, edge/slippage means, Brier, NaN
test_chainlink 46 report parser, time-weighted TWAP, replay
test_fees 25 the production fee model (rewritten from the dead one)
test_maker_quote 41 placement, reward band, inventory skew, tick rounding
test_quote_registry 37 fills, supersede, price/age/size replace rules
test_cancel_id 14 order-id validation incl. JSON injection
test_market_metadata 25 fractional-cent band → bps, tick, feeSchedule.rate
test_resting_mock 30 fills only on a cross
test_resting_client 26 OPEN vs FILL, cancel, malformed bodies
test_maker_loop 20 place → keep → replace (cancel first) → withdraw
test_maker_runner 24 decision executed against a client; a crossing fills, the loop re-quotes; cancel-before-place
test_gateway_cancel 15 per-order withdrawal through the egress worker, incl. a latched kill switch
test_reconcile 16 position-diff decision: silent on no drift, drift == max tolerated, zero max disables the kill
test_markout 25 conditional adverse move; sign pinned; clock-domain contract; ring-drop visibility; exact int64 sum

7. Operational surface

# Region preflight — run this BEFORE signing any lease
./scripts/probe_region.sh                 # probe this host
./scripts/probe_region.sh --aws eu-west-1 # provision spot, probe, tear down

# Research gates (each reproduces one row of the table in §1)
python3 scripts/experiment_twap_edge.py     --asset btc --duration 15m
python3 scripts/experiment_copy_edge.py     --wallets 40
python3 scripts/experiment_maker_edge.py    --markets 22

# Dead-code policy (fails on a NEW orphan)
./scripts/check_dead_headers.sh

probe_region.sh exits 2 when the host is geo-blocked. That is the answer that matters: a blocked region cannot trade no matter how fast it is.

Endpoints on BOT_METRICS_PORT (127.0.0.1, 0 disables): /health /ready /metrics (Prometheus) /status. The series that decides whether a strategy has alpha at all is crowdintel_decisions_by_result — the labelled histogram of every verdict. For the maker sleeve the decisive series is crowdintel_maker_markout_cents (mean adverse move after a fill, with …_samples as its trust gauge).


8. Configuration

Full reference: docs/CONFIGURATION.md. The ones that change behaviour:

Variable Default Meaning
BOT_MODE required mock / paper / live
BOT_ENABLE_LIVE_TRADING 0 second key; live is disarmed without it
BOT_MARKET_CATEGORY – geopolitics/politics/sports/crypto
BOT_ENTRY_ORDER_TYPE FAK GTC for resting maker entries
BOT_MK_ADVERSE_BPS 25 the maker sleeve's decisive number
BOT_MK_REWARDS_BAND_BPS from venue overridden by market_metadata
BOT_MK_MARKOUT_HORIZON_MS 60000 fill → mid settlement horizon for the mark-out
BOT_BANKROLL_USD (or BOT_MAX_BANKROLL_USDC) 1000 both spellings, conflict = fatal
BOT_MAX_DAILY_LOSS_USD 50 refused at startup if 0 in live
BOT_STATE_FILE /var/lib/crowdintel/daily_state.bin durable risk state
BOT_METRICS_PORT 8080 0 disables

9. Known limitations — read before trusting anything

  1. No live order or fill has been claimed from this environment. Every statement above is offline verification, public-API measurement, or a mock.
  2. No paper engine exists. BOT_MODE=paper runs the mock transport: MockCLOBClient synthesizes a fill on every accepted submit, so it cannot observe a rest. The maker sleeve instead runs against resting_mock_client.hpp, whose orders actually rest and fill only on a cross — which is why its mark-out reads 0 samples: the mock book never crosses, so there is nothing to settle.
  3. The Motor C conditional mark-out is built but not yet populated. The +0.392¢/fill figure uses unconditional one-minute volatility. Adverse selection is conditional on being filled, which is strictly worse. MarkoutTracker measures it in-process (crowdintel_maker_markout_cents, one BOT_MK_MARKOUT_HORIZON_MS after each real fill), but it reads 0 samples here because no real fill has occurred in this environment. If the real number is 2¢ rather than 0.09¢, every net in that table flips negative — this is still the number that can kill the only funded sleeve, and it needs real fills.
  4. Motor A's result has a proxy gap. The TWAP replica uses Binance VWAP, not Chainlink's own aggregate. Part of the accuracy difference could be proxy error. Closing it needs Data Streams credentials.
  5. Samples are small: 17 and 26 settled rounds for Motor A, 19 wallets for Motor B, 22 markets for Motor C. Directionally consistent, not powered studies.
  6. layer_units fails (see §6).

10. Repository map

core/src/          engine, config, fee model, metrics, feeds, maker path
core/include/      risk, book, SPSC rings, daily state, maker_quote, quote_registry
core/crypto/       EIP-712 + libsecp256k1 signing, keccak, ChaCha20 salts
infra/             Docker, systemd (SCHED_FIFO + CPU isolation), Prometheus, health
scripts/           region preflight, three research gates, dead-code guard
tests/             41 CTest targets: unit, integration, acceptance, replay
docs/              architecture, ADRs, three experiment write-ups, dead-code ledger,
                   CANARY_RUNBOOK.md (the two steps that need a human)
experimental/      (planned) frozen research code behind -DCROWDINTEL_BUILD_EXPERIMENTAL

Governing documents: docs/STATUS.md (what is verified vs not), docs/DEAD_CODE.md (a decision for all 32 orphan headers), docs/EXPERIMENT_MOTOR_{A,B,C}.md (the gates), docs/ADR-*.md (design records).


11. What is genuinely next

In dependency order, not in wish order:

  1. Execute the canary — docs/CANARY_RUNBOOK.md reduces it to copy-paste, and MarkoutTracker now makes the decisive number a metric instead of a manual calculation: crowdintel_maker_markout_cents. That is the only thing left between this repository and a funded decision.
  2. Two narrower maker risks remain. BOT_MK_LIVE_ACK stays because inventory reservations before the wire are absent (the taker path reserves so a second order cannot duplicate exposure while a fill is in flight) and resting orders are not reconciled across a restart.
  3. Only then: capital, at a fraction of the originally planned allocation.

The planned $10,000 allocation is not justified by the evidence. Motor C's positive result is real, but its decisive risk term — the conditional mark-out — is now measured in-process and still reads zero real fills.

About

Bot de baja latencia para transacciones de polymarket

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages