Skip to content

DIO Software Product Specification

Document: DIO-SPS-001

CAUTION. The auto-hedge strategy (Hyperliquid perpetual shorts) is experimental. Review auto_hedger.ml and validate the strategy on a testnet before committing capital.


Contents


1.0 SCOPE

1.1 Identification

This document, DIO-SPS-001, is the software product specification for the DIO trading engine (hereinafter "the engine"). It defines the system architecture, configuration interface, exchange adapters, trading strategies, capital oracle, and execution procedures of the engine.

1.2 System Overview

DIO is an OCaml trading engine that performs market making, grid trading, and capital management. The engine executes grid and market-making strategies on Kraken, Hyperliquid, Lighter, Interactive Brokers, and Alpaca. A single configuration file, config.json, specifies all traded instruments. Position sizing is governed by a capital-survival oracle. A live terminal user interface (TUI) dashboard is served over a Unix domain socket (UDS).

The engine is partitioned into the following principal components:

a. one OCaml domain per trading asset; b. a supervisor that owns connection lifecycle and health; c. a lock-free order executor; and d. a supervised, event-driven oracle that re-sizes positions on every fill or cancel.

The sizing decision path executes in memory and does not wait on network I/O.

1.3 Document Overview

Section 2 lists referenced documents. Section 3 defines abbreviations and acronyms. Section 4 describes the system architecture. Section 5 specifies the configuration file. Section 6 specifies environment variables. Section 7 describes the exchange adapters. Section 8 describes the trading strategies. Section 9 describes the capital oracle. Section 10 specifies build and execution. Section 11 records cautions and limitations.


2.0 REFERENCED DOCUMENTS

2.1 Project Documents

Document Description
config.json Runtime configuration; see Section 5.0
dio.opam OCaml package manifest and dependency declarations
dune-project Dune build-system project definition
Dockerfile.base OxCaml base image: compiler and all opam dependencies; see Section 10.5
Dockerfile Application container image, built from the base image; see Section 10.5
oxcaml-port/opam-overlay/ Local opam repository overlay patching dependencies that do not build under OxCaml; see Section 10.5
LICENSE Project license
THIRD_PARTY_LICENSES Third-party license notices

Source modules cited throughout Sections 4.0 through 9.0 are authoritative over the descriptions in this document.


3.0 ABBREVIATIONS AND ACRONYMS

Abbreviation Definition
ATH All-Time High
ATL All-Time Low
CLI Command-Line Interface
DEX Decentralized Exchange
EIP Ethereum Improvement Proposal
ET Eastern Time
GC Garbage Collector
GTC Good-Til-Cancelled
GTT Good-Til-Time
IBKR Interactive Brokers
IEX Investors Exchange
JSON JavaScript Object Notation
MM Market Maker
MPSC Multi-Producer, Single-Consumer
N/A Not Applicable
p99 99th Percentile
REST Representational State Transfer
SIP Securities Information Processor
SSE Server-Sent Events
TBD To Be Determined
TIF Time In Force
TUI Text User Interface
UDS Unix Domain Socket
USD United States Dollar
USDC USD Coin
WS WebSocket

4.0 SYSTEM ARCHITECTURE

4.1 Architectural Overview

Figure 1 depicts the runtime architecture. The supervisor owns connection lifecycle and health. Each active trading asset is serviced by an independent domain. Strategies and exchange feeds meet at the domain boundary; order intents funnel through a lock-free executor, and the oracle re-sizes positions on fill and cancel events.

config.json
   |
   v
Supervisor ── health monitor / circuit breaker / connection registry
   |
   +-- Domain (one per trading asset)
   |      strategies -> order executor -> exchange action
   |      exchange feeds -> ring buffers -> strategy loop
   |
   +-- Oracle (supervised, event-driven: re-sizes on fills/cancels, no network wait)
   |
   +-- Order executor (MPSC lock-free queue, in-flight tracking)
   |
   +-- Fill event bus -> Discord notifier, dashboard
   |
   +-- Dashboard server (UDS, JSON over length-prefixed frames)

Figure 1. Engine architecture.

4.2 Supervisor and Health Monitoring

supervisor.ml is a thin orchestrator. start_monitoring starts the health monitor loop, a monitor for non-active assets, and the order-processing loop; it then initializes feeds synchronously and returns the fee-augmented trading configurations to the domain spawner.

The connection registry contains the following connections: hyperliquid_ws, lighter_ws, kraken_orderbook_ws, kraken_auth_ws, alpaca_data_ws, alpaca_trading_ws, ibkr_gateway, and oracle. The order executor is deliberately excluded from the registry; it never blocks on network I/O.

Health rules are as follows:

a. Restart uses exponential backoff 0/2/4/... capped at 30 seconds (300 seconds for ibkr_gateway and lighter_ws; Alpaca feeds wait 2 seconds on their first attempt). b. A connection idle for 60 seconds is restarted; a websocket stuck connecting for 120 seconds is terminated. c. Websocket ping/pong uses a 15-second interval and a 5-second timeout (10 seconds for kraken_auth_ws), with 3 missed pongs before restart. kraken_orderbook_ws is not pinged and relies on the passive heartbeat. Passive data feeds shall heartbeat at least every 60 seconds (ibkr_gateway is exempt). d. The circuit breaker opens for 5 minutes after 5 consecutive failures and then re-tries in half-open mode. It is currently wired only for ibkr_gateway. e. The main-loop watchdog (main_loop_watchdog.ml) requires the main Lwt loop to beat every 5 seconds; a stall exceeding 60 seconds shall force-exit the process for supervised restart. Setting DIO_WATCHDOG_OFF disables the watchdog.

4.3 Trading Domains and Wakeups

Each active asset runs in its own domain. Exchange feeds write into single-writer/multi-reader ring buffers. The strategy loop wakes through Exchange_wakeup, which maintains a monotonic per-symbol generation counter. Producers increment the counter after writing data; the domain captures the counter at cycle start and waits against that baseline at cycle end, so a signal that lands mid-cycle makes the wait return immediately instead of parking through pending data (the classic check-then-sleep lost-wakeup race). The wait spins briefly on the lock-free counter before parking on the condition variable, absorbing near-simultaneous signals without a kernel round-trip. Quiet domains park normally; no component busy-polls.

Ring buffer cursors are absolute write positions, not slot indices. They remain valid across laps (a stalled reader resumes at the oldest surviving entry instead of aliasing to "empty") and across clears (resubscribes), and slots carry sequence numbers so that a writer racing an iteration cannot cause duplicated or torn reads.

Order lifecycle events from REST callbacks (acks, rejects, amend results) do not touch strategy state directly. They are pushed onto a per-symbol lock-free queue and drained by the symbol's own domain at the top of each cycle, so strategy state has exactly one writer thread.

4.4 Feed Parsing

High-rate frame parsing runs on a dedicated worker domain (src/engine/concurrency/parse_worker.ml) rather than on the Lwt scheduler thread that multiplexes all venue sockets. Kraken's WebSocket client diverts execution frames by raw-string prefix before any JSON parsing; orderbook frames are submitted unconditionally. The worker parses and dispatches them sequentially, preserving per-venue order. When the worker's queue fills, frames fall back to inline parsing and are never dropped (Kraken book updates are deltas, so a dropped update would desynchronize the local book). Hyperliquid's l2Book channel requires no offloading: its top-of-book is extracted by a zero-copy string scan into an atomic snapshot, and the full-book JSON parse occurs only on the dashboard cadence.

4.5 Order Executor

Strategies enqueue intents into a lock-free MPSC queue (capacity 64k). The executor shards in-flight orders by duplicate_key across 64 shards, tracks amendments, and suppresses redundant no-change amendments. After a restart, the supervisor re-synchronizes against open orders fetched from the venue before domains resume.

4.6 Error Handling

All exchange I/O funnels through error_handling.ml. Callers classify errors (Connection, Timeout, RateLimit, ServerError, ParseError, OrderRejected, InvalidRequest, Unknown) and retry with exponential backoff (default: 3 attempts, 1000 ms base, 30 s maximum, factor 2). Feed loops self-restart with backoff instead of crashing the domain.

4.7 Logging

logging.ml provides five levels and per-section colors, with column width autodetected from the terminal or COLUMNS. Messages drain through an asynchronous worker; CRITICAL flushes synchronously. The log format is HH:MM:SS.mmm LVL SECTION message.

4.8 Persistence

State resides in two JSON files under the state directory (data/, $DIO_DATA_DIR, or /app/data in Docker), written atomically (temporary file plus rename):

a. accumulation_state.json (base_accumulation_store.ml, orchestrated by persistence_orchestrator.ml): keyed by {strategy}:{symbol}:{venue}, with fields reserved_base, accumulated_profit, last_fill_oid, last_buy_fill_price, last_sell_fill_price, last_buy_fill_qty, and last_sell_fill_qty. Enabled per trading entry via base_accumulation (default true). b. sell_levels_state.json (sell_levels_store.ml): pending sell levels for entries with sell_levels: true (default false).

On startup, a legacy flat data/accumulated_state.json is migrated into these files and renamed to accumulated_state.json.migrated.<ts>. Persistence is used by the Jacobs ladder on all venues, including Kraken. In Docker, /app/data shall be mounted.


5.0 CONFIGURATION

5.1 Configuration File

The engine reads config.json from the working directory. Table 1 defines the top-level keys.

Table 1. Top-level configuration keys.

Key Default Description
logging_level INFO One of DEBUG, INFO, WARN, ERROR, CRITICAL
logging_sections unset Comma-separated section filters; unset enables all sections
logging_width autodetect Message column width; autodetected from the terminal or COLUMNS
cycle_mod 10000 Legacy interval for periodic background work; unused by current strategies
latency_window_seconds 5.0 Rolling window for network latency profiling statistics
latency_spike_threshold_us 10.0 Per-stage ceiling; a window that breaches it emits one INFO line naming the offending stages, their worst spike, and breach count
latency_spike_report internal Which latency families emit spike logs: internal (per-domain pipeline), network (ws_ping/ws_feed/rest_request/signer), both, or none
latency_spike_report_seconds 30.0 Minimum wall-clock seconds between per-domain internal spike log lines; 0 logs every window
latency_network_spike_threshold_us 20000.0 Ceiling for the network spike family, in microseconds (20 ms); separate from the 10 us internal-operation target
gc see Table 2 OCaml GC tunables applied before the engine starts
oracle see Table 4 Capital oracle knobs (runtime and tuning CLI)
trading required One entry per instrument to trade; see Section 5.3
fng_check_threshold 1.5 Price movement (percent) from baseline that re-triggers a Fear & Greed check
theme unset Default dashboard theme identifier; overridable with dio-dashboard --theme

The trading schema is strict: an unknown key under trading shall cause the engine to exit at startup.

5.2 GC Tunables

GC tunables are applied at process start through Gc.set. Units for minor_heap_size and major_heap_increment are OCaml words (8 bytes on 64-bit platforms). Table 2 defines the keys and the values used by the repository configuration.

Table 2. GC tunables.

Key Default Repository value
minor_heap_size 33554432 262144
space_overhead 120 80
max_overhead 1000000 1000000
window_size 10 5
allocation_policy 2 2
major_heap_increment 100 1048576

The repository values are tuned for tail latency rather than throughput. A small minor heap (262144 words = 2 MiB, OCaml's own default) makes minor collections short and frequent instead of one long pause, and the smaller major-heap increment keeps heap-growth slices fine. Combined with the allocation reductions on the trading hot path, this targets a sub-10 us internal-pipeline p99.

The per-window internal spike line's worst-cycle continuation reports per-stage allocation and GC deltas, for example:

ob:true ex:0 lev:0 st:false al:293w[ob:12 ex:0 prep:281 strat:0] (GC: minor=1)

A regression can therefore be attributed to allocation (per stage) or to a minor/major collection without a profiler attached.

5.3 Trading Entries

Each element of the trading array configures one symbol on one exchange. The following is a representative entry:

{
  "symbol": "BTC/USDC",
  "exchange": "hyperliquid",
  "qty": "0.01",
  "grid_interval": [1.0, 5.0],
  "strategy": "jacobs_ladder",
  "maker_fee": null
}

Table 3 defines the trading-entry keys.

Table 3. Trading-entry keys.

Key Applies to Description
symbol all Exchange symbol, e.g. BTC/USD (Kraken), BTC (Hyperliquid perpetual), BTC/USDC (Hyperliquid spot), AAPL (IBKR, Alpaca)
exchange all kraken, hyperliquid, lighter, ibkr, or alpaca
qty all Base order size in base currency, encoded as a string (e.g. "0.01")
grid_interval jacobs_ladder [gi_min, gi_max]: the hardened bounds (in percent) walked by the oracle's parameter search; the strategy reads only the oracle's resolved interval, never these bounds
min_usd_balance MM only Lower bound on account quote balance; MM pauses buys below this value
max_exposure MM only Upper bound on quote exposure for one symbol; MM pauses buys above this value
strategy all jacobs_ladder (alias Ladder) or market making (MM, alias market_maker)
maker_fee, taker_fee all Explicit fee overrides (fractions, e.g. 0.0016); null selects the venue default or a live fee lookup
testnet HL, Lighter, IBKR, Alpaca Route to sandbox/paper endpoints; rejected for Kraken
hedge Hyperliquid only Enable the experimental perpetual-short auto-hedge; rejected elsewhere
accumulation_buffer all [min, max] retained quote profit buffer required before base accumulation; resolved live from Fear & Greed on crypto venues
data_feed Alpaca iex (free) or sip (paid, full-market); accepted on any recognized venue, consumed only by Alpaca
sell_mult jacobs_ladder Fraction of each ladder rung's quantity sold per rung fill (1.0 sells the full rung; smaller values accrue base)
base_accumulation all Persist accumulated base and profit state for this entry (default true)
sell_levels jacobs_ladder Persist pending sell levels for this entry (default false)

Venue-specific restrictions are enforced at startup:

a. hedge is Hyperliquid-only. b. testnet is accepted for Hyperliquid, Lighter, IBKR, and Alpaca; it is rejected for Kraken. c. testnet, hedge, data_feed, and accumulation_buffer are rejected for unrecognized (custom) exchanges.

5.4 Oracle Section

The oracle runs inside the engine as a supervised module. The oracle section is optional; every key falls back to the defaults in Table 4. Unknown keys are rejected at startup.

Table 4. Oracle section keys.

Key Default Description
qty_cap_mult 1.5 Buy-size upper-bound multiplier: the search sizes buy_qty within [qty, qty * qty_cap_mult]
target_survival 0.95 Fraction of the historical maximum drawdown the runway covers: runway_pct = max_drawdown_pct * target_survival; drives sizing only, never activity
min_active_dsurv 0.0 Active gate: a strategy is active if and only if its replayed d_surv >= min_active_dsurv (or it has a resting buy to preserve), subject to affordability
refresh_seconds 300.0 Background fallback poll for history and balances; decisions remain event-driven, because fills and cancels re-resolve immediately
assets unset Per-symbol overrides keyed by symbol, each accepting { target_survival, min_active_dsurv, qty_cap_mult }

6.0 ENVIRONMENT VARIABLES

Credentials and one-off knobs are supplied through the environment. The engine loads .env if present (dotenv-style, KEY=value per line). Table 5 defines the recognized variables.

Table 5. Environment variables.

Variable Used by Notes
KRAKEN_API_KEY Kraken
KRAKEN_API_SECRET Kraken
HYPERLIQUID_WALLET_ADDRESS Hyperliquid Account address used for balance and fee queries
HYPERLIQUID_PRIVATE_KEY Hyperliquid Private key for the EIP-712 order signer
IBKR_GATEWAY_HOST IBKR Default 127.0.0.1
IBKR_GATEWAY_PORT IBKR Default 4002; live mode forces 4001 when unset
IBKR_TRADING_MODE IBKR paper or live; default paper
IBKR_CLIENT_ID IBKR Default 0
IBKR_ACCOUNT_ID IBKR Optional; auto-detected when unset
ALPACA_API_KEY Alpaca
ALPACA_API_SECRET Alpaca
LIGHTER_API_PRIVATE_KEY Lighter
LIGHTER_API_KEY_INDEX Lighter
LIGHTER_ACCOUNT_INDEX Lighter
LIGHTER_SIGNER_LIB_PATH Lighter Path to the signer shared library (.dylib on macOS, .so on Linux)
LIGHTER_PROXY_URL Lighter Comma-separated list of relay proxy URLs
CMC_API_KEY Fear and Greed CoinMarketCap API key; a missing key falls back to a neutral value
DISCORD_WEBHOOK_URL Discord notifier Fill notifications; unset disables Discord
DIO_BACKTRACE Engine When set, pretty-prints OCaml backtraces on crashes
DIO_DATA_DIR Persistence State-directory override; defaults to /app/data when /app exists, otherwise data
DIO_WATCHDOG_OFF Engine When set, disables the main-loop watchdog
DIO_CANARY Engine 0/false/off/no disables the stop-the-world canary domain (which busy-spins one core while enabled)
DIO_CANARY_THRESHOLD_US Engine Canary spike threshold in microseconds (default 10)
DIO_CANARY_WINDOW_S Engine Canary report window in seconds (default 5)
COLUMNS Engine Fallback log width when stdout is not a TTY (default width 200)
DIO_MOTION Dashboard off/0/false/no disables animations
DIO_FPS Dashboard Caps the animated frame rate (default 30.0)
DIO_DAMAGE Dashboard off/0/false/no forces full-frame redraws instead of incremental damage rendering
HOME Dashboard Used for the persisted ~/.dio_theme theme path

7.0 EXCHANGE ADAPTERS

7.1 Kraken

Kraken uses REST and WebSocket interfaces. Order book and authenticated feeds arrive over websockets; balance is obtained from the authenticated feed. Fees are looked up live from volume tiers (TradeVolume). The capital-oracle adapter falls back to 0.0016 / 0.0026 maker/taker, and a failed live fee fetch at startup is fatal. Prices are rounded to the nearest venue tick, and the strategy layer floors order quantities to the venue lot size. No testnet mode is available.

7.2 Hyperliquid

Hyperliquid supports spot and perpetual trading with an EIP-712 signer. Bare symbols (BTC) resolve to the perpetual; BTC/USDC maps through the spot universe. Testnet is selected with "testnet": true. min_notional is enforced at 10.0 USDC for spot symbols containing / (the oracle sizes against spot balances).

The engine signs with the wallet key from HYPERLIQUID_PRIVATE_KEY. The agent contract, Agent(string source, bytes32 connectionId), is constructed internally; there is no separate agent-address environment variable.

7.3 Lighter

Lighter is a perpetual DEX. It requires the Lighter signer shared library (lighter-signer-darwin-arm64.dylib on macOS, lighter-signer-linux-amd64.so on Linux), a funded account, and usually a relay proxy. LIGHTER_PROXY_URL may be a comma-separated pool of Cloudflare relay endpoints; the client round-robins and retries across them.

Lighter orders are time-limited (approximately 28-day GTT). A renewal daemon cancel-and-replaces orders to approximate good-til-cancelled behavior; order identifiers shall not be relied upon to survive a restart boundary.

7.4 Interactive Brokers

Interactive Brokers connectivity is provided through an IB Gateway (for example, gnzsnz/ib-gateway-docker) over TCP. testnet forces paper mode on port 4002; live mode forces port 4001 unless IBKR_GATEWAY_PORT is set. The grid strategy submits limit orders only, and the engine floors order quantities to whole shares. The account identifier is auto-detected unless IBKR_ACCOUNT_ID is set.

7.5 Alpaca

Alpaca provides US equities in paper or live mode. data_feed selects iex (free) or sip (paid, full-market). The engine respects extended trading hours (pre-market 4:00 AM to 9:30 AM, after-hours 4:00 PM to 8:00 PM, overnight 8:00 PM to 4:00 AM ET). It attaches the extended-hours flag to limit orders placed in extended sessions and forces day TIF only for fractional equity orders; otherwise the requested TIF (default gtc) passes through. min_notional is 1.0. Alpaca pairs are 1:1 (no accumulation up-sizing), and fees default to zero.


8.0 STRATEGIES

8.1 Grid (jacobs_ladder)

The ladder strategy is selected with "strategy": "jacobs_ladder" or "Ladder". It is a pure executor: it buys price drops and sells the purchased base to offset volatility drag. The oracle's decision record is {active, grid_interval, buy_qty, sell_qty}. The grid consumes active, grid_interval, and buy_qty, and sizes sells locally as described below. There is no configuration fallback path; before an oracle decision exists for an asset, the strategy places nothing.

a. Buy side. Exactly one resting buy is placed below the current price and trails upward. It is amended down when it would intrude into the sell zone. b. Sell side. Layered sells are placed above the current price, one per filled buy (and on a buy placement). Each is fill-anchored at buy_fill_price * (1 + grid_interval / 100). On non-Alpaca venues the anchor re-bases to the live bid once the bid has drifted more than one grid step from the last fill. Sells are not proactively repriced or cancelled once placed, although a failed amendment can cancel a stale order. Sells run even while the strategy is inactive because they require inventory, not quote. c. Balance model. Sell size is the last buy fill quantity (falling back to the venue lot of the grid quantity), capped by sellable base. On non-accumulation venues that is base - reserved_base - base locked in resting sells; on accumulation venues it is ledger balance - reserved_base - unnetted hold (resting-sell base is deliberately not subtracted).

Accrual resides in the persistence layer. Profitable sell fills reserve base through Base_accumulation_store, which survives engine restarts. Base accumulation is pre-funded out of realized quote earnings: base is reserved only when accumulated net profit covers the acquisition cost of the withheld base plus the Fear & Greed-interpolated accumulation_buffer. Accumulated profit is debited by that acquisition cost upon reservation, preventing quote-balance bleed.

8.2 Market Maker (MM)

market_maker.ml places one buy and one sell around the top of book:

a. Buy price. Best ask minus the fee backoff, ask * (1 - (2*fee + 0.0001)), clamped so that it never crosses the best bid. When the maker fee is 0.0, the buy rests exactly at the best bid. b. Sell price. The best ask, rounded to the venue tick. c. Profitability guard. A spread that cannot cover round-trip fees is refused: the rounded spread (sell minus buy) shall be at least ask * (2 * fee + 0.0001).

The per-symbol min_usd_balance and max_exposure bounds constrain the account. Crossing either bound pauses new buys on that symbol and places an emergency sell of free inventory.

8.3 Auto-Hedge (Hyperliquid, experimental)

auto_hedger.ml maintains one perpetual short per grid cycle. When the grid buys spot, the strategy opens a short if none is open; when the grid sells, it closes the hedge. Hedges use GTC limit orders at the perpetual top of book, falling back to a market order when no perpetual top of book is available. The strategy is enabled with "hedge": true on a Hyperliquid entry. Refer to the caution in Section 11.0.


9.0 CAPITAL ORACLE

The oracle is a capital-survival sizing engine. For each asset it answers the question: given the asset's all-time price history and the quote capital available on its venue, which order size and grid interval keep the ladder alive across the runway that the asset has actually walked?

9.1 Decision Pipeline

The pipeline is implemented in one code path:

a. History. The asset's all-time merged daily series consists of the venue adapter's bars (Exchange_intf.Oracle.Registry), disk-cached and delta-fetched, extended backwards with Yahoo deep history for the same underlying. Venue bars win on overlap. Nothing is synthesized; there is no gap tolerance and no minimum length. b. References. max_drawdown_pct is the single worst peak-to-trough percentage decline in the whole series (running peak of closes to the deepest subsequent low; recovery is irrelevant). ATH and ATL are taken from intrabar extremes. c. Runway mathematics. realized_dd = (ath - current) / ath; runway_pct = max_drawdown_pct * target_survival; and floor_price = ath * (1 - runway_pct). Three regimes are evaluated in order: Normal (current > floor_price, funding the remaining drop); Unprecedented lows (at the deepest drawdown and at or below ATL, maximum conservatism); and Floor extension (at or below the floor but not at the deepest drawdown, funding extending down to ATL). realized_dd is clamped to [0,1], and max_drawdown_pct is capped just below 1.0. Aggressiveness is realized_dd / max_drawdown_pct (clamped to [0,1]), which biases parameter selection toward tighter grids and larger sizes deeper in the runway; it never overrides survival requirements or bounds. d. Survival replay. A candidate (grid_interval, buy_qty) walks the funded depth geometrically, paying each buy plus venue fees. d_surv is the fraction of that depth survived before the quote runs out (>= 1.0 means fully funded). The exhaustion price, that is, the deepest rung fillable with available capital, is reported alongside it: the venue-simulated shared-capital bottom rung (P_funded) for active strategies, and the single-asset replay's exhaustion point for inactive ones (inactive decisions report d_surv = 0.0). e. Parameter search. buy_qty is searched within [qty, qty * qty_cap_mult], and grid_interval within [gi_min, gi_max] (the strategy entry's bounds). Branches are evaluated in order: the deepest-drawdown Unprecedented lows case short-circuits to the conservative corner (qty / gi_max); Surplus (the aggressive corner is fully funded with quote left over) resolves to qty_max / gi_min; Reachable (largest size at tightest spacing that is still fully funded, aggressiveness-biased) is selected when available; otherwise Unreachable resolves to the conservative corner. The funded-depth test is the fully-funded d_surv >= 1.0, not target_survival; target_survival sets only the funded floor via runway_pct. f. Decision record. The record is exactly {active, grid_interval, buy_qty, sell_qty}, as raw floats, generated by exactly one code path. Values are emitted for inactive strategies as well, for visibility and immediate reactivation. All normalization (tick and lot rounding, minimum notional) occurs at the exchange layer. A strategy is active when d_surv >= min_active_dsurv (or a resting buy must be preserved) and the next buy is affordable. Sells always run because they require inventory, not quote.

9.2 Pooling, Priority, and Cascades

Pools are per venue: one quote pool is shared by that venue's strategies and never crosses venues (two strategies may share one symbol on one venue). Allocation walks strategies in configuration presentation order, where first is highest priority. Each strategy sizes against the entire remaining availability; its next buy ties up buy_qty * current; and a strategy whose next buy does not fit is skipped while remaining capacity passes down the list. When a higher-priority need cannot fit, the cancellation cascade cancels lower-priority resting buys, many lesser orders if necessary, to satisfy one greater, until the need fits. If no combination fits, resolution proceeds to the next-highest priority. Every cancelled strategy re-evaluates on that event and resumes if and only if quote covers its buy.

9.3 Runtime Behavior

Inside the engine the oracle is a supervised module registered as oracle, heartbeated on every published pass plus a liveness ticker, and auto-restarted by the health monitor. Decision computation is pure and in-memory (<10 ms p99): histories reside in a cache refreshed on the background cadence; balances come from the websocket-fed live store or the last snapshot adjusted in process by fill and cancel deltas; and live prices read the in-process top of book. Event bursts coalesce into a single re-resolve. Only domains whose decision changed are woken per pass.

9.4 Command-Line Interface

dio-oracle runs the exact decision pipeline offline against configured assets and prints the decision surface (the four contract values plus diagnostics) without touching live balances. It supports the Kraken, Hyperliquid, and Alpaca venue adapters; Lighter and IBKR have no oracle adapter.

dune exec dio-oracle                                # every trading entry, table output
dune exec dio-oracle -- --symbol BTC/USDC           # one asset
dune exec dio-oracle -- --quote 50000 --base 0.5     # synthetic pool sizing
dune exec dio-oracle -- --cache-only                # never touch the network
dune exec dio-oracle -- --json                      # machine-readable

The oracle configuration section of config.json drives both the runtime and this CLI. Strategy entries carry the hardened bounds that the search walks.


10.0 BUILD AND EXECUTION

10.1 Prerequisites

a. OCaml 5.2 (any distribution: opam, Nix, or Homebrew). b. opam and dune. c. Linux or macOS. WSL2 is supported. d. The Docker (production) image compiles the engine with OxCaml, Jane Street's flambda2 compiler, pinned to ocaml-variants.5.2.0+ox and oxcaml-compiler.5.2.0minus40 (see Section 10.5). The release profile adds -O3 (see ./dune); non-flambda compilers ignore it. Local release builds may use a classic-flambda 5.2.0+flambda switch instead, since the engine compiles under either toolchain.

10.2 Build

opam install . --deps-only
dune build

Local development builds use the dev profile, so they do not need flambda. To build a release binary locally on the classic-flambda toolchain:

opam switch create 5.2.0+flambda ocaml-variants.5.2.0+options ocaml-option-flambda
eval $(opam env)
opam install . --deps-only
dune build --profile=release

10.3 Execution

./_build/default/bin/main.exe

On first start the engine loads config.json from the working directory, connects the exchanges listed in trading, and allows each asset to settle into its own trading domain. Logs are written to stderr asynchronously in the format HH:MM:SS.mmm LVL SECTION message.

10.4 Executables

Table 6 defines the executables.

Table 6. Executables.

Executable Source Function
dio bin/main.ml The engine itself
dio-dashboard bin/dashboard.ml TUI dashboard; connects to the running engine
dio-oracle bin/oracle.ml Configuration-tuning CLI; prints the decision surface offline (see Section 9.4)
dune exec dio-dashboard
dune exec dio-oracle -- --symbol BTC/USDC --quote 10000

10.5 Docker

The container build is split into two images. Dockerfile.base creates the OxCaml switch and installs every opam dependency; compiling the compiler is slow, so the base is tagged and rebuilt only when a dependency changes. Dockerfile starts from it, compiles only the engine, and copies the binaries into a minimal runtime stage.

docker build -f Dockerfile.base -t dio-oxcaml-base:5.2.0minus40 .
docker build --build-arg DIO_BASE_IMAGE=dio-oxcaml-base:5.2.0minus40 -t dio .

The base image embeds oxcaml-port/opam-overlay, a local opam repository overlay carrying patched builds of the dependencies that do not compile under OxCaml as released (msgpck.1.7+dio1, digestif.1.3.1+dio1, and cohttp-lwt.4.0.0+dio1 with its version-coupled cohttp and cohttp-lwt-unix companions).


11.0 CAUTIONS AND LIMITATIONS

a. Experimental auto-hedge. The auto-hedge strategy (auto_hedger.ml) opens Hyperliquid perpetual shorts against spot grid inventory. It is experimental. Review the module and validate it on a testnet before committing capital. b. Restart boundaries. Lighter order identifiers do not survive a restart boundary; the renewal daemon cancel-and-replaces orders to approximate good-til-cancelled behavior. c. Startup strictness. The configuration schema is strict. Unknown keys under trading, unknown keys in the oracle section, and venue-inapplicable keys shall cause the engine to exit at startup. d. Fee lookup. A failed live Kraken fee fetch at startup is fatal. e. Canary cost. The stop-the-world canary domain busy-spins one core while enabled. Disable it with DIO_CANARY when core utilization is constrained. f. cohttp pin. The OxCaml build pins cohttp-lwt-unix < 6.0.0, below the CVE-2026-82481 server-side path-traversal fix, because newer releases change the dependency closure away from the compiler's bundled Conduit. The engine uses cohttp as a client only and never serves files; a CI guard fails the build if server-side file serving is introduced.