Skip to content

Commit 58e9800

Browse files
committed
Add market_data_websocket_per_coin example and enhance ping loop efficiency
1 parent c707efd commit 58e9800

5 files changed

Lines changed: 184 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313
- Add a `market_data_websocket` example that subscribes to public testnet `allMids` and per-coin `l2Book` WebSocket updates for one or more coins.
14+
- Add a `market_data_websocket_per_coin` example that subscribes to ETH and BTC market data using one WebSocket connection per coin.
1415

1516
### Changed
1617
- Normalize line ending to LF

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ if(HYPERLIQUID_BUILD_EXAMPLES)
5656

5757
add_executable(market_data_websocket examples/market_data_websocket.cpp)
5858
target_link_libraries(market_data_websocket PRIVATE hyperliquid)
59+
60+
add_executable(market_data_websocket_per_coin examples/market_data_websocket_per_coin.cpp)
61+
target_link_libraries(market_data_websocket_per_coin PRIVATE hyperliquid)
5962
endif()
6063

6164
if(HYPERLIQUID_BUILD_TESTS)

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ Targets produced:
8787
|--------|--------|
8888
| `hyperliquid` | Static library |
8989
| `basic_order` | Example executable |
90+
| `market_data_websocket` | Public market data WebSocket example |
91+
| `market_data_websocket_per_coin` | Public market data WebSocket example using one connection per coin and slick-net logging hooks |
9092
| `hyperliquid_tests` | Offline unit tests |
9193
| `hyperliquid_integration_tests` | Live testnet integration tests |
9294

@@ -184,6 +186,9 @@ info->unsubscribe({{"type", "l2Book"}, {"coin", "ETH"}}, sid);
184186
info->unsubscribe({{"type", "l2Book"}, {"coin", "ETH"}}, sid2);
185187
```
186188
189+
The WebSocket manager sends periodic pings to keep connections alive and uses
190+
bounded atomic shutdown checks so teardown does not wait for the full ping interval.
191+
187192
---
188193
189194
## API Reference — Info
@@ -402,17 +407,27 @@ Each callback receives the full message object:
402407

403408
---
404409

405-
## Running the Example
410+
## Running Examples
406411

407412
```bash
408413
# Using the hardhat test key (no real funds)
409414
./build/Debug/basic_order
410415

411416
# Using your own key
412417
./build/Debug/basic_order 0xYOUR_PRIVATE_KEY_HEX
418+
419+
# Subscribe to allMids and l2Book updates on one WebSocket connection
420+
./build/Debug/market_data_websocket ETH BTC --seconds 30
421+
422+
# Subscribe to ETH and BTC l2Book updates using one WebSocket connection per coin
423+
./build/Debug/market_data_websocket_per_coin 30
413424
```
414425

415-
The example places a resting limit buy on testnet and cancels it if it is resting.
426+
`basic_order` places a resting limit buy on testnet and cancels it if it is resting.
427+
The market data examples subscribe to public testnet WebSocket feeds and print
428+
received updates for the requested duration.
429+
`market_data_websocket_per_coin` also demonstrates configuring slick-net's
430+
runtime logging hooks and routing `LOG_INFO` / `LOG_ERROR` output to `std::cout`.
416431

417432
---
418433

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// WebSocket market data example: one WebSocket connection per coin.
2+
// Subscribes to ETH and BTC l2Book updates on testnet.
3+
// Usage: market_data_websocket_per_coin [seconds]
4+
// Defaults: seconds=30
5+
6+
#include <hyperliquid/hyperliquid.hpp>
7+
8+
#include <slick/net/logging.hpp>
9+
10+
#include <atomic>
11+
#include <chrono>
12+
#include <format>
13+
#include <iostream>
14+
#include <memory>
15+
#include <stdexcept>
16+
#include <string>
17+
#include <thread>
18+
#include <utility>
19+
#include <vector>
20+
21+
namespace {
22+
23+
const char* log_level_name(slick::net::LogLevel level) {
24+
switch (level) {
25+
case slick::net::LogLevel::Trace:
26+
return "TRACE";
27+
case slick::net::LogLevel::Debug:
28+
return "DEBUG";
29+
case slick::net::LogLevel::Info:
30+
return "INFO";
31+
case slick::net::LogLevel::Warn:
32+
return "WARN";
33+
case slick::net::LogLevel::Error:
34+
return "ERROR";
35+
}
36+
return "UNKNOWN";
37+
}
38+
39+
void configure_slick_net_logging() {
40+
slick::net::set_log_handler(
41+
[](slick::net::LogLevel level, const char* format_text, std::format_args args) {
42+
std::string message;
43+
try {
44+
message = std::vformat(format_text, args);
45+
} catch (...) {
46+
message = format_text ? format_text : "";
47+
}
48+
49+
std::cout << "[" << log_level_name(level) << "] " << message << "\n";
50+
});
51+
}
52+
53+
struct SlickNetLogHandler {
54+
SlickNetLogHandler() {
55+
configure_slick_net_logging();
56+
}
57+
58+
~SlickNetLogHandler() {
59+
slick::net::clear_log_handler();
60+
}
61+
};
62+
63+
int parse_duration_seconds(int argc, char* argv[]) {
64+
if (argc < 2)
65+
return 30;
66+
67+
std::size_t parsed = 0;
68+
const int seconds = std::stoi(argv[1], &parsed);
69+
if (parsed != std::string(argv[1]).size() || seconds <= 0)
70+
throw std::invalid_argument("expected a positive integer duration");
71+
return seconds;
72+
}
73+
74+
void print_top_of_book(const nlohmann::json& msg, const std::string& coin) {
75+
if (!msg.contains("data"))
76+
return;
77+
78+
const auto& data = msg["data"];
79+
if (!data.contains("levels") || !data["levels"].is_array() || data["levels"].size() < 2)
80+
return;
81+
82+
const auto& bids = data["levels"][0];
83+
const auto& asks = data["levels"][1];
84+
const std::string bid = (!bids.empty() && bids[0].contains("px"))
85+
? bids[0]["px"].get<std::string>()
86+
: "n/a";
87+
const std::string ask = (!asks.empty() && asks[0].contains("px"))
88+
? asks[0]["px"].get<std::string>()
89+
: "n/a";
90+
91+
LOG_INFO("[l2Book] {} best_bid={} best_ask={}", coin, bid, ask);
92+
}
93+
94+
struct CoinFeed {
95+
std::string coin;
96+
hyperliquid::Info info;
97+
nlohmann::json subscription;
98+
int subscription_id = 0;
99+
std::atomic<int> updates{0};
100+
101+
explicit CoinFeed(std::string coin_)
102+
: coin(std::move(coin_))
103+
, info(hyperliquid::TESTNET_API_URL, /*skip_ws=*/false)
104+
, subscription{{"type", "l2Book"}, {"coin", coin}}
105+
{}
106+
};
107+
108+
} // namespace
109+
110+
int main(int argc, char* argv[]) {
111+
const SlickNetLogHandler log_handler;
112+
113+
int seconds = 30;
114+
try {
115+
seconds = parse_duration_seconds(argc, argv);
116+
} catch (const std::exception& ex) {
117+
LOG_ERROR("Invalid arguments: {}", ex.what());
118+
LOG_INFO("Usage: market_data_websocket_per_coin [seconds]");
119+
return 1;
120+
}
121+
122+
std::vector<std::unique_ptr<CoinFeed>> feeds;
123+
feeds.push_back(std::make_unique<CoinFeed>("ETH"));
124+
feeds.push_back(std::make_unique<CoinFeed>("BTC"));
125+
126+
for (auto& feed : feeds) {
127+
feed->subscription_id = feed->info.subscribe(
128+
feed->subscription,
129+
[feed_ptr = feed.get()](const nlohmann::json& msg) {
130+
feed_ptr->updates.fetch_add(1, std::memory_order_relaxed);
131+
print_top_of_book(msg, feed_ptr->coin);
132+
});
133+
}
134+
135+
LOG_INFO("Listening for ETH and BTC market data on testnet for {} seconds using one WebSocket per coin...",
136+
seconds);
137+
138+
std::this_thread::sleep_for(std::chrono::seconds(seconds));
139+
140+
for (auto& feed : feeds)
141+
feed->info.unsubscribe(feed->subscription, feed->subscription_id);
142+
143+
std::string summary = "Done.";
144+
for (const auto& feed : feeds) {
145+
summary += " " + feed->coin + "=" +
146+
std::to_string(feed->updates.load(std::memory_order_relaxed)) +
147+
" l2Book updates.";
148+
}
149+
LOG_INFO("{}", summary);
150+
151+
return 0;
152+
}

src/websocket_manager.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,22 @@ void WebsocketManager::on_error(std::string err) {
220220

221221
void WebsocketManager::ping_loop() {
222222
static constexpr auto kPingInterval = std::chrono::seconds(50);
223+
static constexpr auto kShutdownCheckInterval = std::chrono::milliseconds(100);
223224
static const std::string ping_msg = R"({"method":"ping"})";
224225

226+
auto next_ping = std::chrono::steady_clock::now() + kPingInterval;
225227
while (running_.load(std::memory_order_acquire)) {
226-
std::this_thread::sleep_for(kPingInterval);
227-
if (!running_.load(std::memory_order_acquire))
228-
break;
228+
const auto now = std::chrono::steady_clock::now();
229+
if (now < next_ping) {
230+
const auto remaining = next_ping - now;
231+
std::this_thread::sleep_for(
232+
remaining < kShutdownCheckInterval ? remaining : kShutdownCheckInterval);
233+
continue;
234+
}
235+
229236
if (connected_.load(std::memory_order_acquire) && ws_)
230237
ws_->send(ping_msg.c_str(), ping_msg.size());
238+
next_ping = std::chrono::steady_clock::now() + kPingInterval;
231239
}
232240
}
233241

0 commit comments

Comments
 (0)