Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8629f99
squashed:
papadpickle Feb 22, 2022
8d04f8a
* using MAINNET.endpoint instead of magic numbers
papadpickle Mar 1, 2022
587e1aa
* added on_close callbacks to handle error in wss connections and sto…
papadpickle Mar 2, 2022
46f206a
style fixup
Mar 2, 2022
2ade336
replaced mtx with shared_ptr trick
papadpickle Mar 4, 2022
3671454
style fixup
Mar 4, 2022
06c7e5d
* added accountSubscriber template class
papadpickle Mar 5, 2022
400efea
minor interface change
papadpickle Apr 1, 2022
2efe235
Merge branch 'main' into example_orderbook_subscribe_sol
papadpickle Apr 1, 2022
3a514b0
style fixup
Apr 1, 2022
c644ad2
Merge branch 'main' into example_orderbook_subscribe_sol
papadpickle May 19, 2022
d014f4c
merged upstream changes and minor improvement
papadpickle May 19, 2022
17946c9
style fixup
May 19, 2022
701d150
merge upstream
papadpickle May 21, 2022
a8b9e1a
decoding the message at account subscriber before passing the data to…
papadpickle May 21, 2022
322b78c
style fixup
May 21, 2022
f8a589f
corrected expired order culling
papadpickle May 22, 2022
b84d2c4
storing whole fill event in trades instead of only price. client can …
papadpickle May 22, 2022
155320f
using leafNode as order struct itself
papadpickle May 22, 2022
e625ded
Merge branch 'mschneider:main' into origin/example_orderbook_subscrib…
papadpickle May 22, 2022
b77315d
added NativeToUi conversion functionality
papadpickle May 24, 2022
d886062
minor log adjustment
papadpickle May 30, 2022
7ac7765
added utest for NativeToUi conversions
papadpickle Jun 4, 2022
1ade8c2
strict concurrency mtxing
papadpickle Jun 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
target_link_libraries(example-place-order ${CONAN_LIBS} sol)
target_link_libraries(example-orderbook-subscribe ${CONAN_LIBS} sol)
87 changes: 87 additions & 0 deletions examples/orderbookSubscribe.cpp
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;
}
247 changes: 247 additions & 0 deletions include/mango_v3.hpp
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"
Expand All @@ -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;
Expand Down Expand Up @@ -135,8 +139,10 @@ struct EventQueueHeader {
uint64_t seqNum;
};

// todo: change to scoped enum class

Copy link
Copy Markdown
Owner

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?

@papadpickle papadpickle Mar 1, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. #22

enum EventType : uint8_t { Fill, Out, Liquidate };

// todo: change to scoped enum class
enum Side : uint8_t { Buy, Sell };

struct AnyEvent {
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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?
first four lines could be moved one layer below to wss subscription code in case we know that each msg has the "result" object.
the other parsing code must be handled by the account itself.

// 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));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs critical section from before memcpy to end of iterator lifetime.
concurrent update calls else could cause memory corruption.
another option would be to not update the same buffer and to swap references to a new buffer, so that iterator can still iterate over the "old buffer" and de-allocate it on iterator destruction.


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();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you dont want to recalculate the current time inside the loop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const auto now = std::chrono::system_clock::now();

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"];

@mschneider mschneider Apr 1, 2022

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

const int slot = parsedMsg["params"]["result"]["context"]["slot"];

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);

@mschneider mschneider Apr 1, 2022

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
event based synchronization between getLastTrade and handleMsg.
can change to nullptr though so client of getLastTrade could parse the validity of returned value.

};

#pragma pack(pop)

// instructions are even tighter packed, every byte counts
Expand Down
Loading