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
34 changes: 30 additions & 4 deletions cpp/matching_engine_core.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,35 @@ class MatchingEngineCpp {
int64_t price_ticks,
int32_t size,
int64_t order_id
) {
return apply_event_code(
ts_ns,
action_code(action),
side,
price_ticks,
size,
order_id
);
}

std::vector<FillRow> apply_event_code(
int64_t ts_ns,
char action,
char side,
int64_t price_ticks,
int32_t size,
int64_t order_id
) {
now_ns_ = ts_ns;
const size_t before = passive_fills_.size();

if (action == "add") {
if (action == 'A') {
add_public(order_id, side, price_ticks, size);
} else if (action == "cancel") {
} else if (action == 'C') {
cancel_public(order_id, side, price_ticks, size);
} else if (action == "modify") {
} else if (action == 'M') {
modify_public(order_id, side, price_ticks, size);
} else if (action == "trade") {
} else if (action == 'T') {
consume_level(side, price_ticks, size, ts_ns);
} else {
throw std::runtime_error("unknown MBO action");
Expand Down Expand Up @@ -188,6 +206,14 @@ class MatchingEngineCpp {

static constexpr int64_t missing_price() { return -1; }

static char action_code(const std::string& action) {
if (action == "add") return 'A';
if (action == "cancel") return 'C';
if (action == "modify") return 'M';
if (action == "trade") return 'T';
throw std::runtime_error("unknown MBO action");
}

static void validate_order(int32_t size, int64_t price_ticks) {
if (size <= 0) throw std::runtime_error("size must be positive");
if (price_ticks <= 0) throw std::runtime_error("price must be positive");
Expand Down
100 changes: 100 additions & 0 deletions cpp/matching_engine_cpp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,105 @@

namespace py = pybind11;

namespace {

template <typename T>
const T* checked_buffer(
const py::buffer& buffer,
const std::string& expected_format,
const char* name,
py::ssize_t expected_size = -1
) {
const py::buffer_info info = buffer.request();
if (info.ndim != 1) {
throw py::value_error(std::string(name) + " must be one-dimensional");
}
if (info.itemsize != static_cast<py::ssize_t>(sizeof(T))
|| info.format != expected_format) {
throw py::value_error(std::string(name) + " has the wrong dtype");
}
if (info.strides[0] != static_cast<py::ssize_t>(sizeof(T))) {
throw py::value_error(std::string(name) + " must be contiguous");
}
if (expected_size >= 0 && info.shape[0] != expected_size) {
throw py::value_error("batch columns must have equal length");
}
return static_cast<const T*>(info.ptr);
}

std::vector<FillRow> apply_events_batch(
MatchingEngineCpp& engine,
const py::buffer& ts_ns,
const py::buffer& action,
const py::buffer& side,
const py::buffer& price_ticks,
const py::buffer& size,
const py::buffer& order_id
) {
const py::buffer_info ts_info = ts_ns.request();
if (ts_info.ndim != 1) {
throw py::value_error("ts_ns must be one-dimensional");
}
if (ts_info.itemsize != static_cast<py::ssize_t>(sizeof(int64_t))
|| ts_info.format != py::format_descriptor<int64_t>::format()) {
throw py::value_error("ts_ns has the wrong dtype");
}
if (ts_info.strides[0] != static_cast<py::ssize_t>(sizeof(int64_t))) {
throw py::value_error("ts_ns must be contiguous");
}

const py::ssize_t n = ts_info.shape[0];
const auto* ts_data = static_cast<const int64_t*>(ts_info.ptr);
const auto* action_data = checked_buffer<uint8_t>(
action,
py::format_descriptor<uint8_t>::format().c_str(),
"action",
n
);
const auto* side_data = checked_buffer<uint8_t>(
side,
py::format_descriptor<uint8_t>::format().c_str(),
"side",
n
);
const auto* price_data = checked_buffer<int64_t>(
price_ticks,
py::format_descriptor<int64_t>::format().c_str(),
"price_ticks",
n
);
const auto* size_data = checked_buffer<int32_t>(
size,
py::format_descriptor<int32_t>::format().c_str(),
"size",
n
);
const auto* order_data = checked_buffer<int64_t>(
order_id,
py::format_descriptor<int64_t>::format().c_str(),
"order_id",
n
);

std::vector<FillRow> fills;

for (py::ssize_t i = 0; i < n; ++i) {
const auto event_fills = engine.apply_event_code(
ts_data[i],
static_cast<char>(action_data[i]),
static_cast<char>(side_data[i]),
price_data[i],
size_data[i],
order_data[i]
);
fills.insert(fills.end(), event_fills.begin(), event_fills.end());
}

return fills;
}

} // namespace

PYBIND11_MODULE(_matching_engine_cpp, module) {
py::class_<FillRow>(module, "FillRow")
.def_readonly("order_id", &FillRow::order_id)
Expand All @@ -16,6 +115,7 @@ PYBIND11_MODULE(_matching_engine_cpp, module) {
py::class_<MatchingEngineCpp>(module, "MatchingEngineCpp")
.def(py::init<>())
.def("apply_event", &MatchingEngineCpp::apply_event)
.def("apply_events_batch", &apply_events_batch)
.def("place_limit", &MatchingEngineCpp::place_limit)
.def("place_market", &MatchingEngineCpp::place_market)
.def("cancel", &MatchingEngineCpp::cancel)
Expand Down
14 changes: 14 additions & 0 deletions docs/execution-engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ strategy has isolated order state while sharing the same immutable event stream.
stores integer ticks; the Python wrapper accepts an explicit `tick_size` so the
public API remains exact-`Decimal`.

For callers that already hold normalized events in memory, the wrapper also
exposes a compiled batch-ingest path. It accepts primitive columns derived from
the same `MBOEvent` schema and returns passive fills without changing the public
matching semantics. Ordinary `Replay(...)` still applies one event at a time so
it can record the per-event valuation marks that build the default equity curve.

```python
from ordersim import CompiledEventColumns, CppMatchingEngine

columns = CompiledEventColumns.from_events(events, tick_size=spec.tick_size)
engine = CppMatchingEngine(tick_size=spec.tick_size)
fills = engine.apply_events_batch(columns.slice(0, len(events)))
```

Install a source checkout normally to build the extension:

```bash
Expand Down
3 changes: 2 additions & 1 deletion src/ordersim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
default_latency_model_factory,
)
from ordersim.recording import RecordingGateway
from ordersim.replay import Replay, ReplayGateway, ReplayResult
from ordersim.replay import CompiledEventColumns, Replay, ReplayGateway, ReplayResult
from ordersim.sim import (
CppMatchingEngine,
ExecutionEngine,
Expand Down Expand Up @@ -59,6 +59,7 @@

__all__ = [
"BookSide",
"CompiledEventColumns",
"ConstantLatency",
"CppMatchingEngine",
"CsvSource",
Expand Down
3 changes: 2 additions & 1 deletion src/ordersim/replay/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Replay orchestration for strategy functions."""

from ordersim.replay.compiled_events import CompiledEventColumns
from ordersim.replay.simulator import Replay, ReplayGateway, ReplayResult

__all__ = ["Replay", "ReplayGateway", "ReplayResult"]
__all__ = ["CompiledEventColumns", "Replay", "ReplayGateway", "ReplayResult"]
82 changes: 82 additions & 0 deletions src/ordersim/replay/compiled_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Internal columnar view of one canonical replay stream."""

from array import array
from dataclasses import dataclass
from decimal import Decimal

from ordersim.types import MBOEvent, Price

_ACTION_CODE = {
"add": ord("A"),
"cancel": ord("C"),
"modify": ord("M"),
"trade": ord("T"),
}
_SIDE_CODE = {
"bid": ord("B"),
"ask": ord("A"),
}


@dataclass(frozen=True, slots=True)
class CompiledEventColumns:
"""Primitive event columns shared by repeated compiled-engine runs."""

ts_ns: array
action: array
side: array
price_ticks: array
size: array
order_id: array

@classmethod
def from_events(
cls,
events: tuple[MBOEvent, ...],
*,
tick_size: Price,
) -> "CompiledEventColumns":
"""Compile immutable public events into integer columns once."""

return cls(
ts_ns=array("q", (event.ts_ns for event in events)),
action=array("B", (_ACTION_CODE[event.action] for event in events)),
side=array("B", (_SIDE_CODE[event.side] for event in events)),
price_ticks=array(
"q",
(_price_to_ticks(event.price, tick_size) for event in events),
),
size=array("i", (event.size for event in events)),
order_id=array("q", (event.order_id for event in events)),
)

def slice(self, start: int, stop: int) -> "CompiledEventSlice":
"""Return zero-copy memory views for one replay interval."""

return CompiledEventSlice(
ts_ns=memoryview(self.ts_ns)[start:stop],
action=memoryview(self.action)[start:stop],
side=memoryview(self.side)[start:stop],
price_ticks=memoryview(self.price_ticks)[start:stop],
size=memoryview(self.size)[start:stop],
order_id=memoryview(self.order_id)[start:stop],
)


@dataclass(frozen=True, slots=True)
class CompiledEventSlice:
"""Zero-copy primitive columns for one contiguous replay interval."""

ts_ns: memoryview
action: memoryview
side: memoryview
price_ticks: memoryview
size: memoryview
order_id: memoryview


def _price_to_ticks(price: Decimal, tick_size: Price) -> int:
ticks = price / tick_size
if ticks != ticks.to_integral_value():
raise ValueError(f"price {price} is not aligned to tick_size {tick_size}")
return int(ticks)
21 changes: 19 additions & 2 deletions src/ordersim/sim/cpp_matching_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from decimal import Decimal
from typing import Any

from ordersim.replay.compiled_events import CompiledEventSlice
from ordersim.sim.matching_engine import PriceLevel
from ordersim.types import (
Fill,
Expand Down Expand Up @@ -36,6 +37,22 @@ def apply_event(self, event: MBOEvent) -> list[Fill]:
)
return [self._fill_from_row(row) for row in rows]

def apply_events_batch(
self,
events: CompiledEventSlice,
) -> list[Fill]:
"""Apply one compiled event slice and return passive fills."""

rows = self._core.apply_events_batch(
events.ts_ns,
events.action,
events.side,
events.price_ticks,
events.size,
events.order_id,
)
return [self._fill_from_row(row) for row in rows]

def advance_time(self, ts_ns: int) -> None:
self._core.advance_time(ts_ns)

Expand Down Expand Up @@ -141,8 +158,8 @@ def _load_cpp_module() -> Any:
from ordersim import _matching_engine_cpp
except ImportError as exc:
raise ImportError(
"ordersim C++ engine is not built; run "
"`python setup_cpp.py build_ext --inplace` first"
"ordersim C++ engine is not built; install the project normally "
'with `python -m pip install -e ".[dev]"` first'
) from exc
return _matching_engine_cpp

Expand Down
Loading
Loading