Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Numpy API #18

Merged
merged 12 commits into from
Mar 27, 2024
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions crates/order_book/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ pub enum Side {
Ask,
}

impl From<bool> for Side {
fn from(side: bool) -> Side {
match side {
true => Self::Bid,
false => Self::Ask,
}
}
}

impl From<Side> for bool {
fn from(side: Side) -> bool {
match side {
Side::Bid => true,
Side::Ask => false,
}
}
}

/// Order status
#[derive(Clone, PartialEq, Eq, Copy, Debug, Serialize, Deserialize)]
pub enum Status {
Expand All @@ -40,6 +58,18 @@ pub enum Status {
Rejected,
}

impl From<Status> for u8 {
fn from(status: Status) -> u8 {
match status {
Status::New => 0,
Status::Active => 1,
Status::Filled => 2,
Status::Cancelled => 3,
Status::Rejected => 4,
}
}
}

/// Order data
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct Order {
Expand Down
19 changes: 13 additions & 6 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,10 @@

html_theme_options = {
"repo_url": "https://github.com/zombie-einstein/bourse",
"icon": {
"repo": "fontawesome/brands/github",
},
"icon": {"repo": "fontawesome/brands/github", "logo": "material/chart-line"},
"palette": {
"scheme": "slate",
"primary": "teal",
"primary": "black",
},
"toc_title_is_page_title": True,
}
Expand All @@ -59,8 +57,8 @@
python_apigen_modules = {
"bourse.data_processing": "pages/generated/data_processing/",
"bourse.step_sim": "pages/generated/step_sim/",
"bourse.step_sim.agents.base_agent": "pages/generated/step_sim/agents/base",
"bourse.step_sim.agents.random_agent": "pages/generated/step_sim/agents/random",
"bourse.step_sim.agents.base_agent": "pages/generated/step_sim/agents/base/",
"bourse.step_sim.agents.random_agent": "pages/generated/step_sim/agents/random/",
"bourse.core": "pages/generated/core/",
}

Expand All @@ -70,13 +68,22 @@
(r"class:.*BaseAgent.*", "base_agent_class"),
(r"method:.*BaseAgent.*", "BaseAgent Methods"),
(r"attribute:.*BaseAgent.*", "BaseAgent Attributes"),
(r"class:.*BaseNumpyAgent.*", "base_numpy_agent_class"),
(r"method:.*BaseNumpyAgent.*", "BaseNumpyAgent Methods"),
(r"attribute:.*BaseNumpyAgent.*", "BaseNumpyAgent Attributes"),
(r"class:.*RandomAgent.*", "random_agent_class"),
(r"method:.*RandomAgent.*", "RandomAgent Methods"),
(r"attribute:.*RandomAgent.*", "RandomAgent Attributes"),
(r"class:.*NumpyRandomAgents.*", "n_random_agent_class"),
(r"method:.*NumpyRandomAgents.*", "NumpyRandomAgents Methods"),
(r"attribute:.*NumpyRandomAgents.*", "NumpyRandomAgents Attributes"),
(r"class:.*OrderBook.*", "order_book_class"),
(r"method:.*OrderBook.*", "OrderBook Methods"),
(r"attribute:.*OrderBook.*", "OrderBook Attributes"),
(r"class:.*StepEnv.*", "step_env_class"),
(r"method:.*StepEnv.*", "StepEnv Methods"),
(r"attribute:.*StepEnv.*", "StepEnv Attributes"),
(r"class:.*StepEnvNumpy.*", "step_env_numpy_class"),
(r"method:.*StepEnvNumpy.*", "StepEnvNumpy Methods"),
(r"attribute:.*StepEnvNumpy.*", "StepEnvNumpy Attributes"),
]
6 changes: 6 additions & 0 deletions docs/source/pages/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ API Reference

.. python-apigen-group:: base_agent_class

.. python-apigen-group:: base_numpy_agent_class

.. python-apigen-group:: random_agent_class

.. python-apigen-group:: n_random_agent_class

.. python-apigen-group:: order_book_class

.. python-apigen-group:: step_env_class

.. python-apigen-group:: step_env_numpy_class

.. python-apigen-group:: public-members
54 changes: 54 additions & 0 deletions docs/source/pages/discrete_event_usage.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Discrete Event Simulation Environment
-------------------------------------

A discrete event simulation environment can be initialised from
a random seed, start-time, tick-size, and step-size (i.e. how
long in time each simulated step is)

.. testcode:: sim_usage

import bourse

seed = 101
start_time = 0
tick_size = 2
step_size = 100_000

env = bourse.core.StepEnv(seed, start_time, tick_size, step_size)

The state of the simulation is updated in discrete
steps, with transactions submitted to a queue to
be processed at the end of the step. For example
placing new orders

.. testcode:: sim_usage

order_id_a = env.place_order(False, 100, 101, price=60)
order_id_b = env.place_order(True, 100, 101, price=70)

To actually update the state of the simulation we call
:py:meth:`bourse.core.StepEnv.step` which shuffles and
processes the queued instructions. Each step also increments
time to correctly order transactions.

The simulation environment also tracks market data for each
step, for example bid-ask prices can be retrieved using

.. testcode:: sim_usage

bid_prices, ask_prices = env.get_prices()

the full level 2 data (price and volumes along with volumes
and number of orders at top 10 levels) records can be
retrieved with

.. testcode:: sim_usage

level_2_data = env.get_market_data()

See :py:class:`bourse.core.StepEnv` for full details
of the environment API.

:py:meth:`bourse.step_sim.run` is a utility for running a
simulation from an environment and set of agents. See
:ref:`Simulation Example` for a full simulation example.
2 changes: 1 addition & 1 deletion docs/source/pages/example.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ then define an agent class

.. testcode:: random_example

class RandomAgent:
class RandomAgent(bourse.step_sim.agents.BaseAgent):
def __init__(self, i, price_range):
self.i = i
self.price_range = price_range
Expand Down
146 changes: 146 additions & 0 deletions docs/source/pages/numpy_discrete_event_usage.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
Numpy Discrete Event Simulation Environment
-------------------------------------------

This environment allows market state and market instructions
to be returned/submitted as Numpy arrays. This has the
potential for higher performance (over native Python) using
vectorisation (with some limitations on functionality) in
particular for ML and RL use-cases.

The simulation environment can be initialised from
a random seed, start-time, tick-size, and step-size (i.e. how
long in time each simulated step is)

.. testcode:: numpy_sim_usage

import numpy as np
import bourse

seed = 101
start_time = 0
tick_size = 2
step_size = 100_000

env = bourse.core.StepEnvNumpy(seed, start_time, tick_size, step_size)

New order and cancellations can be submitted as Numpy arrays

.. testcode:: numpy_sim_usage

new_orders = (
# Order sides
np.array([True, False]),
# Order volumes
np.array([10, 20], dtype=np.uint32),
# Trader ids
np.array([101, 202], dtype=np.uint32),
# Order prices
np.array([50, 60], dtype=np.uint32),
)

new_order_ids = env.submit_limit_orders(new_orders)

# Update the environment state (placing orders)
env.step()

# Cancel the new orders
env.submit_cancellations(new_order_ids)

Multiple instruction types can be submitted as a tuple of arrays:

- The instruction type where ``0 = no action``, ``1 = new-order``, and
``2 = cancellation``.
- Order sides (as bool, ``True`` for bid side) (used for new orders)
- Order volumes (used for new orders)
- Trader ids (used for new orders)
- Order prices (used for new orders)
- Order ids (used for cancellations)

.. note::

Values that are not used for a given action (e.g. order-ids for
new orders) are ignored, so can be set to an arbitrary default.

For example, if we want to submit one instruction with no change
and one new-order we could use:

.. testcode:: numpy_sim_usage

instructions = (
np.array([0, 1], dtype=np.uint32),
np.array([True, True]),
np.array([0, 20], dtype=np.uint32),
np.array([0, 101], dtype=np.uint32),
np.array([0, 50], dtype=np.uint32),
np.array([0, 0], dtype=np.uint64)
)

new_order_ids = env.submit_instructions(instructions)

env.step()

.. warning::

This method currently only supports submitting limit
orders and cancelling orders.

The state of the order book can be retrieved as an array
of values representing the current touch-prices, volumes and
volumes and orders at price levels

.. testcode:: numpy_sim_usage

level_1_data = env.level_1_data()

level_2_data = env.level_2_data()

where the level-1 data only contains the touch volume and
number of orders, and level-2 data contains the volume and
number of orders for the first 10 price levels from the touch.

See :py:class:`bourse.core.StepEnvNumpy` for full details
of the API.

Agents that interact with the Numpy API can implement
:py:class:`bourse.step_sim.agents.base_agent.BaseNumpyAgent` with an
``update`` method that takes a random number generator
and array representing the current level 2 data of the
order book (the current touch price, and volumes and orders
at the top 10 price levels). It should return a tuple of
arrays encoding market instructions, for example this
agent simply places new orders either side of the spread

.. testcode:: numpy_sim_usage

from bourse.step_sim.agents import BaseNumpyAgent

class Agent(BaseNumpyAgent):

def update(self, rng, level_2_data):
bid = max(level_2_data[1], 20)
ask = min(level_2_data[2], 40)

return (
np.array([1, 1], dtype=np.uint32),
np.array([True, False]),
np.array([10, 20], dtype=np.uint32),
np.array([101, 202], dtype=np.uint32),
np.array([bid, ask], dtype=np.uint32),
np.array([0, 0], dtype=np.uint64),
)

These agents can be used in simulation by setting the
``use_numpy`` argument, and passing an array
of agents implementing :py:class:`bourse.step_sim.agents.base_agent.BaseNumpyAgent`,
for example

.. testcode:: numpy_sim_usage

agents = [Agent()]

n_steps = 50
seed = 101

market_data = bourse.step_sim.run(
env, agents, n_steps, seed, use_numpy = True
)
64 changes: 64 additions & 0 deletions docs/source/pages/order_book_usage.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Orderbook
---------

An orderbook is initialised with a start time
(this is the time used to record events) and a
tick-size

.. testcode:: book_usage

import bourse

start_time = 0
tick_size = 1

book = bourse.core.OrderBook(start_time, tick_size)

The state of the orderbook an then be directly
updated, for example placing a limit bid order

.. testcode:: book_usage

order_vol = 10
trader_id = 101
order_id = book.place_order(
True, order_vol, trader_id, price=50
)

or cancelling the same order

.. testcode:: book_usage

book.cancel_order(order_id)

When directly interacting with the orderbook
updates are immediately applied and the state
of the market updated.

The orderbook also tracks updates, for example
trades executed on the order book can be
retrieved with

.. testcode:: book_usage

trades = book.get_trades()
# Convert trade data to a dataframe
trade_df = bourse.data_processing.trades_to_dataframe(
trades
)

The state of the order book can be written to a JSON
file using :py:meth:`bourse.core.OrderBook.save_json_snapshot`,
the same snapshot can then be used to initialise an
orderbook using :py:meth:`bourse.core.order_book_from_json`

.. code-block:: python

# Save order book state to foo.json
book.save_json_snapshot("foo.json")

# Create a new order book with state from the snapshot
loaded_book = bourse.core.order_book_from_json("foo.json")

See :py:class:`bourse.core.OrderBook`
for details of the full order book API.
Loading