Skip to content

Commit 4c82894

Browse files
committed
convert all timestamp to nanoseconds; implement timestamp parsing tests for Level2Update messages
1 parent 5a8eeae commit 4c82894

5 files changed

Lines changed: 74 additions & 36 deletions

File tree

CHANGELOG.md

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

1010
### Changed
1111
- Enhance ISO 8601 parsing with microsecond and nanosecond support
12+
- Convert all timestamp to nanoseconds
13+
14+
### Added
15+
- timestamp parsing tests
1216

1317
## [0.2.1] - 2026-02-19
1418

include/coinbase/market_data.hpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
namespace coinbase {
1212

1313
struct Level2Update {
14-
uint64_t seq_num;
1514
uint64_t event_time;
1615
Side side;
1716
double price_level;

include/coinbase/utils.hpp

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -46,39 +46,6 @@ inline uint64_t nanoseconds_from_json(const json &j, std::string_view field) {
4646
return j.at(field).is_null() ? 0 : to_nanoseconds(j.at(field).get<std::string>());
4747
}
4848

49-
// Automatically detect timestamp precision and parse accordingly
50-
inline uint64_t timestamp_from_json(const json &j, std::string_view field) {
51-
if (j.at(field).is_null()) {
52-
return 0;
53-
}
54-
55-
auto timestamp_str = j.at(field).get<std::string>();
56-
57-
// Find fractional seconds
58-
size_t dot_pos = timestamp_str.find('.');
59-
if (dot_pos == std::string::npos) {
60-
// No fractional seconds, use milliseconds
61-
return to_milliseconds(timestamp_str);
62-
}
63-
64-
// Count fractional digits
65-
size_t end_pos = timestamp_str.find_first_of("Z+", dot_pos);
66-
if (end_pos == std::string::npos) end_pos = timestamp_str.length();
67-
size_t frac_digits = end_pos - dot_pos - 1;
68-
69-
// Choose precision based on fractional digits:
70-
// 1-3 digits: milliseconds (e.g., .123)
71-
// 4-6 digits: microseconds (e.g., .123456)
72-
// 7-9 digits: nanoseconds (e.g., .123456789)
73-
if (frac_digits <= 3) {
74-
return to_milliseconds(timestamp_str);
75-
} else if (frac_digits <= 6) {
76-
return to_microseconds(timestamp_str);
77-
} else {
78-
return to_nanoseconds(timestamp_str);
79-
}
80-
}
81-
8249
inline double double_from_json(const json &j, std::string_view field) {
8350
try
8451
{
@@ -148,7 +115,7 @@ inline std::string to_string(double value, Side side, double min_increment) {
148115
return oss.str();
149116
}
150117

151-
#define TIMESTAMP_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = timestamp_from_json(j, #field)
118+
#define TIMESTAMP_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = nanoseconds_from_json(j, #field)
152119
#define NANOSECONDS_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = nanoseconds_from_json(j, #field)
153120
#define DOUBLE_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = double_from_json(j, #field)
154121
#define INT_FROM_JSON(j, o, field) if (j.contains(#field)) o.field = int_from_json(j, #field)

tests/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ endif()
3434

3535
include(GoogleTest)
3636

37-
add_executable(coinbase_advance_tests rest_api_tests.cpp websocket_tests.cpp rest_awaitable_tests.cpp)
37+
add_executable(coinbase_advance_tests rest_api_tests.cpp websocket_tests.cpp rest_awaitable_tests.cpp timestamp_parsing_tests.cpp)
3838
target_include_directories(coinbase_advance_tests PRIVATE ${CMAKE_SOURCE_DIR}/include)
3939
target_link_libraries(coinbase_advance_tests PRIVATE coinbase-advanced-cpp slick::net GTest::gtest_main)
4040

tests/timestamp_parsing_tests.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#include <gtest/gtest.h>
2+
#include <coinbase/utils.hpp>
3+
#include <coinbase/market_data.hpp>
4+
#include <nlohmann/json.hpp>
5+
6+
using json = nlohmann::json;
7+
8+
namespace coinbase::tests {
9+
10+
class TimestampParsingTests : public ::testing::Test {};
11+
12+
TEST_F(TimestampParsingTests, ParseL2UpdateMessage) {
13+
// Real L2 update message from Coinbase WebSocket
14+
std::string msg = R"({
15+
"channel":"l2_data",
16+
"timestamp":"2026-03-05T09:05:32.483569449Z",
17+
"sequence_num":229,
18+
"events":[{
19+
"type":"update",
20+
"product_id":"BIP-20DEC30-CDE",
21+
"updates":[
22+
{"side":"bid","event_time":"2026-03-05T09:05:32.449176Z","price_level":"72575","new_quantity":"0"},
23+
{"side":"bid","event_time":"2026-03-05T09:05:32.449176Z","price_level":"72570","new_quantity":"68"},
24+
{"side":"offer","event_time":"2026-03-05T09:05:32.449176Z","price_level":"72580","new_quantity":"13"}
25+
]
26+
}]
27+
})";
28+
29+
auto j = json::parse(msg);
30+
31+
// Test main timestamp parsing (nanoseconds - 9 fractional digits)
32+
ASSERT_TRUE(j.contains("timestamp"));
33+
uint64_t main_timestamp = nanoseconds_from_json(j, "timestamp");
34+
35+
// Expected: 2026-03-05T09:05:32.483569449Z
36+
// Breakdown: 2026-03-05 09:05:32 = Unix timestamp base
37+
// .483569449 = 483569449 nanoseconds
38+
39+
// The timestamp should be in nanoseconds since it has 9 fractional digits
40+
EXPECT_EQ(main_timestamp, 1772701532483569449ULL); // Exact nanosecond timestamp
41+
42+
// Test Level2Update parsing (microseconds - 6 fractional digits)
43+
ASSERT_TRUE(j.contains("events"));
44+
ASSERT_GT(j["events"].size(), 0);
45+
46+
Level2UpdateBatch batch = j["events"][0];
47+
ASSERT_EQ(batch.product_id, "BIP-20DEC30-CDE");
48+
ASSERT_EQ(batch.updates.size(), 3);
49+
50+
// Expected: 2026-03-05T09:05:32.449176Z
51+
// .449176 = 6 fractional digits = microseconds
52+
53+
EXPECT_EQ(batch.updates[0].event_time, 1772701532449176000ULL); // Exact microsecond timestamp
54+
55+
// Verify side parsing
56+
EXPECT_EQ(batch.updates[0].side, Side::BUY); // "bid" = BUY
57+
58+
// Verify price and quantity parsing
59+
EXPECT_DOUBLE_EQ(batch.updates[0].price_level, 72575.0);
60+
EXPECT_DOUBLE_EQ(batch.updates[0].new_quantity, 0.0);
61+
62+
// Test the third update (offer side)
63+
EXPECT_EQ(batch.updates[2].side, Side::SELL); // "offer" = SELL
64+
EXPECT_DOUBLE_EQ(batch.updates[2].price_level, 72580.0);
65+
EXPECT_DOUBLE_EQ(batch.updates[2].new_quantity, 13.0);
66+
}
67+
68+
} // namespace coinbase::tests

0 commit comments

Comments
 (0)