Skip to content

Commit c707efd

Browse files
committed
Add market_data_websocket example and update CMake configuration; enhance test discovery timeout
1 parent c1bc42c commit c707efd

4 files changed

Lines changed: 176 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [unreleased]
1111

12+
### Added
13+
- Add a `market_data_websocket` example that subscribes to public testnet `allMids` and per-coin `l2Book` WebSocket updates for one or more coins.
14+
1215
### Changed
1316
- Normalize line ending to LF
1417
- Move test CMake setup into `tests/CMakeLists.txt`, register GoogleTest cases with `gtest_discover_tests()`, and keep CI integration tests running as a single executable to avoid repeated testnet setup per discovered test case.
18+
- Defer GoogleTest discovery to CTest with a longer discovery timeout so test executables are not run during the build.
1519
- Stabilize the WebSocket partial-unsubscribe integration test by waiting for the next `allMids` event instead of assuming one arrives within a fixed one-second sleep.
1620

1721
## [0.1.2] - 2026-06-09

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ target_link_libraries(hyperliquid PUBLIC
5353
if(HYPERLIQUID_BUILD_EXAMPLES)
5454
add_executable(basic_order examples/basic_order.cpp)
5555
target_link_libraries(basic_order PRIVATE hyperliquid)
56+
57+
add_executable(market_data_websocket examples/market_data_websocket.cpp)
58+
target_link_libraries(market_data_websocket PRIVATE hyperliquid)
5659
endif()
5760

5861
if(HYPERLIQUID_BUILD_TESTS)

examples/market_data_websocket.cpp

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// WebSocket market data example: subscribe to public testnet updates.
2+
// Usage: market_data_websocket [coin ...] [--seconds N]
3+
// Defaults: coins=ETH, seconds=30
4+
5+
#include <hyperliquid/hyperliquid.hpp>
6+
7+
#include <atomic>
8+
#include <chrono>
9+
#include <cstdlib>
10+
#include <iostream>
11+
#include <memory>
12+
#include <mutex>
13+
#include <stdexcept>
14+
#include <string>
15+
#include <thread>
16+
#include <vector>
17+
18+
namespace {
19+
20+
struct Options {
21+
std::vector<std::string> coins{"ETH"};
22+
int seconds = 30;
23+
};
24+
25+
int parse_positive_int(const std::string& value) {
26+
std::size_t parsed = 0;
27+
const int result = std::stoi(value, &parsed);
28+
if (parsed != value.size() || result <= 0)
29+
throw std::invalid_argument("expected a positive integer");
30+
return result;
31+
}
32+
33+
Options parse_options(int argc, char* argv[]) {
34+
Options options;
35+
std::vector<std::string> coins;
36+
37+
for (int i = 1; i < argc; ++i) {
38+
const std::string arg = argv[i];
39+
if (arg == "--seconds") {
40+
if (i + 1 >= argc)
41+
throw std::invalid_argument("--seconds requires a value");
42+
options.seconds = parse_positive_int(argv[++i]);
43+
} else {
44+
coins.push_back(arg);
45+
}
46+
}
47+
48+
if (!coins.empty())
49+
options.coins = std::move(coins);
50+
return options;
51+
}
52+
53+
void print_top_of_book(const nlohmann::json& msg, const std::string& coin) {
54+
if (!msg.contains("data"))
55+
return;
56+
57+
const auto& data = msg["data"];
58+
if (!data.contains("levels") || !data["levels"].is_array() || data["levels"].size() < 2)
59+
return;
60+
61+
const auto& bids = data["levels"][0];
62+
const auto& asks = data["levels"][1];
63+
const std::string bid = (!bids.empty() && bids[0].contains("px"))
64+
? bids[0]["px"].get<std::string>()
65+
: "n/a";
66+
const std::string ask = (!asks.empty() && asks[0].contains("px"))
67+
? asks[0]["px"].get<std::string>()
68+
: "n/a";
69+
70+
std::cout << "[l2Book] " << coin
71+
<< " best_bid=" << bid
72+
<< " best_ask=" << ask << "\n";
73+
}
74+
75+
std::string mid_price_from_message(const nlohmann::json& msg, const std::string& coin) {
76+
if (!msg.contains("data") || !msg["data"].is_object())
77+
return {};
78+
79+
const auto& data = msg["data"];
80+
if (data.contains(coin) && data[coin].is_string())
81+
return data[coin].get<std::string>();
82+
83+
if (data.contains("mids") && data["mids"].is_object() &&
84+
data["mids"].contains(coin) && data["mids"][coin].is_string())
85+
return data["mids"][coin].get<std::string>();
86+
87+
return {};
88+
}
89+
90+
} // namespace
91+
92+
int main(int argc, char* argv[]) {
93+
Options options;
94+
try {
95+
options = parse_options(argc, argv);
96+
} catch (const std::exception& ex) {
97+
std::cerr << "Invalid arguments: " << ex.what() << "\n"
98+
<< "Usage: market_data_websocket [coin ...] [--seconds N]\n";
99+
return 1;
100+
}
101+
102+
hyperliquid::Info info(hyperliquid::TESTNET_API_URL, /*skip_ws=*/false);
103+
104+
std::mutex cout_mutex;
105+
std::atomic<int> all_mids_updates{0};
106+
std::vector<std::shared_ptr<std::atomic<int>>> l2_book_updates;
107+
l2_book_updates.reserve(options.coins.size());
108+
for (std::size_t i = 0; i < options.coins.size(); ++i)
109+
l2_book_updates.push_back(std::make_shared<std::atomic<int>>(0));
110+
111+
const nlohmann::json all_mids_sub{{"type", "allMids"}};
112+
113+
const int all_mids_id = info.subscribe(all_mids_sub, [&](const nlohmann::json& msg) {
114+
const int update_count = all_mids_updates.fetch_add(1, std::memory_order_relaxed) + 1;
115+
116+
std::lock_guard lock(cout_mutex);
117+
std::cout << "[allMids] update #" << update_count;
118+
for (const auto& coin : options.coins) {
119+
const std::string mid_price = mid_price_from_message(msg, coin);
120+
if (!mid_price.empty())
121+
std::cout << " " << coin << "=" << mid_price;
122+
}
123+
std::cout << "\n";
124+
});
125+
126+
std::vector<std::pair<nlohmann::json, int>> l2_book_subscriptions;
127+
l2_book_subscriptions.reserve(options.coins.size());
128+
129+
for (std::size_t i = 0; i < options.coins.size(); ++i) {
130+
const std::string coin = options.coins[i];
131+
const nlohmann::json subscription{{"type", "l2Book"}, {"coin", coin}};
132+
const int id = info.subscribe(subscription, [&, i, coin](const nlohmann::json& msg) {
133+
l2_book_updates[i]->fetch_add(1, std::memory_order_relaxed);
134+
135+
std::lock_guard lock(cout_mutex);
136+
print_top_of_book(msg, coin);
137+
});
138+
l2_book_subscriptions.emplace_back(subscription, id);
139+
}
140+
141+
{
142+
std::lock_guard lock(cout_mutex);
143+
std::cout << "Listening for";
144+
for (const auto& coin : options.coins)
145+
std::cout << " " << coin;
146+
std::cout << " market data on testnet for " << options.seconds << " seconds...\n";
147+
}
148+
149+
std::this_thread::sleep_for(std::chrono::seconds(options.seconds));
150+
151+
info.unsubscribe(all_mids_sub, all_mids_id);
152+
for (const auto& [subscription, id] : l2_book_subscriptions)
153+
info.unsubscribe(subscription, id);
154+
155+
std::cout << "Done. Received "
156+
<< all_mids_updates.load(std::memory_order_relaxed)
157+
<< " allMids updates";
158+
for (std::size_t i = 0; i < options.coins.size(); ++i) {
159+
std::cout << ", " << l2_book_updates[i]->load(std::memory_order_relaxed)
160+
<< " " << options.coins[i] << " l2Book updates";
161+
}
162+
std::cout << ".\n";
163+
164+
return 0;
165+
}

tests/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ target_link_libraries(hyperliquid_tests PRIVATE
1717

1818
gtest_discover_tests(hyperliquid_tests
1919
TEST_PREFIX unit.
20+
DISCOVERY_MODE PRE_TEST
21+
DISCOVERY_TIMEOUT 60
2022
PROPERTIES
2123
LABELS unit
2224
)
@@ -34,6 +36,8 @@ target_link_libraries(hyperliquid_integration_tests PRIVATE
3436

3537
gtest_discover_tests(hyperliquid_integration_tests
3638
TEST_PREFIX integration.
39+
DISCOVERY_MODE PRE_TEST
40+
DISCOVERY_TIMEOUT 60
3741
PROPERTIES
3842
LABELS integration
3943
)

0 commit comments

Comments
 (0)