Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ extraction targets and should not be imported until they exist.
| `ordersim/recording.py` | Recording wrapper for order-intent logs | Yes |
| `ordersim/specs.py` | Instrument specifications | Public extension surface |
| `ordersim/types.py` | Public dataclasses and type aliases | Yes |
| `ordersim/valuation.py` | Valuation marks and compact mark transport | Yes |
| `ordersim/fixtures/` | Tiny public fixtures for examples and tests | Public |
| `ordersim/connectors/` | Data source contracts | Yes |
| `ordersim/connectors/csv.py` | Normalized CSV `MBOEvent` source | Yes |
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ flowchart LR
engine["ExecutionEngine"]
python["MatchingEngine<br/>Python reference"]
cpp["CppMatchingEngine<br/>preferred when available"]
valuation["Valuation marks"]
result["ReplayResult<br/>fills, order log, economics"]

raw --> connector
Expand All @@ -29,6 +30,8 @@ flowchart LR
gateway --> engine
engine --> python
engine --> cpp
gateway --> valuation
valuation --> result
gateway --> result
recording --> result
```
Expand All @@ -39,6 +42,7 @@ The main boundaries are:
- strategies depend on `OrderGateway`, not on storage or engine internals;
- replay normalizes inputs once, chooses an execution engine, and gathers
results;
- valuation marks are collected during replay and consumed by economics;
- the Python engine defines behavior; the C++ engine must match it.

## Recommended Data Flow
Expand Down
4 changes: 4 additions & 0 deletions docs/economics.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ midpoints as `bid_ticks + ask_ticks` until equity construction. Public
only avoids creating an intermediate Python `ValuationMark` object for every
market-data event.

Valuation mark inputs live in `ordersim.valuation`; economics consumes them to
build realized and marked output. The top-level package re-exports
`ValuationMark` and `CompiledValuationMarks` for ordinary user code.

Replay only marks times it actually advances through. Full-session intraday
drawdown therefore requires the strategy or harness to advance through the
session window being studied, or to call `build_equity_curve(...)` directly with
Expand Down
9 changes: 5 additions & 4 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,17 @@ See `docs/economics.md` for the assumptions and explicit non-goals.

## `ValuationMark` And `EquityPoint`

`ValuationMark` is an input mark used to value open lots.
`ValuationMark` lives in `ordersim.valuation` and is re-exported from
`ordersim`. It is an input mark used to value open lots.

| Field | Type | Meaning |
|---|---|---|
| `ts_ns` | `int` | Mark timestamp as UTC Unix-epoch nanoseconds. |
| `price` | `Decimal` | Price used for open-lot valuation. |

`CompiledValuationMarks` is the compact internal form used by the C++ replay
path. It stores mark timestamps and midpoint prices as primitive integer
columns:
`CompiledValuationMarks` also lives in `ordersim.valuation`. It is the compact
internal form used by the C++ replay path. It stores mark timestamps and
midpoint prices as primitive integer columns:

| Field | Type | Meaning |
|---|---|---|
Expand Down
3 changes: 1 addition & 2 deletions src/ordersim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@
write_parquet,
)
from ordersim.economics import (
CompiledValuationMarks,
EquityPoint,
ExecutionSummary,
PositionLot,
ValuationMark,
build_equity_curve,
summarize_fills,
)
Expand Down Expand Up @@ -64,6 +62,7 @@
Side,
TimeInForce,
)
from ordersim.valuation import CompiledValuationMarks, ValuationMark

__all__ = [
"BookSide",
Expand Down
88 changes: 20 additions & 68 deletions src/ordersim/economics.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
"""Execution economics computed directly from fills."""

from collections.abc import Iterable
from dataclasses import dataclass
from decimal import Decimal
from typing import TypeAlias

from ordersim.specs import InstrumentSpec
from ordersim.types import Fill, Price, Side
from ordersim.valuation import (
CompiledValuationMarks,
ValuationMark,
ValuationMarkInput,
iter_valuation_mark_pairs,
)

__all__ = [
"CompiledValuationMarks",
"EquityPoint",
"ExecutionSummary",
"PositionLot",
"ValuationMark",
"ValuationMarkInput",
"build_equity_curve",
"summarize_fills",
]


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -37,46 +52,6 @@ class ExecutionSummary:
open_lots: tuple[PositionLot, ...]


@dataclass(frozen=True, slots=True)
class ValuationMark:
"""One mark price used to value open lots."""

ts_ns: int
price: Price


@dataclass(frozen=True, slots=True)
class CompiledValuationMarks:
"""Compact valuation marks stored as timestamp and midpoint tick columns.

`mid_ticks_x2` stores `bid_ticks + ask_ticks`, so half-tick midpoints stay
exact until the public `Decimal` equity curve is built.
"""

ts_ns: memoryview
mid_ticks_x2: memoryview
tick_size: Decimal

@classmethod
def from_bytes(
cls,
*,
ts_ns: bytes,
mid_ticks_x2: bytes,
tick_size: Decimal,
) -> "CompiledValuationMarks":
"""Build compact marks from native int64 byte columns."""

timestamps = memoryview(ts_ns).cast("q")
mids = memoryview(mid_ticks_x2).cast("q")
if len(timestamps) != len(mids):
raise ValueError("valuation mark columns must have equal length")
return cls(ts_ns=timestamps, mid_ticks_x2=mids, tick_size=tick_size)

def __len__(self) -> int:
return len(self.ts_ns)


@dataclass(frozen=True, slots=True)
class EquityPoint:
"""One point on a mark-to-market equity curve."""
Expand All @@ -89,14 +64,6 @@ class EquityPoint:
equity: Decimal
drawdown: Decimal


ValuationMarkInput: TypeAlias = (
tuple[ValuationMark | CompiledValuationMarks, ...]
| list[ValuationMark | CompiledValuationMarks]
| CompiledValuationMarks
)


def summarize_fills(
fills: tuple[Fill, ...] | list[Fill],
instrument: InstrumentSpec,
Expand Down Expand Up @@ -160,7 +127,9 @@ def build_equity_curve(
instrument,
)

sorted_marks = tuple(sorted(_iter_mark_pairs(marks), key=lambda mark: mark[0]))
sorted_marks = tuple(
sorted(iter_valuation_mark_pairs(marks), key=lambda mark: mark[0])
)
open_lots: list[PositionLot] = []
realized_pnl = Decimal("0")
commission = Decimal("0")
Expand Down Expand Up @@ -196,23 +165,6 @@ def build_equity_curve(
return tuple(points)


def _iter_mark_pairs(
marks: ValuationMarkInput,
) -> Iterable[tuple[int, Price]]:
for mark in marks:
if isinstance(mark, CompiledValuationMarks):
yield from _iter_compiled_mark_pairs(mark)
else:
yield mark.ts_ns, mark.price


def _iter_compiled_mark_pairs(
marks: CompiledValuationMarks,
) -> Iterable[tuple[int, Price]]:
for ts_ns, mid_ticks_x2 in zip(marks.ts_ns, marks.mid_ticks_x2, strict=True):
yield ts_ns, marks.tick_size * Decimal(mid_ticks_x2) / 2


def _build_equity_curve_from_compiled_marks(
sorted_fills: tuple[Fill, ...],
marks: CompiledValuationMarks,
Expand Down
8 changes: 5 additions & 3 deletions src/ordersim/replay/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@

from ordersim.connectors import EventInput, normalize_events
from ordersim.economics import (
CompiledValuationMarks,
EquityPoint,
ExecutionSummary,
ValuationMark,
ValuationMarkInput,
build_equity_curve,
summarize_fills,
)
Expand Down Expand Up @@ -42,6 +39,11 @@
Side,
TimeInForce,
)
from ordersim.valuation import (
CompiledValuationMarks,
ValuationMark,
ValuationMarkInput,
)

Strategy = Callable[[OrderGateway], Any]

Expand Down
2 changes: 1 addition & 1 deletion src/ordersim/sim/cpp_matching_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from decimal import Decimal
from typing import Any

from ordersim.economics import CompiledValuationMarks
from ordersim.replay.compiled_events import CompiledEventSlice
from ordersim.sim.matching_engine import PriceLevel
from ordersim.types import (
Expand All @@ -16,6 +15,7 @@
Side,
TimeInForce,
)
from ordersim.valuation import CompiledValuationMarks


class CppMatchingEngine:
Expand Down
80 changes: 80 additions & 0 deletions src/ordersim/valuation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Valuation marks used to build mark-to-market equity curves."""

from collections.abc import Iterable
from dataclasses import dataclass
from decimal import Decimal
from typing import TypeAlias

from ordersim.types import Price


@dataclass(frozen=True, slots=True)
class ValuationMark:
"""One public mark price used to value open lots."""

ts_ns: int
price: Price


@dataclass(frozen=True, slots=True)
class CompiledValuationMarks:
"""Compact valuation marks stored as timestamp and midpoint tick columns.

`mid_ticks_x2` stores `bid_ticks + ask_ticks`, so half-tick midpoints stay
exact until the public `Decimal` equity curve is built.
"""

ts_ns: memoryview
mid_ticks_x2: memoryview
tick_size: Decimal

@classmethod
def from_bytes(
cls,
*,
ts_ns: bytes,
mid_ticks_x2: bytes,
tick_size: Decimal,
) -> "CompiledValuationMarks":
"""Build compact marks from native int64 byte columns."""

timestamps = memoryview(ts_ns).cast("q")
mids = memoryview(mid_ticks_x2).cast("q")
if len(timestamps) != len(mids):
raise ValueError("valuation mark columns must have equal length")
return cls(ts_ns=timestamps, mid_ticks_x2=mids, tick_size=tick_size)

def __len__(self) -> int:
return len(self.ts_ns)


ValuationMarkInput: TypeAlias = (
tuple[ValuationMark | CompiledValuationMarks, ...]
| list[ValuationMark | CompiledValuationMarks]
| CompiledValuationMarks
)


def iter_valuation_mark_pairs(
marks: ValuationMarkInput,
) -> Iterable[tuple[int, Price]]:
"""Yield `(timestamp, price)` pairs from public or compact mark inputs."""

if isinstance(marks, CompiledValuationMarks):
yield from iter_compiled_valuation_mark_pairs(marks)
return

for mark in marks:
if isinstance(mark, CompiledValuationMarks):
yield from iter_compiled_valuation_mark_pairs(mark)
else:
yield mark.ts_ns, mark.price


def iter_compiled_valuation_mark_pairs(
marks: CompiledValuationMarks,
) -> Iterable[tuple[int, Price]]:
"""Yield Decimal midpoint prices from compact integer mark columns."""

for ts_ns, mid_ticks_x2 in zip(marks.ts_ns, marks.mid_ticks_x2, strict=True):
yield ts_ns, marks.tick_size * Decimal(mid_ticks_x2) / 2
11 changes: 0 additions & 11 deletions tests/test_economics.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from decimal import Decimal

import pytest

from ordersim import (
CompiledValuationMarks,
EquityPoint,
Expand Down Expand Up @@ -168,12 +166,3 @@ def test_build_equity_curve_accepts_mixed_public_and_compact_marks() -> None:
(1, Decimal("100.0")),
(2, Decimal("100.5")),
]


def test_compact_valuation_marks_reject_mismatched_columns() -> None:
with pytest.raises(ValueError, match="equal length"):
CompiledValuationMarks.from_bytes(
ts_ns=int64_bytes((1, 2)),
mid_ticks_x2=int64_bytes((2000,)),
tick_size=Decimal("0.10"),
)
Loading
Loading