Skip to content

Commit 4202c01

Browse files
committed
Add performance option and command
1 parent 78a719b commit 4202c01

File tree

5 files changed

+190
-3
lines changed

5 files changed

+190
-3
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ python:
66
- pypy
77

88
install:
9-
- pip install .
9+
- pip install .[performance]
1010

1111
script:
1212
- make test
13+
- python tests/performance/performance_test.py --freq 20

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,29 @@ The trade object contains the following information:
9494
* Trade quantity (trade_qty)
9595
* Trade side (trade_side)
9696

97+
## Performance
98+
99+
To run the performance test, run the commands
100+
101+
```
102+
pip install lightmatchingengine[performance]
103+
python tests/performance/performance_test.py --freq 20
104+
```
105+
106+
It returns the latency in nanosecond like below
107+
108+
| | add | cancel | add (trade > 0) | add (trade > 2.0) |
109+
|:------|---------:|---------:|------------------:|--------------------:|
110+
| count | 100 | 61 | 27 | 6 |
111+
| mean | 107.954 | 50.3532 | 164.412 | 205.437 |
112+
| std | 58.1438 | 16.3396 | 36.412 | 24.176 |
113+
| min | 17.1661 | 11.4441 | 74.1482 | 183.105 |
114+
| 25% | 81.3007 | 51.9753 | 141.382 | 188.47 |
115+
| 50% | 92.5064 | 58.4126 | 152.349 | 200.748 |
116+
| 75% | 140.19 | 59.3662 | 190.496 | 211.239 |
117+
| max | 445.604 | 71.0487 | 248.909 | 248.909 |
118+
119+
97120
## Contact
98121

99122
For any inquiries, please feel free to contact me by gavincyi at gmail dot com.

lightmatchingengine/lightmatchingengine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,11 @@ def cancel_order(self, order_id, instmt):
205205

206206
if side == Side.BUY:
207207
assert order_price in order_book.bids.keys(), \
208-
"Order price %.6f is not in the bid price depth"
208+
"Order price %.6f is not in the bid price depth" % order_price
209209
price_level = order_book.bids[order_price]
210210
else:
211211
assert order_price in order_book.asks.keys(), \
212-
"Order price %.6f is not in the ask price depth"
212+
"Order price %.6f is not in the ask price depth" % order_price
213213
price_level = order_book.asks[order_price]
214214

215215
index = 0

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
tests_require=[
1919
'pytest'
2020
],
21+
extra_requires={
22+
'performance': ['pandas']
23+
},
2124

2225
classifiers=[
2326
'Development Status :: 2 - Pre-Alpha',
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Performance test for light matching engine.
2+
3+
Usage:
4+
perf-test-light-matching-engine --freq <freq> [options]
5+
6+
Options:
7+
-h --help Show help.
8+
--freq=<freq> Order frequency per second. [Default: 10]
9+
--num-orders=<num_orders> Number of orders. [Default: 100]
10+
--add-order-prob=<prob> Add order probability. [Default: 0.6]
11+
--mean-price=<mean-price> Mean price in the standard normal distribution.
12+
[Default: 100]
13+
--std-price=<std-price> Standard derivation of the price in the standing
14+
derivation. [Default: 0.5]
15+
--tick-size=<tick-size> Tick size. [Default: 0.1]
16+
--gamma-quantity=<gamma> Gamma value in the gamma distribution for the
17+
order quantity. [Default: 2]
18+
"""
19+
from docopt import docopt
20+
import logging
21+
from math import log
22+
from random import uniform, seed
23+
from time import sleep, time
24+
25+
from tabulate import tabulate
26+
from tqdm import tqdm
27+
28+
import numpy as np
29+
import pandas as pd
30+
31+
from lightmatchingengine.lightmatchingengine import (
32+
LightMatchingEngine, Side)
33+
34+
LOGGER = logging.getLogger(__name__)
35+
36+
37+
class Timer:
38+
def __enter__(self):
39+
self.start = time()
40+
return self
41+
42+
def __exit__(self, *args):
43+
self.end = time()
44+
self.interval = self.end - self.start
45+
46+
47+
def run(args):
48+
engine = LightMatchingEngine()
49+
50+
symbol = "EUR/USD"
51+
add_order_prob = float(args['--add-order-prob'])
52+
num_of_orders = int(args['--num-orders'])
53+
gamma_quantity = float(args['--gamma-quantity'])
54+
mean_price = float(args['--mean-price'])
55+
std_price = float(args['--std-price'])
56+
tick_size = float(args['--tick-size'])
57+
freq = float(args['--freq'])
58+
orders = {}
59+
add_statistics = []
60+
cancel_statistics = []
61+
62+
# Initialize random seed
63+
seed(42)
64+
65+
progress_bar = tqdm(num_of_orders)
66+
while num_of_orders > 0:
67+
if uniform(0, 1) <= add_order_prob or len(orders) == 0:
68+
price = np.random.standard_normal() * std_price + mean_price
69+
price = int(price / tick_size) * tick_size
70+
quantity = np.random.gamma(gamma_quantity) + 1
71+
side = Side.BUY if uniform(0, 1) <= 0.5 else Side.SELL
72+
73+
# Add the order
74+
with Timer() as timer:
75+
order, trades = engine.add_order(symbol, price, quantity, side)
76+
77+
LOGGER.debug('Order %s is added at side %s, price %s '
78+
'and quantity %s',
79+
order.order_id, order.side, order.price, order.qty)
80+
81+
# Save the order if there is any quantity left
82+
if order.leaves_qty > 0:
83+
orders[order.order_id] = order
84+
85+
# Remove the trades
86+
for trade in trades:
87+
if (trade.order_id != order.order_id and
88+
orders[trade.order_id].leaves_qty == 0.0):
89+
del orders[trade.order_id]
90+
91+
# Save the statistics
92+
add_statistics.append((order, len(trades), timer))
93+
94+
num_of_orders -= 1
95+
progress_bar.update(1)
96+
else:
97+
index = int(uniform(0, 1) * len(orders))
98+
if index == len(orders):
99+
index -= 1
100+
101+
order_id = list(orders.keys())[index]
102+
103+
with Timer() as timer:
104+
engine.cancel_order(order_id, order.instmt)
105+
106+
LOGGER.debug('Order %s is deleted', order_id)
107+
del orders[order_id]
108+
109+
# Save the statistics
110+
cancel_statistics.append((order, timer))
111+
112+
# Next time = -ln(U) / lambda
113+
sleep(-log(uniform(0, 1)) / freq)
114+
115+
return add_statistics, cancel_statistics
116+
117+
118+
def describe_statistics(add_statistics, cancel_statistics):
119+
add_statistics = pd.DataFrame([
120+
(trade_num, timer.interval * 1e6)
121+
for _, trade_num, timer in add_statistics],
122+
columns=['trade_num', 'interval'])
123+
124+
# Trade statistics
125+
trade_statistics = add_statistics['trade_num'].describe()
126+
LOGGER.info('Trade statistics:\n%s',
127+
tabulate(trade_statistics.to_frame(name='trade'),
128+
tablefmt='pipe'))
129+
130+
cancel_statistics = pd.Series([
131+
timer.interval * 1e6 for _, timer in cancel_statistics],
132+
name='interval')
133+
134+
statistics = pd.concat([
135+
add_statistics['interval'].describe(),
136+
cancel_statistics.describe()],
137+
keys=['add', 'cancel'],
138+
axis=1)
139+
140+
statistics['add (trade > 0)'] = (
141+
add_statistics.loc[
142+
add_statistics['trade_num'] > 0, 'interval'].describe())
143+
144+
percentile_75 = trade_statistics['75%']
145+
statistics['add (trade > %s)' % percentile_75] = (
146+
add_statistics.loc[add_statistics['trade_num'] > percentile_75,
147+
'interval'].describe())
148+
149+
LOGGER.info('Matching engine latency (nanoseconds):\n%s',
150+
tabulate(statistics,
151+
headers=statistics.columns,
152+
tablefmt='pipe'))
153+
154+
if __name__ == '__main__':
155+
args = docopt(__doc__, version='1.0.0')
156+
logging.basicConfig(level=logging.INFO)
157+
158+
LOGGER.info('Running the performance benchmark')
159+
add_statistics, cancel_statistics = run(args)
160+
describe_statistics(add_statistics, cancel_statistics)

0 commit comments

Comments
 (0)