From 8629f994629d47a305f2fbc1b5ec8bd5b18c3f99 Mon Sep 17 00:00:00 2001 From: papadpickle <99397192+papadpickle@users.noreply.github.com> Date: Tue, 22 Feb 2022 23:00:51 +0100 Subject: [PATCH 01/20] squashed: initial example adapted from max getting lowest ask using wss to subscribe to bids and asks moved the subscription code to own class for reuse added orderbook class which takes book sides and logs the info on updates from either of the book side added a reusable class for wss boilerplate and a subscription class for trade event queue refactored the example so the subscription classes can be reused by other clients atomic snapshot for orderbook struct to avoid potential data race using bsp instead of bps. atomic variables for bookside and added empty calls for book depth getting all leaf nodes and sorting them as per prices const correctness at solana api added market depth logic further refactoring midpoint double replaced atomic with mtx due to issues at macos, can be investigated later simplified the fill reception callback --- examples/CMakeLists.txt | 4 +- examples/orderbookSubscribe.cpp | 84 ++++++++++++++++ include/mango_v3.hpp | 92 ++++++++++++++++++ include/orderbook/levelOne.hpp | 21 ++++ include/orderbook/order.hpp | 22 +++++ include/orderbook/orderbook.hpp | 74 +++++++++++++++ include/solana.hpp | 8 +- include/subscriptions/bookSide.hpp | 121 ++++++++++++++++++++++++ include/subscriptions/trades.hpp | 88 +++++++++++++++++ include/subscriptions/wssSubscriber.hpp | 100 ++++++++++++++++++++ lib/solana.cpp | 4 +- 11 files changed, 612 insertions(+), 6 deletions(-) create mode 100644 examples/orderbookSubscribe.cpp create mode 100644 include/orderbook/levelOne.hpp create mode 100644 include/orderbook/order.hpp create mode 100644 include/orderbook/orderbook.hpp create mode 100644 include/subscriptions/bookSide.hpp create mode 100644 include/subscriptions/trades.hpp create mode 100644 include/subscriptions/wssSubscriber.hpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 3042f47..89d0bfc 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -5,9 +5,11 @@ add_executable(example-get-account-info getAccountInfo.cpp) add_executable(example-account-subscribe accountSubscribe.cpp) add_executable(example-send-transaction sendTransaction.cpp) add_executable(example-place-order placeOrder.cpp) +add_executable(example-orderbook-subscribe orderbookSubscribe.cpp) # link target_link_libraries(example-get-account-info ${CONAN_LIBS} sol) target_link_libraries(example-account-subscribe ${CONAN_LIBS} sol) target_link_libraries(example-send-transaction ${CONAN_LIBS} sol) -target_link_libraries(example-place-order ${CONAN_LIBS} sol) \ No newline at end of file +target_link_libraries(example-place-order ${CONAN_LIBS} sol) +target_link_libraries(example-orderbook-subscribe ${CONAN_LIBS} sol) \ No newline at end of file diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp new file mode 100644 index 0000000..3a928f9 --- /dev/null +++ b/examples/orderbookSubscribe.cpp @@ -0,0 +1,84 @@ +#include +#include + +#include +#include +#include +#include + +#include "mango_v3.hpp" +#include "orderbook/levelOne.hpp" +#include "orderbook/orderbook.hpp" +#include "solana.hpp" +#include "subscriptions/bookSide.hpp" +#include "subscriptions/trades.hpp" + +class updateLogger { + public: + updateLogger(mango_v3::orderbook::book& orderbook, + mango_v3::subscription::trades& trades) + : orderbook(orderbook), trades(trades) { + orderbook.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); + trades.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); + } + + void start() { + orderbook.subscribe(); + trades.subscribe(); + } + + void logUpdate() { + const std::scoped_lock lock(updateMtx); + auto level1Snapshot = orderbook.getLevel1(); + if (level1Snapshot.valid()) { + spdlog::info("============Update============"); + spdlog::info("Latest trade: {}", trades.getLastTrade() + ? to_string(trades.getLastTrade()) + : "not received yet"); + spdlog::info("Bid-Ask {}-{}", level1Snapshot.highestBid, + level1Snapshot.lowestAsk); + spdlog::info("MidPrice: {}", level1Snapshot.midPoint); + spdlog::info("Spread: {0:.2f} bps", level1Snapshot.spreadBps); + + constexpr auto depth = 2; + spdlog::info("Market depth -{}%: {}", depth, orderbook.getDepth(-depth)); + spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); + } + } + + private: + std::mutex updateMtx; + mango_v3::orderbook::book& orderbook; + mango_v3::subscription::trades& trades; +}; + +int main() { + const auto& config = mango_v3::MAINNET; + const solana::rpc::Connection solCon; + const auto group = solCon.getAccountInfo(config.group); + + const auto symbolIt = + std::find(config.symbols.begin(), config.symbols.end(), "SOL"); + const auto marketIndex = symbolIt - config.symbols.begin(); + assert(config.symbols[marketIndex] == "SOL"); + + const auto perpMarketPk = group.perpMarkets[marketIndex].perpMarket; + auto market = + solCon.getAccountInfo(perpMarketPk.toBase58()); + assert(market.mangoGroup.toBase58() == config.group); + + mango_v3::subscription::bookSide bids(mango_v3::Buy, market.bids.toBase58()); + mango_v3::subscription::bookSide asks(mango_v3::Sell, market.asks.toBase58()); + mango_v3::subscription::trades trades(market.eventQueue.toBase58()); + + mango_v3::orderbook::book orderbook(bids, asks); + + updateLogger logger(orderbook, trades); + logger.start(); + + while (true) { + } + return 0; +} + +// void updateReceived() {} diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 4f65c2a..43b32e3 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "fixedp.h" @@ -15,6 +16,8 @@ const int MAX_PAIRS = 15; const int QUOTE_INDEX = 15; const int EVENT_SIZE = 200; const int EVENT_QUEUE_SIZE = 256; +const int BOOK_NODE_SIZE = 88; +const int BOOK_SIZE = 1024; struct Config { std::string endpoint; @@ -135,8 +138,10 @@ struct EventQueueHeader { uint64_t seqNum; }; +// todo: change to scoped enum class enum EventType : uint8_t { Fill, Out, Liquidate }; +// todo: change to scoped enum class enum Side : uint8_t { Buy, Sell }; struct AnyEvent { @@ -197,6 +202,93 @@ struct EventQueue { AnyEvent items[EVENT_QUEUE_SIZE]; }; +// todo: change to scoped enum class +enum NodeType : uint32_t { + Uninitialized = 0, + InnerNode, + LeafNode, + FreeNode, + LastFreeNode +}; + +struct AnyNode { + NodeType tag; + uint8_t padding[BOOK_NODE_SIZE - 4]; +}; + +struct InnerNode { + NodeType tag; + uint32_t prefixLen; + __uint128_t key; + uint32_t children[2]; + uint8_t padding[BOOK_NODE_SIZE - 32]; +}; + +struct LeafNode { + NodeType tag; + uint8_t ownerSlot; + uint8_t orderType; + uint8_t version; + uint8_t timeInForce; + __uint128_t key; + solana::PublicKey owner; + uint64_t quantity; + uint64_t clientOrderId; + uint64_t bestInitial; + uint64_t timestamp; +}; + +struct FreeNode { + NodeType tag; + uint32_t next; + uint8_t padding[BOOK_NODE_SIZE - 8]; +}; + +struct BookSide { + MetaData metaData; + uint64_t bumpIndex; + uint64_t freeListLen; + uint32_t freeListHead; + uint32_t rootNode; + uint64_t leafCount; + AnyNode nodes[BOOK_SIZE]; + + struct iterator { + Side side; + const BookSide &bookSide; + std::stack stack; + uint32_t left, right; + + iterator(Side side, const BookSide &bookSide) + : side(side), bookSide(bookSide) { + stack.push(bookSide.rootNode); + left = side == Side::Buy ? 1 : 0; + right = side == Side::Buy ? 0 : 1; + } + + bool operator==(const iterator &other) const { + return &bookSide == &other.bookSide && stack.top() == other.stack.top(); + } + + iterator &operator++() { + if (stack.size() > 0) { + const auto &elem = **this; + stack.pop(); + + if (elem.tag == NodeType::InnerNode) { + const auto innerNode = + reinterpret_cast(&elem); + stack.push(innerNode->children[right]); + stack.push(innerNode->children[left]); + } + } + return *this; + } + + const AnyNode &operator*() const { return bookSide.nodes[stack.top()]; } + }; +}; + #pragma pack(pop) // instructions are even tighter packed, every byte counts diff --git a/include/orderbook/levelOne.hpp b/include/orderbook/levelOne.hpp new file mode 100644 index 0000000..92f0954 --- /dev/null +++ b/include/orderbook/levelOne.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace mango_v3 { +namespace orderbook { + +struct levelOne { + uint64_t highestBid; + uint64_t highestBidSize; + uint64_t lowestAsk; + uint64_t lowestAskSize; + double midPoint; + double spreadBps; + + bool valid() const { + return ((highestBid && lowestAsk) && (lowestAsk > highestBid)) ? true + : false; + } +}; + +} // namespace orderbook +} // namespace mango_v3 diff --git a/include/orderbook/order.hpp b/include/orderbook/order.hpp new file mode 100644 index 0000000..bb1efde --- /dev/null +++ b/include/orderbook/order.hpp @@ -0,0 +1,22 @@ +#pragma once + +namespace mango_v3 { +namespace orderbook { + +struct order { + order(uint64_t price, uint64_t quantity) : price(price), quantity(quantity) {} + + bool operator<(const order& compare) const { + return (price < compare.price) ? true : false; + } + + bool operator>(const order& compare) const { + return (price > compare.price) ? true : false; + } + + uint64_t price; + uint64_t quantity; +}; + +} // namespace orderbook +} // namespace mango_v3 diff --git a/include/orderbook/orderbook.hpp b/include/orderbook/orderbook.hpp new file mode 100644 index 0000000..c1bf633 --- /dev/null +++ b/include/orderbook/orderbook.hpp @@ -0,0 +1,74 @@ +#pragma once +#include + +#include +#include + +#include "levelOne.hpp" +#include "orderbook/order.hpp" +#include "subscriptions/bookSide.hpp" + +namespace mango_v3 { +namespace orderbook { +class book { + public: + book(subscription::bookSide& bids, subscription::bookSide& asks) + : bids(bids), asks(asks) { + bids.registerUpdateCallback(std::bind(&book::updateCallback, this)); + asks.registerUpdateCallback(std::bind(&book::updateCallback, this)); + } + + void registerUpdateCallback(std::function callback) { + onUpdateCb = callback; + } + + void subscribe() { + bids.subscribe(); + asks.subscribe(); + } + + void updateCallback() { + const std::scoped_lock lock(callbackMtx); + levelOne newL1; + auto bestBid = bids.getBestOrder(); + auto bestAsk = asks.getBestOrder(); + newL1.highestBid = bestBid.price; + newL1.highestBidSize = bestBid.quantity; + newL1.lowestAsk = bestAsk.price; + newL1.lowestAskSize = bestAsk.quantity; + + if (newL1.valid()) { + newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; + newL1.spreadBps = + ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; + { + const std::scoped_lock lock(levelOneMtx); + level1 = newL1; + } + onUpdateCb(); + } + } + + levelOne getLevel1() const { + const std::scoped_lock lock(levelOneMtx); + return level1; + } + + uint64_t getDepth(int8_t percent) { + const std::scoped_lock lock(levelOneMtx); + auto price = (level1.midPoint * (100 + percent)) / 100; + return (percent > 0) ? asks.getVolume>(price) + : bids.getVolume>(price); + } + + private: + levelOne level1; + // todo:macos latomic not found issue, otherwise replace mtx with std::atomic + mutable std::mutex levelOneMtx; + std::function onUpdateCb; + std::mutex callbackMtx; + subscription::bookSide& bids; + subscription::bookSide& asks; +}; +} // namespace orderbook +} // namespace mango_v3 diff --git a/include/solana.hpp b/include/solana.hpp index eafeb0c..033ad0b 100644 --- a/include/solana.hpp +++ b/include/solana.hpp @@ -250,7 +250,8 @@ class Connection { /// json getAccountInfoRequest(const std::string &account, const std::string &encoding = "base64", - const size_t offset = 0, const size_t length = 0); + const size_t offset = 0, + const size_t length = 0) const; json getRecentBlockhashRequest(const std::string &commitment = "finalized"); json sendTransactionRequest( const std::string &transaction, const std::string &encoding = "base58", @@ -269,7 +270,8 @@ class Connection { template inline T getAccountInfo(const std::string &account, const std::string &encoding = "base64", - const size_t offset = 0, const size_t length = 0) { + const size_t offset = 0, + const size_t length = 0) const { const json req = getAccountInfoRequest(account, encoding, offset, length); cpr::Response r = cpr::Post(cpr::Url{rpc_url_}, cpr::Body{req.dump()}, @@ -309,4 +311,4 @@ inline json accountSubscribeRequest(const std::string &account, } } // namespace subscription } // namespace rpc -} // namespace solana \ No newline at end of file +} // namespace solana diff --git a/include/subscriptions/bookSide.hpp b/include/subscriptions/bookSide.hpp new file mode 100644 index 0000000..0c47bae --- /dev/null +++ b/include/subscriptions/bookSide.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "mango_v3.hpp" +#include "orderbook/order.hpp" +#include "solana.hpp" +#include "wssSubscriber.hpp" + +namespace mango_v3 { +namespace subscription { + +using json = nlohmann::json; + +class bookSide { + public: + bookSide(Side side, const std::string& account) + : side(side), wssConnection(account) {} + + void registerUpdateCallback(std::function callback) { + notifyCb = callback; + } + + void subscribe() { + wssConnection.registerOnMessageCallback( + std::bind(&bookSide::onMessage, this, std::placeholders::_1)); + wssConnection.start(); + } + + orderbook::order getBestOrder() const { + std::scoped_lock lock(ordersMtx); + return (!orders.empty()) ? orders.front() : orderbook::order{0, 0}; + } + + template + uint64_t getVolume(uint64_t price) const { + Op operation; + uint64_t volume = 0; + std::scoped_lock lock(ordersMtx); + for (auto&& order : orders) { + if (operation(order.price, price)) { + volume += order.quantity; + } else { + break; + } + } + return volume; + } + + private: + void onMessage(const json& parsedMsg) { + // ignore subscription confirmation + const auto itResult = parsedMsg.find("result"); + if (itResult != parsedMsg.end()) { + spdlog::info("on_result {}", parsedMsg.dump()); + return; + } + + const std::string encoded = + parsedMsg["params"]["result"]["value"]["data"][0]; + + const std::string decoded = solana::b64decode(encoded); + if (decoded.size() != sizeof(BookSide)) + throw std::runtime_error("invalid response length " + + std::to_string(decoded.size()) + " expected " + + std::to_string(sizeof(BookSide))); + + BookSide bookSide; + memcpy(&bookSide, decoded.data(), sizeof(decltype(bookSide))); + + auto iter = BookSide::iterator(side, bookSide); + + decltype(orders) newOrders; + + while (iter.stack.size() > 0) { + if ((*iter).tag == NodeType::LeafNode) { + const auto leafNode = + reinterpret_cast(&(*iter)); + const auto now = std::chrono::system_clock::now(); + const auto nowUnix = + chrono::duration_cast(now.time_since_epoch()) + .count(); + const auto isValid = + !leafNode->timeInForce || + leafNode->timestamp + leafNode->timeInForce < nowUnix; + if (isValid) { + newOrders.emplace_back((uint64_t)(leafNode->key >> 64), + leafNode->quantity); + } + } + ++iter; + } + + if (!newOrders.empty()) { + { + std::scoped_lock lock(ordersMtx); + orders = std::move(newOrders); + if (side == Side::Buy) { + std::sort(orders.begin(), orders.end(), std::greater{}); + } else { + std::sort(orders.begin(), orders.end()); + } + } + notifyCb(); + } + } + + wssSubscriber wssConnection; + const Side side; + mutable std::mutex ordersMtx; + std::vector orders; + std::function notifyCb; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/include/subscriptions/trades.hpp b/include/subscriptions/trades.hpp new file mode 100644 index 0000000..eb7a591 --- /dev/null +++ b/include/subscriptions/trades.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include + +#include +#include + +#include "mango_v3.hpp" +#include "solana.hpp" +#include "wssSubscriber.hpp" + +namespace mango_v3 { +namespace subscription { + +using json = nlohmann::json; + +class trades { + public: + trades(const std::string &account) : wssConnection(account) {} + + void registerUpdateCallback(std::function callback) { + notifyCb = callback; + } + + void subscribe() { + wssConnection.registerOnMessageCallback( + std::bind(&trades::onMessage, this, std::placeholders::_1)); + wssConnection.start(); + } + + int64_t getLastTrade() const { + const std::scoped_lock lock(latestTradeMtx); + return latestTrade; + } + + private: + void onMessage(const json &parsedMsg) { + // ignore subscription confirmation + const auto itResult = parsedMsg.find("result"); + if (itResult != parsedMsg.end()) { + spdlog::info("on_result {}", parsedMsg.dump()); + return; + } + + // all other messages are event queue updates + const std::string method = parsedMsg["method"]; + const int subscription = parsedMsg["params"]["subscription"]; + const int slot = parsedMsg["params"]["result"]["context"]["slot"]; + const std::string data = parsedMsg["params"]["result"]["value"]["data"][0]; + + const auto decoded = solana::b64decode(data); + const auto events = reinterpret_cast(decoded.data()); + const auto seqNumDiff = events->header.seqNum - lastSeqNum; + const auto lastSlot = + (events->header.head + events->header.count) % EVENT_QUEUE_SIZE; + + bool gotLatest = false; + if (events->header.seqNum > lastSeqNum) { + for (int offset = seqNumDiff; offset > 0; --offset) { + const auto slot = + (lastSlot - offset + EVENT_QUEUE_SIZE) % EVENT_QUEUE_SIZE; + const auto &event = events->items[slot]; + + if (event.eventType == EventType::Fill) { + const auto &fill = (FillEvent &)event; + const std::scoped_lock lock(latestTradeMtx); + latestTrade = fill.price; + gotLatest = true; + } + // no break; let's iterate to the last fill to get the latest fill order + } + } + + if (gotLatest) { + notifyCb(); + } + lastSeqNum = events->header.seqNum; + } + + uint64_t lastSeqNum = INT_MAX; + // todo:macos latomic not found issue, otherwise replace mtx with std::atomic + mutable std::mutex latestTradeMtx; + uint64_t latestTrade = 0; + wssSubscriber wssConnection; + std::function notifyCb; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/include/subscriptions/wssSubscriber.hpp b/include/subscriptions/wssSubscriber.hpp new file mode 100644 index 0000000..a68b4b4 --- /dev/null +++ b/include/subscriptions/wssSubscriber.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include + +#include +#include +#include +#include + +typedef websocketpp::client ws_client; +typedef websocketpp::config::asio_client::message_type::ptr ws_message_ptr; +typedef std::shared_ptr context_ptr; + +namespace mango_v3 { +namespace subscription { + +using json = nlohmann::json; + +class wssSubscriber { + public: + wssSubscriber(const std::string& account) : account(account) {} + + ~wssSubscriber() { runThread.join(); } + + void registerOnMessageCallback( + std::function callback) { + onMessageCb = callback; + } + + void start() { + runThread = std::thread(&wssSubscriber::subscriptionThread, this); + } + + private: + void subscriptionThread() { + try { + c.set_access_channels(websocketpp::log::alevel::all); + c.init_asio(); + c.set_tls_init_handler( + websocketpp::lib::bind(&wssSubscriber::on_tls_init, this)); + + c.set_open_handler(websocketpp::lib::bind( + &wssSubscriber::on_open, this, websocketpp::lib::placeholders::_1)); + c.set_message_handler(websocketpp::lib::bind( + &wssSubscriber::on_message, this, websocketpp::lib::placeholders::_1, + websocketpp::lib::placeholders::_2)); + + websocketpp::lib::error_code ec; + ws_client::connection_ptr con = c.get_connection( + "wss://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88", ec); + if (ec) { + spdlog::error("could not create connection because: {}", ec.message()); + } + c.connect(con); + c.run(); + } catch (websocketpp::exception const& e) { + std::cout << e.what() << std::endl; + } + } + + context_ptr on_tls_init() { + context_ptr ctx = std::make_shared( + boost::asio::ssl::context::sslv23); + + try { + ctx->set_options(boost::asio::ssl::context::default_workarounds | + boost::asio::ssl::context::no_sslv2 | + boost::asio::ssl::context::no_sslv3 | + boost::asio::ssl::context::single_dh_use); + } catch (std::exception& e) { + spdlog::error("Error in context pointer: {}", e.what()); + } + return ctx; + } + + void on_open(websocketpp::connection_hdl hdl) { + websocketpp::lib::error_code ec; + + c.send(hdl, + solana::rpc::subscription::accountSubscribeRequest(account).dump(), + websocketpp::frame::opcode::value::text, ec); + if (ec) { + spdlog::error("subscribe failed because: {}", ec.message()); + } else { + spdlog::info("subscribed to account {}", account); + } + } + + void on_message(websocketpp::connection_hdl hdl, ws_message_ptr msg) { + const json parsedMsg = json::parse(msg->get_payload()); + onMessageCb(parsedMsg); + } + + ws_client c; + const std::string account; + std::thread runThread; + std::function onMessageCb; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/lib/solana.cpp b/lib/solana.cpp index 8d5e528..aec4e07 100644 --- a/lib/solana.cpp +++ b/lib/solana.cpp @@ -20,7 +20,7 @@ Connection::Connection(const std::string &rpc_url, json Connection::getAccountInfoRequest(const std::string &account, const std::string &encoding, const size_t offset, - const size_t length) { + const size_t length) const { json params = {account}; json options = {{"encoding", encoding}}; if (offset && length) { @@ -107,4 +107,4 @@ std::string Connection::signAndSendTransaction( } } // namespace rpc -} // namespace solana \ No newline at end of file +} // namespace solana From 8d04f8a813baf3e8498d83c6a3f5c7f696df202a Mon Sep 17 00:00:00 2001 From: papadpickle <99397192+papadpickle@users.noreply.github.com> Date: Wed, 2 Mar 2022 00:36:02 +0100 Subject: [PATCH 02/20] * using MAINNET.endpoint instead of magic numbers * moving book parsing code out of subscriber class --- examples/orderbookSubscribe.cpp | 2 - include/mango_v3.hpp | 133 ++++++++++++++++++------ include/orderbook/order.hpp | 23 ++++ include/orderbook/orderbook.hpp | 3 +- include/subscriptions/bookSide.hpp | 66 +----------- include/subscriptions/wssSubscriber.hpp | 5 +- 6 files changed, 131 insertions(+), 101 deletions(-) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index 3a928f9..96ed9a7 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -80,5 +80,3 @@ int main() { } return 0; } - -// void updateReceived() {} diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 43b32e3..a7d51fd 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -1,11 +1,13 @@ #pragma once #include +#include #include #include #include "fixedp.h" #include "int128.hpp" +#include "orderbook/order.hpp" #include "solana.hpp" namespace mango_v3 { @@ -244,49 +246,112 @@ struct FreeNode { uint8_t padding[BOOK_NODE_SIZE - 8]; }; -struct BookSide { - MetaData metaData; - uint64_t bumpIndex; - uint64_t freeListLen; - uint32_t freeListHead; - uint32_t rootNode; - uint64_t leafCount; - AnyNode nodes[BOOK_SIZE]; - - struct iterator { - Side side; - const BookSide &bookSide; - std::stack stack; - uint32_t left, right; - - iterator(Side side, const BookSide &bookSide) - : side(side), bookSide(bookSide) { - stack.push(bookSide.rootNode); - left = side == Side::Buy ? 1 : 0; - right = side == Side::Buy ? 0 : 1; +class BookSide { + public: + BookSide(Side side) : side(side) {} + + bool update(const std::string decoded) { + if (decoded.size() != sizeof(BookSideRaw)) { + throw std::runtime_error("invalid response length " + + std::to_string(decoded.size()) + " expected " + + std::to_string(sizeof(BookSideRaw))); + } + memcpy(&raw, decoded.data(), sizeof(BookSideRaw)); + + auto iter = BookSide::BookSideRaw::iterator(side, raw); + orderbook::order_container newOrders; + while (iter.stack.size() > 0) { + if ((*iter).tag == NodeType::LeafNode) { + const auto leafNode = + reinterpret_cast(&(*iter)); + const auto now = std::chrono::system_clock::now(); + const auto nowUnix = + chrono::duration_cast(now.time_since_epoch()) + .count(); + const auto isValid = + !leafNode->timeInForce || + leafNode->timestamp + leafNode->timeInForce < nowUnix; + if (isValid) { + newOrders.orders.emplace_back((uint64_t)(leafNode->key >> 64), + leafNode->quantity); + } + } + ++iter; } - bool operator==(const iterator &other) const { - return &bookSide == &other.bookSide && stack.top() == other.stack.top(); + if (!newOrders.orders.empty()) { + std::scoped_lock lock(mtx); + orders = std::move(newOrders); + return true; + } else { + return false; } + } - iterator &operator++() { - if (stack.size() > 0) { - const auto &elem = **this; - stack.pop(); + orderbook::order getBestOrder() const { + std::scoped_lock lock(mtx); + return orders.getBest(); + } - if (elem.tag == NodeType::InnerNode) { - const auto innerNode = - reinterpret_cast(&elem); - stack.push(innerNode->children[right]); - stack.push(innerNode->children[left]); + uint64_t getVolume(uint64_t price) const { + std::scoped_lock lock(mtx); + if (side == Side::Buy) { + return orders.getVolume>(price); + } else { + return orders.getVolume>(price); + } + } + + private: + struct BookSideRaw { + MetaData metaData; + uint64_t bumpIndex; + uint64_t freeListLen; + uint32_t freeListHead; + uint32_t rootNode; + uint64_t leafCount; + AnyNode nodes[BOOK_SIZE]; + + struct iterator { + Side side; + const BookSideRaw &bookSide; + std::stack stack; + uint32_t left, right; + + iterator(Side side, const BookSideRaw &bookSide) + : side(side), bookSide(bookSide) { + stack.push(bookSide.rootNode); + left = side == Side::Buy ? 1 : 0; + right = side == Side::Buy ? 0 : 1; + } + + bool operator==(const iterator &other) const { + return &bookSide == &other.bookSide && stack.top() == other.stack.top(); + } + + iterator &operator++() { + if (stack.size() > 0) { + const auto &elem = **this; + stack.pop(); + + if (elem.tag == NodeType::InnerNode) { + const auto innerNode = + reinterpret_cast(&elem); + stack.push(innerNode->children[right]); + stack.push(innerNode->children[left]); + } } + return *this; } - return *this; - } - const AnyNode &operator*() const { return bookSide.nodes[stack.top()]; } + const AnyNode &operator*() const { return bookSide.nodes[stack.top()]; } + }; }; + + Side side; + BookSideRaw raw; + orderbook::order_container orders; + mutable std::mutex mtx; }; #pragma pack(pop) diff --git a/include/orderbook/order.hpp b/include/orderbook/order.hpp index bb1efde..8ddcc0f 100644 --- a/include/orderbook/order.hpp +++ b/include/orderbook/order.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace mango_v3 { namespace orderbook { @@ -18,5 +20,26 @@ struct order { uint64_t quantity; }; +struct order_container { + std::vector orders; + + order getBest() const { + return (!orders.empty()) ? orders.front() : orderbook::order{0, 0}; + } + + template + uint64_t getVolume(uint64_t price) const { + Op operation; + uint64_t volume = 0; + for (auto&& order : orders) { + if (operation(order.price, price)) { + volume += order.quantity; + } else { + break; + } + } + return volume; + } +}; } // namespace orderbook } // namespace mango_v3 diff --git a/include/orderbook/orderbook.hpp b/include/orderbook/orderbook.hpp index c1bf633..b292f7e 100644 --- a/include/orderbook/orderbook.hpp +++ b/include/orderbook/orderbook.hpp @@ -57,8 +57,7 @@ class book { uint64_t getDepth(int8_t percent) { const std::scoped_lock lock(levelOneMtx); auto price = (level1.midPoint * (100 + percent)) / 100; - return (percent > 0) ? asks.getVolume>(price) - : bids.getVolume>(price); + return (percent > 0) ? asks.getVolume(price) : bids.getVolume(price); } private: diff --git a/include/subscriptions/bookSide.hpp b/include/subscriptions/bookSide.hpp index 0c47bae..54b321e 100644 --- a/include/subscriptions/bookSide.hpp +++ b/include/subscriptions/bookSide.hpp @@ -21,7 +21,7 @@ using json = nlohmann::json; class bookSide { public: bookSide(Side side, const std::string& account) - : side(side), wssConnection(account) {} + : bookSide_(side), wssConnection(account) {} void registerUpdateCallback(std::function callback) { notifyCb = callback; @@ -33,24 +33,10 @@ class bookSide { wssConnection.start(); } - orderbook::order getBestOrder() const { - std::scoped_lock lock(ordersMtx); - return (!orders.empty()) ? orders.front() : orderbook::order{0, 0}; - } + orderbook::order getBestOrder() const { return bookSide_.getBestOrder(); } - template uint64_t getVolume(uint64_t price) const { - Op operation; - uint64_t volume = 0; - std::scoped_lock lock(ordersMtx); - for (auto&& order : orders) { - if (operation(order.price, price)) { - volume += order.quantity; - } else { - break; - } - } - return volume; + return bookSide_.getVolume(price); } private: @@ -66,55 +52,13 @@ class bookSide { parsedMsg["params"]["result"]["value"]["data"][0]; const std::string decoded = solana::b64decode(encoded); - if (decoded.size() != sizeof(BookSide)) - throw std::runtime_error("invalid response length " + - std::to_string(decoded.size()) + " expected " + - std::to_string(sizeof(BookSide))); - - BookSide bookSide; - memcpy(&bookSide, decoded.data(), sizeof(decltype(bookSide))); - - auto iter = BookSide::iterator(side, bookSide); - - decltype(orders) newOrders; - - while (iter.stack.size() > 0) { - if ((*iter).tag == NodeType::LeafNode) { - const auto leafNode = - reinterpret_cast(&(*iter)); - const auto now = std::chrono::system_clock::now(); - const auto nowUnix = - chrono::duration_cast(now.time_since_epoch()) - .count(); - const auto isValid = - !leafNode->timeInForce || - leafNode->timestamp + leafNode->timeInForce < nowUnix; - if (isValid) { - newOrders.emplace_back((uint64_t)(leafNode->key >> 64), - leafNode->quantity); - } - } - ++iter; - } - - if (!newOrders.empty()) { - { - std::scoped_lock lock(ordersMtx); - orders = std::move(newOrders); - if (side == Side::Buy) { - std::sort(orders.begin(), orders.end(), std::greater{}); - } else { - std::sort(orders.begin(), orders.end()); - } - } + if (bookSide_.update(decoded)) { notifyCb(); } } wssSubscriber wssConnection; - const Side side; - mutable std::mutex ordersMtx; - std::vector orders; + BookSide bookSide_; std::function notifyCb; }; } // namespace subscription diff --git a/include/subscriptions/wssSubscriber.hpp b/include/subscriptions/wssSubscriber.hpp index a68b4b4..5755241 100644 --- a/include/subscriptions/wssSubscriber.hpp +++ b/include/subscriptions/wssSubscriber.hpp @@ -7,6 +7,8 @@ #include #include +#include "mango_v3.hpp" + typedef websocketpp::client ws_client; typedef websocketpp::config::asio_client::message_type::ptr ws_message_ptr; typedef std::shared_ptr context_ptr; @@ -46,8 +48,7 @@ class wssSubscriber { websocketpp::lib::placeholders::_2)); websocketpp::lib::error_code ec; - ws_client::connection_ptr con = c.get_connection( - "wss://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88", ec); + ws_client::connection_ptr con = c.get_connection(MAINNET.endpoint, ec); if (ec) { spdlog::error("could not create connection because: {}", ec.message()); } From 587e1aac9995a09a2e2eddb0afa2748ccdc429f9 Mon Sep 17 00:00:00 2001 From: papadpickle <99397192+papadpickle@users.noreply.github.com> Date: Wed, 2 Mar 2022 14:31:57 +0100 Subject: [PATCH 03/20] * added on_close callbacks to handle error in wss connections and stop logging * some refactoring --- examples/orderbookSubscribe.cpp | 67 ++++---- include/mango_v3.hpp | 6 +- include/orderbook/levelOne.hpp | 4 +- include/orderbook/order.hpp | 6 +- include/subscriptions/bookSide.hpp | 18 ++- .../orderbook.hpp | 151 +++++++++--------- include/subscriptions/trades.hpp | 16 +- include/subscriptions/wssSubscriber.hpp | 50 +++--- 8 files changed, 189 insertions(+), 129 deletions(-) rename include/{orderbook => subscriptions}/orderbook.hpp (66%) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index 96ed9a7..ea746b2 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -8,48 +8,62 @@ #include "mango_v3.hpp" #include "orderbook/levelOne.hpp" -#include "orderbook/orderbook.hpp" #include "solana.hpp" -#include "subscriptions/bookSide.hpp" +#include "subscriptions/orderbook.hpp" #include "subscriptions/trades.hpp" class updateLogger { public: - updateLogger(mango_v3::orderbook::book& orderbook, + updateLogger(mango_v3::subscription::orderbook& orderbook, mango_v3::subscription::trades& trades) : orderbook(orderbook), trades(trades) { orderbook.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); trades.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); - } + orderbook.registerCloseCallback(std::bind(&updateLogger::stop, this)); + trades.registerCloseCallback(std::bind(&updateLogger::stop, this)); - void start() { orderbook.subscribe(); trades.subscribe(); } + bool isOk() { + const std::scoped_lock lock(updateMtx); + return !subscriptionError; + } + void logUpdate() { const std::scoped_lock lock(updateMtx); - auto level1Snapshot = orderbook.getLevel1(); - if (level1Snapshot.valid()) { - spdlog::info("============Update============"); - spdlog::info("Latest trade: {}", trades.getLastTrade() - ? to_string(trades.getLastTrade()) - : "not received yet"); - spdlog::info("Bid-Ask {}-{}", level1Snapshot.highestBid, - level1Snapshot.lowestAsk); - spdlog::info("MidPrice: {}", level1Snapshot.midPoint); - spdlog::info("Spread: {0:.2f} bps", level1Snapshot.spreadBps); - - constexpr auto depth = 2; - spdlog::info("Market depth -{}%: {}", depth, orderbook.getDepth(-depth)); - spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); + if (!subscriptionError) { + auto level1Snapshot = orderbook.getLevel1(); + if (level1Snapshot.valid()) { + spdlog::info("============Update============"); + spdlog::info("Latest trade: {}", trades.getLastTrade() + ? to_string(trades.getLastTrade()) + : "not received yet"); + spdlog::info("Bid-Ask {}-{}", level1Snapshot.highestBid, + level1Snapshot.lowestAsk); + spdlog::info("MidPrice: {}", level1Snapshot.midPoint); + spdlog::info("Spread: {0:.2f} bps", level1Snapshot.spreadBps); + + constexpr auto depth = 2; + spdlog::info("Market depth -{}%: {}", depth, + orderbook.getDepth(-depth)); + spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); + } } } + void stop() { + const std::scoped_lock lock(updateMtx); + spdlog::error("websocket subscription error"); + subscriptionError = true; + } + private: std::mutex updateMtx; - mango_v3::orderbook::book& orderbook; + mango_v3::subscription::orderbook& orderbook; mango_v3::subscription::trades& trades; + bool subscriptionError = false; }; int main() { @@ -67,16 +81,15 @@ int main() { solCon.getAccountInfo(perpMarketPk.toBase58()); assert(market.mangoGroup.toBase58() == config.group); - mango_v3::subscription::bookSide bids(mango_v3::Buy, market.bids.toBase58()); - mango_v3::subscription::bookSide asks(mango_v3::Sell, market.asks.toBase58()); mango_v3::subscription::trades trades(market.eventQueue.toBase58()); + mango_v3::subscription::orderbook book(market.bids.toBase58(), + market.asks.toBase58()); - mango_v3::orderbook::book orderbook(bids, asks); + updateLogger logger(book, trades); - updateLogger logger(orderbook, trades); - logger.start(); - - while (true) { + while (logger.isOk()) { + std::this_thread::sleep_for(1s); } + return 0; } diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index a7d51fd..920d637 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -259,7 +259,7 @@ class BookSide { memcpy(&raw, decoded.data(), sizeof(BookSideRaw)); auto iter = BookSide::BookSideRaw::iterator(side, raw); - orderbook::order_container newOrders; + book::order_container newOrders; while (iter.stack.size() > 0) { if ((*iter).tag == NodeType::LeafNode) { const auto leafNode = @@ -288,7 +288,7 @@ class BookSide { } } - orderbook::order getBestOrder() const { + book::order getBestOrder() const { std::scoped_lock lock(mtx); return orders.getBest(); } @@ -350,7 +350,7 @@ class BookSide { Side side; BookSideRaw raw; - orderbook::order_container orders; + book::order_container orders; mutable std::mutex mtx; }; diff --git a/include/orderbook/levelOne.hpp b/include/orderbook/levelOne.hpp index 92f0954..747e139 100644 --- a/include/orderbook/levelOne.hpp +++ b/include/orderbook/levelOne.hpp @@ -1,7 +1,7 @@ #pragma once namespace mango_v3 { -namespace orderbook { +namespace book { struct levelOne { uint64_t highestBid; @@ -17,5 +17,5 @@ struct levelOne { } }; -} // namespace orderbook +} // namespace book } // namespace mango_v3 diff --git a/include/orderbook/order.hpp b/include/orderbook/order.hpp index 8ddcc0f..1041d12 100644 --- a/include/orderbook/order.hpp +++ b/include/orderbook/order.hpp @@ -3,7 +3,7 @@ #include namespace mango_v3 { -namespace orderbook { +namespace book { struct order { order(uint64_t price, uint64_t quantity) : price(price), quantity(quantity) {} @@ -24,7 +24,7 @@ struct order_container { std::vector orders; order getBest() const { - return (!orders.empty()) ? orders.front() : orderbook::order{0, 0}; + return (!orders.empty()) ? orders.front() : order{0, 0}; } template @@ -41,5 +41,5 @@ struct order_container { return volume; } }; -} // namespace orderbook +} // namespace book } // namespace mango_v3 diff --git a/include/subscriptions/bookSide.hpp b/include/subscriptions/bookSide.hpp index 54b321e..c679bc5 100644 --- a/include/subscriptions/bookSide.hpp +++ b/include/subscriptions/bookSide.hpp @@ -27,19 +27,30 @@ class bookSide { notifyCb = callback; } + void registerCloseCallback(std::function callback) { + closeCb = callback; + } + void subscribe() { wssConnection.registerOnMessageCallback( std::bind(&bookSide::onMessage, this, std::placeholders::_1)); + wssConnection.registerOnCloseCallback(std::bind(&bookSide::onClose, this)); wssConnection.start(); } - orderbook::order getBestOrder() const { return bookSide_.getBestOrder(); } + book::order getBestOrder() const { return bookSide_.getBestOrder(); } uint64_t getVolume(uint64_t price) const { return bookSide_.getVolume(price); } private: + void onClose() { + if (closeCb) { + closeCb(); + } + } + void onMessage(const json& parsedMsg) { // ignore subscription confirmation const auto itResult = parsedMsg.find("result"); @@ -53,13 +64,16 @@ class bookSide { const std::string decoded = solana::b64decode(encoded); if (bookSide_.update(decoded)) { - notifyCb(); + if (notifyCb) { + notifyCb(); + } } } wssSubscriber wssConnection; BookSide bookSide_; std::function notifyCb; + std::function closeCb; }; } // namespace subscription } // namespace mango_v3 diff --git a/include/orderbook/orderbook.hpp b/include/subscriptions/orderbook.hpp similarity index 66% rename from include/orderbook/orderbook.hpp rename to include/subscriptions/orderbook.hpp index b292f7e..166b467 100644 --- a/include/orderbook/orderbook.hpp +++ b/include/subscriptions/orderbook.hpp @@ -1,73 +1,78 @@ -#pragma once -#include - -#include -#include - -#include "levelOne.hpp" -#include "orderbook/order.hpp" -#include "subscriptions/bookSide.hpp" - -namespace mango_v3 { -namespace orderbook { -class book { - public: - book(subscription::bookSide& bids, subscription::bookSide& asks) - : bids(bids), asks(asks) { - bids.registerUpdateCallback(std::bind(&book::updateCallback, this)); - asks.registerUpdateCallback(std::bind(&book::updateCallback, this)); - } - - void registerUpdateCallback(std::function callback) { - onUpdateCb = callback; - } - - void subscribe() { - bids.subscribe(); - asks.subscribe(); - } - - void updateCallback() { - const std::scoped_lock lock(callbackMtx); - levelOne newL1; - auto bestBid = bids.getBestOrder(); - auto bestAsk = asks.getBestOrder(); - newL1.highestBid = bestBid.price; - newL1.highestBidSize = bestBid.quantity; - newL1.lowestAsk = bestAsk.price; - newL1.lowestAskSize = bestAsk.quantity; - - if (newL1.valid()) { - newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; - newL1.spreadBps = - ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; - { - const std::scoped_lock lock(levelOneMtx); - level1 = newL1; - } - onUpdateCb(); - } - } - - levelOne getLevel1() const { - const std::scoped_lock lock(levelOneMtx); - return level1; - } - - uint64_t getDepth(int8_t percent) { - const std::scoped_lock lock(levelOneMtx); - auto price = (level1.midPoint * (100 + percent)) / 100; - return (percent > 0) ? asks.getVolume(price) : bids.getVolume(price); - } - - private: - levelOne level1; - // todo:macos latomic not found issue, otherwise replace mtx with std::atomic - mutable std::mutex levelOneMtx; - std::function onUpdateCb; - std::mutex callbackMtx; - subscription::bookSide& bids; - subscription::bookSide& asks; -}; -} // namespace orderbook -} // namespace mango_v3 +#pragma once +#include + +#include +#include + +#include "bookSide.hpp" +#include "orderbook/levelOne.hpp" +#include "orderbook/order.hpp" + +namespace mango_v3 { +namespace subscription { +class orderbook { + public: + orderbook(const std::string& bidsAccount, const std::string& asksAccount) + : bids(Buy, bidsAccount), asks(Sell, asksAccount) { + bids.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); + asks.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); + } + + void registerUpdateCallback(std::function callback) { + onUpdateCb = callback; + } + + void registerCloseCallback(std::function callback) { + bids.registerCloseCallback(callback); + asks.registerCloseCallback(callback); + } + + void subscribe() { + bids.subscribe(); + asks.subscribe(); + } + + void updateCallback() { + const std::scoped_lock lock(callbackMtx); + decltype(level1) newL1; + auto bestBid = bids.getBestOrder(); + auto bestAsk = asks.getBestOrder(); + newL1.highestBid = bestBid.price; + newL1.highestBidSize = bestBid.quantity; + newL1.lowestAsk = bestAsk.price; + newL1.lowestAskSize = bestAsk.quantity; + + if (newL1.valid()) { + newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; + newL1.spreadBps = + ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; + { + const std::scoped_lock lock(levelOneMtx); + level1 = newL1; + } + onUpdateCb(); + } + } + + book::levelOne getLevel1() const { + const std::scoped_lock lock(levelOneMtx); + return level1; + } + + uint64_t getDepth(int8_t percent) { + const std::scoped_lock lock(levelOneMtx); + auto price = (level1.midPoint * (100 + percent)) / 100; + return (percent > 0) ? asks.getVolume(price) : bids.getVolume(price); + } + + private: + book::levelOne level1; + // todo:macos latomic not found issue, otherwise replace mtx with std::atomic + mutable std::mutex levelOneMtx; + std::function onUpdateCb; + std::mutex callbackMtx; + subscription::bookSide bids; + subscription::bookSide asks; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/include/subscriptions/trades.hpp b/include/subscriptions/trades.hpp index eb7a591..908b779 100644 --- a/include/subscriptions/trades.hpp +++ b/include/subscriptions/trades.hpp @@ -22,9 +22,14 @@ class trades { notifyCb = callback; } + void registerCloseCallback(std::function callback) { + closeCb = callback; + } + void subscribe() { wssConnection.registerOnMessageCallback( std::bind(&trades::onMessage, this, std::placeholders::_1)); + wssConnection.registerOnCloseCallback(std::bind(&trades::onClose, this)); wssConnection.start(); } @@ -34,6 +39,12 @@ class trades { } private: + void onClose() { + if (closeCb) { + closeCb(); + } + } + void onMessage(const json &parsedMsg) { // ignore subscription confirmation const auto itResult = parsedMsg.find("result"); @@ -72,7 +83,9 @@ class trades { } if (gotLatest) { - notifyCb(); + if (notifyCb) { + notifyCb(); + } } lastSeqNum = events->header.seqNum; } @@ -83,6 +96,7 @@ class trades { uint64_t latestTrade = 0; wssSubscriber wssConnection; std::function notifyCb; + std::function closeCb; }; } // namespace subscription } // namespace mango_v3 diff --git a/include/subscriptions/wssSubscriber.hpp b/include/subscriptions/wssSubscriber.hpp index 5755241..ab1e2e9 100644 --- a/include/subscriptions/wssSubscriber.hpp +++ b/include/subscriptions/wssSubscriber.hpp @@ -22,43 +22,50 @@ class wssSubscriber { public: wssSubscriber(const std::string& account) : account(account) {} - ~wssSubscriber() { runThread.join(); } + ~wssSubscriber() { + if (runThread.joinable()) { + websocketpp::lib::error_code ec; + c.stop(); + runThread.join(); + } + } void registerOnMessageCallback( std::function callback) { onMessageCb = callback; } - void start() { - runThread = std::thread(&wssSubscriber::subscriptionThread, this); + void registerOnCloseCallback(std::function callback) { + onCloseCb = callback; } - private: - void subscriptionThread() { - try { - c.set_access_channels(websocketpp::log::alevel::all); - c.init_asio(); - c.set_tls_init_handler( - websocketpp::lib::bind(&wssSubscriber::on_tls_init, this)); - - c.set_open_handler(websocketpp::lib::bind( - &wssSubscriber::on_open, this, websocketpp::lib::placeholders::_1)); - c.set_message_handler(websocketpp::lib::bind( - &wssSubscriber::on_message, this, websocketpp::lib::placeholders::_1, - websocketpp::lib::placeholders::_2)); + void start() { + c.set_access_channels(websocketpp::log::alevel::all); + c.init_asio(); + c.set_tls_init_handler( + websocketpp::lib::bind(&wssSubscriber::on_tls_init, this)); + c.set_open_handler(websocketpp::lib::bind( + &wssSubscriber::on_open, this, websocketpp::lib::placeholders::_1)); + c.set_message_handler(websocketpp::lib::bind( + &wssSubscriber::on_message, this, websocketpp::lib::placeholders::_1, + websocketpp::lib::placeholders::_2)); + c.set_close_handler(websocketpp::lib::bind( + &wssSubscriber::on_close, this, websocketpp::lib::placeholders::_1)); + try { websocketpp::lib::error_code ec; ws_client::connection_ptr con = c.get_connection(MAINNET.endpoint, ec); if (ec) { spdlog::error("could not create connection because: {}", ec.message()); } c.connect(con); - c.run(); + runThread = std::thread(&ws_client::run, &c); } catch (websocketpp::exception const& e) { - std::cout << e.what() << std::endl; + spdlog::error("{}", e.what()); } } + private: context_ptr on_tls_init() { context_ptr ctx = std::make_shared( boost::asio::ssl::context::sslv23); @@ -87,6 +94,12 @@ class wssSubscriber { } } + void on_close(websocketpp::connection_hdl hdl) { + if (onCloseCb) { + onCloseCb(); + } + } + void on_message(websocketpp::connection_hdl hdl, ws_message_ptr msg) { const json parsedMsg = json::parse(msg->get_payload()); onMessageCb(parsedMsg); @@ -96,6 +109,7 @@ class wssSubscriber { const std::string account; std::thread runThread; std::function onMessageCb; + std::function onCloseCb; }; } // namespace subscription } // namespace mango_v3 From 46f206aa320449fad40edcf21681b87a2eaa4b97 Mon Sep 17 00:00:00 2001 From: clang-format Date: Wed, 2 Mar 2022 13:33:49 +0000 Subject: [PATCH 04/20] style fixup --- include/subscriptions/wssSubscriber.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/subscriptions/wssSubscriber.hpp b/include/subscriptions/wssSubscriber.hpp index ab1e2e9..d42e05e 100644 --- a/include/subscriptions/wssSubscriber.hpp +++ b/include/subscriptions/wssSubscriber.hpp @@ -59,7 +59,7 @@ class wssSubscriber { spdlog::error("could not create connection because: {}", ec.message()); } c.connect(con); - runThread = std::thread(&ws_client::run, &c); + runThread = std::thread(&ws_client::run, &c); } catch (websocketpp::exception const& e) { spdlog::error("{}", e.what()); } From 2ade33601123393adbb4654d038f7c6ccefcac45 Mon Sep 17 00:00:00 2001 From: papadpickle <99397192+papadpickle@users.noreply.github.com> Date: Fri, 4 Mar 2022 13:30:31 +0100 Subject: [PATCH 05/20] replaced mtx with shared_ptr trick --- examples/orderbookSubscribe.cpp | 47 ++++----- include/orderbook/levelOne.hpp | 12 +-- include/subscriptions/orderbook.hpp | 146 +++++++++++++--------------- include/subscriptions/trades.hpp | 12 +-- 4 files changed, 94 insertions(+), 123 deletions(-) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index ea746b2..df937a2 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -26,44 +25,32 @@ class updateLogger { trades.subscribe(); } - bool isOk() { - const std::scoped_lock lock(updateMtx); - return !subscriptionError; - } - void logUpdate() { - const std::scoped_lock lock(updateMtx); - if (!subscriptionError) { - auto level1Snapshot = orderbook.getLevel1(); - if (level1Snapshot.valid()) { - spdlog::info("============Update============"); - spdlog::info("Latest trade: {}", trades.getLastTrade() - ? to_string(trades.getLastTrade()) - : "not received yet"); - spdlog::info("Bid-Ask {}-{}", level1Snapshot.highestBid, - level1Snapshot.lowestAsk); - spdlog::info("MidPrice: {}", level1Snapshot.midPoint); - spdlog::info("Spread: {0:.2f} bps", level1Snapshot.spreadBps); - - constexpr auto depth = 2; - spdlog::info("Market depth -{}%: {}", depth, - orderbook.getDepth(-depth)); - spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); - } + auto level1Snapshot = orderbook.getLevel1(); + if (level1Snapshot->valid()) { + auto latestTrade = trades.getLastTrade(); + spdlog::info("============Update============"); + spdlog::info("Latest trade: {}", + *latestTrade ? to_string(*latestTrade) : "not received yet"); + spdlog::info("Bid-Ask {}-{}", level1Snapshot->highestBid, + level1Snapshot->lowestAsk); + spdlog::info("MidPrice: {}", level1Snapshot->midPoint); + spdlog::info("Spread: {0:.2f} bps", level1Snapshot->spreadBps); + + constexpr auto depth = 2; + spdlog::info("Market depth -{}%: {}", depth, orderbook.getDepth(-depth)); + spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); } } void stop() { - const std::scoped_lock lock(updateMtx); spdlog::error("websocket subscription error"); - subscriptionError = true; + throw std::runtime_error("websocket subscription error"); } private: - std::mutex updateMtx; mango_v3::subscription::orderbook& orderbook; mango_v3::subscription::trades& trades; - bool subscriptionError = false; }; int main() { @@ -87,8 +74,8 @@ int main() { updateLogger logger(book, trades); - while (logger.isOk()) { - std::this_thread::sleep_for(1s); + while (true) { + std::this_thread::sleep_for(10000s); } return 0; diff --git a/include/orderbook/levelOne.hpp b/include/orderbook/levelOne.hpp index 747e139..08943c5 100644 --- a/include/orderbook/levelOne.hpp +++ b/include/orderbook/levelOne.hpp @@ -4,12 +4,12 @@ namespace mango_v3 { namespace book { struct levelOne { - uint64_t highestBid; - uint64_t highestBidSize; - uint64_t lowestAsk; - uint64_t lowestAskSize; - double midPoint; - double spreadBps; + uint64_t highestBid = 0; + uint64_t highestBidSize = 0; + uint64_t lowestAsk = 0; + uint64_t lowestAskSize = 0; + double midPoint = 0.0; + double spreadBps = 0.0; bool valid() const { return ((highestBid && lowestAsk) && (lowestAsk > highestBid)) ? true diff --git a/include/subscriptions/orderbook.hpp b/include/subscriptions/orderbook.hpp index 166b467..2c6c468 100644 --- a/include/subscriptions/orderbook.hpp +++ b/include/subscriptions/orderbook.hpp @@ -1,78 +1,68 @@ -#pragma once -#include - -#include -#include - -#include "bookSide.hpp" -#include "orderbook/levelOne.hpp" -#include "orderbook/order.hpp" - -namespace mango_v3 { -namespace subscription { -class orderbook { - public: - orderbook(const std::string& bidsAccount, const std::string& asksAccount) - : bids(Buy, bidsAccount), asks(Sell, asksAccount) { - bids.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); - asks.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); - } - - void registerUpdateCallback(std::function callback) { - onUpdateCb = callback; - } - - void registerCloseCallback(std::function callback) { - bids.registerCloseCallback(callback); - asks.registerCloseCallback(callback); - } - - void subscribe() { - bids.subscribe(); - asks.subscribe(); - } - - void updateCallback() { - const std::scoped_lock lock(callbackMtx); - decltype(level1) newL1; - auto bestBid = bids.getBestOrder(); - auto bestAsk = asks.getBestOrder(); - newL1.highestBid = bestBid.price; - newL1.highestBidSize = bestBid.quantity; - newL1.lowestAsk = bestAsk.price; - newL1.lowestAskSize = bestAsk.quantity; - - if (newL1.valid()) { - newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; - newL1.spreadBps = - ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; - { - const std::scoped_lock lock(levelOneMtx); - level1 = newL1; - } - onUpdateCb(); - } - } - - book::levelOne getLevel1() const { - const std::scoped_lock lock(levelOneMtx); - return level1; - } - - uint64_t getDepth(int8_t percent) { - const std::scoped_lock lock(levelOneMtx); - auto price = (level1.midPoint * (100 + percent)) / 100; - return (percent > 0) ? asks.getVolume(price) : bids.getVolume(price); - } - - private: - book::levelOne level1; - // todo:macos latomic not found issue, otherwise replace mtx with std::atomic - mutable std::mutex levelOneMtx; - std::function onUpdateCb; - std::mutex callbackMtx; - subscription::bookSide bids; - subscription::bookSide asks; -}; -} // namespace subscription -} // namespace mango_v3 +#pragma once +#include + +#include + +#include "bookSide.hpp" +#include "orderbook/levelOne.hpp" +#include "orderbook/order.hpp" + +namespace mango_v3 { +namespace subscription { +class orderbook { + public: + orderbook(const std::string& bidsAccount, const std::string& asksAccount) + : bids(Buy, bidsAccount), asks(Sell, asksAccount) { + bids.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); + asks.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); + } + + void registerUpdateCallback(std::function callback) { + onUpdateCb = callback; + } + + void registerCloseCallback(std::function callback) { + bids.registerCloseCallback(callback); + asks.registerCloseCallback(callback); + } + + void subscribe() { + bids.subscribe(); + asks.subscribe(); + } + + void updateCallback() { + book::levelOne newL1; + auto bestBid = bids.getBestOrder(); + auto bestAsk = asks.getBestOrder(); + newL1.highestBid = bestBid.price; + newL1.highestBidSize = bestBid.quantity; + newL1.lowestAsk = bestAsk.price; + newL1.lowestAskSize = bestAsk.quantity; + + if (newL1.valid()) { + newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; + newL1.spreadBps = + ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; + level1 = std::make_shared(std::move(newL1)); + onUpdateCb(); + } + } + + auto getLevel1() const { + return level1; + } + + auto getDepth(int8_t percent) { + auto price = (level1->midPoint * (100 + percent)) / 100; + return (percent > 0) ? asks.getVolume(price) : bids.getVolume(price); + } + + private: + std::shared_ptr level1 = std::make_shared(); + std::function onUpdateCb; + subscription::bookSide bids; + subscription::bookSide asks; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/include/subscriptions/trades.hpp b/include/subscriptions/trades.hpp index 908b779..8844c6e 100644 --- a/include/subscriptions/trades.hpp +++ b/include/subscriptions/trades.hpp @@ -33,10 +33,7 @@ class trades { wssConnection.start(); } - int64_t getLastTrade() const { - const std::scoped_lock lock(latestTradeMtx); - return latestTrade; - } + auto getLastTrade() const { return latestTrade; } private: void onClose() { @@ -74,8 +71,7 @@ class trades { if (event.eventType == EventType::Fill) { const auto &fill = (FillEvent &)event; - const std::scoped_lock lock(latestTradeMtx); - latestTrade = fill.price; + latestTrade = std::make_shared(fill.price); gotLatest = true; } // no break; let's iterate to the last fill to get the latest fill order @@ -91,9 +87,7 @@ class trades { } uint64_t lastSeqNum = INT_MAX; - // todo:macos latomic not found issue, otherwise replace mtx with std::atomic - mutable std::mutex latestTradeMtx; - uint64_t latestTrade = 0; + std::shared_ptr latestTrade = std::make_shared(0); wssSubscriber wssConnection; std::function notifyCb; std::function closeCb; From 3671454757d30635ffc3dcd011b7734b20d188bc Mon Sep 17 00:00:00 2001 From: clang-format Date: Fri, 4 Mar 2022 12:30:59 +0000 Subject: [PATCH 06/20] style fixup --- include/subscriptions/orderbook.hpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/include/subscriptions/orderbook.hpp b/include/subscriptions/orderbook.hpp index 2c6c468..59a0744 100644 --- a/include/subscriptions/orderbook.hpp +++ b/include/subscriptions/orderbook.hpp @@ -44,14 +44,12 @@ class orderbook { newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; newL1.spreadBps = ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; - level1 = std::make_shared(std::move(newL1)); + level1 = std::make_shared(std::move(newL1)); onUpdateCb(); } } - auto getLevel1() const { - return level1; - } + auto getLevel1() const { return level1; } auto getDepth(int8_t percent) { auto price = (level1->midPoint * (100 + percent)) / 100; From 06c7e5d151cbad826dc6a79bcfb6fd62ff06424a Mon Sep 17 00:00:00 2001 From: papadpickle <99397192+papadpickle@users.noreply.github.com> Date: Sat, 5 Mar 2022 18:14:03 +0100 Subject: [PATCH 07/20] * added accountSubscriber template class * removed unnecessary structs --- examples/orderbookSubscribe.cpp | 39 +++--- include/mango_v3.hpp | 148 ++++++++++++++++---- include/orderbook/levelOne.hpp | 21 --- include/orderbook/order.hpp | 45 ------ include/subscriptions/accountSubscriber.hpp | 60 ++++++++ include/subscriptions/bookSide.hpp | 79 ----------- include/subscriptions/orderbook.hpp | 33 ++--- include/subscriptions/trades.hpp | 96 ------------- 8 files changed, 218 insertions(+), 303 deletions(-) delete mode 100644 include/orderbook/levelOne.hpp delete mode 100644 include/orderbook/order.hpp create mode 100644 include/subscriptions/accountSubscriber.hpp delete mode 100644 include/subscriptions/bookSide.hpp delete mode 100644 include/subscriptions/trades.hpp diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index df937a2..8000b88 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -2,33 +2,37 @@ #include #include +#include #include #include #include "mango_v3.hpp" -#include "orderbook/levelOne.hpp" #include "solana.hpp" +#include "subscriptions/accountSubscriber.hpp" #include "subscriptions/orderbook.hpp" -#include "subscriptions/trades.hpp" class updateLogger { public: - updateLogger(mango_v3::subscription::orderbook& orderbook, - mango_v3::subscription::trades& trades) + updateLogger( + mango_v3::subscription::orderbook& orderbook, + mango_v3::subscription::accountSubscriber& trades) : orderbook(orderbook), trades(trades) { orderbook.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); trades.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); - orderbook.registerCloseCallback(std::bind(&updateLogger::stop, this)); - trades.registerCloseCallback(std::bind(&updateLogger::stop, this)); + orderbook.registerCloseCallback(std::bind(&updateLogger::abort, this)); + trades.registerCloseCallback(std::bind(&updateLogger::abort, this)); + } + void start() { orderbook.subscribe(); trades.subscribe(); } void logUpdate() { + std::scoped_lock lock(logMtx); auto level1Snapshot = orderbook.getLevel1(); if (level1Snapshot->valid()) { - auto latestTrade = trades.getLastTrade(); + auto latestTrade = trades.getAccount()->getLastTrade(); spdlog::info("============Update============"); spdlog::info("Latest trade: {}", *latestTrade ? to_string(*latestTrade) : "not received yet"); @@ -43,20 +47,22 @@ class updateLogger { } } - void stop() { + void abort() { spdlog::error("websocket subscription error"); throw std::runtime_error("websocket subscription error"); } private: mango_v3::subscription::orderbook& orderbook; - mango_v3::subscription::trades& trades; + mango_v3::subscription::accountSubscriber& trades; + std::mutex logMtx; }; int main() { - const auto& config = mango_v3::MAINNET; + using namespace mango_v3; + const auto& config = MAINNET; const solana::rpc::Connection solCon; - const auto group = solCon.getAccountInfo(config.group); + const auto group = solCon.getAccountInfo(config.group); const auto symbolIt = std::find(config.symbols.begin(), config.symbols.end(), "SOL"); @@ -64,18 +70,17 @@ int main() { assert(config.symbols[marketIndex] == "SOL"); const auto perpMarketPk = group.perpMarkets[marketIndex].perpMarket; - auto market = - solCon.getAccountInfo(perpMarketPk.toBase58()); + auto market = solCon.getAccountInfo(perpMarketPk.toBase58()); assert(market.mangoGroup.toBase58() == config.group); - mango_v3::subscription::trades trades(market.eventQueue.toBase58()); - mango_v3::subscription::orderbook book(market.bids.toBase58(), - market.asks.toBase58()); + subscription::accountSubscriber trades(market.eventQueue.toBase58()); + subscription::orderbook book(market.bids.toBase58(), market.asks.toBase58()); updateLogger logger(book, trades); + logger.start(); while (true) { - std::this_thread::sleep_for(10000s); + std::this_thread::sleep_for(100s); } return 0; diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 920d637..f542a3d 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -1,13 +1,12 @@ #pragma once #include -#include +#include #include #include #include "fixedp.h" #include "int128.hpp" -#include "orderbook/order.hpp" #include "solana.hpp" namespace mango_v3 { @@ -246,20 +245,79 @@ struct FreeNode { uint8_t padding[BOOK_NODE_SIZE - 8]; }; +struct Order { + Order(uint64_t price, uint64_t quantity) : price(price), quantity(quantity) {} + uint64_t price = 0; + uint64_t quantity = 0; +}; + +struct L1Orderbook { + uint64_t highestBid = 0; + uint64_t highestBidSize = 0; + uint64_t lowestAsk = 0; + uint64_t lowestAskSize = 0; + double midPoint = 0.0; + double spreadBps = 0.0; + + bool valid() const { + return ((highestBid && lowestAsk) && (lowestAsk > highestBid)) ? true + : false; + } +}; + class BookSide { public: BookSide(Side side) : side(side) {} + bool handleMsg(const nlohmann::json &msg) { + // ignore subscription confirmation + const auto itResult = msg.find("result"); + if (itResult != msg.end()) { + return false; + } + + const std::string encoded = msg["params"]["result"]["value"]["data"][0]; + const std::string decoded = solana::b64decode(encoded); + return update(decoded); + } + + Order getBestOrder() const { + return (!orders->empty()) ? orders->front() : Order(0, 0); + } + + uint64_t getVolume(uint64_t price) const { + if (side == Side::Buy) { + return getVolume>(price); + } else { + return getVolume>(price); + } + } + + private: + template + uint64_t getVolume(uint64_t price) const { + Op operation; + uint64_t volume = 0; + for (auto &&order : *orders) { + if (operation(order.price, price)) { + volume += order.quantity; + } else { + break; + } + } + return volume; + } + bool update(const std::string decoded) { if (decoded.size() != sizeof(BookSideRaw)) { throw std::runtime_error("invalid response length " + std::to_string(decoded.size()) + " expected " + std::to_string(sizeof(BookSideRaw))); } - memcpy(&raw, decoded.data(), sizeof(BookSideRaw)); + memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw)); - auto iter = BookSide::BookSideRaw::iterator(side, raw); - book::order_container newOrders; + auto iter = BookSide::BookSideRaw::iterator(side, *raw); + std::vector newOrders; while (iter.stack.size() > 0) { if ((*iter).tag == NodeType::LeafNode) { const auto leafNode = @@ -272,37 +330,21 @@ class BookSide { !leafNode->timeInForce || leafNode->timestamp + leafNode->timeInForce < nowUnix; if (isValid) { - newOrders.orders.emplace_back((uint64_t)(leafNode->key >> 64), - leafNode->quantity); + newOrders.emplace_back((uint64_t)(leafNode->key >> 64), + leafNode->quantity); } } ++iter; } - if (!newOrders.orders.empty()) { - std::scoped_lock lock(mtx); - orders = std::move(newOrders); + if (!newOrders.empty()) { + orders = std::make_shared>(std::move(newOrders)); return true; } else { return false; } } - book::order getBestOrder() const { - std::scoped_lock lock(mtx); - return orders.getBest(); - } - - uint64_t getVolume(uint64_t price) const { - std::scoped_lock lock(mtx); - if (side == Side::Buy) { - return orders.getVolume>(price); - } else { - return orders.getVolume>(price); - } - } - - private: struct BookSideRaw { MetaData metaData; uint64_t bumpIndex; @@ -348,10 +390,58 @@ class BookSide { }; }; - Side side; - BookSideRaw raw; - book::order_container orders; - mutable std::mutex mtx; + const Side side; + std::shared_ptr raw = std::make_shared(); + std::shared_ptr> orders = + std::make_shared>(); +}; + +class Trades { + public: + auto getLastTrade() const { return latestTrade; } + + bool handleMsg(const nlohmann::json &msg) { + // ignore subscription confirmation + const auto itResult = msg.find("result"); + if (itResult != msg.end()) { + return false; + } + + // all other messages are event queue updates + const std::string method = msg["method"]; + const int subscription = msg["params"]["subscription"]; + const int slot = msg["params"]["result"]["context"]["slot"]; + const std::string data = msg["params"]["result"]["value"]["data"][0]; + + const auto decoded = solana::b64decode(data); + const auto events = reinterpret_cast(decoded.data()); + const auto seqNumDiff = events->header.seqNum - lastSeqNum; + const auto lastSlot = + (events->header.head + events->header.count) % EVENT_QUEUE_SIZE; + + bool gotLatest = false; + if (events->header.seqNum > lastSeqNum) { + for (int offset = seqNumDiff; offset > 0; --offset) { + const auto slot = + (lastSlot - offset + EVENT_QUEUE_SIZE) % EVENT_QUEUE_SIZE; + const auto &event = events->items[slot]; + + if (event.eventType == EventType::Fill) { + const auto &fill = (FillEvent &)event; + latestTrade = std::make_shared(fill.price); + gotLatest = true; + } + // no break; let's iterate to the last fill to get the latest fill order + } + } + + lastSeqNum = events->header.seqNum; + return gotLatest; + } + + private: + uint64_t lastSeqNum = INT_MAX; + std::shared_ptr latestTrade = std::make_shared(0); }; #pragma pack(pop) diff --git a/include/orderbook/levelOne.hpp b/include/orderbook/levelOne.hpp deleted file mode 100644 index 08943c5..0000000 --- a/include/orderbook/levelOne.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -namespace mango_v3 { -namespace book { - -struct levelOne { - uint64_t highestBid = 0; - uint64_t highestBidSize = 0; - uint64_t lowestAsk = 0; - uint64_t lowestAskSize = 0; - double midPoint = 0.0; - double spreadBps = 0.0; - - bool valid() const { - return ((highestBid && lowestAsk) && (lowestAsk > highestBid)) ? true - : false; - } -}; - -} // namespace book -} // namespace mango_v3 diff --git a/include/orderbook/order.hpp b/include/orderbook/order.hpp deleted file mode 100644 index 1041d12..0000000 --- a/include/orderbook/order.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include - -namespace mango_v3 { -namespace book { - -struct order { - order(uint64_t price, uint64_t quantity) : price(price), quantity(quantity) {} - - bool operator<(const order& compare) const { - return (price < compare.price) ? true : false; - } - - bool operator>(const order& compare) const { - return (price > compare.price) ? true : false; - } - - uint64_t price; - uint64_t quantity; -}; - -struct order_container { - std::vector orders; - - order getBest() const { - return (!orders.empty()) ? orders.front() : order{0, 0}; - } - - template - uint64_t getVolume(uint64_t price) const { - Op operation; - uint64_t volume = 0; - for (auto&& order : orders) { - if (operation(order.price, price)) { - volume += order.quantity; - } else { - break; - } - } - return volume; - } -}; -} // namespace book -} // namespace mango_v3 diff --git a/include/subscriptions/accountSubscriber.hpp b/include/subscriptions/accountSubscriber.hpp new file mode 100644 index 0000000..3c1fb34 --- /dev/null +++ b/include/subscriptions/accountSubscriber.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +#include "wssSubscriber.hpp" + +namespace mango_v3 { +namespace subscription { + +template +class accountSubscriber { + public: + template + accountSubscriber(const std::string &name, Args... args) + : wssConnection(name), account(std::make_shared(args...)) {} + + template + void registerUpdateCallback(func &&callback) { + notifyCb = std::forward(callback); + } + + template + void registerCloseCallback(func &&callback) { + closeCb = std::forward(callback); + ; + } + + std::shared_ptr getAccount() { return account; } + + void subscribe() { + wssConnection.registerOnMessageCallback( + std::bind(&accountSubscriber::onMessage, this, std::placeholders::_1)); + wssConnection.registerOnCloseCallback( + std::bind(&accountSubscriber::onClose, this)); + wssConnection.start(); + } + + private: + void onMessage(const nlohmann::json &msg) { + if (account->handleMsg(msg)) { + if (notifyCb) { + notifyCb(); + } + } + } + void onClose() { + if (closeCb) { + closeCb(); + } + } + + std::shared_ptr account; + wssSubscriber wssConnection; + std::function notifyCb; + std::function closeCb; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/include/subscriptions/bookSide.hpp b/include/subscriptions/bookSide.hpp deleted file mode 100644 index c679bc5..0000000 --- a/include/subscriptions/bookSide.hpp +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include - -#include "mango_v3.hpp" -#include "orderbook/order.hpp" -#include "solana.hpp" -#include "wssSubscriber.hpp" - -namespace mango_v3 { -namespace subscription { - -using json = nlohmann::json; - -class bookSide { - public: - bookSide(Side side, const std::string& account) - : bookSide_(side), wssConnection(account) {} - - void registerUpdateCallback(std::function callback) { - notifyCb = callback; - } - - void registerCloseCallback(std::function callback) { - closeCb = callback; - } - - void subscribe() { - wssConnection.registerOnMessageCallback( - std::bind(&bookSide::onMessage, this, std::placeholders::_1)); - wssConnection.registerOnCloseCallback(std::bind(&bookSide::onClose, this)); - wssConnection.start(); - } - - book::order getBestOrder() const { return bookSide_.getBestOrder(); } - - uint64_t getVolume(uint64_t price) const { - return bookSide_.getVolume(price); - } - - private: - void onClose() { - if (closeCb) { - closeCb(); - } - } - - void onMessage(const json& parsedMsg) { - // ignore subscription confirmation - const auto itResult = parsedMsg.find("result"); - if (itResult != parsedMsg.end()) { - spdlog::info("on_result {}", parsedMsg.dump()); - return; - } - - const std::string encoded = - parsedMsg["params"]["result"]["value"]["data"][0]; - - const std::string decoded = solana::b64decode(encoded); - if (bookSide_.update(decoded)) { - if (notifyCb) { - notifyCb(); - } - } - } - - wssSubscriber wssConnection; - BookSide bookSide_; - std::function notifyCb; - std::function closeCb; -}; -} // namespace subscription -} // namespace mango_v3 diff --git a/include/subscriptions/orderbook.hpp b/include/subscriptions/orderbook.hpp index 59a0744..8878893 100644 --- a/include/subscriptions/orderbook.hpp +++ b/include/subscriptions/orderbook.hpp @@ -3,27 +3,27 @@ #include -#include "bookSide.hpp" -#include "orderbook/levelOne.hpp" -#include "orderbook/order.hpp" +#include "mango_v3.hpp" namespace mango_v3 { namespace subscription { class orderbook { public: orderbook(const std::string& bidsAccount, const std::string& asksAccount) - : bids(Buy, bidsAccount), asks(Sell, asksAccount) { + : bids(bidsAccount, Buy), asks(asksAccount, Sell) { bids.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); asks.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); } - void registerUpdateCallback(std::function callback) { - onUpdateCb = callback; + template + void registerUpdateCallback(func&& callback) { + onUpdateCb = std::forward(callback); } - void registerCloseCallback(std::function callback) { + template + void registerCloseCallback(func&& callback) { bids.registerCloseCallback(callback); - asks.registerCloseCallback(callback); + asks.registerCloseCallback(std::forward(callback)); } void subscribe() { @@ -32,9 +32,9 @@ class orderbook { } void updateCallback() { - book::levelOne newL1; - auto bestBid = bids.getBestOrder(); - auto bestAsk = asks.getBestOrder(); + L1Orderbook newL1; + auto bestBid = bids.getAccount()->getBestOrder(); + auto bestAsk = asks.getAccount()->getBestOrder(); newL1.highestBid = bestBid.price; newL1.highestBidSize = bestBid.quantity; newL1.lowestAsk = bestAsk.price; @@ -44,7 +44,7 @@ class orderbook { newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; newL1.spreadBps = ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; - level1 = std::make_shared(std::move(newL1)); + level1 = std::make_shared(std::move(newL1)); onUpdateCb(); } } @@ -53,14 +53,15 @@ class orderbook { auto getDepth(int8_t percent) { auto price = (level1->midPoint * (100 + percent)) / 100; - return (percent > 0) ? asks.getVolume(price) : bids.getVolume(price); + return (percent > 0) ? asks.getAccount()->getVolume(price) + : bids.getAccount()->getVolume(price); } private: - std::shared_ptr level1 = std::make_shared(); + std::shared_ptr level1 = std::make_shared(); std::function onUpdateCb; - subscription::bookSide bids; - subscription::bookSide asks; + subscription::accountSubscriber bids; + subscription::accountSubscriber asks; }; } // namespace subscription } // namespace mango_v3 diff --git a/include/subscriptions/trades.hpp b/include/subscriptions/trades.hpp deleted file mode 100644 index 8844c6e..0000000 --- a/include/subscriptions/trades.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include - -#include -#include - -#include "mango_v3.hpp" -#include "solana.hpp" -#include "wssSubscriber.hpp" - -namespace mango_v3 { -namespace subscription { - -using json = nlohmann::json; - -class trades { - public: - trades(const std::string &account) : wssConnection(account) {} - - void registerUpdateCallback(std::function callback) { - notifyCb = callback; - } - - void registerCloseCallback(std::function callback) { - closeCb = callback; - } - - void subscribe() { - wssConnection.registerOnMessageCallback( - std::bind(&trades::onMessage, this, std::placeholders::_1)); - wssConnection.registerOnCloseCallback(std::bind(&trades::onClose, this)); - wssConnection.start(); - } - - auto getLastTrade() const { return latestTrade; } - - private: - void onClose() { - if (closeCb) { - closeCb(); - } - } - - void onMessage(const json &parsedMsg) { - // ignore subscription confirmation - const auto itResult = parsedMsg.find("result"); - if (itResult != parsedMsg.end()) { - spdlog::info("on_result {}", parsedMsg.dump()); - return; - } - - // all other messages are event queue updates - const std::string method = parsedMsg["method"]; - const int subscription = parsedMsg["params"]["subscription"]; - const int slot = parsedMsg["params"]["result"]["context"]["slot"]; - const std::string data = parsedMsg["params"]["result"]["value"]["data"][0]; - - const auto decoded = solana::b64decode(data); - const auto events = reinterpret_cast(decoded.data()); - const auto seqNumDiff = events->header.seqNum - lastSeqNum; - const auto lastSlot = - (events->header.head + events->header.count) % EVENT_QUEUE_SIZE; - - bool gotLatest = false; - if (events->header.seqNum > lastSeqNum) { - for (int offset = seqNumDiff; offset > 0; --offset) { - const auto slot = - (lastSlot - offset + EVENT_QUEUE_SIZE) % EVENT_QUEUE_SIZE; - const auto &event = events->items[slot]; - - if (event.eventType == EventType::Fill) { - const auto &fill = (FillEvent &)event; - latestTrade = std::make_shared(fill.price); - gotLatest = true; - } - // no break; let's iterate to the last fill to get the latest fill order - } - } - - if (gotLatest) { - if (notifyCb) { - notifyCb(); - } - } - lastSeqNum = events->header.seqNum; - } - - uint64_t lastSeqNum = INT_MAX; - std::shared_ptr latestTrade = std::make_shared(0); - wssSubscriber wssConnection; - std::function notifyCb; - std::function closeCb; -}; -} // namespace subscription -} // namespace mango_v3 From 400efea93d1c8cdfebcd73df1346dc072f118895 Mon Sep 17 00:00:00 2001 From: papadpickle <99397192+papadpickle@users.noreply.github.com> Date: Fri, 1 Apr 2022 17:50:30 +0200 Subject: [PATCH 08/20] minor interface change --- examples/orderbookSubscribe.cpp | 30 +++++++++++++++++------------ include/mango_v3.hpp | 2 +- include/subscriptions/orderbook.hpp | 12 ++++++++---- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index 8000b88..f2ceb12 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -31,19 +31,25 @@ class updateLogger { void logUpdate() { std::scoped_lock lock(logMtx); auto level1Snapshot = orderbook.getLevel1(); - if (level1Snapshot->valid()) { - auto latestTrade = trades.getAccount()->getLastTrade(); - spdlog::info("============Update============"); - spdlog::info("Latest trade: {}", - *latestTrade ? to_string(*latestTrade) : "not received yet"); - spdlog::info("Bid-Ask {}-{}", level1Snapshot->highestBid, - level1Snapshot->lowestAsk); - spdlog::info("MidPrice: {}", level1Snapshot->midPoint); - spdlog::info("Spread: {0:.2f} bps", level1Snapshot->spreadBps); + if (level1Snapshot) { + if (level1Snapshot->valid()) { + spdlog::info("============Update============"); + auto latestTrade = trades.getAccount()->getLastTrade(); + if (latestTrade) { + spdlog::info("Latest trade: {}", to_string(*latestTrade)); + } else { + spdlog::info("Latest trade not yet received"); + } + spdlog::info("Bid-Ask {}-{}", level1Snapshot->highestBid, + level1Snapshot->lowestAsk); + spdlog::info("MidPrice: {}", level1Snapshot->midPoint); + spdlog::info("Spread: {0:.2f} bps", level1Snapshot->spreadBps); - constexpr auto depth = 2; - spdlog::info("Market depth -{}%: {}", depth, orderbook.getDepth(-depth)); - spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); + constexpr auto depth = 2; + spdlog::info("Market depth -{}%: {}", depth, + orderbook.getDepth(-depth)); + spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); + } } } diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index f542a3d..a31f11a 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -441,7 +441,7 @@ class Trades { private: uint64_t lastSeqNum = INT_MAX; - std::shared_ptr latestTrade = std::make_shared(0); + std::shared_ptr latestTrade; }; #pragma pack(pop) diff --git a/include/subscriptions/orderbook.hpp b/include/subscriptions/orderbook.hpp index 8878893..2b2fc0f 100644 --- a/include/subscriptions/orderbook.hpp +++ b/include/subscriptions/orderbook.hpp @@ -52,13 +52,17 @@ class orderbook { auto getLevel1() const { return level1; } auto getDepth(int8_t percent) { - auto price = (level1->midPoint * (100 + percent)) / 100; - return (percent > 0) ? asks.getAccount()->getVolume(price) - : bids.getAccount()->getVolume(price); + uint64_t depth = 0; + if (level1) { + auto price = (level1->midPoint * (100 + percent)) / 100; + depth = (percent > 0) ? asks.getAccount()->getVolume(price) + : bids.getAccount()->getVolume(price); + } + return depth; } private: - std::shared_ptr level1 = std::make_shared(); + std::shared_ptr level1; std::function onUpdateCb; subscription::accountSubscriber bids; subscription::accountSubscriber asks; From 3a514b0ca93d41f1aa76c34a5e87000e5e332397 Mon Sep 17 00:00:00 2001 From: clang-format Date: Fri, 1 Apr 2022 16:08:05 +0000 Subject: [PATCH 09/20] style fixup --- include/subscriptions/orderbook.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/subscriptions/orderbook.hpp b/include/subscriptions/orderbook.hpp index 2b2fc0f..1cf7454 100644 --- a/include/subscriptions/orderbook.hpp +++ b/include/subscriptions/orderbook.hpp @@ -56,7 +56,7 @@ class orderbook { if (level1) { auto price = (level1->midPoint * (100 + percent)) / 100; depth = (percent > 0) ? asks.getAccount()->getVolume(price) - : bids.getAccount()->getVolume(price); + : bids.getAccount()->getVolume(price); } return depth; } From d014f4c0c9b1f0bf106771cbd8af1abb6ee3346f Mon Sep 17 00:00:00 2001 From: papadpickle <99397192+papadpickle@users.noreply.github.com> Date: Thu, 19 May 2022 13:52:25 +0200 Subject: [PATCH 10/20] merged upstream changes and minor improvement --- examples/orderbookSubscribe.cpp | 1 + include/mango_v3.hpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index f2ceb12..cb975ef 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -85,6 +85,7 @@ int main() { updateLogger logger(book, trades); logger.start(); + using namespace std::literals::chrono_literals; while (true) { std::this_thread::sleep_for(100s); } diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 31d7f52..465ee33 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -385,7 +385,7 @@ class BookSide { reinterpret_cast(&(*iter)); const auto now = std::chrono::system_clock::now(); const auto nowUnix = - chrono::duration_cast(now.time_since_epoch()) + std::chrono::duration_cast(now.time_since_epoch()) .count(); const auto isValid = !leafNode->timeInForce || @@ -590,4 +590,4 @@ solana::Instruction cancelAllPerpOrdersInstruction( #pragma pack(pop) -} // namespace mango_v3 \ No newline at end of file +} // namespace mango_v3 From 17946c9c16076d146b2d06c753e9a9e7b5bc3391 Mon Sep 17 00:00:00 2001 From: clang-format Date: Thu, 19 May 2022 11:55:41 +0000 Subject: [PATCH 11/20] style fixup --- include/mango_v3.hpp | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 465ee33..5a92d14 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -330,7 +330,7 @@ class BookSide { public: BookSide(Side side) : side(side) {} - bool handleMsg(const nlohmann::json &msg) { + bool handleMsg(const nlohmann::json& msg) { // ignore subscription confirmation const auto itResult = msg.find("result"); if (itResult != msg.end()) { @@ -359,7 +359,7 @@ class BookSide { uint64_t getVolume(uint64_t price) const { Op operation; uint64_t volume = 0; - for (auto &&order : *orders) { + for (auto&& order : *orders) { if (operation(order.price, price)) { volume += order.quantity; } else { @@ -382,11 +382,11 @@ class BookSide { while (iter.stack.size() > 0) { if ((*iter).tag == NodeType::LeafNode) { const auto leafNode = - reinterpret_cast(&(*iter)); + reinterpret_cast(&(*iter)); const auto now = std::chrono::system_clock::now(); - const auto nowUnix = - std::chrono::duration_cast(now.time_since_epoch()) - .count(); + const auto nowUnix = std::chrono::duration_cast( + now.time_since_epoch()) + .count(); const auto isValid = !leafNode->timeInForce || leafNode->timestamp + leafNode->timeInForce < nowUnix; @@ -417,29 +417,29 @@ class BookSide { struct iterator { Side side; - const BookSideRaw &bookSide; + const BookSideRaw& bookSide; std::stack stack; uint32_t left, right; - iterator(Side side, const BookSideRaw &bookSide) + iterator(Side side, const BookSideRaw& bookSide) : side(side), bookSide(bookSide) { stack.push(bookSide.rootNode); left = side == Side::Buy ? 1 : 0; right = side == Side::Buy ? 0 : 1; } - bool operator==(const iterator &other) const { + bool operator==(const iterator& other) const { return &bookSide == &other.bookSide && stack.top() == other.stack.top(); } - iterator &operator++() { + iterator& operator++() { if (stack.size() > 0) { - const auto &elem = **this; + const auto& elem = **this; stack.pop(); if (elem.tag == NodeType::InnerNode) { const auto innerNode = - reinterpret_cast(&elem); + reinterpret_cast(&elem); stack.push(innerNode->children[right]); stack.push(innerNode->children[left]); } @@ -447,7 +447,7 @@ class BookSide { return *this; } - const AnyNode &operator*() const { return bookSide.nodes[stack.top()]; } + const AnyNode& operator*() const { return bookSide.nodes[stack.top()]; } }; }; @@ -461,7 +461,7 @@ class Trades { public: auto getLastTrade() const { return latestTrade; } - bool handleMsg(const nlohmann::json &msg) { + bool handleMsg(const nlohmann::json& msg) { // ignore subscription confirmation const auto itResult = msg.find("result"); if (itResult != msg.end()) { @@ -475,7 +475,7 @@ class Trades { const std::string data = msg["params"]["result"]["value"]["data"][0]; const auto decoded = solana::b64decode(data); - const auto events = reinterpret_cast(decoded.data()); + const auto events = reinterpret_cast(decoded.data()); const auto seqNumDiff = events->header.seqNum - lastSeqNum; const auto lastSlot = (events->header.head + events->header.count) % EVENT_QUEUE_SIZE; @@ -485,10 +485,10 @@ class Trades { for (int offset = seqNumDiff; offset > 0; --offset) { const auto slot = (lastSlot - offset + EVENT_QUEUE_SIZE) % EVENT_QUEUE_SIZE; - const auto &event = events->items[slot]; + const auto& event = events->items[slot]; if (event.eventType == EventType::Fill) { - const auto &fill = (FillEvent &)event; + const auto& fill = (FillEvent&)event; latestTrade = std::make_shared(fill.price); gotLatest = true; } From a8b9e1a7e902425f56ffd514989ad9882d50d557 Mon Sep 17 00:00:00 2001 From: papadpickle Date: Sun, 22 May 2022 00:19:02 +0200 Subject: [PATCH 12/20] decoding the message at account subscriber before passing the data to type itself to interpret --- include/mango_v3.hpp | 97 ++++++++------------- include/subscriptions/accountSubscriber.hpp | 9 +- include/subscriptions/wssSubscriber.hpp | 4 +- 3 files changed, 46 insertions(+), 64 deletions(-) diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 2ae21f7..d981781 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -331,16 +331,41 @@ class BookSide { public: BookSide(Side side) : side(side) {} - bool handleMsg(const nlohmann::json& msg) { - // ignore subscription confirmation - const auto itResult = msg.find("result"); - if (itResult != msg.end()) { - return false; - } - - const std::string encoded = msg["params"]["result"]["value"]["data"][0]; - const std::string decoded = solana::b64decode(encoded); - return update(decoded); + bool update(const std::string& decoded) { + if (decoded.size() != sizeof(BookSideRaw)) { + throw std::runtime_error("invalid response length " + + std::to_string(decoded.size()) + " expected " + + std::to_string(sizeof(BookSideRaw))); + } + memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw)); + + auto iter = BookSide::BookSideRaw::iterator(side, *raw); + std::vector newOrders; + while (iter.stack.size() > 0) { + if ((*iter).tag == NodeType::LeafNode) { + const auto leafNode = + reinterpret_cast(&(*iter)); + const auto now = std::chrono::system_clock::now(); + const auto nowUnix = std::chrono::duration_cast( + now.time_since_epoch()) + .count(); + const auto isValid = + !leafNode->timeInForce || + leafNode->timestamp + leafNode->timeInForce < nowUnix; + if (isValid) { + newOrders.emplace_back((uint64_t)(leafNode->key >> 64), + leafNode->quantity); + } + } + ++iter; + } + + if (!newOrders.empty()) { + orders = std::make_shared>(std::move(newOrders)); + return true; + } else { + return false; + } } Order getBestOrder() const { @@ -370,43 +395,6 @@ class BookSide { return volume; } - bool update(const std::string decoded) { - if (decoded.size() != sizeof(BookSideRaw)) { - throw std::runtime_error("invalid response length " + - std::to_string(decoded.size()) + " expected " + - std::to_string(sizeof(BookSideRaw))); - } - memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw)); - - auto iter = BookSide::BookSideRaw::iterator(side, *raw); - std::vector newOrders; - while (iter.stack.size() > 0) { - if ((*iter).tag == NodeType::LeafNode) { - const auto leafNode = - reinterpret_cast(&(*iter)); - const auto now = std::chrono::system_clock::now(); - const auto nowUnix = std::chrono::duration_cast( - now.time_since_epoch()) - .count(); - const auto isValid = - !leafNode->timeInForce || - leafNode->timestamp + leafNode->timeInForce < nowUnix; - if (isValid) { - newOrders.emplace_back((uint64_t)(leafNode->key >> 64), - leafNode->quantity); - } - } - ++iter; - } - - if (!newOrders.empty()) { - orders = std::make_shared>(std::move(newOrders)); - return true; - } else { - return false; - } - } - struct BookSideRaw { MetaData metaData; uint64_t bumpIndex; @@ -462,20 +450,7 @@ class Trades { public: auto getLastTrade() const { return latestTrade; } - bool handleMsg(const nlohmann::json& msg) { - // ignore subscription confirmation - const auto itResult = msg.find("result"); - if (itResult != msg.end()) { - return false; - } - - // all other messages are event queue updates - const std::string method = msg["method"]; - const int subscription = msg["params"]["subscription"]; - const int slot = msg["params"]["result"]["context"]["slot"]; - const std::string data = msg["params"]["result"]["value"]["data"][0]; - - const auto decoded = solana::b64decode(data); + bool update(const std::string& decoded) { const auto events = reinterpret_cast(decoded.data()); const auto seqNumDiff = events->header.seqNum - lastSeqNum; const auto lastSlot = diff --git a/include/subscriptions/accountSubscriber.hpp b/include/subscriptions/accountSubscriber.hpp index 3c1fb34..ecd4640 100644 --- a/include/subscriptions/accountSubscriber.hpp +++ b/include/subscriptions/accountSubscriber.hpp @@ -39,7 +39,14 @@ class accountSubscriber { private: void onMessage(const nlohmann::json &msg) { - if (account->handleMsg(msg)) { + const auto itResult = msg.find("result"); + if (itResult != msg.end()) { + return; + } + + const std::string encoded = msg["params"]["result"]["value"]["data"][0]; + const std::string decoded = solana::b64decode(encoded); + if (account->update(decoded)) { if (notifyCb) { notifyCb(); } diff --git a/include/subscriptions/wssSubscriber.hpp b/include/subscriptions/wssSubscriber.hpp index d42e05e..3be4111 100644 --- a/include/subscriptions/wssSubscriber.hpp +++ b/include/subscriptions/wssSubscriber.hpp @@ -40,8 +40,8 @@ class wssSubscriber { } void start() { - c.set_access_channels(websocketpp::log::alevel::all); - c.init_asio(); + c.clear_access_channels(websocketpp::log::alevel::all); + c.init_asio(); c.set_tls_init_handler( websocketpp::lib::bind(&wssSubscriber::on_tls_init, this)); c.set_open_handler(websocketpp::lib::bind( From 322b78ca43a44db965ee3b48d53250e3199449e7 Mon Sep 17 00:00:00 2001 From: clang-format Date: Sat, 21 May 2022 22:22:14 +0000 Subject: [PATCH 13/20] style fixup --- include/mango_v3.hpp | 68 ++++++++++----------- include/subscriptions/accountSubscriber.hpp | 12 ++-- include/subscriptions/wssSubscriber.hpp | 2 +- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index d981781..ee5fed9 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -332,40 +332,40 @@ class BookSide { BookSide(Side side) : side(side) {} bool update(const std::string& decoded) { - if (decoded.size() != sizeof(BookSideRaw)) { - throw std::runtime_error("invalid response length " + - std::to_string(decoded.size()) + " expected " + - std::to_string(sizeof(BookSideRaw))); - } - memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw)); - - auto iter = BookSide::BookSideRaw::iterator(side, *raw); - std::vector newOrders; - while (iter.stack.size() > 0) { - if ((*iter).tag == NodeType::LeafNode) { - const auto leafNode = - reinterpret_cast(&(*iter)); - const auto now = std::chrono::system_clock::now(); - const auto nowUnix = std::chrono::duration_cast( - now.time_since_epoch()) - .count(); - const auto isValid = - !leafNode->timeInForce || - leafNode->timestamp + leafNode->timeInForce < nowUnix; - if (isValid) { - newOrders.emplace_back((uint64_t)(leafNode->key >> 64), - leafNode->quantity); - } - } - ++iter; - } - - if (!newOrders.empty()) { - orders = std::make_shared>(std::move(newOrders)); - return true; - } else { - return false; - } + if (decoded.size() != sizeof(BookSideRaw)) { + throw std::runtime_error("invalid response length " + + std::to_string(decoded.size()) + " expected " + + std::to_string(sizeof(BookSideRaw))); + } + memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw)); + + auto iter = BookSide::BookSideRaw::iterator(side, *raw); + std::vector newOrders; + while (iter.stack.size() > 0) { + if ((*iter).tag == NodeType::LeafNode) { + const auto leafNode = + reinterpret_cast(&(*iter)); + const auto now = std::chrono::system_clock::now(); + const auto nowUnix = std::chrono::duration_cast( + now.time_since_epoch()) + .count(); + const auto isValid = + !leafNode->timeInForce || + leafNode->timestamp + leafNode->timeInForce < nowUnix; + if (isValid) { + newOrders.emplace_back((uint64_t)(leafNode->key >> 64), + leafNode->quantity); + } + } + ++iter; + } + + if (!newOrders.empty()) { + orders = std::make_shared>(std::move(newOrders)); + return true; + } else { + return false; + } } Order getBestOrder() const { diff --git a/include/subscriptions/accountSubscriber.hpp b/include/subscriptions/accountSubscriber.hpp index ecd4640..3987705 100644 --- a/include/subscriptions/accountSubscriber.hpp +++ b/include/subscriptions/accountSubscriber.hpp @@ -39,13 +39,13 @@ class accountSubscriber { private: void onMessage(const nlohmann::json &msg) { - const auto itResult = msg.find("result"); - if (itResult != msg.end()) { - return; - } + const auto itResult = msg.find("result"); + if (itResult != msg.end()) { + return; + } - const std::string encoded = msg["params"]["result"]["value"]["data"][0]; - const std::string decoded = solana::b64decode(encoded); + const std::string encoded = msg["params"]["result"]["value"]["data"][0]; + const std::string decoded = solana::b64decode(encoded); if (account->update(decoded)) { if (notifyCb) { notifyCb(); diff --git a/include/subscriptions/wssSubscriber.hpp b/include/subscriptions/wssSubscriber.hpp index 3be4111..67e1349 100644 --- a/include/subscriptions/wssSubscriber.hpp +++ b/include/subscriptions/wssSubscriber.hpp @@ -41,7 +41,7 @@ class wssSubscriber { void start() { c.clear_access_channels(websocketpp::log::alevel::all); - c.init_asio(); + c.init_asio(); c.set_tls_init_handler( websocketpp::lib::bind(&wssSubscriber::on_tls_init, this)); c.set_open_handler(websocketpp::lib::bind( From f8a589f4239a4832191480ca2df6ea8b43719403 Mon Sep 17 00:00:00 2001 From: papadpickle Date: Sun, 22 May 2022 19:26:43 +0200 Subject: [PATCH 14/20] corrected expired order culling added bookdelay mechanism --- include/mango_v3.hpp | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index ee5fed9..603e7bd 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -329,7 +329,29 @@ struct L1Orderbook { class BookSide { public: - BookSide(Side side) : side(side) {} + BookSide(Side side, uint8_t maxBookDelay = 255) + : side(side), maxBookDelay(maxBookDelay) {} + + auto getMaxTimestamp() { + const auto now = std::chrono::system_clock::now(); + const auto nowUnix = + std::chrono::duration_cast(now.time_since_epoch()) + .count(); + + auto maxTimestamp = nowUnix - maxBookDelay; + auto iter = BookSide::BookSideRaw::iterator(side, *raw); + while (iter.stack.size() > 0) { + if ((*iter).tag == NodeType::LeafNode) { + const auto leafNode = + reinterpret_cast(&(*iter)); + if (leafNode->timestamp > maxTimestamp) { + maxTimestamp = leafNode->timestamp; + } + } + ++iter; + } + return maxTimestamp; + } bool update(const std::string& decoded) { if (decoded.size() != sizeof(BookSideRaw)) { @@ -341,17 +363,16 @@ class BookSide { auto iter = BookSide::BookSideRaw::iterator(side, *raw); std::vector newOrders; + + const auto now = getMaxTimestamp(); + while (iter.stack.size() > 0) { if ((*iter).tag == NodeType::LeafNode) { const auto leafNode = reinterpret_cast(&(*iter)); - const auto now = std::chrono::system_clock::now(); - const auto nowUnix = std::chrono::duration_cast( - now.time_since_epoch()) - .count(); const auto isValid = !leafNode->timeInForce || - leafNode->timestamp + leafNode->timeInForce < nowUnix; + ((leafNode->timestamp + leafNode->timeInForce) > now); if (isValid) { newOrders.emplace_back((uint64_t)(leafNode->key >> 64), leafNode->quantity); @@ -441,6 +462,7 @@ class BookSide { }; const Side side; + uint8_t maxBookDelay; std::shared_ptr raw = std::make_shared(); std::shared_ptr> orders = std::make_shared>(); From b84d2c4a800561f18359af2206c37c751cba681b Mon Sep 17 00:00:00 2001 From: papadpickle Date: Sun, 22 May 2022 23:39:55 +0200 Subject: [PATCH 15/20] storing whole fill event in trades instead of only price. client can use the struct whichever way it wants --- examples/orderbookSubscribe.cpp | 9 +++++---- include/mango_v3.hpp | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index cb975ef..badf713 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -34,11 +34,12 @@ class updateLogger { if (level1Snapshot) { if (level1Snapshot->valid()) { spdlog::info("============Update============"); - auto latestTrade = trades.getAccount()->getLastTrade(); - if (latestTrade) { - spdlog::info("Latest trade: {}", to_string(*latestTrade)); + auto lastTrade = trades.getAccount()->getLastTrade(); + if (lastTrade) { + spdlog::info("Last trade: price {}, quantity {}", lastTrade->price, + lastTrade->quantity); } else { - spdlog::info("Latest trade not yet received"); + spdlog::info("No trade since the subscription started"); } spdlog::info("Bid-Ask {}-{}", level1Snapshot->highestBid, level1Snapshot->lowestAsk); diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 603e7bd..7548c2e 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -470,7 +470,7 @@ class BookSide { class Trades { public: - auto getLastTrade() const { return latestTrade; } + auto getLastTrade() const { return lastTrade; } bool update(const std::string& decoded) { const auto events = reinterpret_cast(decoded.data()); @@ -487,7 +487,7 @@ class Trades { if (event.eventType == EventType::Fill) { const auto& fill = (FillEvent&)event; - latestTrade = std::make_shared(fill.price); + lastTrade = std::make_shared(fill); gotLatest = true; } // no break; let's iterate to the last fill to get the latest fill order @@ -500,7 +500,7 @@ class Trades { private: uint64_t lastSeqNum = INT_MAX; - std::shared_ptr latestTrade; + std::shared_ptr lastTrade; }; #pragma pack(pop) From 155320fcd09b96d8b750ccaa79bc44140e48cdd8 Mon Sep 17 00:00:00 2001 From: papadpickle Date: Mon, 23 May 2022 00:27:32 +0200 Subject: [PATCH 16/20] using leafNode as order struct itself --- include/mango_v3.hpp | 30 ++++++++++++++--------------- include/subscriptions/orderbook.hpp | 22 +++++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 7548c2e..95e942b 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -307,12 +307,6 @@ struct FreeNode { uint8_t padding[BOOK_NODE_SIZE - 8]; }; -struct Order { - Order(uint64_t price, uint64_t quantity) : price(price), quantity(quantity) {} - uint64_t price = 0; - uint64_t quantity = 0; -}; - struct L1Orderbook { uint64_t highestBid = 0; uint64_t highestBidSize = 0; @@ -329,6 +323,9 @@ struct L1Orderbook { class BookSide { public: + using order_t = struct LeafNode; + using orders_t = std::vector; + BookSide(Side side, uint8_t maxBookDelay = 255) : side(side), maxBookDelay(maxBookDelay) {} @@ -362,7 +359,7 @@ class BookSide { memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw)); auto iter = BookSide::BookSideRaw::iterator(side, *raw); - std::vector newOrders; + orders_t newOrders; const auto now = getMaxTimestamp(); @@ -374,23 +371,23 @@ class BookSide { !leafNode->timeInForce || ((leafNode->timestamp + leafNode->timeInForce) > now); if (isValid) { - newOrders.emplace_back((uint64_t)(leafNode->key >> 64), - leafNode->quantity); + newOrders.push_back(*leafNode); } } ++iter; } if (!newOrders.empty()) { - orders = std::make_shared>(std::move(newOrders)); + orders = std::make_shared(std::move(newOrders)); return true; } else { return false; } } - Order getBestOrder() const { - return (!orders->empty()) ? orders->front() : Order(0, 0); + std::shared_ptr getBestOrder() const { + return orders->empty() ? nullptr + : std::make_shared(orders->front()); } uint64_t getVolume(uint64_t price) const { @@ -407,7 +404,8 @@ class BookSide { Op operation; uint64_t volume = 0; for (auto&& order : *orders) { - if (operation(order.price, price)) { + auto orderPrice = (uint64_t)(order.key >> 64); + if (operation(orderPrice, price)) { volume += order.quantity; } else { break; @@ -464,8 +462,7 @@ class BookSide { const Side side; uint8_t maxBookDelay; std::shared_ptr raw = std::make_shared(); - std::shared_ptr> orders = - std::make_shared>(); + std::shared_ptr orders = std::make_shared(); }; class Trades { @@ -490,7 +487,8 @@ class Trades { lastTrade = std::make_shared(fill); gotLatest = true; } - // no break; let's iterate to the last fill to get the latest fill order + // no break; let's iterate to the last fill to get the latest fill + // order } } diff --git a/include/subscriptions/orderbook.hpp b/include/subscriptions/orderbook.hpp index 1cf7454..2b2227e 100644 --- a/include/subscriptions/orderbook.hpp +++ b/include/subscriptions/orderbook.hpp @@ -35,17 +35,19 @@ class orderbook { L1Orderbook newL1; auto bestBid = bids.getAccount()->getBestOrder(); auto bestAsk = asks.getAccount()->getBestOrder(); - newL1.highestBid = bestBid.price; - newL1.highestBidSize = bestBid.quantity; - newL1.lowestAsk = bestAsk.price; - newL1.lowestAskSize = bestAsk.quantity; + if (bestBid && bestAsk) { + newL1.highestBid = (uint64_t)(bestBid->key >> 64); + newL1.highestBidSize = bestBid->quantity; + newL1.lowestAsk = (uint64_t)(bestAsk->key >> 64); + newL1.lowestAskSize = bestAsk->quantity; - if (newL1.valid()) { - newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; - newL1.spreadBps = - ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; - level1 = std::make_shared(std::move(newL1)); - onUpdateCb(); + if (newL1.valid()) { + newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; + newL1.spreadBps = + ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; + level1 = std::make_shared(std::move(newL1)); + onUpdateCb(); + } } } From b77315d61a4bb3f576e4346b7d5800b38e9433ca Mon Sep 17 00:00:00 2001 From: papadpickle Date: Wed, 25 May 2022 00:36:32 +0200 Subject: [PATCH 17/20] added NativeToUi conversion functionality minor improvements --- examples/orderbookSubscribe.cpp | 28 ++++++++++++++++++---------- include/mango_v3.hpp | 22 ++++++++++++++++++++++ include/subscriptions/orderbook.hpp | 5 +++-- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index badf713..8214b1d 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -15,8 +15,9 @@ class updateLogger { public: updateLogger( mango_v3::subscription::orderbook& orderbook, - mango_v3::subscription::accountSubscriber& trades) - : orderbook(orderbook), trades(trades) { + mango_v3::subscription::accountSubscriber& trades, + const mango_v3::NativeToUi& nativeToUi) + : orderbook(orderbook), trades(trades), nativeToUi(nativeToUi) { orderbook.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); trades.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); orderbook.registerCloseCallback(std::bind(&updateLogger::abort, this)); @@ -36,15 +37,18 @@ class updateLogger { spdlog::info("============Update============"); auto lastTrade = trades.getAccount()->getLastTrade(); if (lastTrade) { - spdlog::info("Last trade: price {}, quantity {}", lastTrade->price, - lastTrade->quantity); + spdlog::info("Last trade: price {:.2f}, quantity {}", + nativeToUi.getPrice(lastTrade->price), + nativeToUi.getQuantity(lastTrade->quantity)); } else { spdlog::info("No trade since the subscription started"); } - spdlog::info("Bid-Ask {}-{}", level1Snapshot->highestBid, - level1Snapshot->lowestAsk); - spdlog::info("MidPrice: {}", level1Snapshot->midPoint); - spdlog::info("Spread: {0:.2f} bps", level1Snapshot->spreadBps); + spdlog::info("Bid-Ask {:.2f}-{:.2f}", + nativeToUi.getPrice(level1Snapshot->highestBid), + nativeToUi.getPrice(level1Snapshot->lowestAsk)); + spdlog::info("MidPrice: {:.2f}", + nativeToUi.getPrice(level1Snapshot->midPoint)); + spdlog::info("Spread: {:.2f} bps", level1Snapshot->spreadBps); constexpr auto depth = 2; spdlog::info("Market depth -{}%: {}", depth, @@ -62,6 +66,7 @@ class updateLogger { private: mango_v3::subscription::orderbook& orderbook; mango_v3::subscription::accountSubscriber& trades; + const mango_v3::NativeToUi& nativeToUi; std::mutex logMtx; }; @@ -81,9 +86,12 @@ int main() { assert(market.mangoGroup.toBase58() == config.group); subscription::accountSubscriber trades(market.eventQueue.toBase58()); - subscription::orderbook book(market.bids.toBase58(), market.asks.toBase58()); + subscription::orderbook book(market); - updateLogger logger(book, trades); + mango_v3::NativeToUi nativeToUi(market.quoteLotSize, market.baseLotSize, + group.tokens[QUOTE_INDEX].decimals, + group.tokens[marketIndex].decimals); + updateLogger logger(book, trades, nativeToUi); logger.start(); using namespace std::literals::chrono_literals; diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 95e942b..e028862 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -321,6 +322,27 @@ struct L1Orderbook { } }; +class NativeToUi { + public: + NativeToUi(int64_t quoteLotSize, int64_t baseLotSize, uint8_t quoteDecimals, + uint8_t baseDecimals) { + baseLotsToUiConvertor = baseLotSize / std::pow(10, baseDecimals); + priceLotsToUiConvertor = std::pow(10, (baseDecimals - quoteDecimals)) * + quoteLotSize / baseLotSize; + } + + double getPrice(int64_t price) const { + return price * priceLotsToUiConvertor; + } + double getQuantity(int64_t quantity) const { + return quantity * baseLotsToUiConvertor; + } + + private: + double baseLotsToUiConvertor; + double priceLotsToUiConvertor; +}; + class BookSide { public: using order_t = struct LeafNode; diff --git a/include/subscriptions/orderbook.hpp b/include/subscriptions/orderbook.hpp index 2b2227e..c7b191d 100644 --- a/include/subscriptions/orderbook.hpp +++ b/include/subscriptions/orderbook.hpp @@ -9,8 +9,9 @@ namespace mango_v3 { namespace subscription { class orderbook { public: - orderbook(const std::string& bidsAccount, const std::string& asksAccount) - : bids(bidsAccount, Buy), asks(asksAccount, Sell) { + orderbook(const PerpMarket& perpMarket) + : bids(perpMarket.bids.toBase58(), Buy), + asks(perpMarket.asks.toBase58(), Sell) { bids.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); asks.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); } From d8860627d666810c38844316edd4971a322ab5f1 Mon Sep 17 00:00:00 2001 From: papadpickle Date: Mon, 30 May 2022 23:29:00 +0200 Subject: [PATCH 18/20] minor log adjustment --- examples/orderbookSubscribe.cpp | 2 +- include/mango_v3.hpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index 8214b1d..254f777 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -37,7 +37,7 @@ class updateLogger { spdlog::info("============Update============"); auto lastTrade = trades.getAccount()->getLastTrade(); if (lastTrade) { - spdlog::info("Last trade: price {:.2f}, quantity {}", + spdlog::info("Last trade: price {:.2f}, quantity {:.2f}", nativeToUi.getPrice(lastTrade->price), nativeToUi.getQuantity(lastTrade->quantity)); } else { diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index e028862..172f482 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -334,6 +334,7 @@ class NativeToUi { double getPrice(int64_t price) const { return price * priceLotsToUiConvertor; } + double getQuantity(int64_t quantity) const { return quantity * baseLotsToUiConvertor; } From 7ac77657417de19f84c5814f1ad447f695ab1d46 Mon Sep 17 00:00:00 2001 From: papadpickle Date: Sun, 5 Jun 2022 00:02:19 +0200 Subject: [PATCH 19/20] added utest for NativeToUi conversions --- examples/orderbookSubscribe.cpp | 6 ++-- tests/main.cpp | 57 ++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp index 254f777..6420824 100644 --- a/examples/orderbookSubscribe.cpp +++ b/examples/orderbookSubscribe.cpp @@ -43,9 +43,11 @@ class updateLogger { } else { spdlog::info("No trade since the subscription started"); } - spdlog::info("Bid-Ask {:.2f}-{:.2f}", + spdlog::info("Bid-Ask ({:.2f}) {:.2f}-{:.2f} ({:.2f})", + nativeToUi.getQuantity(level1Snapshot->highestBidSize), nativeToUi.getPrice(level1Snapshot->highestBid), - nativeToUi.getPrice(level1Snapshot->lowestAsk)); + nativeToUi.getPrice(level1Snapshot->lowestAsk), + nativeToUi.getQuantity(level1Snapshot->lowestAskSize)); spdlog::info("MidPrice: {:.2f}", nativeToUi.getPrice(level1Snapshot->midPoint)); spdlog::info("Spread: {:.2f} bps", level1Snapshot->spreadBps); diff --git a/tests/main.cpp b/tests/main.cpp index d0dd338..b92b250 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -630,4 +630,59 @@ TEST_CASE("account9") { auto leverage = mangoAccount.getLeverage(mangoGroup, mangoCache); CHECK_EQ(leverage.to_double(), 3.919442838288937); CHECK_FALSE(mangoAccount.isLiquidatable(mangoGroup, mangoCache)); -} \ No newline at end of file +} + +TEST_CASE("NativeToUi Conversions") { + using namespace mango_v3; + const auto& config = MAINNET; + const solana::rpc::Connection solCon; + const auto group = solCon.getAccountInfo(config.group); + + for (auto&& symbol : config.symbols) { + const auto marketIndex = (&symbol - &config.symbols[0]); + const auto perpMarketPk = group.perpMarkets[marketIndex].perpMarket; + /* todo: currently getAccountInfo throws invalid response length exception + * for following tokens, so filtering them*/ + if (symbol == "USDT" or symbol == "COPE" or symbol == "BNB" or + symbol == "USDC" or symbol == "") { + continue; + } + auto market = solCon.getAccountInfo(perpMarketPk.toBase58()); + + mango_v3::NativeToUi nativeToUi(market.quoteLotSize, market.baseLotSize, + group.tokens[QUOTE_INDEX].decimals, + group.tokens[marketIndex].decimals); + + if (symbol == "MNGO") { + CHECK_EQ(nativeToUi.getPrice(10000), 1.0); + CHECK_EQ(nativeToUi.getQuantity(10000), 10000); + } else if (symbol == "BTC") { + CHECK_EQ(nativeToUi.getPrice(10000), 1000); + CHECK_EQ(nativeToUi.getQuantity(10000), 1); + } else if (symbol == "ETH") { + CHECK_EQ(nativeToUi.getPrice(10000), 1000); + CHECK_EQ(nativeToUi.getQuantity(10000), 10); + } else if (symbol == "SOL") { + CHECK_EQ(nativeToUi.getPrice(10000), 100); + CHECK_EQ(nativeToUi.getQuantity(10000), 100); + } else if (symbol == "SRM") { + CHECK_EQ(nativeToUi.getPrice(10000), 10); + CHECK_EQ(nativeToUi.getQuantity(10000), 1000); + } else if (symbol == "RAY") { + CHECK_EQ(nativeToUi.getPrice(10000), 10); + CHECK_EQ(nativeToUi.getQuantity(10000), 1000); + } else if (symbol == "FTT") { + CHECK_EQ(nativeToUi.getPrice(10000), 10); + CHECK_EQ(nativeToUi.getQuantity(10000), 1000); + } else if (symbol == "MSOL") { + CHECK_EQ(nativeToUi.getPrice(10000), 1); + CHECK_EQ(nativeToUi.getQuantity(10000), 10000); + } else if (symbol == "AVAX") { + CHECK_EQ(nativeToUi.getPrice(10000), 1000); + CHECK_EQ(nativeToUi.getQuantity(10000), 10); + } else if (symbol == "LUNA") { + CHECK_EQ(nativeToUi.getPrice(10000), 100); + CHECK_EQ(nativeToUi.getQuantity(10000), 100); + } + } +} From 1ade8c21f9b42619038a185c8e3f2788bab265f3 Mon Sep 17 00:00:00 2001 From: papadpickle Date: Sun, 12 Jun 2022 23:45:29 +0200 Subject: [PATCH 20/20] strict concurrency mtxing --- include/mango_v3.hpp | 53 +++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 172f482..ed21c1b 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -352,35 +353,14 @@ class BookSide { BookSide(Side side, uint8_t maxBookDelay = 255) : side(side), maxBookDelay(maxBookDelay) {} - auto getMaxTimestamp() { - const auto now = std::chrono::system_clock::now(); - const auto nowUnix = - std::chrono::duration_cast(now.time_since_epoch()) - .count(); - - auto maxTimestamp = nowUnix - maxBookDelay; - auto iter = BookSide::BookSideRaw::iterator(side, *raw); - while (iter.stack.size() > 0) { - if ((*iter).tag == NodeType::LeafNode) { - const auto leafNode = - reinterpret_cast(&(*iter)); - if (leafNode->timestamp > maxTimestamp) { - maxTimestamp = leafNode->timestamp; - } - } - ++iter; - } - return maxTimestamp; - } - bool update(const std::string& decoded) { if (decoded.size() != sizeof(BookSideRaw)) { throw std::runtime_error("invalid response length " + std::to_string(decoded.size()) + " expected " + std::to_string(sizeof(BookSideRaw))); } + std::scoped_lock lock(updateMtx); memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw)); - auto iter = BookSide::BookSideRaw::iterator(side, *raw); orders_t newOrders; @@ -409,11 +389,13 @@ class BookSide { } std::shared_ptr getBestOrder() const { + std::scoped_lock lock(updateMtx); return orders->empty() ? nullptr : std::make_shared(orders->front()); } uint64_t getVolume(uint64_t price) const { + std::scoped_lock lock(updateMtx); if (side == Side::Buy) { return getVolume>(price); } else { @@ -422,11 +404,33 @@ class BookSide { } private: + long long getMaxTimestamp() { + const auto now = std::chrono::system_clock::now(); + const auto nowUnix = + std::chrono::duration_cast(now.time_since_epoch()) + .count(); + + auto maxTimestamp = nowUnix - maxBookDelay; + auto iter = BookSide::BookSideRaw::iterator(side, *raw); + while (iter.stack.size() > 0) { + if ((*iter).tag == NodeType::LeafNode) { + const auto leafNode = + reinterpret_cast(&(*iter)); + if (leafNode->timestamp > maxTimestamp) { + maxTimestamp = leafNode->timestamp; + } + } + ++iter; + } + return maxTimestamp; + } + template uint64_t getVolume(uint64_t price) const { Op operation; uint64_t volume = 0; - for (auto&& order : *orders) { + auto tmpOrders = *orders; + for (auto&& order : tmpOrders) { auto orderPrice = (uint64_t)(order.key >> 64); if (operation(orderPrice, price)) { volume += order.quantity; @@ -486,6 +490,7 @@ class BookSide { uint8_t maxBookDelay; std::shared_ptr raw = std::make_shared(); std::shared_ptr orders = std::make_shared(); + mutable std::mutex updateMtx; }; class Trades { @@ -493,6 +498,7 @@ class Trades { auto getLastTrade() const { return lastTrade; } bool update(const std::string& decoded) { + std::scoped_lock lock(updateMtx); const auto events = reinterpret_cast(decoded.data()); const auto seqNumDiff = events->header.seqNum - lastSeqNum; const auto lastSlot = @@ -522,6 +528,7 @@ class Trades { private: uint64_t lastSeqNum = INT_MAX; std::shared_ptr lastTrade; + std::mutex updateMtx; }; #pragma pack(pop)