Engine and data
Build a market, run it, and read back what it made.
Running a market
import tradefloor as tf universe = tf.Universe.random(20, seed=11) macro = tf.Macro(federal_funds_rate=0.025, corporate_bond_yield=0.052, vix=16.0) engine = tf.Engine(seed=42, universe=universe, macro_state=macro) engine.run_days(60) bars = engine.bars(grain="day") # OHLCV per name per day truth = engine.truth() # fair value, mispricing, eleven factors economy = engine.macro_table() # the macro state, one row per day
Three inputs fix a market. The universe is the roster of companies, in order: the engine draws random numbers in roster order, so a re-sorted roster is a different market from the same seed, and universe.fingerprint covers order as well as content. The Macro is the economy on day zero only, because the engine moves rates, inflation, the business cycle and the VIX forward at every close after that. The seed fixes every random draw.
There are two seeds. The one passed to Universe.random picks each company's fundamentals, and the one passed to Engine picks the market. To find how much of a result was luck, hold the universe fixed and change the engine's seed. A seed can be any integer from 0 to 2**64 - 1.
A pin replaces the model's own update of a macro field once, and the next days move on from it. To hold a field on every day, pin it every day or use a scenario.
Output tables
| Call | What it holds |
|---|---|
bars(grain="day") | OHLCV per name per bar. Volume is the shares traded in the bar. |
truth() | Fair value, mispricing and the eleven factor contributions behind every move, per tick. |
macro_table() | The economy, one row per day. |
take_fills() | What an agent asked for against what it got. |
book_table() | Recorded book depth, per tick, side and level. |
Each is an Arrow table, which polars, pandas, pyarrow and duckdb read without copying, and tradefloor depends on none of them. Value columns are float64 throughout, and a missing value is NaN, never zero.
Universe.random(20, seed=101, bonds=True) adds three rate indices, UST2Y, UST10Y and IGCORP, priced from the simulated yield curve, so a portfolio can hold bonds beside the stocks.
Real companies from EDGAR
snap = tf.edgar.fetch(as_of="2024-06-30", limit=100,
user_agent="Jane Roe jane@example.org")
snap.save("edgar-2024h1.json") # hashed, so it can be cited
universe = tf.Universe.from_edgar(snap, federal_funds_rate=0.03)The universe keeps the spread of valuations, the sector weights and the share of loss-making companies in the filings. Its price paths are still simulated. user_agent must carry a contact address, as the SEC's fair-access policy asks. EDGAR can revise past records, so cite the saved snapshot file. rank_by="equity", the default, favors large balance sheets; rank_by="public_float" gives a list closer to a real index.
Units and conventions
The engine does not clamp what you pass it: a malformed input raises ValidationError and a refused order raises OrderError. Some layers above it resize orders. An LLM adapter cuts an order to the participation cap and records the cut, tf.baselines.rebalance caps each trade at a share of the name's average daily volume, 2% by default, and the gym environment clips an action to [-1, 1] and scales one over the leverage cap down, setting info["scaled"].
| Write | Not | Why |
|---|---|---|
0.052 | 5.2 | Rates are decimals. 5.2 raises an error that says so. |
None | 0.0 | corporate_bond_yield=None falls back to the policy rate; 0.0 is a real value. |
eps=-1.20 | filtering losses out | A loss-making company is valued from book value, a path the model needs. |
3_000_000 | 0.03 | Short interest is a number of shares. |
model="pt-v20" | Engine(garch_alpha=...) | Coefficients are chosen by preset name, so two results can be compared. |
Type aliases
Literal types the signatures below refer to.
| Name | Accepted values |
|---|---|
| ColumnField | Literal[ "price", "previous_close", "previous_tick_price", "open", "high", "low", "volume", "avg_volume", "market_cap", "mispricing_s", "mispricing_s_prev_close", "mispricing_momentum", "last_daily_return", "maker_inventory", "garch_variance", "beta", "short_interest", "float_shares", ] |
| CycleName | Literal["expansion", "peak", "contraction", "trough", "recovery"] |
| FactorName | Literal[ "reversion", "momentum", "crowd_lean", "company_news", "order_flow_impact", "short_squeeze_effect", "random_noise", "circuit_breaker", "jump", "overnight", "fair_value_shift", ] |
| Grain | Literal["tick", "day"] |
| MarketStatusName | Literal["open", "pre_market", "after_hours", "closed"] |
| Side | Literal["buy", "sell"] |
Engine
A running market. You build it from a seed (the number that fixes every random draw), a roster and the economy on day zero, then advance it a day or a tick at a time.
run_session changed in 0.8.5: an agent's trades go in fills= and a standing rate in flow_per_tick=, and order_flow= raises ValidationError. The 0.8.5 release notes say why and what it changes.
Engine(
*,
seed: int,
universe: Sequence[Instrument],
macro_state: Macro | None = None,
model: str | ModelParams | None = None,
) -> None
Running the market
| Signature | Meaning |
|---|---|
| def run_days( days: int, *, hour: int = 9, minute: int = 30, day_of_week: int = 3, ticks_per_day: int = 390, volatility: float = 1.0, record: bool = True, first_day: int | None = None, ledger: Any | None = None, ) -> int | Advance whole trading days: open, session, close, repeat. The usual way to run a market. |
| def run_session( hour: int, minute: int, day_of_week: int, ticks: int, *, volatility: float = 1.0, close_at_end: bool = False, news: Sequence[News] | None = None, news_impacts: Sequence[NewsImpact] | None = None, fills: dict[str, tuple[float, float]] | None = None, flow_per_tick: dict[str, tuple[float, float]] | None = None, order_flow: None = None, ) -> int | Run many ticks inside one day, between an explicit open and close. fills takes an agent's trades as {ticker: (bought, sold)} in shares and applies them once, on the session's first tick. flow_per_tick takes the same shape and applies it on every tick, as a standing rate. Since 0.8.5 order_flow raises ValidationError, and the note at the top of this section links to the release notes. |
| def tick( hour: int, minute: int, day_of_week: int, *, volatility: float = 1.0, news: Sequence[News] | None = None, news_impacts: Sequence[NewsImpact] | None = None, order_flow: dict[str, tuple[float, float]] | None = None, ) -> TickResult | Advance one game-minute. order_flow is what traders bought and sold in that minute, as {ticker: (bought, sold)} in shares, and 0.8.5 left it unchanged. |
| def run_until( *, ticker: str, above: float | None = None, below: float | None = None, max_ticks: int = 390, hour: int = 9, minute: int = 30, day_of_week: int = 3, volatility: float = 1.0, ) -> int | None | Advance until a named instrument's price leaves a band, or until max_ticks elapses. Refuses a rate index, whose level moves only when a yield is written, between sessions or by pin_macro. |
| def open_market(*, day: int | None = None) -> None | Roll the day's opening marks and draw the day's endogenous news. Call once before a session's ticks. |
| def close_market() -> None | Run the close bookkeeping: the variance update, the daily roll and the end-of-day state. |
| session_tick: int | None | Ticks run since the current day's open. It reads 0 at the open, 390 after a full session, and stays at 390 after the close until the next open. None until this engine, or one restored from a snapshot, has opened a day. It reads a counter the engine already keeps, so it cannot change a run. |
Reading what happened
| Signature | Meaning |
|---|---|
| def bars( *, day: int | None = None, minutes: int | None = None, grain: Grain | None = None, ) -> ArrowStream | OHLCV per instrument, as an Arrow stream. A bar's volume is the shares traded inside it, at every grain, so a day's tick rows add up to its day bar. day=None (the default) returns every recorded day. day=N returns that day alone, and an unrecorded day raises. Output tables lists the columns. |
| def truth(*, day: int | None = None) -> ArrowStream | The labeled dataset: true value, mispricing and the eleven factor contributions behind every simulated move. day=None (the default) returns every recorded day. An unrecorded day raises. |
| def macro_table() -> ArrowStream | One row per recorded day of the macro state. Row d holds the values day d traded under, recorded before its close, so it is what day d - 1's close produced. To pair a day's return with the macro move it caused, compare row d + 1 with row d. The values after the last recorded close are on macro_state, not in the table. |
| def book_table() -> ArrowStream | Recorded book depth, one row per tick, instrument, side and level. |
| def prices() -> bytes | Current price per instrument, as little-endian f64 bytes in roster order. |
| def column(field: ColumnField) -> bytes | One named column across every instrument, as little-endian f64 bytes in roster order. |
| def attribution(factor: FactorName) -> bytes | One named factor's contribution across every instrument, as little-endian f64 bytes. Zero in every rate index's slot, because an index moves by the repricing formula that rate_attribution splits. |
| def session_prices() -> bytes | The last session's price path, ticks_written by instruments, row-major. |
| def session_volumes() -> bytes | The last session's volume path, same shape as session_prices. Each value is the name's running total for the day, which the open resets to zero. |
| def session_mispricing_s() -> bytes | The last session's mispricing path, same shape as session_prices. |
| def session_news() -> list[dict[str, Any]] | The current news day's endogenous events, one dict each with ticker, sector, price_impact and day. ticker is None when the event names no company on the roster. The list is empty before the first open and on a preset with endogenous_news_intensity at 0. price_impact is the whole move the event adds by the close, which makes it the answer key, so never pass it to an agent. It is a read. It takes no draw and cannot change a run. |
| def noise_split( part: Literal["market", "sector", "idio"], ) -> bytes | The day's random_noise column split into the three draws it sums, for part='market', 'sector' or 'idio', as little-endian f64 bytes in roster order. It is a read. It takes no draw and cannot change a run. |
The order book
| Signature | Meaning |
|---|---|
| def book(ticker: str) -> OrderBook | A detached OrderBook for one instrument, as it stands now. |
| def snapshot_book( *, day: int = 0, tick: int = 0, levels: int = 10, ) -> int | Record current depth for every instrument into the book table. |
Orders from agents
| Signature | Meaning |
|---|---|
| def submit( agent: str, ticker: str, quantity: float, *, limit_price: float | None = None, order_id: str | None = None, ) -> dict[str, Any] | Send one agent's order to this engine's book and return a report dict. quantity is signed, positive to buy. limit_price=None is a market order, and what the book cannot fill comes back as unfilled. A limit order's unfilled part waits in the book's queue with book_resting on. With it off, the part waits outside the book and fills in full at its limit on the first tick whose print reaches it. Every share taken reaches the market once, on the next tick the market is open. The report carries requested, filled, average_price, worst_price, reference (the mid the order met), resting, unfilled, mode and fills. Raises OrderError for a rate index and for the labels mm, depth, flow and range. Recorded in the order log. |
| def submit_many( orders: Sequence[dict[str, Any]], ) -> list[dict[str, Any]] | Send several agents' orders for one step in a fixed order. Each dict has agent, ticker and quantity, and optionally limit_price and order_id. Orders run sorted by agent label, and one agent's in list order, so the same orders give the same market however the list was built. Each order meets the book the orders before it left. Returns one report per order, in the order processed. A refused order raises, and the orders before it stand. |
| def cancel( order_id: str, *, agent: str | None = None, ) -> bool | Cancel a waiting order by id, and with agent only if that agent sent it. Returns whether an order was removed. Recorded in the order log. |
| def open_orders( agent: str | None = None, ) -> list[dict[str, Any]] | Waiting orders for one agent, or for every agent with None, in arrival order. Each dict has order_id, agent, ticker, side, limit_price, quantity, remaining, sequence and mode, which is queue or range. Changes nothing. |
| def take_fills( agent: str | None = None, ) -> list[dict[str, Any]] | Collect the fills the book holds for one agent, or for all, in the order they happened, and forget them. liquidity is taker, maker or range, and counterparty is mm, depth, flow, range or another agent's label. A waiting order that the market's own flow filled during a session is reported here and nowhere else. Recorded in the order log. |
| def take_impacts( agent: str | None = None, ) -> list[dict[str, Any]] | Collect the permanent impact of each agent's flow, one row per agent, name and tick, and forget it. permanent is the change the flow made to the name's mispricing_s, in log units. It is exact under fill_impact_coefficient, and otherwise the tick's order-flow impact shared among agents by signed shares. Recorded in the order log. |
| def book_live() -> bool | True when book_shared or book_resting is on, so an agent's order executes in this engine's book and Portfolio.execute sends it there. True on pt-v20 and False on every earlier preset. |
Rate indices
| Signature | Meaning |
|---|---|
| def rate_instruments() -> list[dict[str, Any]] | One dict per rate index this engine holds, in roster order, with ticker, name, curve_point, duration, convexity, spread_bps, level, yield, price and avg_volume. Empty on an engine without them. |
| def curve() -> dict[str, float] | The curve now, as fractions: policy_rate, treasury_2y, treasury_10y and corporate, the yield equities are discounted off. investment_grade, the yield IGCORP reads, is there only on an engine holding rate indices. |
| def rate_attribution( component: Literal["carry", "duration", "convexity", "flow"], ) -> bytes | One rate component of today's move for every instrument, as little-endian f64 bytes in roster order, zero for each equity. carry, duration and convexity are the terms of the repricing formula summed over the day, as fractions of the level. flow is the print's premium over the index level now, price / level - 1. Reset at each open. |
| RATE_COMPONENTS: list[str] | The components rate_attribution takes, in order: carry, duration, convexity and flow. |
Recording
| Signature | Meaning |
|---|---|
| def record(day: int) -> None | Capture the session just run, and the macro state, as one recorded day. |
| def clear_recording() -> None | Discard every recorded day. The market itself is untouched. |
| recorded_days: int | How many days have been recorded. |
| recorded_book_rows: int | How many book rows have been recorded. |
| session_ticks_written: int | Ticks written by the last session. |
Market state
| Signature | Meaning |
|---|---|
| def fork(count: int = 2) -> list["Engine"] | Copy the engine, including the order book, the day's endogenous news and the generator position. Returns count independent engines identical to this one. tf.branch calls it. Orders waiting in the agent-facing book are copied too. |
| def state_snapshot() -> dict[str, Any] | Market state as one dict: every column, the variance and crisis state, and the generator position on every stream. It gains a rates entry on an engine holding rate indices, and a book entry once an agent has sent an order. |
| def state_hash() -> str | This market's state as one 64-character hex digest: the ledger leaf, covering every field state_snapshot carries. manifest.state_hash computes the same digest in Python. The field set grew at 0.8.0, so a state hashes differently under 0.7.x and 0.8.0 even where the prices agree. From 0.8.5 it also covers the rate indices and the agent-facing book where the engine has them, so an engine with neither hashes as it did under 0.8.1. |
| def restore_state(snapshot: dict[str, Any]) -> None | Put a market back to a captured state. A snapshot taken under 0.7.x is refused, because the engine now carries ten random streams where 0.7.x carried eight. |
| order_log: list[dict[str, Any]] | Every input that crossed into this engine, in order, including every submit, cancel, take_fills and take_impacts call. What a Checkpoint replays. A run_session entry names its flows fills and flow_per_tick, and a log written by 0.8.x names one order_flow, which replays as flow_per_tick. |
| draws_consumed: int | Cumulative draws across all three streams. Two runs that agree here consumed the same randomness. |
| def draws_by_stream() -> dict[str, int] | Cumulative draws on three of the streams, as {'market': n, 'economy': n, 'external': n}. stream_positions reports all ten. |
| def stream_positions() -> dict[str, tuple[int, int]] | (uniforms, normals) taken so far on each stream, keyed by stream name: the address the next draw of each kind would take. |
| def draw_normal() -> float | One normal draw from the engine's external stream. |
| def draw_uniform() -> float | One uniform draw from the engine's external stream. |
The roster
| Signature | Meaning |
|---|---|
| tickers: list[str] | Instrument tickers, in roster order. |
| len: int | The number of instruments on the roster. |
| def index_of(ticker: str) -> int | None | The roster index of a ticker, or None. |
| def list_instrument(instrument: Instrument) -> int | Add an equity to a running market, after the last equity. Returns its index. A rate index is refused, because rate indices are fixed when the engine is built. |
| def delist(index: int) -> str | Remove the instrument at an index, returning its ticker. A rate index cannot be delisted. |
Model and economy
| Signature | Meaning |
|---|---|
| model: ModelParams | The coefficient set this engine runs. |
| model_params: dict[str, Any] | The full coefficient dictionary, as ModelParams.to_dict() returns it. |
| model_fingerprint: str | The model fingerprint: the preset name for an unmodified preset, or custom-<digest> after any override. |
| macro_state: Macro | The current macro state. |
| macro_fields: dict[str, Any] | Every field pin_macro writes, in the units it takes. Distinct from macro_state, the Macro object. |
| def pin_macro( *, vix: float | None = None, federal_funds_rate: float | None = None, corporate_bond_yield: float | None = None, inflation_rate: float | None = None, qe_pe_boost: float | None = None, qe_assets_ratio: float | None = None, fear_greed_index: float | None = None, gdp_growth: float | None = None, unemployment_rate: float | None = None, tariff_rate: float | None = None, oil_price: float | None = None, cycle: CycleName | None = None, epicentre: str | None = None, vix_sets_variance: bool = False, treasury_yield_2y: float | None = None, treasury_yield_10y: float | None = None, ) -> None | Hold one or more macro series at given values instead of letting them evolve. epicentre names the sector that carries the next crisis episode, or 'none', and persists once written. vix_sets_variance=True makes tonight's close set the market's variance from the VIX pinned today, where it would otherwise move one step toward it. Everything is validated before anything is written. treasury_yield_2y and treasury_yield_10y write the curve the rate indices read, as fractions. The chain carries on from a pinned yield, and every close recomputes the 2-year from the policy rate and the 10-year, so a 2-year pinned alone lasts until that close. |
| def vix_sets_variance_pending() -> bool | True when tonight's close will set the market factor's variance from the VIX, because the VIX was pinned today with vix_sets_variance=True. The close clears it. |
| def crisis_episode() -> tuple[bool, int, str | None] | The crisis episode, as (in_episode, sessions_under, epicentre). An episode starts when the VIX crosses crisis_vix_threshold and ends after crisis_epicentre_end_sessions consecutive sessions under it, and sessions_under counts toward that end. epicentre is a sector key, 'none' for a crisis with no epicentre, or None when no episode is running. It is always (False, 0, None) on a preset with crisis_epicentre_extra at 0.0, which is every preset before pt-v19. |
| def set_avg_volume(values: Sequence[float]) -> None | Write the avg_volume column the market maker quotes off, one value per instrument in roster order. |
| FACTORS: list[str] | The eleven factor names, in the order truth() reports them. |
An Engine is mutable. Every run and tick method advances it in place and returns None unless the table says otherwise. fork() returns independent copies of the current engine state. Checkpoint is the serialized form, which you can save and load in a later process. From 0.8.0, state_snapshot carries thirteen more fields, including the crisis episode, the sector variance and the slow variance levels, and state_hash covers them. So the same prices hash differently under 0.7.x and 0.8.0. A snapshot taken under 0.7.x cannot be restored, because the engine now draws from ten random streams where 0.7.x drew from eight. From 0.8.5, state_snapshot and state_hash also cover the rate indices on an engine that holds them, and the agent-facing book once an agent has sent it an order. An engine with neither hashes as it did under 0.8.1.
Instrument
One tradable company. Every monetary field is in currency units per share unless it says otherwise.
Instrument(
ticker: str,
sector: str,
*,
initial_price: float,
shares_outstanding: float,
eps: float | None = None,
book_value_per_share: float | None = None,
revenue_growth: float | None = None,
avg_volume: float = 1000000.0,
beta: float = 1.0,
short_interest: float = 0.0,
) -> None
Fields
| Signature | Meaning |
|---|---|
| ticker: str | The instrument's symbol. Unique within a universe. |
| sector: str | One of the twelve sector names. |
| initial_price: float | Day-zero price, in currency units per share. |
| shares_outstanding: float | Share count. |
| eps: float | None | Earnings per share, in currency units. None marks a loss-maker, which is priced on book value instead. |
| book_value_per_share: float | None | Book value per share, in currency units. |
| revenue_growth: float | None | Revenue growth as a decimal fraction: 0.08 is 8%. |
| avg_volume: float | Average daily volume, in shares. |
| beta: float | Loading on the market factor. |
| short_interest: float | Short interest as a SHARE COUNT, not a fraction of float. |
| market_cap: float | Derived, read-only: initial_price times shares_outstanding. |
A simulated rate index is an Instrument too, with sector "rates", and tf.bonds() builds all three. Instrument() accepts that sector only for UST2Y, UST10Y and IGCORP, and refuses eps, book_value_per_share, revenue_growth or a non-zero short_interest on one.
Macro
The economy on day zero. Rates are always decimal fractions, so 0.052 is 5.2%. Passing 5.2 raises ValidationError, and the message names the fraction you probably meant.
Macro(
*,
vix: float = 15.0,
federal_funds_rate: float = 0.025,
corporate_bond_yield: float | None = None,
inflation_rate: float = 0.02,
qe_pe_boost: float = 0.0,
qe_assets_ratio: float = 1.0,
fear_greed_index: float = 50.0,
cycle: CycleName = 'expansion',
) -> None
Fields
| Signature | Meaning |
|---|---|
| vix: float | Volatility index level, in points. |
| federal_funds_rate: float | Policy rate, as a decimal fraction. |
| corporate_bond_yield: float | None | Corporate yield, as a decimal fraction. None falls through to the policy rate plus a spread. |
| inflation_rate: float | Inflation, as a decimal fraction. |
| qe_pe_boost: float | Additive boost to the target price/earnings multiple. |
| qe_assets_ratio: float | Central-bank asset stock relative to the neutral portfolio. 1.0 is neutral. Enters fair value as a concave stock term. |
| fear_greed_index: float | Sentiment index, 0 to 100. |
| cycle: str | The business-cycle phase. |
This is the state on day zero only, and every close advances it. To set a path for the whole run, use a Scenario, or hold fields still with Engine.pin_macro.
OrderBook
One company's order book. Orders match against resting orders on price-time priority (best price first, then earliest), so an order's price depends on how many levels it uses up.
OrderBook(
company_id: str,
last_price: float | None = None,
) -> None
Members
| Signature | Meaning |
|---|---|
| company_id: str | The instrument this book belongs to. |
| best_bid: float | None | Best resting bid, or None when that side is empty. |
| best_ask: float | None | Best resting ask, or None when that side is empty. |
| mid_price: float | None | Midpoint of the touch, or None when either side is empty. |
| spread: float | None | Ask minus bid, or None when either side is empty. |
| def post_limit( side: Side, price: float, quantity: float, *, owner: str, order_id: str | None = None, ) -> str | Rest a limit order on the book. Returns its id. |
| def submit( side: Side, quantity: float, *, taker: str = 'taker', limit_price: float | None = None, post_remainder: bool = False, order_id: str | None = None, ) -> MatchResult | Submit an order against the book. Matches against resting depth. |
| def append_maker_level( side: Side, price: float, quantity: float, *, owner: str, ) -> str | None | Append a level to the end of one side, skipping the sorted insert. |
| def sweep_cost( side: Side, quantity: float, ) -> SweepCost | None | What sweeping a quantity would cost, without executing it. None when the side cannot fill it. |
| def price_levels( side: Side, max_levels: int = 10, ) -> list[PriceLevel] | Aggregated levels on one side, best first. |
| def depth(side: Side) -> float | Total resting quantity on one side, in shares. |
| def cancel_order(order_id: str) -> bool | Cancel one resting order by id. True when it was there. |
| def cancel_all_for(owner_id: str) -> int | Cancel every resting order owned by one party. Returns how many. |
A book from Engine.book() is a detached copy. Reading it or filling orders against it leaves the engine unchanged, byte for byte.
PriceLevel
One price level with its resting orders added together, as price_levels() returns it.
PriceLevel
Fields
| Signature | Meaning |
|---|---|
| price: float | The level's price. |
| quantity: float | Total resting quantity at that price, in shares. |
| orders: int | How many separate orders stand at that price. |
SweepCost
What a sweep, one order that takes several price levels, would cost. sweep_cost() returns it without executing anything.
SweepCost
Fields
| Signature | Meaning |
|---|---|
| average_price: float | Volume-weighted average price the sweep would pay. |
| worst_price: float | The last, worst price the sweep would reach. |
| filled: float | How many shares would fill, which may be fewer than asked. |
MatchResult
The outcome of submit().
MatchResult
Fields
| Signature | Meaning |
|---|---|
| fills: list[Fill] | The fills the order produced, in match order. |
| unfilled: float | Shares that could not fill. Zero when a remainder was posted. |
| average_price: float | None | Volume-weighted average across the fills, or None if nothing filled. |
| resting_order_id: str | None | Id of the resting remainder, when one was posted. |
Fill
One match between an incoming order and a resting one.
Fill
Fields
| Signature | Meaning |
|---|---|
| price: float | The RESTING order's price, never the incoming one. |
| quantity: float | Shares filled. |
| maker_order_id: str | Id of the resting order. |
| maker_id: str | Owner of the resting order. |
| taker_id: str | Party that crossed the spread. |
| taker_side: str | Which side the taker was on. |
Universe
The roster, meaning the list of companies in a market, followed by any rate indices. It is a list subclass, so indexing and iteration work as usual.
| Signature | Meaning |
|---|---|
| Universe(instruments: Sequence[Instrument]) | Build a roster from instruments you made yourself. |
| Universe.random( n: int = 108, *, seed: int = 0, bonds: bool = False, ) -> Universe | Generate a roster, filling twelve sectors in turn. Tickers follow roster position: AAA, AAB, AAC. bonds=True, new in 0.8.5, appends UST2Y, UST10Y and IGCORP after the n equities, which are the same n names either way. |
| Universe.from_edgar(snapshot, **kwargs) -> Universe | Build from an EDGAR snapshot, as tf.edgar.fetch() returns it. See Real companies from EDGAR. |
| Universe.from_json(text: str) -> Universe | Rebuild from to_json() output. |
| to_json(**kwargs) -> str | Serialize the roster to JSON. |
| tickers() -> list[str] | Tickers, in roster order. |
| fingerprint: str | A sha256 hash of the roster in its standard serialized form, including its order. |
| with_bonds( tickers: Sequence[str] | None = None, ) -> Universe | New in 0.8.5. A new roster with rate indices appended, the ones named in the order given or all three. Leaves this roster alone, and refuses one that already holds any of them. |
| equities() -> Universe | New in 0.8.5. A new roster of the equities alone, in roster order. |
Roster order is part of the universe's identity and is included in fingerprint. If you reorder the roster, the same seed gives a different market. Reproducibility explains how the fingerprint is built.
Rate indices
Three simulated rate indices can trade beside the equities from 0.8.5. UST2Y and UST10Y are constant-maturity 2-year and 10-year treasury indices, and IGCORP is an investment-grade corporate bond index. None is a real security. They are priced off the engine's own curve and take no random draws, so every equity price, draw and macro value is identical to the same run without them.
| Signature | Meaning |
|---|---|
| tf.bonds( tickers: Sequence[str] | None = None, ) -> list[Instrument] | The rate indices as instruments with their default terms, the ones named or all three. An unknown ticker raises ValidationError. |
| tf.rate_specs() -> list[dict[str, Any]] | Each index's fixed terms: ticker, name, curve_point, duration, convexity, spread_bps, avg_volume, units_outstanding and initial_price. |
| tf.RATE_TICKERS: tuple[str, ...] | The three tickers in their default order: UST2Y, UST10Y and IGCORP. |
| tf.RATE_SECTOR: str | The sector every rate index carries, "rates". tf.sectors() does not list it. |
Each index reads one yield. UST2Y reads the 2-year treasury yield and UST10Y the 10-year. IGCORP reads the 10-year plus a credit spread, which is re-marked whenever the engine sets the corporate yield or a caller pins it. When that yield changes by dy, the level moves by carry - D * dy + 0.5 * C * dy**2, where carry is the yield over 252 at the first open after a close and zero otherwise. Duration D and convexity C are 1.9 and 4.6 for UST2Y, 8.5 and 84 for UST10Y, and 7.0 and 100 for IGCORP.
The economy moves yields only at the close, so a level holds still through a session unless pin_macro writes a yield. The level reprices at the next open, or on the first tick after the pin. Nothing interpolates toward a later yield, so no price shows a yield before the close that sets it. Engine.rate_attribution splits the day's move into carry, duration, convexity and the book's flow.
An index trades like an equity through Portfolio.execute, the fills= argument of run_session, the tape and tca.analyse, on a book of its own. That book is the maker's ladder around the index level, 0.6 basis points wide before cent rounding for the treasuries and 0.8 for IGCORP, and it widens with the VIX by the equity rule. Its depth comes from the median dollar volume of SHY, IEF and LQD over 2015 to 2025. The maker lays off a trade's inventory with a 15-minute half-life, so a trade's impact on an index fades.
Rate indices come after every equity. An engine refuses a roster that lists an equity after one, lists one twice, or holds rate indices and no equities. The agent-facing book holds equities only, so Engine.submit and Portfolio.submit_limit refuse a rate index. So do run_until, a News item that names one, explain, list_instrument, delist and EngineBatch. To move an index, write the yield it reads with pin_macro(treasury_yield_2y=..., treasury_yield_10y=...), or with a scenario on macro.treasury_2y or macro.treasury_10y.
import tradefloor as tf u = tf.Universe.random(20, seed=101, bonds=True) u.tickers()[-3:] # ['UST2Y', 'UST10Y', 'IGCORP'] e = tf.Engine(seed=3, universe=u) e.run_days(5) e.curve["treasury_10y"] e.rate_instruments[1]["level"]
On pt-v20, the default, the 2-year yield moves 3.87 basis points a day against 5.23 in the real market over 2015 to 2025, and the 10-year 4.96 against 5.41. The index's daily correlation with a Treasury bond's return is -0.14 against a real -0.16 (IEF), and with an investment-grade bond's +0.20 against +0.27 (LQD). These are long-run rows R1 to R4. Under pt-v19 the curve is much quieter, 0.46 and 3.1 basis points a day, and bonds and stocks are uncorrelated. The library's tools/bonds/realism.py makes that comparison. Scenarios documents curve_shock.yml, which moves the whole curve 200 basis points in one day, and Agents and evaluation documents baselines.Balanced, a 60/40 book with a bond sleeve.
The agent-facing book
Engine.submit sends an order to the engine's own book under an agent's label, and the engine keeps each fill until take_fills collects it. Every share an order takes reaches the market once, on the next tick the market is open, and take_impacts reports the permanent impact it left. Portfolio.submit_limit and a tf.Limit in a World agent's orders both go through it. The table under Engine lists the seven methods.
Seven ModelParams dials, listed on Model parameters, decide what the book holds. They are 0.0 on pt-v1 through pt-v19, where Engine.book_live is False: an order meets the maker's ladder exactly as Engine.book shows it, two agents trading one name in one step fill at the same prices because neither order removes a level, and a limit order's unfilled part waits outside the book, with mode range, and fills in full at its limit on the first tick whose print reaches it. pt-v20, the default, sets all seven, so Engine.book_live is True there and an order executes in the engine's own book. A waiting limit order then has mode queue.
book_depth_coefficient, book_depth_exponent and book_depth_reach put latent depth beside the maker's ladder, where a larger order pays more per share, by the square-root law unless the exponent sets another power. book_shared makes an order consume what it takes. The maker's ladder is whole again when the maker quotes at the next tick, and the latent depth refills with a half-life of book_refill_half_life ticks. book_resting queues a limit order's unfilled part in the book, behind the depth already at its price, where the market's own flow or another agent can fill it. The flow fills it only at a price inside the maker's quote for that tick, so an order resting past the quote waits until the price comes to it. An order that the maker's next quote crosses trades at the maker's price, and its fill is recorded as liquidity="taker". fill_impact_coefficient gives each agent's fills a linear permanent impact and attributes it to that agent.
None of the seven changes a market nobody trades, and an engine no agent has sent an order to hashes and snapshots as it did under 0.8.1. The book holds equities only, so a rate index trades through Portfolio.execute on a book of its own.
e.open_market()
r = e.submit("alice", u[0].ticker, 1_000)
r["filled"], r["average_price"]
w = e.submit("alice", u[0].ticker, 500,
limit_price=0.98 * r["reference"])
e.open_orders("alice") # mode "queue" on pt-v20
e.cancel(w["order_id"], agent="alice")
e.take_fills("alice") # the market order's fill
Portfolio
Cash, positions and P&L for one trader. evaluate, TradingEnv, tca.analyse and World keep one per agent, and a loop of your own can use one the same way. execute fills an order at the prices the book gives and adds the trade to pending_flow, which the loop hands to the next session as run_session(fills=...).
Portfolio(
cash: float = 1_000_000.0,
*,
max_leverage: float | None = None,
cash_interest: bool = False,
owner: str = "agent",
margin_interest: bool = True,
)
| Signature | Meaning |
|---|---|
| execute(engine, ticker, quantity) -> dict | Trade quantity shares, positive to buy, at the prices the book gives, and return the fill. A partial fill is reported as partial. Raises OrderError when nothing can fill or the trade would take leverage past max_leverage. |
| submit_limit( engine, ticker, quantity, price, ) -> dict | New in 0.8.5. Send a limit order to Engine.submit under owner and return the engine's report. What the book holds at price or better fills at once, and the rest waits until it fills or is canceled. max_leverage is checked as though the whole order filled at price, before anything is sent. A rate index raises ValidationError. |
| cancel( engine, *, ticker=None, order_id=None, ) -> int | New in 0.8.5. Cancel this portfolio's waiting orders, one by id, every one on a ticker, or all of them. Returns how many it canceled. |
| open_orders(engine) -> list[dict] | New in 0.8.5. This portfolio's waiting orders, in arrival order, as Engine.open_orders returns them. |
| sync(engine) -> list[dict] | New in 0.8.5. Apply every fill the engine holds for this portfolio, and return them. A waiting order that fills during a session reaches the portfolio this way, so a loop calls it after each session. It asks the engine nothing for a portfolio that has never sent an order to the book. |
| pending_flow() -> dict[str, tuple[float, float]] | Shares bought and sold per ticker since the last clear_flow. Pass it to the next run_session as fills=, or to a single tick as order_flow=, then call clear_flow, because flow left in place is sent again with the next step's. |
| clear_flow() -> None | Forget the accumulated flow once it has been passed on. |
| accrue(engine) -> float | New in 0.8.5. Book one trading day's interest, cash times the policy rate over 252, to cash and to interest, and return it. A positive balance earns it only with cash_interest on. A negative balance is charged it while margin_interest is on, the default, whatever cash_interest says, which is cheaper than any broker lends. evaluate, rank and World call it once a day, before the close. |
| net_worth(engine) -> float pnl(engine) -> float | Cash plus every position marked at the engine's prices, and that figure less starting_cash. |
| leverage(engine) -> float gross_exposure(engine) -> float | Gross exposure as a multiple of net worth, and the absolute market value of every position, longs and shorts alike. |
| market_value(engine) -> float unrealised(engine) -> float realised() -> float | The market value of the positions, their unrealised P&L, and the P&L realised on the parts already closed. |
| marks(engine) -> dict[str, float] | The current price per ticker, from the engine. |
| fills_table(tickers) | The fill log as an Arrow stream keyed by instrument_id, so it joins to bars and truth. |
| stamp(day, step, tick) -> None | Tag the fills that follow with a day, a step counted across the run and a tick within the day. |
| owner: str | New in 0.8.5. The label this portfolio's orders carry in the engine's book, "agent" by default. Portfolios that share one engine need different owners, and World gives each agent's portfolio that agent's label. |
| cash_interest: bool interest: float | New in 0.8.5. Whether accrue pays interest on idle cash, off by default, and the interest credited so far, net of any charged. margin_interest, on by default, is whether a negative balance is charged. |
| cash, starting_cash: float positions: dict[str, Position] fills: list[dict] | Cash now and at the start, each position by ticker, and every fill in the order it happened. |
When Engine.book_live is True, execute sends the order to Engine.submit under owner instead of pricing it off a copy of the book. The engine then applies the flow itself and pending_flow holds nothing for that trade, so a loop that passes fills=p.pending_flow() still counts it once. A rate index is always priced off its own book.
import tradefloor as tf
u = tf.Universe.random(20, seed=7)
e = tf.Engine(seed=42, universe=u)
p = tf.Portfolio(cash_interest=True)
e.open_market()
minute = 9 * 60 + 30
for step in range(6): # six 65-tick steps make one day
if step == 0:
p.execute(e, u[0].ticker, 500)
e.run_session(minute // 60, minute % 60, 3, 65,
fills=p.pending_flow())
p.clear_flow()
minute += 65
p.accrue(e) # a day's interest, before the close
e.close_market()
p.net_worth(e)
Exceptions
| Type | Base | Raised when |
|---|---|---|
| ValidationError | ValueError | An input is refused, for example an unknown preset or parameter name, a universe that does not match a checkpoint's fingerprint, a rate passed as a percentage, or a count below one. |
| OrderError | ValueError | The book cannot accept an order, for example one with a quantity of zero or less, an unknown side, or a cancel for an order id that is not resting in the book. Engine.submit raises it for an order the engine's book refuses, such as one on a rate index or under a label the book keeps for itself: mm, depth, flow or range. |
EngineBatch
Runs one market under many seeds in lockstep, in one process. Build it like Engine, with seeds in place of seed. The run and read methods match Engine's and return one result per seed. It runs equities only and refuses a roster that holds a rate index. Run one on Engine, or through run_many, one seed per engine.
seeds: list[int] tickers: list[str] draws_consumed: list[int] shape: tuple[int, int] model: ModelParams model_fingerprint: str EngineBatch(*, seeds, universe, ...) __len__() -> int open_market() -> None tick(...) / run_session(...) prices() -> bytes column(field) -> bytes
News
One news item that you inject into the market.
News(
*,
ticker: str | None = None,
sector: str | None = None,
price_impact: float = 0.0,
) -> None
NewsImpact
The effect of one news item on prices.
NewsImpact(
*,
ticker: str | None = None,
sector: str | None = None,
sectors: Sequence[str] | None = None,
remaining_impact: float = 0.0,
reversal_phase: bool = False,
) -> None
TickResult
What one tick did, as tick() returns it.
market_status: str draws_consumed: int active: int
run_many
tf.run_many(
seeds: Iterable[int],
*,
universe: Sequence[Instrument],
macro: Macro | None = None,
days: int = 1,
ticks: int = 390,
start: tuple[int, int, int] = (9, 30, 3),
workers: int | None = None,
collect: str = "prices",
scenario: Any = None,
model: str | ModelParams | None = None,
) -> list[Any]
Runs one simulation per seed in a thread pool and returns the results in input order. collect chooses what comes back: "prices" (raw f64 bytes), "attribution" (the factor columns) or "summary" (prices, draw count, tickers and fingerprints). Every seed runs the same model. A single seed always runs in-process.
Examples
Forking and pinning
e = tf.Engine(seed=42, universe=u) e.run_days(60) a, b = e.fork(2) # two engines, one past b.pin_macro(vix=45.0) a.run_days(20) b.run_days(20) # same noise, higher fear
Reading a run back
e.run_days(30, record=True) bars = e.bars(grain="day") # every recorded day one = e.truth(day=12) # that day alone # day=None means every recorded day; # an unrecorded day raises
The book
book = e.book(u[0].ticker)
est = book.sweep_cost("buy", 50_000)
# cost without executing; None on an empty side
res = book.submit("buy", 50_000, taker="me")
[f.price for f in res.fills]
Universes
u = tf.Universe.random(40, seed=7) u2 = tf.Universe.from_json(u.to_json()) u.fingerprint == u2.fingerprint # True # order is contractual: a reordered roster # is a different market from the same seed
See also
How prices are made explains how truth() breaks each price move into parts. Model parameters lists the coefficient set. Forks and counterfactuals covers saving, forking and resuming a market.