Skip to content
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

Example orderbook subscribe sol. #19

Open
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

papadpickle
Copy link
Contributor

@papadpickle papadpickle commented Feb 27, 2022

this example subscribes to SOL perp market on-chain events for bids, asks and fill.
following info is logged at each update of orderbook as well as fill event:

  • latest trade
  • bid-ask values
  • midprice
  • spread in bps
  • market depth at +/-2%

note: in case no fill event received before an orderbook update, the latest trade will be empty. maybe there is a way to get the last traded price(mango-bowl recent trades maybe?), in that case we can add that in future.

snippet:
image

additions:

  • a subscription::wssSubscriber class, which can be/is reused to subscribe to given uri. callbacks can be registered by its clients to handle the incoming wss messages.
  • a subscription::bookSide class, which uses wssSubscriber to subscribe to bids and asks and offers its clients interfaces to get best prices and volume. additionaly it can notify the clients on each update via a callback.
  • an orderbook::book class which contains two bookSide instances representing bids and asks and offers its clients an interface to get the level1 info.
  • a subscription::trades class, which uses wssSubscriber to subscribe to market event queue and offers its clients interfaces to get last traded price. additionaly it can notify the clients on each update via a callback.
  • a orderbook::level1 and orderbook::order structs

minor improvements:

  • const correctness at solana api
  • added todo to use scoped enums at existing mango code, didnt change it here as this might require changes in other examples as well.

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

websocketpp::lib::error_code ec;
ws_client::connection_ptr con = c.get_connection(
"wss://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88", ec);
Copy link
Owner

Choose a reason for hiding this comment

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

add a new constant to mango.hpp

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yea, MAINNET.endpoint should work.

@mschneider
Copy link
Owner

[2022-03-01 20:24:16.874] [info] ============Update============
[2022-03-01 20:24:16.874] [info] Latest trade: 9880
[2022-03-01 20:24:16.874] [info] Bid-Ask 9879-9880
[2022-03-01 20:24:16.874] [info] MidPrice: 9879.5
[2022-03-01 20:24:16.874] [info] Spread: 1.01 bps
[2022-03-01 20:24:16.874] [info] Market depth -2%: 1458616
[2022-03-01 20:24:16.874] [info] Market depth +2%: 950953

output should probably display ui units instead of native units, i'll open a separate ticket for this

if (!newOrders.empty()) {
{
std::scoped_lock lock(ordersMtx);
orders = std::move(newOrders);
Copy link
Owner

@mschneider mschneider Mar 1, 2022

Choose a reason for hiding this comment

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

newOrders should already be sorted by price as the order book tree data structure is sorted by price

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok. will have to make sure though that the bidside and askside sorting is from midpoint to either ends.

leafNode->timestamp + leafNode->timeInForce < nowUnix;
if (isValid) {
newOrders.emplace_back((uint64_t)(leafNode->key >> 64),
leafNode->quantity);
Copy link
Owner

@mschneider mschneider Mar 1, 2022

Choose a reason for hiding this comment

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

i have a few issues with the way the code is organized:

  1. parsing the order tree should be independent from how the data is transported (rpc / ws), the code should hence not live under subscriptions
  2. same is true for getVolume
  3. each LeafNode actually contains a lot of valuable information that is discarded here in favor of the very incomplete order struct, this makes this code very hard to re-use (we have an order type in most sdks, it usually contains more data then the LeafNode, ui values for instance)
  4. i think there's value in a generic accountSubscriber<typename T> that stores an std::shared_ptr<T> so data can be accessed concurrently without the use of critical sections. just let a public method return new shared_ptr instances

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will ping you on discord to catch up here.

Copy link

@alexeyneu alexeyneu Sep 29, 2022

Choose a reason for hiding this comment

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

std::shared_ptr so data can be accessed concurrently

where only headers of shared_ptr are atomic. stuff it holds isn't.

@mschneider
Copy link
Owner

tried it locally and the code works well for a few minutes but then one by one the different sockets disconnect. once a single socket disconnects, aggregated data can be invalid (e.g. mid price, after one side of the book stopped updating)

@@ -135,8 +138,10 @@ struct EventQueueHeader {
uint64_t seqNum;
};

// todo: change to scoped enum class
Copy link
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?

Copy link
Contributor Author

@papadpickle papadpickle Mar 1, 2022

Choose a reason for hiding this comment

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

yes. #22

papadpickle and others added 3 commits March 2, 2022 00:42
@papadpickle
Copy link
Contributor Author

added changes to stop logging and exit in case of problem at any of the wss connections.
image

@papadpickle papadpickle force-pushed the example_orderbook_subscribe_sol branch from c4bca17 to 06c7e5d Compare March 5, 2022 17:41
@papadpickle
Copy link
Contributor Author

re your comment #19 (comment):

  1. parsing of the messages is moved to the classes(BookSide and Trade in mango_v3.hpp) out of subscriptions.
  2. handled at point 1
  3. this is first iteration of parsing the incoming messages and extracted only necessary info. We can fill up/edit the parsing and data struct as we go was my idea. You can create an issue here which can be handled later.
  4. I have refactored the code by adding AccountSubscriber class, removed unnecessary subscriber classes and replacing critical sections with shared_ptr.

I have one point about the structure of the code overall:
we can refactor the monolithic mango_v3.hpp into individual hpp and cpp files to speed up dev work. Now a single line change at those interfaces forces the recompilation of everything. Since we are anyway creating a static lib, we may as well go with seperate files instead of having one huge hpp.
Unless of course you decide to make it a header only lib.

}

private:
std::shared_ptr<L1Orderbook> level1 = std::make_shared<L1Orderbook>();
Copy link
Owner

Choose a reason for hiding this comment

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

nullptr makes sense here to differentiate between waiting for first data update vs empty book

Copy link
Contributor Author

Choose a reason for hiding this comment

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

makes sense. will change it.

if ((*iter).tag == NodeType::LeafNode) {
const auto leafNode =
reinterpret_cast<const struct LeafNode *>(&(*iter));
const auto now = std::chrono::system_clock::now();
Copy link
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
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.


private:
uint64_t lastSeqNum = INT_MAX;
std::shared_ptr<uint64_t> latestTrade = std::make_shared<uint64_t>(0);
Copy link
Owner

@mschneider mschneider Apr 1, 2022

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

// 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"];
Copy link
Owner

@mschneider mschneider Apr 1, 2022

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

public:
BookSide(Side side) : side(side) {}

bool handleMsg(const nlohmann::json &msg) {
Copy link
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
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.

@papadpickle
Copy link
Contributor Author

updated the PR with following changes:

  • Example orderbook subscribe sol. #19 (comment) -> decoding in the account subscriber itself.
  • handling bookTimeDelay to filter expired orders
  • using FillEvent struct for trades.
  • using LeafNode struct as order struct
  • minor improvements

std::to_string(decoded.size()) + " expected " +
std::to_string(sizeof(BookSideRaw)));
}
memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw));
Copy link
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.

uint64_t getVolume(uint64_t price) const {
Op operation;
uint64_t volume = 0;
for (auto&& order : *orders) {
Copy link
Owner

Choose a reason for hiding this comment

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

orders needs to be dereferenced once before entering loop


try {
websocketpp::lib::error_code ec;
ws_client::connection_ptr con = c.get_connection(MAINNET.endpoint, ec);
Copy link
Collaborator

Choose a reason for hiding this comment

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

MAINNET.endpoint should not be statically passed but we should create an endpoint argument in the constructor.

Copy link

@alexeyneu alexeyneu Oct 10, 2022

Choose a reason for hiding this comment

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

again:

std::shared_ptr so data can be accessed concurrently

if it's so it's a bad thing. Is it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create advanced example to fetch orderbook via websocket
4 participants