Steel: measuring a market that never closes, and refusing to pretend
1. A market that never closes
Flint's six principles stand: distributions rather than points, the clock as the only train/test split, one network for the cross-section, defined risk only, everything observable, and one machine that nothing leaves. Crypto adds a seventh, which this paper returns to repeatedly: the simulation pays every cost a real action would, including the costs that equities do not have. A paper book that skipped a withdrawal fee or a ten-minute settlement would flatter the policy on exactly the trades that fail in practice. And an eighth, which gives Section 8 its name: a map that is modeled says so, and says how well it has done.
Five differences from equities drive the rest of the design.
- No session. There is no close to train through, no overnight gap, no open to wait for. Budgets, limits and refreshes turn over at 00:00 UTC. A gap in the tape is never a holiday; it is an alarm.
- Many venues, one asset. The same coin trades on twenty exchanges with different quote currencies, fees, pair spellings, and occasionally a different coin under the same ticker. The data layer has to merge, rank and disambiguate before anything is modeled.
- Derivatives instead of borrowing. There is no stock loan. A short is a perpetual future with isolated margin, which bounds its loss by construction and pays or receives funding every hour or eight. A long can be the same instrument at 1x, which matters because spot retail fees are an order of magnitude higher than perp fees.
- Money moves on chains. Capital on one venue is not capital on another until a withdrawal clears and a deposit confirms: minutes for a stablecoin on a fast rail, most of an hour for bitcoin on its own chain, with a fee each way.
- The crowd is visible. Funding, open interest, liquidations, the long/short ratio, the basis and the options dealers' gamma are published by the venues themselves, which gives the policy a direct reading of positioning rather than a proxy.
2. Measuring the market
A trading terminal shows what the venues publish. Steel stores it, so that the model, the dashboard and other programs read the same thing, and so that every number can say where it came from. Six stores, each a module with an API route:
- The tape. Every real print from every enabled venue, active or standby, never the simulator, in a ring per pair: the last prints, volume and delta by eleven size bands, CVD per bar, candles at any timeframe, the footprint (sells at the bid and buys at the ask per price bucket, the point of control, the value area, stacked imbalances, the large prints), and each venue's volume-weighted price against a reference venue, which is the premium.
- The books. Each venue's resting bids and asks from its depth channel, kept within one dollar class at a time (a USDT book merged with a USD one puts the best bid and ask a basis apart), the seven depth bands in dollars per side, the aggregate ladder with venue shares, and a decimated level history for the heatmap.
- The derivatives. Liquidations from Binance, Bybit and OKX (a long liquidated is a forced sell), and open interest, funding, mark and index from those three and Hyperliquid, aggregated with open interest summed in dollars and funding weighted by it. Net positioning reads the open-interest change against the taker side: new longs, new shorts, longs closing, shorts covering; heavy volume on flat open interest adds nothing.
- The options surface. Deribit's BTC and ETH chains every ten minutes: Black–Scholes gamma from each mark's implied vol, dealer gamma by strike, the smile per expiry, the term structure, max pain and the zero-gamma level.
- The calendar and the crowd. The FOMC, CPI and jobs dates as published, the monthly and quarterly expiries computed, anything else from a file; Polymarket's open crypto markets with the crowd's probabilities; Fear & Greed, Reddit, funding, the long/short ratio, open interest and the basis as the crowd overlay reads them.
- The dislocations. Every venue's top of book against every other's, within the same dollar and the same kind, net of both venues' taker fees (Section 7).
Each name is owned by the highest-priority enabled venue that lists it and is fresh; ownership fails over without operator action. Three venues are on out of the box (Coinbase, Kraken and Hyperliquid, the one perpetuals venue); the rest are present but off, because a box in a region a venue blocks, or one that wants two websockets rather than twenty, should start clean. Perpetuals venues rank below every spot venue for price, because a perp is a derivative that funding keeps near spot rather than spot itself. Two rules were learned on the first all-venue run. Quote class. Every venue declares whether its dollar pairs are quoted in USD, USDT or USDC; a standby venue may refresh a name's bid and ask only when its quote matches the active venue's, because a USDT book sat ten basis points above Coinbase's last trade and the policy read that basis as the cost of a fill. Identity. A ticker is not an asset: the same three letters named two different coins on two venues, and the dislocation monitor reported a 300-basis-point arbitrage between them. Steel keeps CoinGecko's top thousand by symbol and the names venues publish, marks a symbol with several ranked coins as ambiguous, and lets such a name pair across venues, or be served at all, only from venues resolved to the same coin. A third rule followed: the simulator never speaks for a name a live venue lists; when every live venue is stale the provider is nobody, so the stall alarm fires instead of a made-up price reaching the labels.
Bars are built only from real trades; a quote or heartbeat updates the last price and the spread but never forms a candle. The taker side comes from each venue's own convention, which differs enough to be worth a table in the source code: Coinbase reports the maker's side, Binance a flag that the buyer was the maker, most venues the taker directly. Stalls are alarms: with no closed market to excuse them, three bar intervals without a candle while prices tick means a trade stream died, and the dashboard and the alerts say so.
3. From measurement to features
Each name contributes 40 features per bar, standardized by a running mean and variance kept per name. Nine come from the bars; thirteen are exogenous signals joined from the signal loops and held between refreshes; four read the resting order book across venues (the imbalance within 0.5%, 2% and 10% of the mid and the depth within 1%); four are forced and confirmed flow (longs and shorts liquidated in the bar, new longs and new shorts the open-interest change confirms); two are the options surface for BTC and ETH (dealer gamma as a share of the chain and the at-the-money implied vol); one is the clock to the next scheduled high-impact event; seven are functions of the bar's timestamp, which matters in a market where the hour of day, the funding settlement clock and the CME session all shape the tape.
| Group | Feature | Definition |
|---|---|---|
| Bars | ret, range, logvol, imbalance, spread | log return, log high/low, log volume, (buy − sell) / volume from the taker side, bid-ask spread in bps |
logtrades, closepos, mom, rvol | log trade count, close position within the bar, the sum and the standard deviation of the last 12 bar returns | |
| Attention | news_sent, news_attn | headline tone from a local FinBERT over the crypto desks, rendered in a headless browser, and the name's share of coverage |
reddit_sent, reddit_attn | the same from Reddit crypto chatter | |
| Positioning | fng | the Fear & Greed index |
funding, oi_chg, longshort, basis | the perp funding rate, the change in open interest, the retail long/short ratio and the perp-spot basis, per name, from Bybit and Hyperliquid | |
dvol | Deribit's DVOL, the crypto volatility index | |
| Market | btc_dom, breadth, stable_flow | bitcoin dominance, advancers' share across the 250-name radar, the change in stablecoin supply |
| Calendar | hour_sin, hour_cos, dow_sin, dow_cos, to_funding, cme_open, expiry | the hour and the day of week as angles, time to the next funding settlement, whether the CME session is open, the Friday options expiry |
| Book | ob_imb_05, ob_imb_2, ob_imb_10, ob_depth_1 | resting-book imbalance within 0.5%, 2% and 10% of the mid across the venues of one dollar class; log USD depth within 1% |
| Flow | liq_long, liq_short, pos_long, pos_short | log USD of longs and shorts liquidated in the bar on the perp venues; log USD of new longs and new shorts opened, as the open-interest change and the taker side confirm |
| Options | gex_norm, iv_atm | Deribit's dealer gamma as a share of the chain's notional and the nearest at-the-money implied vol (BTC and ETH pairs; zero elsewhere) |
| Events | event_hours | hours to the next scheduled high-impact event (FOMC, CPI, the jobs report, a quarterly expiry) over 72, one for every pair |
The net
SteelNet is FlintNet with a horizon axis. The same trunk, dilated causal convolutions over the 128-bar window per name with a per-name embedding, cross-asset attention across names, and a regime gate over expert heads, feeds H quantile sets and tail pairs instead of one. The first horizon, twelve bars, is the policy's; the extras, 48 and 288 bars, are forecasts the dashboard shows beside it and the policy consults as a veto. The loss weighs each extra horizon at half the policy's:
with w1 = 1 and wh = ½ for the extras. Why one trunk and not three models: the extra horizons are there to say whether the hour's call is a scalp into a trend, and a shared representation makes that comparison coherent rather than a disagreement between strangers. Only the first horizon is ever traded. Sizing to the machine is Flint's ladder, with one addition forced by the reference box: Steel shares its GPU with Flint, which holds about fifty gigabytes on weekdays, so the ladder runs under a memory floor and the start refuses, naming the other tenant, when the chosen rung would not fit beside it.
The replay and the universe
A market with no close produces 288 bars a day, every day, and the deep backfill reaches back months, so Steel stores one row per bar with its label, mask, timestamp and links to the bars before and after it, and gathers windows by index at batch time; a window is valid only over time-contiguous bars. Consecutive windows share all but one bar, so this holds sixty-four times the history Flint's window ring did for the same bytes: a year of four hundred names fits in under four gigabytes. The closes the store keeps also give the four-hour and one-day labels for free. Every twentieth bar by timestamp is held out of training for good as the one honest overfitting signal.
There are far fewer liquid crypto pairs than liquid stocks, so there is no reason to curate a small set. In the default liquid mode the universe is a sixty-name core, every holding in the operator's accounts, and every dollar pair the enabled spot venues list whose 24-hour dollar volume clears a floor (one million dollars a day, about 120 Coinbase names today), under a hard cap of 512 that is a limit rather than a target. Stablecoins and the fiat pairs Coinbase lists as dollar products are never modeled. Flint's grace and tenure rules apply, so a thin day never churns the set. The modeling floor is separate from the trading floor, which is twice as high.
4. Learning around the clock
Flint learns in four regimes separated by the session. Steel has no session, so the regimes are separated by budget and by who else wants the accelerator.
- Warmup. A start pages thirty days of candles for the universe from the spot venues, perpetuals venues and the simulator filling only names left empty, and trains one pass over the labeled bars with the held-out check every forty steps, stopping when the held-out pinball stops improving or at a thirty-minute cap. Only then does it forecast.
- Online. Each bar whose forecasts have resolved yields a few steps on recency-biased batches, exactly as in Flint.
- The background trainer. Between bars, up to
train_epochspasses over the replay per UTC day, in four-step chunks so a forecast waits one step at most, under the same held-out guard. The budget is bounded by data rather than hours, and resets at midnight UTC. - The deep backfill. Always on: five-day chunks of history every 150 seconds, each name from the first enabled venue that pages by date range and lists it, with eight training steps per chunk off the event loop.
- The GPU handoff. When Flint is running, or will start within fifteen minutes of its timer, Steel is in
sharedmode: its base budget, its memory floor, the weekly backtest deferred, and in the lead-up the background trainer idle and the CUDA cache released. When Flint is off,solomode triples the training budget and drops the floor. The model is not swapped between modes: continuity is the point of online learning.
5. Trust
Calibration is Flint's: an online conformal scale on the predicted band widths that starts wide and earns its way down as the 10–90 band's coverage approaches 80%, and a temperature on the direction logits that starts sceptical. Trust is where Steel differs, because a restart on a market that never closes should not mean hours without a book. The held-out slice of the replay over the last seven days is scored the way live labels are, by direction hit on the calls the policy would have made, and the model is trusted when enough such calls hit; live labels take over as they mature. A scorecard that fails never revokes a trust the live record has earned. The per-name skill gate reads the same held-out calls on that name until live calls fill its record, then ages them out. The net's weight per feature is reported as what the net can read from a column, not what the column causes; a near-zero share says the column is ignored, which is the honest reading of a feature that was added yesterday.
6. Every cost is real
The policy is Flint's composition of auditable gates, each writing its reason into the name's card: the expected move against its uncertainty, the direction's margin over a coin flip, the edge net of every cost, the per-name record, the live spread, the daily dollar volume, the order's own impact by the square-root law and Amihud's illiquidity, the trend and the direction agreeing, and the same side persisting over several bars. Three gates are new: horizon agreement (a one-hour call whose four-hour median points the other way is held), volatility targeting (a position is sized to a daily volatility target over the name's realized daily volatility, floored at a quarter) and cluster caps (no sector carries more than 40% gross, and the book's net bitcoin beta stays within 1.0). The crowd overlay fades positioning that is visible here in a way it is not in equities; a thin edge into extreme crowding is flipped, and the overlay says so.
The paper book
No position may carry unlimited risk. A bearish view is an isolated-margin perpetual short at 2x: the margin posted, notional over leverage and capped as a share of equity, is the most it can lose, because the venue liquidates it at entry × (1 + (1 − maintenance) / leverage) and the book settles that at exactly the margin. A bullish view is, by default, a 1x perpetual long on the perp venue, whose worst case is the stake, the same as spot. There is no naked borrow and no leverage on the book as a whole. Funding accrues on every open perp at the venue's current rate, pro rata per bar: a positive rate pays the shorts and charges the longs.
Every cost a real fill pays
Fees are the venue's, not a flat number. A profile per venue holds its taker and maker rates for spot and perpetuals, its funding interval and its maintenance margin; two settings say which venue each side would trade on; and once a day each configured venue's reader fetches the account's actual fee tier and the book charges that instead. The policy charges two taker legs of the venue a side trades on, the funding a perp would pay while the rate runs against it (never a credit for funding it would earn), the spread, the impact and the Amihud charge against the median before it trades; the book charges the same on its fills. The consequence is deliberate: a retail spot tier never looks tradable at a one-hour horizon, so longs go to the perp venue.
Money between venues
The book keeps its cash on the spot venue and its collateral on the perp venue, split at the start. When a position needs more than its venue holds, the book sends the shortfall, as USDC on the cheapest rail in a table of withdrawal fees and settlement times per asset, charges the fee, and holds the money in flight for the rail's wait. The position does not open until the money lands, and only if the signal is still there when it does. The same table prices what the arbitrage executor refuses: a leg that would need a balance moved first is declined with the transfer's cost in basis points of the trade and its minutes, because a dislocation measured in seconds does not survive a ten-minute deposit. The chart draws every fill where it happened with its side and notional, the live mirror's slippage beside it, and each transfer as a band on the time axis from sent to landed.
7. Cross-venue structure
With twenty venues quoting the same names, the manager's book hook delivers every venue's top of book, active or standby, to a monitor that opens an episode when the best bid across venues stands above the best ask across venues net of both venues' taker fees and a latency allowance, within the same dollar and the same kind, and closes it when the gap is gone. The episode's window, its peak and mean net edge and the top-of-book sizes go to a log and to the dashboard. The monitor measures; it never executes. Cross-stablecoin and spot-against-perp gaps are deliberately not episodes: those are a basis to carry, not a round trip.
An executor exists, off unless switched on and execution is enabled, that sends both legs at once when an episode's net edge clears a minimum, both venues are armed, both legs are the same kind, and the readers' last balances can fund them, flattening a leg whose partner failed. Before anything like that runs with money, the question is what the rules would have captured, and the log answers it. A replay applies the executor's rules by latency rung, from 50 ms to 3 s: an episode counts only when it outlasted the round trip, at its mean net edge rather than its peak, at the smaller of the book's size and the order cap, after a cooldown per name; and because each capture moves base to the buy venue and cash to the sell venue, every two orders' worth of one-way flow on a pair is charged an on-chain rebalance of two stablecoin transfers.
| Latency | Captured | Too fast | Too thin | Mean net bps | Net USD | Of which transfers |
|---|---|---|---|---|---|---|
| 50 ms | 2,161 | 34,360 | 39,871 | 19.4 | 30.89 | −806.00 |
| 300 ms | 1,974 | 62,014 | 22,610 | 19.8 | 6.76 | −775.00 |
| 1 s | 1,631 | 81,332 | 10,860 | 21.0 | 17.56 | −667.00 |
| 3 s | 1,040 | 91,144 | 4,762 | 22.9 | 36.12 | −440.00 |
Three things in that table are the finding. First, the over-wide episodes were a quote problem, not an opportunity, and the monitor now refuses them. Second, the median episode lasted 171 milliseconds; a human, or a process on the other side of the internet from the venues, sees under half of them. Third, the gross edge is real but small, about twenty basis points on a $200 order, and the inventory it leaves behind costs more to move back on-chain than the captures earn. At this size the honest number is zero. Larger size raises the edge faster than the rebalancing cost, and inventory pre-positioned on both venues removes the transfers, which is what an arbitrage desk is.
8. Honest maps
The order-flow terminals draw two heatmaps that traders act on: where liquidity rests, and where leverage would be force-closed. The first is measured and the second is not, and the honest thing is to say so on the map itself. Steel's book heatmap is the mean resting dollars per price row per column from the level snapshots the venues' depth feeds showed; its label says what rested, not what would fill. Steel's liquidation map is a model: when open interest rises in a bar, the new notional is split by the taker side and projected to each leverage band's liquidation price from the bar's close; when open interest falls the clusters shrink in proportion; when price trades through a level the cluster is spent. No venue publishes aggregate liquidation levels, so the only check the map has is against the forced orders the venues do publish, which is what the calibration score reports, with its tolerance in basis points, beside the map wherever it appears. The options surface is measured from Deribit's marks and modeled through Black–Scholes; the dealer-gamma figure is labelled per 1% move, and max pain and zero gamma are what the chain implies, not where price will go.
The same rule applies to the model's own output. The forecast fan is a calibrated distribution with its coverage tracked; the calls drawn on the chart are filled when they hit and hollow when they missed; the paper book's fills and the live mirrors are drawn where they happened, with the slippage; a feature's weight is called what it is. A dashboard that only showed the hits would be a terminal, not an instrument.
9. From paper to live
Orders exist in one package, behind one gate, and every switch defaults to off. Execution must be enabled; a venue trades only when armed with a trade key of its own, separate from the read-only key; every cap (per order, per name, per venue, per day, exposure, leverage) must be set, because an unset cap is zero; the box clock must be within a second of the venues' server time; the kill switch, a flag, a button or a file, refuses everything; a reject starts a cooldown; and every request and answer goes to an audit log. Clients default to a dry run that signs and logs without sending. Balance readers are a separate package that can only read the endpoints each declares, and a test greps every path-like string in both packages for order, withdrawal and transfer words. The ticket is the operator's: editable, with its limit, stop and target as draggable rays on the chart, and a choice of sending it as one order or as a TWAP, a scale ladder or a chase; every child goes through the same gate.
Promotion is a checklist the operator reads, never a rule the code applies: the model trusted, the weekend backtest scored over enough days with a positive net and an adequate hit rate, the paper book running for enough days with an adequate Sharpe, drawdown and return. Once live, every paper fill becomes an order of a fraction of its notional through the client of the venue that side trades on, the book keeps what it expects to hold, reconciles it against the readers' positions on every poll, reports slippage against the paper price, and demotes itself at once on the kill switch, a drawdown past a limit or a drift past a tolerance. A stop cancels every open order; a panic button closes every position with reduce-only orders; a live book not heard from for half an hour comes back demoted. Alerts carry fills, calls, refusals, stalls, demotions and the kill switch to a phone, and the operator's own series alerts ride beside them.
10. Evaluation
Live scoring is Flint's: every forecast is scored an hour after it is made on direction, band coverage and pinball loss, per name and in aggregate, and the paper book's record is the primary evidence. The held-out scorecard is the second, recomputed on demand over the last seven days and written to a daily scorecard the Model view shows over time beside calibration by name, the drift of each standardized feature over the last day, and the net's weight on it. The walk-forward backtest is Flint's too, with days that are UTC days and a Sharpe that annualizes over 365, run weekly in the thinnest tape, Saturday 00:00 UTC, when it owns the GPU and Flint is off; the policy sweep over its stored forecasts proposes gates without applying them. Whether the forecasts carry economic edge after the costs of Section 6 is the open question the backtest exists to answer, and this paper will carry the number when there is one.
11. Limitations
- The forecasts' economic edge after costs is unproven. On a CPU development box the net is too small to say anything; on the reference GPU the weekly backtest and the paper record are the evidence, and both are young.
- The liquidation map is modeled, and its calibration depends on the three venues that publish forced orders; Hyperliquid publishes none, so its share of the leverage is never scored.
- The transfer table is typical values, not a feed. Venues change withdrawal fees and processing times; the override file is the remedy until a reader fetches them.
- The paper book fills at the touch with the spread charged but without the square-root impact the policy gates on. At the book's size and the names it trades the difference is small; it is the one cost the fill does not pay.
- Identity resolution covers the top thousand coins and the names Coinbase publishes. A collision below that rank, or on a venue that publishes no names, can still pair two different coins; the impossible-gap rule catches the gross cases only.
- The dislocation log was gathered from one site. Latency to each venue, and therefore the share of episodes a real executor would see, depends on where the box sits.
- Every reader and order client was exercised live with dummy credentials up to the venue's "invalid key" answer, which proves the request shape, not the response parsing, and nothing has been sent to a live matching engine.
- The event calendar is the published 2026 dates and the computed expiries; token unlocks and listings are not in it.
12. Future work
- Pairs as a conversion graph. A pair is an edge between two assets on a venue; a rail is an edge between venues; a router prices the path, and the book holds assets rather than symbols. Most of the transfer and quote-class machinery in Sections 2 and 6 is this graph seen one edge at a time.
- Real exogenous history in the replay, so the thirteen signal columns are learned from months of data rather than from the live bars since the start.
- Impact on the paper fill, so the book pays what the policy already charges.
- Withdrawal fees and times from the venues' own endpoints, replacing the static rails.
- Funding as a target. The funding rate is forecastable and paid every hour or eight; a carry book that holds the basis with bounded risk is a second strategy the same data supports3.
- Meta-labeling over the outcome records the book already keeps, sizing by the probability that acting on a call pays after costs4.
- The tape's delta, the premium and the basis as feature columns, each a deliberate reset of the net, with a permutation test for what each contributes.
References
- I. Makarov and A. Schoar, "Trading and Arbitrage in Cryptocurrency Markets," Journal of Financial Economics 135(2), 2020. Article
- I. Gibbs and E. Candès, "Adaptive Conformal Inference Under Distribution Shift," NeurIPS, 2021. arXiv
- A. Schmeling, M. Schrimpf and K. Todorov, "Crypto Carry," BIS Working Paper 1087, 2023. Paper
- M. López de Prado, Advances in Financial Machine Learning, Wiley, 2018: triple-barrier labeling and meta-labeling.
- Y. Amihud, "Illiquidity and Stock Returns: Cross-section and Time-series Effects," Journal of Financial Markets 5(1), 2002.
- R. Almgren, C. Thum, E. Hauptmann and H. Li, "Direct Estimation of Equity Market Impact," Risk, 2005: the square-root law.
- R. Koenker and G. Bassett, "Regression Quantiles," Econometrica 46(1), 1978: the pinball loss.
- Flint: design of a continuously learning intraday market model, thingg, 2026. Whitepaper
Appendix: principal settings and the data API
| Section | Setting | Default | Role |
|---|---|---|---|
| Market data | feed | "auto" | auto | or a single source id to force |
sources_on | "" | comma list to force EXACTLY these on (plus sim); "+bitget,+kucoin" adds to the defaults | |
sources_off | "" | comma list of venues to keep off even if on by default (Coinbase, Kraken, Hyperliquid) | |
coinbase_ws | "wss://ws-feed.exchange.coinbase.com" | ||
coinbase_rest | "https://api.exchange.coinbase.com" | ||
kraken_seconds | 20.0 | REST poll between websocket reconnects | |
bybit_seconds | 15.0 | seconds between Bybit ticker polls | |
okx_seconds | 15.0 | ||
coingecko_seconds | 60.0 | free tier: ~30 calls/min across everything | |
bar_seconds | 300.0 | 5-minute bars (every venue serves them natively) | |
backfill_days | 30.0 | history every name is paged before the first forecast (Coinbase serves 30 days for 60 names in about two minutes) | |
backfill_seconds | 0.0 | the same in seconds; 0 = backfill_days (set either; seconds wins when both are set) | |
backfill_pages | 0 | max REST pages per symbol (0 = whatever backfill_days needs) | |
train_epochs (live) | 1.0 | background training: up to this many passes over the replay per UTC day (0 = off); the market never closes, so this runs between bars | |
overfit_patience (live) | 3 | background training stops for the day when the held-out pinball (5% of windows never trained on) fails to improve over this many 40-step checks (0 = off) | |
deep_backfill_days (live) | 30.0 | keep paging history back this far and train on it (0 = off); the cap on market-data use | |
deep_chunk_days | 5.0 | days per deep-backfill page (5 days = 1,440 five-minute candles, 5 Coinbase requests per name) | |
deep_backfill_seconds | 150.0 | pause between pages, so a 60-name burst stays well under Coinbase's public limit | |
| Problem definition | window | 64 | bars of context fed to the model |
horizon | 12 | bars ahead (12 x 5min = 1h forecast): the policy's horizon | |
extra_horizons | "48,288" | more horizons the net forecasts alongside (4 h and 24 h in bars), shown, never traded; "" = none | |
quantiles | (0.1, 0.25, 0.5, 0.75, 0.9) | ||
| The replay stores absolute labels either way, so this is a run-time switch, not a reset. | target | "absolute" | |
index_symbols | "BTC-USD,ETH-USD" | "the market" for the relative target: modeled index names, first = the prior; unmodeled ones are ignored | |
hedge_index (live) | on | relative target: hold, per index, the weight that cancels the book's loading on it | |
hedge_min (live) | 0.01 | an index hedge smaller than this share of the book is not worth its cost | |
beta_windows | 8640 | label rows the loadings are fit on (about 30 days of 5-minute bars) | |
beta_shrink | 400.0 | pseudo-observations behind the prior (1 on the first index, 0 on the rest) | |
| Compute + auto-sizing (benchmarks the machine on first run and picks the biggest | device | "auto" | auto | cpu | cuda | mps |
compile | off | torch.compile the net (CUDA only); falls back to eager on failure | |
auto_size | on | ||
autotune_util | 0.7 | fraction of the bar interval training may use | |
max_warmup_seconds | 180.0 | the autotune's go-live ceiling over warmup_steps: caps the auto model size | |
max_warmup_minutes | 30.0 | the most time the go-live warmup may take over the backfilled history (0 = no cap) | |
| Model (overridden by auto-sizing unless auto_size is off) | d_model | 48 | |
dilations | (1, 2, 4, 8, 16) | ||
n_experts | 3 | ||
n_heads | 4 | ||
dropout | 0.15 | ||
| Online learning | lr (live) | 5e-4 | |
weight_decay | 1e-2 | ||
label_smoothing | 0.1 | softens the P(up)/P(down) targets so the net cannot claim ~99% certainty | |
label_cap_bps | 3000.0 | cap on label magnitude in bps (0 = off); crypto moves 30% in an hour only on a broken feed | |
direction_threshold_bps | 10.0 | deadband: P(up)=P(ret>+t), P(down)=P(ret<-t); the gap is P(flat) | |
batch_size | 16 | ||
steps_per_label (live) | 2 | ||
replay_size | 8640 | replay capacity in bars when no replay_gb is set (8,640 = 30 days of 5-minute bars) | |
replay_gb | 0.0 | replay budget in GB; when set, the bar count follows the universe and replay_size is ignored | |
replay_days | 400.0 | the most history the replay keeps, whatever the budget allows (0 = no cap) | |
recent_frac | 0.3 | share of each batch drawn from the newest samples | |
recent_n | 256 | ||
warmup_steps | 60 | the least training on backfilled history before going live (the autotune's budget unit) | |
trust_days | 7.0 | the held-out bars of the last N days score the model before live labels can: trusted on history when they clear min_labels calls at min_hit_rate | |
warmup_epochs | 1.0 | passes over the backfilled history before the first forecast, with the held-out guard; 0 = warmup_steps only | |
input_noise | 0.1 | gaussian noise on standardized inputs during training | |
min_labels (live) | 48 | live (out-of-sample) labels before suggestions are counted | |
band_gamma | 0.05 | adaptive conformal step for the band scale | |
temper_lr | 0.01 | online temperature step for P(up) | |
torch_threads | 2 | overridden by autotune (up to half the cores on a GPU box) | |
| Suggestion policy | score_threshold (live) | 0.35 | |q50| / IQR needed to act |
prob_margin (live) | 0.06 | |P(up) - 0.5| needed to act | |
cost_bps (live) | 0.0 | a floor on the spread the policy charges (the live spread replaces it when wider); fees are separate, by venue | |
trade_venue_spot | "coinbase" | the venue a spot position would trade on: its fee profile (sources/fees.py) is what the policy and the book charge | |
trade_venue_perp | "hyperliquid" | the venue a perp (long or short) would trade on | |
long_mode | "perp" | perp: a bullish view is a 1x perp on trade_venue_perp (a stake-capped loss, perp fees and funding) | spot: it is spot on trade_venue_spot | |
long_leverage (live) | 1.0 | isolated margin per perp long = notional / leverage; 1x has no liquidation price and the stake is the worst case | |
spot_fee_bps (live) | 0.0 | taker fee per spot leg; 0 = the spot venue's profile | |
perp_fee_bps (live) | 0.0 | taker fee per perp leg; 0 = the perp venue's profile | |
max_size (live) | 1.0 | ||
move_floor_bps (live) | 8.0 | min |expected move| (bps) to take a side | |
| Risk gates (policy only; the model is untouched). All live controls. | skill_min_n (live) | 8 | matured direction calls a name needs before it may trade |
confirm_bars (live) | 2 | the same side must persist this many bars before entry | |
min_hold_bars (live) | 6 | a spot position is kept at least this long unless the signal reverses | |
short_min_hold_bars (live) | 3 | hold a perp short at least this many bars before closing (anti-churn) | |
| The cost edge beyond the quoted spread (steel/policy.py): square-root price impact of the order the | impact_eta (live) | 0.1 | square-root impact coefficient: cost = eta * sigma_daily * sqrt(Q / ADV) |
max_impact_bps (live) | 10.0 | a name whose Amihud illiquidity says the order would move it more than this does not trade | |
funding_in_edge (live) | on | charge the funding a short would pay (negative rates) against a bearish signal's edge | |
funding_interval_hours | 0.0 | the perp venue's funding interval; 0 = its profile (8 h on Bybit/Binance, 1 h on Hyperliquid). The rates Steel reads are Bybit's 8-hour unit, scaled to the interval | |
| Exits on positions (0 = off) and a daily loss limit that stops new entries for the UTC day. | stop_loss_bps (live) | 300.0 | close a position down this much from entry, min hold or not (crypto: wider than equities) |
take_profit_bps (live) | 0.0 | close a position up this much from entry (0: the forecast decides) | |
max_hold_bars (live) | 0 | close a position held this many bars (0: the forecast decides) | |
exit_cooldown_bars (live) | 12 | after an exit the name waits this many bars before re-entering | |
horizon_agree (live) | True | the next horizon's median (4 h) must not point the other way before a 1 h call trades | |
vol_target (live) | 0.05 | daily volatility a full-size position is sized for: size scales by vol_target / the name's realized daily vol (floor 0.25x; 0 = off) | |
max_cluster_weight (live) | 0.4 | gross weight cap per sector (the market scan's categories); 0 = off | |
max_beta_exposure (live) | 1.0 | cap on the book's net BTC beta (sum of weight x loading on BTC-USD); 0 = off | |
daily_loss_limit (live) | 0.03 | fraction of the UTC day's opening equity lost before new entries stop for the day (0 = off) | |
max_spread_bps (live) | 20.0 | names quoting wider than this do not trade; the live spread also replaces cost_bps when larger | |
min_dollar_volume (live) | 2e6 | names trading less than this a day (from the bars held) do not trade: thin books, fake fills | |
size_by_coverage (live) | on | scale size by band coverage / 0.8, so an overconfident model trades small | |
short_leverage (live) | 2.0 | isolated margin per perp short = notional / leverage; the loss is capped at that margin (liquidation at +1/leverage) | |
short_max_frac (live) | 0.05 | margin per short, as a fraction of equity: the most one bearish view can lose | |
min_hit_rate (live) | 0.5 | only trade once directional skill (hit_ema) beats a coin flip | |
paper_min_trade_frac | 0.015 | skip paper rebalances smaller than this share of equity (anti-churn) | |
kelly_fraction (live) | 0.15 | fractional-Kelly sizing: weight = this * |q50|/IQR (risk-adjusted edge) | |
| Signals + the crowd overlay | muted_symbols | "" | symbols to watch-but-not-suggest (comma list) |
signals_off | "" | signal providers off by default: social, feargreed, derivatives, vol, market | |
signals_minutes (live) | 5.0 | how often to refresh exogenous signals | |
market_scan_seconds | 90.0 | light loop for the whole-market panel (CoinGecko's free tier is ~30 calls/min) | |
portfolio_seconds | 60.0 | how often to poll exchange account balances (read-only) | |
operator_half_life | 1800.0 | decay of an injected human note (seconds) | |
max_universe | 512 | hard cap on modeled symbols (Coinbase lists about 400 USD pairs; attention is cheap, the websockets are the limit) | |
universe_mode | "liquid" | liquid: every dollar pair the enabled spot venues list above model_dollar_volume (plus the core and holdings) | core: the configured core, holdings and the most interesting names (Flint's rule) | |
model_dollar_volume | 1e6 | the modeling floor in liquid mode: names trading less a day are not worth a row (the trading floor, min_dollar_volume, is separate) | |
dislocation_min_bps | 0.0 | an episode needs the cross-venue gap, net of both taker fees, above this | |
dislocation_latency_bps | 0.0 | a margin for the price moving while the orders travel | |
dislocation_max_gap_bps | 500.0 | a gross gap beyond this is a wrong quote (another asset or unit, a broken book), never an episode | |
dislocation_stale_s | 10.0 | a venue's quote older than this is not a leg | |
| The GPU handoff with Flint (steel/handoff.py) | handoff | True | watch for the peer and switch between shared and solo modes |
handoff_peer | "flint-run" | the peer's systemd user unit | |
handoff_peer_process | "python -m flint" | ||
handoff_peer_port | 8000 | ||
handoff_schedule | "Mon..Fri 19:45" | when the peer's timer starts it (local time), for the lead-up release; blank = detection only | |
handoff_lead_minutes | 15.0 | release the GPU this long before the peer starts | |
solo_train_multiplier | 3.0 | train_epochs x this while the peer is off | |
solo_memory_floor_gb | 2.0 | memory_floor_gb while the peer is off | |
| Order books (steel/book.py) | books | True | subscribe to each live venue's depth (Binance, Bybit, OKX, Kraken, Hyperliquid) |
book_mb | 64.0 | memory for the decimated level history (the heatmap), split across the universe | |
book_history_s | 15.0 | how often a row of levels is kept per venue | |
book_levels | 40 | levels per side per venue kept in that row | |
| Derivatives feeds (steel/derivs.py, steel/sources/derivs.py) | derivs | True | liquidations, OI, funding and mark from the perp venues' public streams |
derivs_venues | "binance,bybit,okx,hyperliquid" | which feeds run (ids binance-perp, bybit-perp, okx-perp; a bare venue name works) | |
liq_mb | 16.0 | memory for liquidation events, split across the universe | |
liq_alert_usd | 250000.0 | alert on a single liquidation at or above this notional (0 = off) | |
| The trade tape (steel/tape.py) | tape_mb | 256.0 | memory for every live venue's prints, split across the universe (about 8M prints) |
tape_hours | 24.0 | how far back the tape answers | |
tape_alert_usd | 0.0 | alert on a single print at or above this notional (0 = off) | |
bubble_usd | 100000.0 | a print at or above this notional is a bubble on the chart | |
predictions_minutes | 10.0 | how often Polymarket's crypto markets are read (0 = off) | |
options_minutes | 10.0 | how often Deribit's BTC and ETH chains are refreshed for the GEX, smile and term structure (0 = off) | |
absorption_sensitivity | 50.0 | 10 (few flags) to 90 (many): how large new positioning must be, relative to recent bars, with price not moving | |
| The operator token (steel/auth.py) | auth | "mutate" | mutate: every POST needs the token (the default) | all: reads and the websocket too | off: nothing does |
token | "" | the operator token; empty = generated at first start, sealed in the vault, printed by `steel token` | |
api_weight_per_minute | 1200 | the /api/v1 weight budget per token per minute (steel/apiv1.py) | |
backup_dir | "" | when set, the engine writes a `steel backup` archive there once a day | |
backup_keep | 14 | how many daily archives to keep there | |
backup_replay | False | include the replay buffer in the daily archive (large) | |
flatten_on_stop | False | a stop closes every live position (reduce-only market orders); open orders are always cancelled | |
live_deadman_minutes | 30.0 | a live book not heard from for this long at a start is demoted (and flattened when execution is on); 0 = off | |
clock_max_offset_s | 1.0 | box clock vs venue server time past which execution is refused | |
| Execution (off by default; see steel/execution). Every cap at zero refuses every order. | execution | off | the master switch: nothing is ever sent while it is off |
execution_venues | "" | comma list of armed venue ids; each needs its own <venue>-trade.json key | |
execution_testnet | off | use the venues' testnets where they have one | |
execution_dry_run | on | build, sign and log every order without sending it | |
exec_max_order_usd | 0.0 | ||
exec_max_name_usd | 0.0 | per name per UTC day | |
exec_max_venue_usd | 0.0 | per venue per UTC day | |
exec_max_day_usd | 0.0 | across everything per UTC day | |
exec_max_exposure_frac | 0.5 | open notional over live equity | |
exec_max_leverage | 2.0 | ||
exec_reject_cooldown_s | 300.0 | ||
| The live book (steel/live.py): the paper book mirrored to the armed venues once promoted | live_scale | 0.1 | live notional per paper notional (the paper book is $100k) |
live_min_backtest_days | 10 | ||
live_min_backtest_net_bps | 0.0 | net bps per scored day | |
live_min_hit | 0.52 | ||
live_min_paper_days | 7.0 | ||
live_min_paper_sharpe | 0.05 | per-bar Sharpe of the paper curve | |
live_max_drawdown | 0.05 | paper drawdown to be eligible; live drawdown from peak that demotes | |
live_drift_tol | 0.25 | sent vs held, as a share of the expected size, that demotes | |
| The arbitrage executor (steel/arb.py): acts on the dislocation monitor's episodes, through the gate | arb | off | |
arb_min_edge_bps | 5.0 | net of both taker fees and the latency margin | |
arb_max_order_usd | 200.0 | per leg, or the top of book when smaller | |
arb_max_open | 3 | perp pairs held at once | |
arb_cooldown_s | 60.0 | per name between attempts | |
universe_refresh (live) | on | once a day at the UTC boundary: the universe is rebuilt by its mode (a live control) | |
interesting_count (live) | 40 | how many "interesting" slots the universe keeps beyond core and holdings (a live control) | |
interesting_min_dollar_volume | 2e7 | a radar name needs this much daily dollar volume to be a candidate | |
interesting_grace_days | 14 | a promoted name is demoted after this many days without ranking in the top 1.5 x count | |
interesting_min_tenure_days | 5 | ... and never before this many days in the universe | |
radar_top | 250 | movers watchlist size (CoinGecko serves 250 per page) | |
overlay_enabled | on | the crowd overlay: margin of safety and fade-the-crowd on top of the raw call | |
overlay_aggr (live) | 0.7 | 0..1: how hard the overlay fades crowded trades | |
overlay_fade_at (live) | 0.45 | crowding magnitude above which aligned trades get faded | |
overlay_safety (live) | 0.5 | margin-of-safety penalty on asymmetric downside | |
| News skimmer | news_enabled | on | |
news_minutes (live) | 3.0 | ||
news_scorer | "finbert" | headline tone: finbert (ProsusAI/finbert on the CPU, graded) or lexicon (word lists) | |
finbert_model | "ProsusAI/finbert" | ||
news_browser | on | ||
news_off | "" | news source ids off by default: browser | |
news_sources | "" | "Name|https://url,Name2|https://url2" to override the browser defaults | |
| Narrative brief (a LOCAL model only; never a cloud API) | ollama_host | "http://localhost:11434" | |
brief_model | "qwen3.5:latest" | ||
brief_small_model | "qwen3.5:latest" | ||
brief_minutes | 15.0 | ||
brief_timeout | 1800.0 | ||
brief_enabled | off | ||
brief_backend | "ollama" | "ollama" or "openai" (llama-server / vLLM, local only) | |
brief_openai_base | "http://localhost:8001/v1" | ||
| Server / UI | host | "127.0.0.1" | |
port | 8100 | 8100: Flint owns 8000 on a box that runs both | |
tick_hz | 2.0 | ||
chart_bars | 160 | ~13h at 5-min bars | |
paper_start | 100000.0 | starting cash for the paper-trading book | |
paper_perp_cash_frac | 0.5 | the share of the paper book that starts on the perp venue as collateral (the rest on the spot venue) | |
transfer_asset | "USDC" | the stablecoin cash moves between venues as (fees.RAILS) | |
min_transfer_usd | 250.0 | the smallest transfer the paper book makes between its venues | |
log_size | 60 | ||
| Persistence | state_dir | "state" | |
checkpoint_minutes | 5.0 | ||
replay_save_minutes | 30.0 | the replay buffer (tens of GB when full) is written at most this often, and at shutdown | |
backtest_weekly (live) | on | walk-forward backtest over the replay once a week (a live control; action "backtest" runs one any time) | |
backtest_weekday (live) | 5 | 0 = Monday ... 6 = Sunday (UTC); Saturday is the quietest crypto day | |
backtest_hour (live) | 0 | UTC hour the weekly run may start | |
backtest_hours (live) | 12.0 | time budget for a run; the per-day training budget shrinks to fit it (0 = uncapped); live training waits meanwhile | |
backtest_night_epochs (live) | 1.0 | passes over the history seen so far per simulated day, as the background trainer does per day | |
backtest_days (live) | 0 | score only the last N days (0 = all timestamped history) | |
backtest_cost_bps (live) | 8.0 | round-trip cost per unit of position in the backtest (the live book charges the quoted spread) | |
backtest_warm_days | 7 | training-only days before scoring starts, like warmup | |
backtest_target (live) | "both" | absolute | relative | both (two runs, each on half the budget, reported side by side) | |
memory_floor_gb (live) | 10.0 | GB that must stay free: the backtest sizes its net to fit above it and stops below it; deep paging pauses near it (a live control) |
| Group | Columns | Features | What they read |
|---|---|---|---|
| Micro | 9 | ret, range, logvol, imbalance, spread, logtrades, closepos, mom, rvol | |
| Exogenous | 13 | news_sent, news_attn, social_sent, social_attn, fng, funding, oi_chg, longshort, basis, dvol, btc_dom, breadth, stable_flow | |
| Book | 4 | ob_imb_05, ob_imb_2, ob_imb_10, ob_depth_1 | resting-book imbalance within 0.5%, 2%, 10% of mid; log USD depth within 1% (steel/book.py) |
| Flow | 4 | liq_long, liq_short, pos_long, pos_short | log USD of longs and shorts liquidated in the bar; of new longs and new shorts opened (OI-confirmed) (steel/derivs.py, steel/positioning.py) |
| Options | 2 | gex_norm, iv_atm | dealer gamma as a share of the chain's notional, the nearest ATM implied vol (BTC and ETH pairs; 0 elsewhere) (steel/options.py) |
| Events | 1 | event_hours | hours to the next high-impact scheduled event over 72 (steel/calendar.py), same for every pair |
| Calendar | 7 | hour_sin, hour_cos, dow_sin, dow_cos, to_funding, cme_open, expiry | functions of the bar's timestamp: the hour and the day of week as angles, time to the next funding settlement, whether the CME session is open, the Friday options expiry |
| Route | Weight | Returns |
|---|---|---|
GET /api/v1 | 0 | this index: the routes, their weights and the budget |
GET /api/v1/usage | 0 | the weight used and left for this token in the current minute |
GET /api/v1/markets | 1 | every modeled pair: base, serving venue, kind, price, 24-hour dollar volume, trusted |
GET /api/v1/screener | 5 | one row per pair from every stored series (price, 1-hour change, tape delta, OI, funding, liquidations, book imbalance, dislocations, the model's call) |
GET /api/v1/candles/{pair}?tf=300&n=300&venues=a,b&to= | 2 | OHLCV with buy/sell volume per candle from the tape (any tf for the day held) or the 5-minute bars beyond |
GET /api/v1/tape/{pair}?n=50&min_usd=0&bars=60 | 2 | the last prints, size-band volumes over the hour, per-bar delta and CVD |
GET /api/v1/delta/{pair}?bars=60&band= | 2 | per-bar buy and sell volume, delta and CVD; `band` selects one size band (0..10) or a `min_usd,max_usd` pair |
GET /api/v1/footprint/{pair}?tf=300&n=120&bucket_bps=2 | 5 | bid/ask volume per price bucket per candle, POC, value area, imbalances, large prints |
GET /api/v1/book/{pair}?n=16&bucket_bps=0"e= | 2 | the aggregate ladder with venue shares, the depth bands, per-venue books |
GET /api/v1/heatmap/{pair}?tf=300&n=120&bucket_bps=5 | 5 | resting liquidity per price row per column from the level history |
GET /api/v1/liqmap/{pair}?n=288&bucket_bps=10 | 5 | the modeled liquidation map: clusters per leverage band, their evolution |
GET /api/v1/derivs/{pair}?bars=72 | 2 | open interest, funding and mark per venue and aggregated; liquidations per bar and the recent list |
GET /api/v1/positioning/{pair}?n=288 | 3 | OI-confirmed net longs and shorts per bar, absorption flags |
GET /api/v1/premium/{pair}?tf=300&n=288&reference=binance | 3 | per-venue premium to the reference venue and the perp-spot basis, per candle |
GET /api/v1/levels/{pair}?days=35 | 2 | day, week and month opens, highs, lows and closes |
GET /api/v1/options/{asset} | 2 | Deribit's chain: GEX by strike, smile, term structure, max pain, zero gamma |
GET /api/v1/forecast/{pair} | 1 | the model's latest call: action, quantiles, P(up), size, the extra horizons |
GET /api/v1/calendar | 1 | the next scheduled events and the event clock |
GET /api/v1/predictions | 1 | Polymarket's open crypto markets |
WS /api/v1/stream?pairs=&channels= | 0 | trades, book tops, liquidations, bars and rule hits as they happen |