diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cbe745..e51ea82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,15 +14,19 @@ SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) SET(SOURCES src/lib.cpp) -ADD_EXECUTABLE(test_async tests/test_lib.cpp ${SOURCES}) +ADD_EXECUTABLE(test_async tests/test_async.cpp ${SOURCES}) TARGET_LINK_LIBRARIES(test_async) ADD_EXECUTABLE(test_lib tests/test_lib.cpp ${SOURCES}) TARGET_LINK_LIBRARIES(test_lib) +ADD_EXECUTABLE(test_parser tests/test_parser.cpp ${SOURCES}) +TARGET_LINK_LIBRARIES(test_parser) + ADD_EXECUTABLE(run_app src/main.cpp ${SOURCES}) TARGET_LINK_LIBRARIES(run_app) ENABLE_TESTING() ADD_TEST(AsyncTests bin/test_async) -ADD_TEST(LibTests bin/test_lib) \ No newline at end of file +ADD_TEST(LibTests bin/test_lib) +ADD_TEST(ParserTests bin/test_parser) \ No newline at end of file diff --git a/include/exchange/concepts.hpp b/include/exchange/concepts.hpp new file mode 100644 index 0000000..443fa02 --- /dev/null +++ b/include/exchange/concepts.hpp @@ -0,0 +1,51 @@ +#ifndef INCLUDED_EXCHANGE_CONCEPTS_HPP +#define INCLUDED_EXCHANGE_CONCEPTS_HPP + +#include "util/concepts.hpp" + + +namespace sadhbhcraft::exchange { + + template + concept MsgParserConcept = + requires(T &x) { + util::NumberConcept; + util::NumberConcept; + util::NumberConcept; + typename T::StringType; + { + x.parse_key( + std::declval()) + } -> std::convertible_to; + { + x.parse_value( + std::declval()) + } -> std::convertible_to; + { + x.parse_value( + std::declval()) + } -> std::convertible_to; + { + x.parse_value( + std::declval()) + } -> std::convertible_to; + { + x.parse_field( + std::declval(), + std::declval()) + } -> std::convertible_to; + { + x.parse_field( + std::declval(), + std::declval()) + } -> std::convertible_to; + { + x.parse_field( + std::declval(), + std::declval()) + } -> std::convertible_to; + }; + + +} // end of namespace sadhbhcraft::exchange +#endif//INCLUDED_EXCHANGE_CONCEPTS_HPP \ No newline at end of file diff --git a/include/exchange/istream_msg_parser.hpp b/include/exchange/istream_msg_parser.hpp new file mode 100644 index 0000000..3c8ca23 --- /dev/null +++ b/include/exchange/istream_msg_parser.hpp @@ -0,0 +1,93 @@ +#ifndef INCLUDED_SIMPLE_PARSER_HPP +#define INCLUDED_SIMPLE_PARSER_HPP + +#include "util/concepts.hpp" + +#include + + +namespace sadhbhcraft::exchange { + + template + class IStreamMsgParser + { + public: + using KeyType = _KeyType; + using StringType = std::string; + using IntegerType = int; + using DecimalType = double; + + IStreamMsgParser(std::istream &is): m_is(is) {} + + bool parse_key(KeyType &out_key) + { + if (m_is.eof()) + { + return false; + } + + m_is >> out_key; + + if (auto c = m_is.get(); c != '=') + { + return false; + } + + return true; + } + + bool parse_value(util::NumberConcept auto &out_value) + { + if (m_is.eof()) + { + return false; + } + + m_is >> out_value; + + if (auto c = m_is.get(); c != '\1' && !m_is.eof()) + { + return false; + } + + return true; + } + + bool parse_value(std::string &out_value) + { + if (m_is.eof()) + { + return false; + } + + for (;;) + { + auto c = m_is.get(); + + if (c == '\1' || m_is.eof()) + { + break; + } + out_value += c; + } + + return true; + } + + bool parse_field(KeyType in_key, auto &out_value) + { + if (KeyType key; parse_key(key), key != in_key) + { + return false; + } + + return parse_value(out_value); + } + + private: + std::istream &m_is; + }; + + +} // end of namespace sadhbhcraft::exchange +#endif//INCLUDED_SIMPLE_PARSER_HPP \ No newline at end of file diff --git a/include/exchange/msg_header.hpp b/include/exchange/msg_header.hpp new file mode 100644 index 0000000..7ffe5ca --- /dev/null +++ b/include/exchange/msg_header.hpp @@ -0,0 +1,43 @@ +#ifndef INCLUDED_MSG_HEADER_HPP +#define INCLUDED_MSG_HEADER_HPP + +#include "exchange/concepts.hpp" + + +namespace sadhbhcraft::exchange { + + template + struct MsgHeader + { + using KeyType = typename MsgParserType::KeyType; + using StringType = typename MsgParserType::StringType; + using IntegerType = typename MsgParserType::IntegerType; + + static constexpr KeyType FixVersion = 8; + static constexpr KeyType BodyLength = 9; + static constexpr KeyType MsgType = 35; + static constexpr KeyType SenderCompId = 49; + static constexpr KeyType TargetCompId = 56; + static constexpr KeyType ApplVerID = 1128; + static constexpr KeyType Checksum = 10; + + StringType fix_version; + StringType sender_comp_id; + StringType target_comp_id; + IntegerType body_length; + IntegerType msg_type; + + template + bool parse_message(P &&parser) requires std::is_same_v, MsgParserType> + { + // These fields must be in this exact order + return parser.parse_field(FixVersion, fix_version) && + parser.parse_field(BodyLength, body_length) && + parser.parse_field(MsgType, msg_type) && + parser.parse_field(SenderCompId, sender_comp_id) && + parser.parse_field(TargetCompId, target_comp_id); + } + }; + +} // end of namespace sadhbhcraft::exchange +#endif//INCLUDED_MSG_HEADER_HPP \ No newline at end of file diff --git a/include/exchange/msg_new_order_single.hpp b/include/exchange/msg_new_order_single.hpp new file mode 100644 index 0000000..72e1178 --- /dev/null +++ b/include/exchange/msg_new_order_single.hpp @@ -0,0 +1,98 @@ +#ifndef INCLUDED_MSG_NEW_ORDER_SINGLE_HPP +#define INCLUDED_MSG_NEW_ORDER_SINGLE_HPP + +#include "exchange/concepts.hpp" + + +namespace sadhbhcraft::exchange { + + template + struct NewOrderSingle + { + using KeyType = typename MsgParserType::KeyType; + using StringType = typename MsgParserType::StringType; + using IntegerType = typename MsgParserType::IntegerType; + using DecimalType = typename MsgParserType::DecimalType; + + static constexpr KeyType ClOrdID = 11; + static constexpr KeyType OrdType = 40; // 1 MKT, 2 LMT + static constexpr KeyType Price = 44; + static constexpr KeyType OrderQty = 38; + static constexpr KeyType Side = 54; // 1 BUY, 2 SELL + static constexpr KeyType TransactTime = 60; + static constexpr KeyType Symbol = 55; + static constexpr KeyType TimeInForce = 59; //1 GTC, 3 IOC + + unsigned + cl_ord_id_bit : 1 = {0}, + transact_time_bit : 1 = {0}, + symbol_bit : 1 = {0}, + ord_type_bit : 1 = {0}, + side_bit : 1 = {0}, + time_in_force_bit : 1 = {0}, + price_bit : 1 = {0}, + order_qty_bit : 1 = {0}; + + StringType cl_ord_id; + StringType transact_time; + StringType symbol; + IntegerType ord_type; + IntegerType side; + IntegerType time_in_force; + DecimalType price; + DecimalType order_qty; + + + template + bool parse_message(P &&parser) requires std::is_same_v, MsgParserType> + { + for (;;) + { + KeyType key; + if (!parser.parse_key(key)) + { + break; + } + + switch (key) + { + case ClOrdID: + cl_ord_id_bit = parser.parse_value(cl_ord_id); + break; + case OrdType: + ord_type_bit = parser.parse_value(ord_type); + break; + case Price: + price_bit = parser.parse_value(price); + break; + case OrderQty: + order_qty_bit = parser.parse_value(order_qty); + break; + case Side: + side_bit = parser.parse_value(side); + break; + case TransactTime: + transact_time_bit = parser.parse_value(transact_time); + break; + case Symbol: + symbol_bit = parser.parse_value(symbol); + break; + case TimeInForce: + time_in_force_bit = parser.parse_value(time_in_force); + break; + } + } + + return cl_ord_id_bit && + transact_time_bit && + symbol_bit && + ord_type_bit && + side_bit && + time_in_force_bit && + price_bit && + order_qty_bit; + } + }; + +} // end of namespace sadhbhcraft::exchange +#endif//INCLUDED_MSG_NEW_ORDER_SINGLE_HPP \ No newline at end of file diff --git a/include/lib.hpp b/include/lib.hpp index 687d3a4..435d635 100644 --- a/include/lib.hpp +++ b/include/lib.hpp @@ -5,6 +5,5 @@ namespace sadhbhcraft::orderbook { - } // end of namespace sadhbhcraft::orderbook #endif // INCLUDED_LIB_HPP \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6f157a2..ddae438 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "lib.hpp" #include +#include namespace scob = sadhbhcraft::orderbook; diff --git a/tests/test_async.cpp b/tests/test_async.cpp index 1113abe..0c91847 100644 --- a/tests/test_async.cpp +++ b/tests/test_async.cpp @@ -3,6 +3,7 @@ #include "util/async.hpp" #include "util/generator.hpp" +#include #include #include @@ -20,6 +21,7 @@ struct OrderQuantity int quantity; }; + class OrderSizeLimit { public: @@ -62,7 +64,7 @@ struct Level .order = iter->order, .quantity = quantity }; - co_await ep(oq); + co_await ep(std::ref(oq)); iter->quantity -= oq.quantity; quantity -= oq.quantity; co_yield oq; @@ -74,7 +76,7 @@ struct Level .order = iter->order, .quantity = iter->quantity }; - co_await ep(oq); + co_await ep(std::ref(oq)); quantity -= oq.quantity; iter->quantity = 0; // remainder is cancelled co_yield oq; diff --git a/tests/test_parser.cpp b/tests/test_parser.cpp new file mode 100644 index 0000000..1212b0f --- /dev/null +++ b/tests/test_parser.cpp @@ -0,0 +1,132 @@ +#include "test_util.hpp" + +#include "exchange/istream_msg_parser.hpp" + +#include "exchange/msg_header.hpp" +#include "exchange/msg_new_order_single.hpp" + +#include +#include +#include +#include + + +void test_parse_msg_header() +{ + namespace sce = sadhbhcraft::exchange; + + std::stringstream ss{"8=FIX.4.2" "\1" "9=178" "\1" "35=8" "\1" "49=PHLX" "\1" "56=PERS" }; + + sce::IStreamMsgParser parser{ss}; + + sce::MsgHeader> header; + + assert(header.parse_message(parser)); + + std::cout + << "FixVersion: " << header.fix_version << ", " + << "Sender ID: " << header.sender_comp_id << ", " + << "Target ID: " << header.target_comp_id << ", " + << "BodyLen: " << header.body_length << ", " + << "MsgType: " << header.msg_type + << std::endl; + + assert(header.fix_version == "FIX.4.2"); + assert(header.sender_comp_id == "PHLX"); + assert(header.target_comp_id == "PERS"); + assert(header.body_length == 178); + assert(header.msg_type == 8); +} + +void test_parse_new_order_single() +{ + namespace sce = sadhbhcraft::exchange; + + std::stringstream ss{ + "11=000-001-001" "\1" + "40=2" "\1" + "44=75.250" "\1" + "38=50.500" "\1" + "54=2" "\1" + "60=20230412-18:10:00.000" "\1" + "55=BTCETH" "\1" + "59=1" + }; + + sce::IStreamMsgParser parser{ss}; + + sce::NewOrderSingle> new_order; + + assert(new_order.parse_message(parser)); + + std::cout << "ClOrdID: " << new_order.cl_ord_id + << ", TransactTime: " << new_order.transact_time + << ", Symbol: " << new_order.symbol + << ", OrdType: " << new_order.ord_type + << ", Side: " << new_order.side + << ", TimeInForce: " << new_order.time_in_force + << ", Price: " << new_order.price + << ", OrderQty: " << new_order.order_qty + << std::endl; + + assert(new_order.cl_ord_id == "000-001-001"); + assert(new_order.transact_time == "20230412-18:10:00.000"); + assert(new_order.symbol == "BTCETH"); + assert(new_order.ord_type == 2); + assert(new_order.side == 2); + assert(new_order.time_in_force == 1); + assert(std::fabs(new_order.price - 75.25) < std::numeric_limits::epsilon()); + assert(std::fabs(new_order.order_qty - 50.5) < std::numeric_limits::epsilon()); +} + +void test_parse_new_order_single_full_message() +{ + namespace sce = sadhbhcraft::exchange; + + std::stringstream ss{ + "8=FIX.4.2" "\1" + "9=178" "\1" + "35=13" "\1" + "49=PHLX" "\1" + "56=PERS" + "11=000-001-001" "\1" + "40=2" "\1" + "44=75.250" "\1" + "38=50.500" "\1" + "54=2" "\1" + "60=20230412-18:10:00.000" "\1" + "55=BTCETH" "\1" + "59=1" "\1" + "10=128" + }; + + sce::IStreamMsgParser parser{ss}; + + sce::MsgHeader> header; + sce::NewOrderSingle> new_order; + + assert(header.parse_message(parser)); + assert(new_order.parse_message(parser)); + + assert(header.fix_version == "FIX.4.2"); + assert(header.sender_comp_id == "PHLX"); + assert(header.target_comp_id == "PERS"); + assert(header.body_length == 178); + assert(header.msg_type == 13); + + assert(new_order.cl_ord_id == "000-001-001"); + assert(new_order.transact_time == "20230412-18:10:00.000"); + assert(new_order.symbol == "BTCETH"); + assert(new_order.ord_type == 2); + assert(new_order.side == 2); + assert(new_order.time_in_force == 1); + assert(std::fabs(new_order.price - 75.25) < std::numeric_limits::epsilon()); + assert(std::fabs(new_order.order_qty - 50.5) < std::numeric_limits::epsilon()); +} + +int main(int argc, char **argv) +{ + test_parse_msg_header(); + test_parse_new_order_single(); + return 0; +} \ No newline at end of file