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

Revival #2

Draft
wants to merge 37 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
946d3b7
Formatting run
jmcph4 Aug 28, 2024
de8e7b4
Fix book equality
jmcph4 Aug 29, 2024
278ebd9
Make library
jmcph4 Aug 29, 2024
41978a2
Apply Clippy fixes
jmcph4 Aug 29, 2024
553178c
Remove lockfile as this is a lib now
jmcph4 Aug 29, 2024
f4dfdf1
Update gitignore
jmcph4 Aug 29, 2024
ffae1ff
Update deps
jmcph4 Aug 29, 2024
f98a0d3
Update CI
jmcph4 Aug 29, 2024
2d65bed
Add CODEOWNERS file
jmcph4 Aug 29, 2024
e0c6a56
Bump license year
jmcph4 Aug 29, 2024
e75058a
Add develop branch to CI
jmcph4 Aug 29, 2024
cb4d903
Refactor
jmcph4 Nov 5, 2024
1a76a8c
Fix unit tests for `BTreeBook`
jmcph4 Nov 6, 2024
63ca4d3
Add basic example
jmcph4 Nov 6, 2024
a218f94
Doc comments and iteration order
jmcph4 Nov 6, 2024
cff25ce
Avoid module inception
jmcph4 Nov 6, 2024
908c7f8
Add example JSON blob
jmcph4 Nov 6, 2024
5aa5953
Impl order book display
jmcph4 Nov 6, 2024
6adb665
Provide example order flow
jmcph4 Nov 6, 2024
55b8efe
Add header
jmcph4 Nov 6, 2024
7a6e87c
Add banner to example
jmcph4 Nov 6, 2024
ca6602b
Add histograms
jmcph4 Nov 7, 2024
12e1ec4
Add `Arbitrary`
jmcph4 Nov 7, 2024
6c8a482
lint
jmcph4 Nov 7, 2024
c851e56
Refactor matching logic
jmcph4 Nov 7, 2024
ebd5169
Clippy
jmcph4 Nov 7, 2024
a88d47a
Add test
jmcph4 Nov 7, 2024
51f9f98
Remove example JSON
jmcph4 Nov 7, 2024
0d87f4a
Add docs
jmcph4 Nov 7, 2024
6c05745
Add design document
jmcph4 Nov 7, 2024
a9cb50b
Add performance notes to the design document
jmcph4 Nov 8, 2024
d2ff6e7
Fix mathematics typesetting
jmcph4 Nov 8, 2024
87ff54b
Missed one
jmcph4 Nov 8, 2024
3531521
Add benchmarks
jmcph4 Nov 8, 2024
89bf98b
Fix partial matching and improve tests
jmcph4 Nov 18, 2024
2ed03c8
Add unit test
jmcph4 Nov 18, 2024
c773704
Add additional order of magnitude to benchmark
jmcph4 Nov 18, 2024
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
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [ "master", "develop" ]
pull_request:
branches: [ "master", "develop" ]

env:
CARGO_TERM_COLOR: always

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Clippy
run: cargo clippy --all
- name: Check format
run: cargo fmt --all -- --check
- name: Build
run: cargo build --all --verbose
- name: Run tests
run: cargo test --all --verbose
- name: Check for unused dependencies
run: cargo install cargo-machete && cargo machete
- name: Check for known vulnerabilities in dependencies
run: cargo install cargo-audit && cargo audit
16 changes: 0 additions & 16 deletions .github/workflows/rust.yml

This file was deleted.

26 changes: 25 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,26 @@
/target
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

msg.tmp
*.tmp
*.secret

5 changes: 5 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @jmcph4 will be requested for review when someone opens
# a pull request.
* @jmcph4
102 changes: 0 additions & 102 deletions Cargo.lock

This file was deleted.

26 changes: 21 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
[package]
name = "ironlobe"
version = "0.1.0"
authors = ["jmcph4 <[email protected]>"]
edition = "2018"
version = "0.2.0"
authors = ["Jack McPherson <[email protected]>"]
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4.9"
ordered-float = "1.0.2"
arbitrary = { version = "1", features = ["derive"] }
chrono = { version = "0.4.38", features = ["arbitrary", "serde"] }
clap = { version = "4.5.20", features = ["derive"] }
eq-float = "0.1.0"
eyre = "0.6.12"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand = "0.8.5"

[[example]]
name = "basic"

[[bench]]
name = "btree_order_insertion"
harness = false

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2019 Jack McPherson
Copyright (c) 2019-2024 Jack McPherson


Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Expand Down
93 changes: 93 additions & 0 deletions benches/btree_order_insertion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use arbitrary::{Arbitrary, Unstructured};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ironlobe::{
book::{
btree_book::{BTreeBook, Metadata},
Book,
},
order::PlainOrder,
};
use rand::rngs::StdRng;
use rand::{Rng, RngCore, SeedableRng};

const SAMPLE_SECS: u64 = 5;
const BUFLEN: usize = 256;

fn make_orders(n: usize) -> Vec<PlainOrder> {
let mut rng = StdRng::seed_from_u64(42); // Deterministic RNG for reproducibility
(0..n)
.map(|_| {
let mut bytes = vec![0u8; BUFLEN];
rng.fill_bytes(&mut bytes);
let mut unstructured = Unstructured::new(&bytes);
let mut order = PlainOrder::arbitrary(&mut unstructured)
.expect("Failed to generate instance");
order.price = rng.gen_range(10.0..100.0); // Set realistic price ranges
order
})
.collect()
}

fn insert_into_book(
orders: &Vec<PlainOrder>,
book: &mut BTreeBook<PlainOrder>,
) {
orders.iter().for_each(|x| book.add(x.clone())); // Ensure add can handle references to avoid cloning
}

fn benchmark_1000(c: &mut Criterion) {
let orders = make_orders(black_box(1000));

c.bench_function("insert 1000", |b| {
b.iter(|| {
let mut book = BTreeBook::meta(Metadata {
id: 1,
name: "Benchmark Book".to_string(),
ticker: "BENCH".to_string(),
});
insert_into_book(&orders, &mut book)
})
});
}

fn benchmark_10000(c: &mut Criterion) {
let orders = make_orders(black_box(10000));

c.bench_function("insert 10000", |b| {
b.iter(|| {
let mut book = BTreeBook::meta(Metadata {
id: 1,
name: "Benchmark Book".to_string(),
ticker: "BENCH".to_string(),
});
insert_into_book(&orders, &mut book)
})
});
}

fn benchmark_100000(c: &mut Criterion) {
let orders = make_orders(black_box(100000));

c.bench_function("insert 100000", |b| {
b.iter(|| {
let mut book = BTreeBook::meta(Metadata {
id: 1,
name: "Benchmark Book".to_string(),
ticker: "BENCH".to_string(),
});
insert_into_book(&orders, &mut book)
})
});
}

fn configure_criterion() -> Criterion {
Criterion::default()
.measurement_time(std::time::Duration::from_secs(SAMPLE_SECS))
}

criterion_group! {
name = benches;
config = configure_criterion();
targets = benchmark_1000, benchmark_10000, benchmark_100000
}
criterion_main!(benches);
56 changes: 56 additions & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Design #

## Goals ##

Ironlobe has the following design goals:

- **Speed**, Ironlobe seeks to minimise order matching latency
- **Extensibility**, Ironlobe seeks to support a wide variety of different use cases
- **Ergonomic**, Ironlobe seeks to be a joy to use

All order book implementations must be thread-safe and have good performance on the `Book::add` method.

## Approach ##

Where extension is encouraged, Ironlobe provides traits and generic functions on these traits. The most notable being,

- `Order`, what an order looks like
- `Book`, what an order book looks like

### `BTreeBook` ###

Currently, Ironlobe only has one implementation of an order book: the `BTreeBook`. The `BTreeBook` is an order book where each side of the market is a `BTreeMap` keyed on price. The values are double-ended queues (Rust's [`VecDeque`](https://doc.rust-lang.org/std/collections/struct.VecDeque.html)) containing `Order`s. The rationale for this is:

- Amortised $\mathcal{O}\left(1\right)$ retrieval during the matching loop
- Sorted-order traversal of price levels

Additionally, `BTreeBook` stores an event log which logs fills, posts, and cancellations with timestamps and in chronological order. `BTreeBook` also stores other information such as the last traded price (LTP) and the depth of the book.

#### Performance ####

##### Asymptotic #####

When considering insertion into the book specifically, there are two cases:

- Price level already exists, so $\mathcal{O}\left(1\right)$ as
- Retrieval from the B-tree store, $\mathcal{O}\left(1\right)$
- Insertion into the deque, $\mathcal{O}\left(1\right)$
- Price level does not already exist, so $\mathcal{O}\left(\log{n}\right)$ as
- Insertion into the B-tree store, $\mathcal{O}\left(\log{n}\right)$
- Insertion into the deque, $\mathcal{O}\left(1\right)$

Note that submitting an order to the book (i.e., via `Book<T>::add`) is not purely insertion. This is obvious because if the order crosses the spread then matching will occur (this may result in an eventual insertion but will involve multiple retrievals and possibly deletions).

| Operation | Asymptotic Performance |
| --- | --- |
| `<BTreeBook<T> as Book<T>>::add` | Complicated, TODO |
| `<BTreeBook<T> as Book<T>>::cancel` | TODO |
| `<BTreeBook<T> as Book<T>>::top` | $\mathcal{O}\left(1\right)$ |
| `<BTreeBook<T> as Book<T>>::depth` | $\mathcal{O}\left(1\right)$ |
| `<BTreeBook<T> as Book<T>>::ltp` | $\mathcal{O}\left(1\right)$ |


##### Concrete #####

Consult the relevant [Criterion](https://docs.rs/criterion/latest/criterion) benchmarks in the repository. On a fairly unremarkable consumer-grade ThinkPad `BTreeBook` achieves an average time of **566ns** per order for 1,000 orders.

8 changes: 8 additions & 0 deletions docs/FUNCTIONALITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Functionality #

- Match orders using various order book implementations
- Produce a plain-text, human-readable representation of an order book
- Serialise order books to JSON
- Deserialise order books from JSON
- Expose a REPL for submitting orders to an order book via standard input

Loading
Loading