Current section
Files
Jump to
Current section
Files
lib/paper_ex.ex
defmodule PaperEx do
@moduledoc """
Generic, exchange-agnostic paper-trading and simulated-execution
engine for Elixir.
`PaperEx` is a pure functional core: plain structs and state
transitions for portfolios, positions, orders, and execution
attempts. There is no HTTP, no WebSocket, no database, and no
exchange dependency. Consumers persist state however they like and
connect their own market data through an adapter behaviour.
## What's here
* `PaperEx.Engine` — the pure execution engine. `apply_order/4`
takes a portfolio, an order, a market snapshot, and a config and
returns the updated portfolio plus an `Execution` record. It also
owns the pending-remainder lifecycle (`advance_pending/3`,
`cancel_pending/3`, `open_pending_remainders/1`) and market
resolution (`resolve_market/4`).
* `PaperEx.Adapter` — the behaviour an exchange adapter implements
to translate exchange-specific data into normalized structs and
simulate fills, keeping the engine exchange-agnostic.
* Domain structs — `PaperEx.Order`, `PaperEx.Execution`,
`PaperEx.Position`, `PaperEx.Portfolio`, `PaperEx.Fill`,
`PaperEx.Instrument`, `PaperEx.MarketSnapshot`.
* `PaperEx.Valuation` — pure mark-to-market of open positions
against caller-supplied snapshots.
* `PaperEx.Strategy` — the behaviour for pure decision functions.
* `PaperEx.ReasonCodes` — the bounded set of generic engine reason
codes.
## Two simulation modes — design constraint
The package was extracted from a Polymarket prediction-market trading
bot that ran two paper layers on purpose:
1. **Research / idea simulation.** Idealized fills, simplified
exits. Answers "was the signal directionally good?".
2. **Live-mirror simulation.** Realistic execution constraints —
entry caps, slippage, GTC pending lifecycle, missed fills,
skipped attempts. Answers "what would live execution have
done?".
The engine keeps these modes separable through configuration, not
separate code paths (see `PaperEx.Engine`). It shows up in
`PaperEx.Execution`: `:filled`, `:missed`, `:skipped`, `:pending`,
`:cancelled`, and `:closed` are peer status values, so a missed fill
is recorded with the same first-class shape as a successful fill
rather than being absorbed into "no trade".
## Out of scope
`PaperEx` is not a complete trading platform. It does not own
exchange specifics (Polymarket token/condition IDs, Gamma, CLOB, and
RTDS live in `polymarket_sdk`), HTTP/WebSocket clients, Ecto schemas,
Phoenix, or strategy rules. A ready-made Polymarket adapter lives in
the `paper_ex_polymarket` package.
"""
@doc """
Returns the package version, read from the loaded application spec
(so it always tracks `mix.exs`).
"""
@spec version() :: String.t()
def version, do: Application.spec(:paper_ex, :vsn) |> to_string()
end