0.8.0 - 2026-07-25
Added
decide/2may now cancel resting orders. The callback can return{orders, cancels, state}(a 3-tuple) in addition to the existing{orders, state};cancelsis a list of:order_ids of resting pending remainders to cancel before the newordersare placed. Backward compatible — the 2-tuple form is unchanged and every existing strategy keeps working; a runner distinguishes the two by tuple arity. This is what lets a strategy move a resting quote (cancel the stale one, place a fresh one) instead of only ever adding orders — the prerequisite for genuine two-sided market making. NewPaperEx.Strategy.decision/0andcancel_ref/0types.- Optional
cancels?/0callback — a strategy returnstrueto declare it uses the cancel form, so a host can refuse unsafe pairings up front (e.g. a runner that mirrors orders to a venue it cannot yet cancel on). Optional, defaults tofalse, fully backward compatible.
0.7.0 (unreleased)
Added
PaperEx.Strategybehaviour — the strategy contract (init/1validation, puredecide/2over%{instrument_id, snapshot, portfolio}with threaded state), promoted from thepolymarket_botreference app once multiple strategy families (resting-bid, copy-trade, scanner) proved the abstraction. Exchange-agnostic: contexts may carry extra host-specific keys; strategies match only what they need. Includesimplemented_by?/1. 4 new tests.
0.6.0 (unreleased)
Changed
- GTC rest-on-miss (Codex review finding). Under
pending_remainder: true, a:limitorder that misses entirely (:limit_not_crossedor:no_liquidity) now rests as a full-size pending remainder instead of evaporating — which is what GTC means. Previously only partial fills rested, so a resting-order strategy would re-submit the same intent every tick. The:missedexecution is still recorded (the attempt is auditable); the new:pendingsits alongside it and is advanced /cancelled through the existing lifecycle. Without the flag, and for:marketorders, behavior is unchanged. 5 new tests.
0.5.0 (unreleased)
Added
PaperEx.Engine.resolve_market/4— prediction-market resolution. Closes every open position on a market at its final outcome (:won→:payout_per_share, default1.0;:lost→0.0), computes realized P&L, credits/debits cash (synthetic shorts buy back at the final price), applies an optional:win_fee_rateto winning long payouts, cancels open pending remainders on the market, and appends:closed/:cancelledexecutions with the bounded reason:market_resolved. Returns{:ok, portfolio, %{closed: [...], cancelled_pending: [...]}}.PaperEx.Valuation— pure mark-to-market valuation of open positions against caller-supplied snapshots.open_position_value/3prices longs at best bid and shorts at best ask (:conservative, default) or both at:midpoint, returns a net liquidation value with a per-position breakdown, and refuses partial totals ({:error, {:unpriced, ids}}) rather than silently omitting unpriced positions.:market_resolvedadded toPaperEx.ReasonCodes.engine_codes/0.- 23 new tests covering long/short resolution for both outcomes, win-fee semantics, custom payouts, pending cancellation on resolution, market scoping, multi-strategy resolution, ledger effects, outcome validation, and the full valuation matrix (conservative/midpoint pricing, empty book sides, missing snapshots, netting, portfolio input, closed-position exclusion).
Changed
- Engine moduledoc documents the two position-exit paths (selling and resolution); the "P&L on close not implemented" caveat is gone.
resolve_market/4validates:payout_per_shareand:win_fee_rate(non-negative numbers;ArgumentErrorotherwise) — a negative value would silently corrupt cash/P&L (Codex review finding).- The exit-side P&L convention is now documented explicitly and
pinned by a test:
Position.pnlis(exit - entry) * sharesminus exit-side fees only, on both the sell-close and resolution paths. Opening fees hit cash at open time and are deliberately not re-subtracted intopnl.
0.4.0 (unreleased)
Added
PaperEx.Engine.open_pending_remainders/1— public helper returning the currently-open pending remainders of aPaperEx.Portfolioor a raw[PaperEx.Execution]list, in ledger order. This is now the single source of truth for the open-pending rule (latest pending perorder_id; cleared by a later:cancelledfor that id or a later:filledtaggedmetadata.resolves: {:pending_remainder, order_id}; unrelated fills do not clear).
Changed
cancel_pending/3andadvance_pending/3internals now derive open pendings throughopen_pending_remainders/1instead of a private duplicate. Behavior is unchanged — the privatefind_open_pending/2andopen_pendings_for/2delegate to the public helper.
0.3.0 (unreleased)
Added
PaperEx.Engine.advance_pending/3— pure function that walks open:pendingexecutions matching a freshMarketSnapshot'sinstrument_idand tries to resolve each remainder. Returns{:ok, updated_portfolio, results}whereresultsis a list of{pending_execution_id, outcome}tuples preserving ledger order. Outcomes:{:filled, filled_execution}— full remainder filled.{:partial, filled_execution, new_pending}— partial fill;new_pendinghas the same id with a reducedremaining_size.{:still_pending, pending}— no cross / no liquidity. No ledger churn.{:skipped, reason, skipped_execution}— guardrail rejected (:shorts_disallowed,:insufficient_position,:insufficient_cash,:adapter_rejected,:pending_intent_missing).
- 18 new
paper_extests covering: empty / different-market shortcuts; no-cross and no-liquidity → still pending; partial advance (remaining_size reduced, position grown); full advance (closes pending socancel_pending/3reports:pending_not_found); deterministic ordering across multiple pendings; sell-shorts guard from a held=0 setup;allow_shorts: trueopens a new short;:insufficient_positionfrom a drained long;:insufficient_cashwith strict andallow_negative_cash: trueconfigs; missing-adapter and legacy-pending (no:side) fallbacks;apply_order/4return shape unchanged; ledger /find_open_pending/2semantics after partial vs full advance.
Changed
:pendingremainder metadata now carries:side,:limit_price, and:order_typesoadvance_pending/3can reconstruct the order intent without retaining the originalOrderstruct. This is additive — old callers that read other metadata keys are unaffected. Pendings created before this version are advanced as{:skipped, :pending_intent_missing, …}.find_open_pending/2(private) now also clears the open pending on a later:filledexecution tagged withmetadata.resolves: {:pending_remainder, order_id}. The pre-existing behavior of NOT clearing on the initial filled-then- pending sequence fromapply_order/4is preserved (the initial filled lands BEFORE the pending in the ledger and so its acc precedes the pending).- Engine moduledoc rewritten to document
advance_pending/3and the extended pending-metadata contract.
0.2.0 (unreleased)
Added
- Opt-in pending-remainder policy.
PaperEx.Engine.apply_order/4now accepts:pending_remainderin the config keyword. When set totrueand a:limitorder partially crosses, the engine appends a:pendingPaperEx.Executionto the portfolio's ledger alongside the existing filled execution. The:pendingentry has id{:pending_remainder, order.id}and metadata withremaining_size,filled_size,market_id,strategy, andmode.:marketorders are FAK and never produce pending entries. PaperEx.Engine.cancel_pending/3— pure helper that resolves a pending remainder by appending a:cancelledExecutionto the ledger. Accepts:reasonand:metadataoptions. Returns{:ok, portfolio, cancelled_execution}or{:error, :pending_not_found}. Cash and positions are not moved (pending never moved them either).- 16 new tests covering: default behavior (pending not recorded
when policy off), pending recorded on partial limit fill with the
documented metadata shape, market FAK never records pending,
fully-filled limit orders never record pending,
cancel_pendinghonoring caller:reasonand:metadata, idempotency boundary (can't double-cancel), and return-shape stability (apply_order/4still returns{portfolio, execution}and the primary execution is the filled one).
Changed
PaperEx.Engine.apply_order/4adapter dispatch now wrapsfunction_exported?/3checks withCode.ensure_loaded?/1so adapters living in dependency apps that the BEAM has not yet auto-loaded resolve correctly.- Engine moduledoc rewrites the "what this engine does NOT do"
section to reflect that pending remainders are now first-class
for
:limitorders.
0.1.0 (unreleased)
Initial scaffold of the generic exchange-agnostic paper trading engine. Pure functional core: structs, an adapter behaviour, and a single state-transition function. No HTTP, no database, no Polymarket dependency.
Added
PaperExtop-level module withversion/0and a moduledoc that records the two-modes design constraint (research vs live-mirror) carried over from the production source.- Core domain structs:
PaperEx.Order— buy/sell request keyed bymarket_id, with side, type (:limit | :market), size, price, optional strategy tag, and a free-form metadata map.PaperEx.Execution— first-class attempt record. Status is one of:filled | :missed | :skipped | :pending | :cancelled | :closed, so misses and skips are peer values, not absorbed into "no trade". Carries an optional list ofPaperEx.Fillstructs and aggregates (vwap/1,total_size/1,total_fees/1).PaperEx.Position— open or closed paper position with entry, optional exit, side, shares, strategy tag, and metadata.PaperEx.Portfolio— top-level container for starting balance, current balance, peak balance, open positions, history, the execution-attempt ledger, and stats.PaperEx.Fill— atomic fill at one price level, with notional and cost helpers.PaperEx.Instrument— normalized exchange-agnostic market id with display fields and optional tick size.PaperEx.MarketSnapshot— order-book snapshot for one instrument with best-bid/ask, midpoint, and spread helpers.PaperEx.ReasonCodes— bounded set of generic engine reason atoms.
PaperEx.Adapterbehaviour with requirednormalize_instrument/1andnormalize_snapshot/2, and optionalnormalize_fill/2,simulate_fill/3,fee/2, andreason_codes/0.PaperEx.Engine.apply_order/4— pure state transition: pre-flight guardrails, adapter-driven fill simulation, fee passthrough, VWAP fills, open/merge/close position bookkeeping, history append, ledger append. Configurable via a keyword list::adapter,:adapter_opts,:max_order_notional,:max_position_size,:allow_shorts,:allow_negative_cash,:duplicate_policy,:mode.PaperEx.MockAdapterintest/support— minimal worked example implementing every callback (required and optional).- 150+ tests covering domain struct validation, adapter behaviour
conformance, mock-adapter normalize/simulate/fee/reason paths,
engine happy paths (buy, sell, partial close, multi-level VWAP),
every guardrail skip (insufficient cash, insufficient position,
shorts disallowed, max order notional, max position size,
duplicate position), limit miss, no-liquidity miss, adapter-absent
skip, fees on buy/close, every code path records exactly one
execution, and
:modemetadata round-trip.
Deferred
- Resolving
:pendingresting orders across snapshots. - An optional GenServer wrapper for long-running paper portfolios.
- Mark-to-market P&L updates triggered by snapshots alone.
- The first-party Polymarket adapter — lives in the separate
paper_ex_polymarketpackage per the workspace package map.