-
Notifications
You must be signed in to change notification settings - Fork 20
Example orderbook subscribe sol. #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
8629f99
8d04f8a
587e1aa
46f206a
2ade336
3671454
06c7e5d
400efea
2efe235
3a514b0
c644ad2
d014f4c
17946c9
701d150
a8b9e1a
322b78c
f8a589f
b84d2c4
155320f
e625ded
b77315d
d886062
7ac7765
1ade8c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| #include <cpr/cpr.h> | ||
| #include <spdlog/spdlog.h> | ||
|
|
||
| #include <chrono> | ||
| #include <mutex> | ||
| #include <websocketpp/client.hpp> | ||
| #include <websocketpp/config/asio_client.hpp> | ||
|
|
||
| #include "mango_v3.hpp" | ||
| #include "solana.hpp" | ||
| #include "subscriptions/accountSubscriber.hpp" | ||
| #include "subscriptions/orderbook.hpp" | ||
|
|
||
| class updateLogger { | ||
| public: | ||
| updateLogger( | ||
| mango_v3::subscription::orderbook& orderbook, | ||
| mango_v3::subscription::accountSubscriber<mango_v3::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::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.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); | ||
|
|
||
| constexpr auto depth = 2; | ||
| spdlog::info("Market depth -{}%: {}", depth, orderbook.getDepth(-depth)); | ||
| spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); | ||
| } | ||
| } | ||
|
|
||
| void abort() { | ||
| spdlog::error("websocket subscription error"); | ||
| throw std::runtime_error("websocket subscription error"); | ||
| } | ||
|
|
||
| private: | ||
| mango_v3::subscription::orderbook& orderbook; | ||
| mango_v3::subscription::accountSubscriber<mango_v3::Trades>& trades; | ||
| std::mutex logMtx; | ||
| }; | ||
|
|
||
| int main() { | ||
| using namespace mango_v3; | ||
| const auto& config = MAINNET; | ||
| const solana::rpc::Connection solCon; | ||
| const auto group = solCon.getAccountInfo<MangoGroup>(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<PerpMarket>(perpMarketPk.toBase58()); | ||
| assert(market.mangoGroup.toBase58() == config.group); | ||
|
|
||
| subscription::accountSubscriber<Trades> 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(100s); | ||
| } | ||
|
|
||
| return 0; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||
| #pragma once | ||||
|
|
||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| #include <stack> | ||||
| #include <string> | ||||
|
|
||||
| #include "fixedp.h" | ||||
|
|
@@ -15,6 +17,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 +139,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 +203,247 @@ 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 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) { | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should really live in the websocket subscription code, rather than the account type. in fact the code should work the same for all types of account subscriptions.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the first four lines to check json "result" or the whole of msg handling? |
||||
| // 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<std::greater_equal<uint64_t>>(price); | ||||
| } else { | ||||
| return getVolume<std::less_equal<uint64_t>>(price); | ||||
| } | ||||
| } | ||||
|
|
||||
| private: | ||||
| template <typename Op> | ||||
| 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)); | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. needs critical section from before memcpy to end of iterator lifetime. |
||||
|
|
||||
| auto iter = BookSide::BookSideRaw::iterator(side, *raw); | ||||
| std::vector<Order> newOrders; | ||||
| while (iter.stack.size() > 0) { | ||||
| if ((*iter).tag == NodeType::LeafNode) { | ||||
| const auto leafNode = | ||||
| reinterpret_cast<const struct LeafNode *>(&(*iter)); | ||||
| const auto now = std::chrono::system_clock::now(); | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you dont want to recalculate the current time inside the loop
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line 36 in 9deb4c9
mango gravy from the initial example lol. pls create a seperate bug/issue for this. |
||||
| const auto nowUnix = | ||||
| chrono::duration_cast<std::chrono::seconds>(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::vector<Order>>(std::move(newOrders)); | ||||
| return true; | ||||
| } else { | ||||
| return false; | ||||
| } | ||||
| } | ||||
|
|
||||
| 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<uint32_t> 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<const struct InnerNode *>(&elem); | ||||
| stack.push(innerNode->children[right]); | ||||
| stack.push(innerNode->children[left]); | ||||
| } | ||||
| } | ||||
| return *this; | ||||
| } | ||||
|
|
||||
| const AnyNode &operator*() const { return bookSide.nodes[stack.top()]; } | ||||
| }; | ||||
| }; | ||||
|
|
||||
| const Side side; | ||||
| std::shared_ptr<BookSideRaw> raw = std::make_shared<BookSideRaw>(); | ||||
| std::shared_ptr<std::vector<Order>> orders = | ||||
| std::make_shared<std::vector<Order>>(); | ||||
| }; | ||||
|
|
||||
| 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"]; | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not correct, slots here are related to the chain consensus not the ringbuffer
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well the gravy for mango related code was taken from the original examples from the first commit. solcpp/examples/accountSubscribe.cpp Line 69 in dd58f26
you can create a bug/issue for this. out of scope for this imo. |
||||
| const std::string data = msg["params"]["result"]["value"]["data"][0]; | ||||
|
|
||||
| const auto decoded = solana::b64decode(data); | ||||
| const auto events = reinterpret_cast<const EventQueue *>(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<uint64_t>(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<uint64_t> latestTrade = std::make_shared<uint64_t>(0); | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not just an uint64_t? trade at price 0 is impossible after all
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will need a mtx/atomic otherwise. same reason why we replaced all mtx'es with shared ptrs. |
||||
| }; | ||||
|
|
||||
| #pragma pack(pop) | ||||
|
|
||||
| // instructions are even tighter packed, every byte counts | ||||
|
|
||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, want to open a new issue?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes. #22