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
2 changes: 1 addition & 1 deletion .github/workflows/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --group test
run: uv sync --group test --all-extras
- name: Run test suite
run: uv run pytest --cov=order_matching

Expand Down
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- Use uv to run python commands, e.g.

```shell
uv run pytest
```

- Run prek on all files before each commit:

```shell
uv run prek run -v --show-diff-on-failure --all-files
```
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ This package is a simple order book matching engine implementation in Python. It
- price-time priority
- limit and market orders
- order cancellation and expiration
- conversion into polars LazyFrame of orders, executed trades, order book summary
- conversion into polars LazyFrame of orders, executed trades, order book summary (optional polars dependency)

## Install

```shell
# Core matching engine only
pip install order-matching

# With polars export support (recommended for data science workflows)
pip install order-matching[polars]
```

## Documentation
Expand Down Expand Up @@ -55,6 +59,23 @@ pip install order-matching
timestamp=datetime.datetime(2023, 1, 2, 0, 0))]


```

### Data Export (Polars)

If you installed with `[polars]` extra, you can export data to polars LazyFrame:

```python
>>> from order_matching.orders import Orders
>>> from order_matching.executed_trades import ExecutedTrades
>>> from order_matching.exporters.polars import PolarsExporter

>>> exporter = PolarsExporter()
>>> orders_df = exporter.export_orders(Orders())
>>> trades_df = exporter.export_trades(ExecutedTrades())

>>> trades_df = ExecutedTrades().to_frame()

```

## Related Projects
Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ requires-python = ">=3.10"
dependencies = [
"faker>=33.0.0",
"numpy>=2.2.6",
]

[project.optional-dependencies]
polars = [
"pandera[polars]>=0.21.0",
"polars>=1.42.1",
]
all = [
"order-matching[polars]",
]

[dependency-groups]
test = [
Expand Down
30 changes: 14 additions & 16 deletions src/order_matching/executed_trades.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from __future__ import annotations

from collections import defaultdict
from dataclasses import asdict
from datetime import datetime
from typing import cast
from typing import TYPE_CHECKING

import polars as pl
from pandera.typing.polars import LazyFrame
if TYPE_CHECKING:
from pandera.typing.polars import LazyFrame

from order_matching.schemas import TradeDataSchema

from order_matching.schemas import TradeDataSchema
from order_matching.trade import Trade


Expand Down Expand Up @@ -60,20 +60,18 @@ def get(self, timestamp: datetime) -> list[Trade]:
def to_frame(self) -> LazyFrame[TradeDataSchema]:
"""Get polars DataFrame of all stored trades.

.. deprecated:: 0.5.0
Use ``PolarsExporter().export_trades(trades)`` instead.
This method will be removed in version 1.0.0.

Returns
-------
DataFrame[TradeDataSchema]
polars DataFrame of all stored trades
LazyFrame[TradeDataSchema]
polars LazyFrame of all stored trades
"""
trades = self.trades
if len(trades) == 0:
return cast(LazyFrame[TradeDataSchema], TradeDataSchema.empty().lazy())
else:
data = [asdict(trade) for trade in trades]
for d in data:
d[TradeDataSchema.side] = d[TradeDataSchema.side].name
d[TradeDataSchema.execution] = d[TradeDataSchema.execution].name
return cast(LazyFrame[TradeDataSchema], pl.LazyFrame(data))
from order_matching.exporters.polars import PolarsExporter

return PolarsExporter().export_trades(self)

def __add__(self, other: ExecutedTrades) -> ExecutedTrades:
trades = ExecutedTrades()
Expand Down
6 changes: 6 additions & 0 deletions src/order_matching/exporters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Data export module for converting domain objects to various formats."""

from order_matching.exporters.base import Exporter
from order_matching.exporters.polars import PolarsExporter

__all__ = ["Exporter", "PolarsExporter"]
48 changes: 48 additions & 0 deletions src/order_matching/exporters/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Generic, TypeVar

if TYPE_CHECKING:
from order_matching.executed_trades import ExecutedTrades
from order_matching.orders import Orders

T = TypeVar("T")


class Exporter(ABC, Generic[T]):
"""Abstract base class for data exporters.

Type parameter T represents the output format type
(e.g., LazyFrame, dict, str for JSON/CSV).
"""

@abstractmethod
def export_orders(self, orders: Orders) -> T:
"""Convert Orders collection to target format.

Parameters
----------
orders : Orders
Orders collection to export

Returns
-------
T
Exported data in target format
"""
pass

@abstractmethod
def export_trades(self, trades: ExecutedTrades) -> T:
"""Convert ExecutedTrades collection to target format.

Parameters
----------
trades : ExecutedTrades
Trades collection to export

Returns
-------
T
Exported data in target format
"""
pass
60 changes: 60 additions & 0 deletions src/order_matching/exporters/polars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from dataclasses import asdict
from typing import cast

import polars as pl
from pandera.typing.polars import LazyFrame

from order_matching.executed_trades import ExecutedTrades
from order_matching.exporters.base import Exporter
from order_matching.orders import Orders
from order_matching.schemas import OrderDataSchema, TradeDataSchema


class PolarsExporter(Exporter[LazyFrame]):
"""Export collections to polars LazyFrame format."""

def export_orders(self, orders: Orders) -> LazyFrame[OrderDataSchema]:
"""Export Orders to validated polars LazyFrame.

Parameters
----------
orders : Orders
Orders collection to export

Returns
-------
LazyFrame[OrderDataSchema]
Validated polars LazyFrame with order data
"""
if len(orders) == 0:
return cast(LazyFrame[OrderDataSchema], OrderDataSchema.empty().lazy())

data = [asdict(order) for order in orders]
for d in data:
d[OrderDataSchema.side] = d[OrderDataSchema.side].name
d[OrderDataSchema.execution] = d[OrderDataSchema.execution].name
d[OrderDataSchema.status] = d[OrderDataSchema.status].name
return cast(LazyFrame[OrderDataSchema], pl.LazyFrame(data))

def export_trades(self, trades: ExecutedTrades) -> LazyFrame[TradeDataSchema]:
"""Export ExecutedTrades to validated polars LazyFrame.

Parameters
----------
trades : ExecutedTrades
Trades collection to export

Returns
-------
LazyFrame[TradeDataSchema]
Validated polars LazyFrame with trade data
"""
trade_list = trades.trades
if len(trade_list) == 0:
return cast(LazyFrame[TradeDataSchema], TradeDataSchema.empty().lazy())

data = [asdict(trade) for trade in trade_list]
for d in data:
d[TradeDataSchema.side] = d[TradeDataSchema.side].name
d[TradeDataSchema.execution] = d[TradeDataSchema.execution].name
return cast(LazyFrame[TradeDataSchema], pl.LazyFrame(data))
2 changes: 1 addition & 1 deletion src/order_matching/order_book.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def _get_non_trivial_imbalance(self, price_range: float) -> float:
if buy_volume + sell_volume > 0:
return (buy_volume - sell_volume) / (buy_volume + sell_volume)
else:
return 0
return 0.0

def _get_same_side_orders(self, incoming_order: Order) -> OrderBookOrdersType:
match incoming_order.side:
Expand Down
26 changes: 12 additions & 14 deletions src/order_matching/orders.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from __future__ import annotations

from dataclasses import asdict
from typing import Iterator, Sequence, cast
from typing import TYPE_CHECKING, Iterator, Sequence

import polars as pl
from pandera.typing.polars import LazyFrame
if TYPE_CHECKING:
from pandera.typing.polars import LazyFrame

from order_matching.schemas import OrderDataSchema

from order_matching.order import Order
from order_matching.schemas import OrderDataSchema


class Orders:
Expand Down Expand Up @@ -58,19 +58,17 @@ def remove(self, orders: list[Order]) -> None:
def to_frame(self) -> LazyFrame[OrderDataSchema]:
"""Get polars LazyFrame with all orders in the storage.

.. deprecated:: 0.5.0
Use ``PolarsExporter().export_orders(orders)`` instead.
This method will be removed in version 1.0.0.

Returns
-------
LazyFrame[OrderDataSchema]
"""
if len(self.orders) == 0:
return cast(LazyFrame[OrderDataSchema], OrderDataSchema.empty().lazy())
else:
data = [asdict(order) for order in self.orders]
for d in data:
d[OrderDataSchema.side] = d[OrderDataSchema.side].name
d[OrderDataSchema.execution] = d[OrderDataSchema.execution].name
d[OrderDataSchema.status] = d[OrderDataSchema.status].name
return cast(LazyFrame[OrderDataSchema], pl.LazyFrame(data))
from order_matching.exporters.polars import PolarsExporter

return PolarsExporter().export_orders(self)

@property
def is_empty(self) -> bool:
Expand Down
Loading
Loading