31 #include <type_traits> 32 #include <unordered_map> 66 void validate()
const override;
108 std::mt19937_64 initialize_generator(
int seed) {
109 return jb::testing::initialize_mersenne_twister<std::mt19937_64>(
122 std::vector<operation> create_operations(
123 std::mt19937_64& generator,
int size, fixture_config
const& cfg,
126 template <
typename order_book_s
ide,
typename book_config>
127 std::function<void()> create_iteration(
128 std::mt19937_64& generator,
int size, fixture_config
const& cfg,
129 book_config
const& bkcfg) {
130 order_book_side bk(bkcfg);
131 std::vector<operation> ops =
132 create_operations(generator, size, cfg, bk.is_ascending());
135 [ book = std::move(bk), operations = std::move(ops) ]()
mutable {
137 for (
auto const& op : operations) {
139 book.reduce_order(op.px, -op.delta);
141 book.add_order(op.px, op.delta);
146 return std::function<void()>(std::move(lambda));
150 static int constexpr default_size = 20000;
153 template <
typename order_book,
typename book_config>
158 fixture(fixture_config
const& cfg, book_config
const& bkcfg,
int seed)
159 : fixture(default_size, cfg, bkcfg, seed) {
169 int size, fixture_config
const& cfg, book_config
const& bkcfg,
174 , generator_(initialize_generator(seed)) {
177 void iteration_setup() {
178 std::uniform_int_distribution<> side(0, 1);
179 if (side(generator_)) {
180 iteration_ = create_iteration<typename order_book::buys_t>(
181 generator_, size_, cfg_, bkcfg_);
183 iteration_ = create_iteration<typename order_book::sells_t>(
184 generator_, size_, cfg_, bkcfg_);
205 std::mt19937_64 generator_;
208 std::function<void()> iteration_;
220 template <
typename book_type,
typename book_type_config>
221 void run_benchmark(config
const& cfg, book_type_config
const& book_cfg) {
222 JB_LOG(info) <<
"Running benchmark for " << cfg.microbenchmark().test_case()
223 <<
" with SEED=" << cfg.seed();
226 benchmark bm(cfg.microbenchmark());
227 auto r = bm.
run(cfg.fixture(), book_cfg, cfg.seed());
229 typename benchmark::summary s(r);
232 std::cerr << cfg.microbenchmark().test_case() <<
" summary " << s
234 if (cfg.microbenchmark().verbose()) {
235 bm.write_results(std::cout, r);
240 int main(
int argc,
char* argv[])
try {
242 cfg.process_cmdline(argc, argv);
246 if (cfg.microbenchmark().verbose()) {
247 JB_LOG(info) <<
"Configuration for test\n" << cfg <<
"\n";
248 JB_LOG(info) <<
" default_size=" << default_size;
252 auto test_case = cfg.microbenchmark().test_case();
253 if (test_case ==
"array") {
254 run_benchmark<array_based_order_book>(cfg, cfg.array_book());
255 }
else if (test_case ==
"map") {
256 run_benchmark<map_based_order_book>(cfg, cfg.map_book());
261 std::ostringstream os;
262 os <<
"Unknown test case (" << test_case <<
")" << std::endl;
263 os <<
" --microbenchmark.test-case must be one of" 264 <<
": array, map" << std::endl;
270 std::cerr <<
"usage: " << ex.what() << std::endl;
272 }
catch (std::exception
const& ex) {
273 std::cerr <<
"standard exception raised: " << ex.what() << std::endl;
276 std::cerr <<
"unknown exception raised" << std::endl;
282 #ifndef JB_ITCH5_DEFAULT_bm_order_book_test_case 283 #define JB_ITCH5_DEFAULT_bm_order_book_test_case "array" 284 #endif // JB_ITCH5_DEFAULT_bm_order_book_test_case 286 #ifndef JB_ITCH5_DEFAULT_bm_order_book_p25 287 #define JB_ITCH5_DEFAULT_bm_order_book_p25 0 288 #endif // JB_ITCH5_DEFAULT_bm_order_book_p25 290 #ifndef JB_ITCH5_DEFAULT_bm_order_book_p50 291 #define JB_ITCH5_DEFAULT_bm_order_book_p50 1 292 #endif // JB_ITCH5_DEFAULT_bm_order_book_p50 294 #ifndef JB_ITCH5_DEFAULT_bm_order_book_p75 295 #define JB_ITCH5_DEFAULT_bm_order_book_p75 6 296 #endif // JB_ITCH5_DEFAULT_bm_order_book_p75 298 #ifndef JB_ITCH5_DEFAULT_bm_order_book_p90 299 #define JB_ITCH5_DEFAULT_bm_order_book_p90 14 300 #endif // JB_ITCH5_DEFAULT_bm_order_book_p90 302 #ifndef JB_ITCH5_DEFAULT_bm_order_book_p99 303 #define JB_ITCH5_DEFAULT_bm_order_book_p99 203 304 #endif // JB_ITCH5_DEFAULT_bm_order_book_p99 306 #ifndef JB_ITCH5_DEFAULT_bm_order_book_p999 307 #define JB_ITCH5_DEFAULT_bm_order_book_p999 2135 308 #endif // JB_ITCH5_DEFAULT_bm_order_book_p999 310 #ifndef JB_ITCH5_DEFAULT_bm_order_book_p100 311 #define JB_ITCH5_DEFAULT_bm_order_book_p100 20000000 312 #endif // JB_ITCH5_DEFAULT_bm_order_book_p100 314 #ifndef JB_ITCH5_DEFAULT_bm_order_book_max_p999_delta 315 #define JB_ITCH5_DEFAULT_bm_order_book_max_p999_delta 200 316 #endif // JB_ITCH5_DEFAULT_bm_order_book_max_p999_delta 318 #ifndef JB_ITCH5_DEFAULT_bm_order_book_min_qty 319 #define JB_ITCH5_DEFAULT_bm_order_book_min_qty -5000 320 #endif // JB_ITCH5_DEFAULT_bm_order_book_min_qty 322 #ifndef JB_ITCH5_DEFAULT_bm_order_book_max_qty 323 #define JB_ITCH5_DEFAULT_bm_order_book_max_qty 5000 324 #endif // JB_ITCH5_DEFAULT_bm_order_book_max_qty 341 fixture_config::fixture_config()
342 : p25(desc(
"p25").help(
343 "Define the maximum depth of 25% of the events. " 344 "The benchmark generates random book changes, with the depth of " 345 "these changes controled by this argument (and similar ones), " 346 "the default values are chosen to match the observed behavior " 347 "in real market feeds."),
349 , p50(desc(
"p50").help(
350 "Define the maximum depth of 50% of the events. " 351 "The benchmark generates random book changes, with the depth of " 352 "these changes controled by this argument (and similar ones), " 353 "the default values are chosen to match the observed behavior " 354 "in real market feeds."),
356 , p75(desc(
"p75").help(
357 "Define the maximum depth of 2\75% of the events. " 358 "The benchmark generates random book changes, with the depth of " 359 "these changes controled by this argument (and similar ones), " 360 "the default values are chosen to match the observed behavior " 361 "in real market feeds."),
363 , p90(desc(
"p90").help(
364 "Define the maximum depth of 90% of the events. " 365 "The benchmark generates random book changes, with the depth of " 366 "these changes controled by this argument (and similar ones), " 367 "the default values are chosen to match the observed behavior " 368 "in real market feeds."),
370 , p99(desc(
"p99").help(
371 "Define the maximum depth of 99% of the events. " 372 "The benchmark generates random book changes, with the depth of " 373 "these changes controled by this argument (and similar ones), " 374 "the default values are chosen to match the observed behavior " 375 "in real market feeds."),
379 "Define the maximum depth of 99.9% of the events. " 380 "The benchmark generates random book changes, with the depth of " 381 "these changes controled by this argument (and similar ones), " 382 "the default values are chosen to match the observed behavior " 383 "in real market feeds."),
387 "Define the maximum depth of 100% of the events. " 388 "The benchmark generates random book changes, with the depth of " 389 "these changes controled by this argument (and similar ones), " 390 "the default values are chosen to match the observed behavior " 391 "in real market feeds."),
394 desc(
"max-p999-delta")
396 "The maximum delta for the desired vs. actual event depth " 397 "distribution at the p99.9 level. " 398 "The benchmark generates random book changes, with the " 399 "distribution of the event depths controlled by the " 401 "Any generated set of book changes whose p99.9 differs by " 402 "more than this value from the requested value is rejected, " 403 "and a new set of book changes is generated."),
406 desc(
"min-qty").help(
407 "Generate book changes uniformly distributed between " 408 "--min-qty and --max-qty (both inclusive."),
411 desc(
"max-qty").help(
412 "Generate book changes uniformly distributed between " 413 "--min-qty and --max-qty (both inclusive."),
417 void fixture_config::validate()
const {
418 if (p25() < 0 or p25() > p50()) {
419 std::ostringstream os;
420 os <<
"p25 (" << p25() <<
") must be >= 0 and > p50 (" << p50() <<
")";
424 if (p50() < 0 or p50() > p75()) {
425 std::ostringstream os;
426 os <<
"p50 (" << p50() <<
") must be >= 0 and > p75 (" << p75() <<
")";
430 if (p75() < 0 or p75() > p90()) {
431 std::ostringstream os;
432 os <<
"p75 (" << p75() <<
") must be >= 0 and > p90 (" << p90() <<
")";
436 if (p90() < 0 or p90() > p99()) {
437 std::ostringstream os;
438 os <<
"p90 (" << p90() <<
") must be >= 0 and > p99 (" << p99() <<
")";
442 if (p99() < 0 or p99() > p999()) {
443 std::ostringstream os;
444 os <<
"p99 (" << p99() <<
") must be >= 0 and > p999 (" << p999() <<
")";
448 if (p999() < 0 or p999() > p100()) {
449 std::ostringstream os;
450 os <<
"p999 (" << p999() <<
") must be >= 0 and > p100 (" << p100() <<
")";
455 std::ostringstream os;
456 os <<
"p1000 (" << p100() <<
") must be >= 0";
460 if (max_p999_delta() < 0 or max_p999_delta() > p999() / 2) {
461 std::ostringstream os;
462 os <<
"max-p999-delta (" << max_p999_delta() <<
") must be > 0" 463 <<
" and must be <= p999/2 (" << p999() / 2 <<
")";
467 if (min_qty() >= 0) {
468 std::ostringstream os;
469 os <<
"min-qty (" << min_qty() <<
") must be < 0";
473 if (max_qty() <= 0) {
474 std::ostringstream os;
475 os <<
"max-qty (" << max_qty() <<
") must be > 0";
481 : log(desc(
"log",
"logging"),
this)
483 desc(
"microbenchmark",
"microbenchmark"),
this,
485 , array_book(desc(
"array-book"),
this)
486 , map_book(desc(
"map-book"),
this)
487 , fixture(desc(
"fixture"),
this)
490 "Initial seed for pseudo-random number generator. " 491 "If zero (the default), use the systems random device to set " 496 void config::validate()
const {
498 microbenchmark().validate();
499 array_book().validate();
500 map_book().validate();
501 fixture().validate();
504 std::vector<operation> create_operations_without_validation(
505 std::mt19937_64& generator,
int& actual_p999,
int size,
506 fixture_config
const& cfg,
bool is_ascending) {
509 std::vector<operation> operations;
510 operations.reserve(size);
516 std::vector<int> boundaries({0, cfg.p25(), cfg.p50(), cfg.p75(), cfg.p90(),
517 cfg.p99(), cfg.p999(), cfg.p100()});
518 std::vector<double> weights({0.25, 0.25, 0.25, 0.15, 0.09, 0.009, 0.001});
520 std::piecewise_constant_distribution<> ddis(
521 boundaries.begin(), boundaries.end(), weights.begin());
526 price4_t(0), jb::itch5::max_price_field_value<price4_t>());
533 std::function<price4_t(int)> level2price;
535 level2price = [](
int level) {
536 return jb::itch5::level_to_price<price4_t>(level);
539 level2price = [max_level](
int level) {
540 return jb::itch5::level_to_price<price4_t>(max_level - level);
553 std::map<int, int> book;
555 for (
int i = 0; i !=
size; ++i) {
559 auto const initial_qty =
560 std::uniform_int_distribution<>(1, cfg.max_qty())(generator);
561 auto const initial_level =
562 std::uniform_int_distribution<>(1, max_level - 1)(generator);
563 operations.push_back({level2price(initial_level), initial_qty});
564 book[initial_level] = initial_qty;
565 book_depth_histogram.
sample(0);
566 qty_histogram.
sample(initial_qty);
571 int best_level = book.rbegin()->first;
573 int depth =
static_cast<int>(ddis(generator));
577 if (std::uniform_int_distribution<>(0, 1)(generator)) {
578 level = best_level - depth;
580 level = best_level + depth;
585 }
else if (level >= max_level) {
586 level = max_level - 1;
592 auto f = book.find(level);
593 if (f != book.end()) {
594 min_qty = std::max(cfg.min_qty(), -f->second);
606 qty = std::uniform_int_distribution<>(min_qty, cfg.max_qty())(generator);
611 book_depth_histogram.
sample(depth);
612 qty_histogram.
sample(qty);
613 if (book[level] == 0) {
616 operations.push_back({level2price(level), qty});
619 JB_LOG(trace) <<
"Simulated depth histogram: " 620 << book_depth_histogram.
summary();
629 JB_LOG(trace) <<
"Simulated qty histogram: " << qty_histogram.
summary();
635 std::vector<operation> create_operations(
636 std::mt19937_64& generator,
int size, fixture_config
const& cfg,
639 int const min_p999 = cfg.p999() - cfg.max_p999_delta();
640 int const max_p999 = cfg.p999() + cfg.max_p999_delta();
642 std::vector<operation> operations;
645 if (actual_p999 != 0) {
646 JB_LOG(trace) <<
"retrying for p999 = " << actual_p999
647 <<
", count=" << ++count;
649 operations = create_operations_without_validation(
650 generator, actual_p999, size, cfg, is_ascending);
651 }
while (actual_p999 < min_p999 or max_p999 < actual_p999);
void sample(sample_type const &t)
Record a new sample.
Define defaults for program parameters.
Contains classes and functions to parse NASDAQ ITCH-5.0 messages, more information about ITCH-5...
#define JB_ITCH5_DEFAULT_bm_order_book_max_p999_delta
#define JB_ITCH5_DEFAULT_bm_order_book_p25
Base class for all configuration objects.
virtual void validate() const
Validate the settings.
int main(int argc, char *argv[])
A histogram class with controllable binning and range strategy.
A simple class to capture summary information about a histogram.
#define JB_ITCH5_DEFAULT_bm_order_book_p999
#define JB_ITCH5_DEFAULT_bm_order_book_p50
#define JB_ITCH5_DEFAULT_bm_order_book_test_case
results run(Args &&... args)
Run the microbenchmaark.
#define JB_ITCH5_DEFAULT_bm_order_book_min_qty
sample_type estimated_quantile(double q) const
Estimate a quantile of the sample distribution.
A histogram binning_strategy for integer numbers in a known range.
void init(config const &cfg)
Initialize the logging functions using the configuration provided.
#define JB_ITCH5_DEFAULT_bm_order_book_max_qty
#define config_object_constructors(NAME)
Helper class to easily define configuration attributes.
Configure a micro-benchmark.
Configure an array_based_order_book config object.
#define JB_ASSERT_THROW(PRED)
char const default_initialization_marker[]
histogram_summary summary() const
Return a simple summary.
A simple class to communicate the result of parsing the options.
#define JB_ITCH5_DEFAULT_bm_order_book_p100
#define JB_ITCH5_DEFAULT_bm_order_book_p90
Configure an map_based_order_book config object.
std::size_t price_levels(price_field_t lo, price_field_t hi)
Compute the number of price levels between two prices.
price_field< std::uint32_t, 10000 > price4_t
Convenience definition for Price(4) fields.
Run a micro-benchmark on a given class.
#define JB_ITCH5_DEFAULT_bm_order_book_p99
#define JB_ITCH5_DEFAULT_bm_order_book_p75