From 432f83cec3bb77808286ba7c7e03d8c4d3958f86 Mon Sep 17 00:00:00 2001 From: Tyler Beason Date: Thu, 4 Jun 2026 19:57:00 -0500 Subject: [PATCH 1/3] Add compact Base.show for DBN record types Julia's default struct `show` dumps every field with fully-qualified type names, which is unreadable when streaming a feed (`for rec in ch; println(rec); end`). Add a compact one-line `Base.show(io, r)` for all 18 DBN record types plus `RecordHeader` and `BidAskPair`. Rendering decodes fixed-point prices via `price_to_float`, prints full-nanosecond timestamps, and shows the unset sentinels (`UNDEF_PRICE`, `UNDEF_TIMESTAMP`, `UNDEF_ORDER_SIZE`, the unset stat quantity, and the UInt64-max `ts_recv`) as "-" instead of leaking NaN / huge integers / throwing on conversion. Side is labeled `side=` consistently across record types. Adds test/test_show.jl (47 assertions incl. sentinel safety) and a changelog entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 9 ++ src/DatabentoBinaryEncoding.jl | 1 + src/show.jl | 186 +++++++++++++++++++++++++++++++++ test/runtests.jl | 5 +- test/test_show.jl | 142 +++++++++++++++++++++++++ 5 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 src/show.jl create mode 100644 test/test_show.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 50db7b8..e605245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Compact, single-line `Base.show` for every DBN record type (and `RecordHeader` + / `BidAskPair`), replacing Julia's fully-qualified struct dump. Streaming a feed + (`for rec in ch; println(rec); end`) now renders readable lines such as + `Trade 2026-06-05T00:23:05.040049165 iid=42 side=ASK px=185.42 sz=100 seq=88213`, + with fixed-point prices decoded, full-nanosecond timestamps, and the unset + sentinels (`UNDEF_PRICE`, `UNDEF_TIMESTAMP`, `UNDEF_ORDER_SIZE`) shown as `-`. + ## [0.1.2] - 2026-06-01 ### Added diff --git a/src/DatabentoBinaryEncoding.jl b/src/DatabentoBinaryEncoding.jl index 41adb2d..7767e3b 100644 --- a/src/DatabentoBinaryEncoding.jl +++ b/src/DatabentoBinaryEncoding.jl @@ -93,6 +93,7 @@ using StructTypes # Include all the component files include("types.jl") include("messages.jl") +include("show.jl") include("buffered_io.jl") include("decode.jl") include("encode.jl") diff --git a/src/show.jl b/src/show.jl new file mode 100644 index 0000000..c97dd15 --- /dev/null +++ b/src/show.jl @@ -0,0 +1,186 @@ +# Pretty-printing for DBN records. +# +# Julia's default struct `show` dumps every field with fully-qualified type names +# (e.g. `DatabentoBinaryEncoding.TradeMsg(DatabentoBinaryEncoding.RecordHeader(...`), +# which is unreadable when streaming a feed (`for rec in ch; println(rec); end`). +# +# These compact one-line `Base.show(io, r)` methods render only the few fields that +# matter when scanning a live stream: timestamp, instrument id, side/action, +# price(s), and size(s). A single 2-arg `show` per type is enough — it drives +# `println`, `Vector` element display, and the REPL (whose 3-arg `text/plain` +# `show` falls back to the 2-arg form when no specialization exists). +# +# Field-rendering conventions (the `why` is load-bearing — sentinels otherwise +# leak as `NaN` / `9223372036854775807` / `4294967295` onto the line): +# - prices are Int64 fixed-point (scaled by FIXED_PRICE_SCALE = 1e-9). Rendered +# via `price_to_float`, which maps the UNDEF_PRICE sentinel to NaN; we show +# "-" in that case (detected with `isnan`, never `== UNDEF_PRICE` on a float). +# - timestamps are nanoseconds since the Unix epoch. Rendered with full +# nanosecond precision; the UNDEF_TIMESTAMP sentinel — and the UInt-max +# sentinel on the UInt64-typed `ts_recv` fields — show as "-". +# - sizes carry typemax sentinels (UNDEF_ORDER_SIZE = typemax(UInt32), an unset +# stat quantity = typemax(Int64)); those show as "-" too. + +const _PP_UNDEF = "-" + +# Bare enum member name (e.g. "ASK"). `string` of an EnumX value already yields +# the unqualified member name — unlike its `show`, which prints the fully-qualified +# path "DatabentoBinaryEncoding.Side.ASK". +_pp_enum(e) = string(e) + +# Fixed-point price -> string; UNDEF_PRICE -> "-". +function _pp_px(p::Int64) + v = price_to_float(p) + isnan(v) ? _PP_UNDEF : string(v) +end + +# Size-like unsigned -> string; typemax sentinel (e.g. UNDEF_ORDER_SIZE) -> "-". +_pp_sz(s::Unsigned) = s == typemax(typeof(s)) ? _PP_UNDEF : string(s) + +# Signed quantity -> string; typemax(Int64) "unset" sentinel -> "-". +_pp_qty(q::Integer) = q == typemax(Int64) ? _PP_UNDEF : string(q) + +# Nanosecond timestamp (Int64 or UInt64) -> "YYYY-MM-DDThh:mm:ss.fffffffff". +# Guards the typemax sentinel *before* the Int64 cast so a UInt64 `ts_recv` left +# as typemax(UInt64) does not throw InexactError on conversion. +function _pp_ts(ns::Integer) + (ns == typemax(typeof(ns)) || ns == UNDEF_TIMESTAMP) && return _PP_UNDEF + parts = ts_to_date_time(Int64(ns)) + parts === nothing ? _PP_UNDEF : string(parts.date, "T", parts.time) +end + +# Best bid/ask from a BidAskPair, rendered as "bid=.. bsz=.. ask=.. asz=..". +_pp_bbo(io::IO, lv::BidAskPair) = print(io, + "bid=", _pp_px(lv.bid_px), " bsz=", _pp_sz(lv.bid_sz), + " ask=", _pp_px(lv.ask_px), " asz=", _pp_sz(lv.ask_sz)) + +# --- trades / orders --- + +function Base.show(io::IO, r::TradeMsg) + print(io, "Trade ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " side=", _pp_enum(r.side), " px=", _pp_px(r.price), " sz=", _pp_sz(r.size), + " seq=", r.sequence) +end + +# action is load-bearing for MBO (ADD/MODIFY/CANCEL/CLEAR/...), unlike Trade where +# it is always TRADE; order_id is the defining field so it goes last (widest). +function Base.show(io::IO, r::MBOMsg) + print(io, "MBO ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " act=", _pp_enum(r.action), " side=", _pp_enum(r.side), + " px=", _pp_px(r.price), " sz=", _pp_sz(r.size), " oid=", r.order_id) +end + +# --- book / BBO families --- + +# Incremental top-of-book update: the top-level px/sz/act/sd describe THIS change, +# then the resulting BBO from `levels`. Shared by MBP1 and CMBP1 (identical layout). +function _pp_book_incr(io::IO, label, r, lv::BidAskPair) + print(io, label, " ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " act=", _pp_enum(r.action), " side=", _pp_enum(r.side), + " px=", _pp_px(r.price), " sz=", _pp_sz(r.size), " | ") + _pp_bbo(io, lv) +end + +Base.show(io::IO, r::MBP1Msg) = _pp_book_incr(io, "MBP1", r, r.levels) +Base.show(io::IO, r::CMBP1Msg) = _pp_book_incr(io, "CMBP1", r, r.levels) + +# MBP10: levels is an NTuple{10,BidAskPair}; show only level 1 plus a depth hint. +function Base.show(io::IO, r::MBP10Msg) + _pp_book_incr(io, "MBP10", r, r.levels[1]) + print(io, " (+9 lvls)") +end + +# TCBBO is trade-sampled: each record is emitted on a trade, so px/sz/side ARE the +# trade; show them, then the consolidated BBO at that instant. +function Base.show(io::IO, r::TCBBOMsg) + print(io, "TCBBO ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " side=", _pp_enum(r.side), " px=", _pp_px(r.price), " sz=", _pp_sz(r.size), " | ") + _pp_bbo(io, r.levels) +end + +# Interval BBO snapshots: the BBO is the headline; px/sz are the interval's last +# trade (UNDEF_PRICE in a quiet interval -> "-"). Shared by C/BBO 1s/1m. +function _pp_bbo_snap(io::IO, label, r) + print(io, label, " ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, " ") + _pp_bbo(io, r.levels) + print(io, " last=", _pp_px(r.price), " lsz=", _pp_sz(r.size)) +end + +Base.show(io::IO, r::CBBO1sMsg) = _pp_bbo_snap(io, "CBBO1s", r) +Base.show(io::IO, r::CBBO1mMsg) = _pp_bbo_snap(io, "CBBO1m", r) +Base.show(io::IO, r::BBO1sMsg) = _pp_bbo_snap(io, "BBO1s", r) +Base.show(io::IO, r::BBO1mMsg) = _pp_bbo_snap(io, "BBO1m", r) + +# --- bars / stats --- + +function Base.show(io::IO, r::OHLCVMsg) + print(io, "OHLCV ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " O=", _pp_px(r.open), " H=", _pp_px(r.high), " L=", _pp_px(r.low), + " C=", _pp_px(r.close), " V=", r.volume) +end + +# stat_type is a venue-defined UInt16 code (not an enum). quantity is a signed +# Int64 in v3 (NOT a price) — render as a plain integer, guarding the unset +# sentinel. ts_recv is UInt64; _pp_ts handles the cast/sentinel safely. +function Base.show(io::IO, r::StatMsg) + print(io, "Stat ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " type=", r.stat_type, " px=", _pp_px(r.price), " qty=", _pp_qty(r.quantity), + " recv=", _pp_ts(r.ts_recv)) +end + +function Base.show(io::IO, r::ImbalanceMsg) + print(io, "Imbalance ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " ref=", _pp_px(r.ref_price), " side=", _pp_enum(r.side), + " paired=", r.paired_qty, " imb=", r.total_imbalance_qty, + " match=", _pp_px(r.ind_match_price)) +end + +# action/reason/trading_event are UInt16 venue codes (NOT the Action enum, despite +# the field name). The is_* flags are UInt8 tri-state c_chars; `== 1` -> clean bool. +function Base.show(io::IO, r::StatusMsg) + print(io, "Status ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " action=", r.action, " reason=", r.reason, + " trading=", r.is_trading == 1, " quoting=", r.is_quoting == 1, + " ssr=", r.is_short_sell_restricted == 1) +end + +# --- control / system (gateway-wide, not instrument-scoped) --- + +function Base.show(io::IO, r::ErrorMsg) + print(io, "Error ", _pp_ts(r.hd.ts_event), " msg=", repr(r.err)) +end + +function Base.show(io::IO, r::SymbolMappingMsg) + print(io, "SymMap ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, " ", + r.stype_in_symbol, " [", _pp_enum(r.stype_in), "] -> ", + r.stype_out_symbol, " [", _pp_enum(r.stype_out), "]") +end + +function Base.show(io::IO, r::SystemMsg) + print(io, "System ", _pp_ts(r.hd.ts_event), " code=", repr(r.code), " msg=", repr(r.msg)) +end + +# --- instrument definition (reference record; identification fields) --- + +# strike_price is UNDEF_PRICE for non-options; expiration is UNDEF_TIMESTAMP for +# undated instruments — both -> "-" via the helpers. +function Base.show(io::IO, r::InstrumentDefMsg) + print(io, "Def ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, + " sym=", r.raw_symbol, " cls=", _pp_enum(r.instrument_class), + " exch=", r.exchange, " exp=", _pp_ts(r.expiration), + " strike=", _pp_px(r.strike_price), " tick=", _pp_px(r.min_price_increment), + " ccy=", r.currency, " act=", r.security_update_action) +end + +# --- shared substructures (printed compactly when shown on their own) --- + +function Base.show(io::IO, hd::RecordHeader) + print(io, "RecordHeader(", _pp_enum(hd.rtype), " iid=", hd.instrument_id, + " pub=", hd.publisher_id, " ts_event=", _pp_ts(hd.ts_event), ")") +end + +function Base.show(io::IO, lv::BidAskPair) + print(io, "BidAskPair(") + _pp_bbo(io, lv) + print(io, " bc=", lv.bid_ct, " ac=", lv.ask_ct, ")") +end diff --git a/test/runtests.jl b/test/runtests.jl index 338a57b..5e6d774 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ using Test -using DatabentoBinaryEncoding -import DatabentoBinaryEncoding as DBN +using DatabentoBinaryEncoding +import DatabentoBinaryEncoding as DBN using Dates # Load test utilities (safe_rm, etc.) @@ -20,6 +20,7 @@ include("test_utils.jl") include("test_convenience_functions.jl") # Test all convenience read_*/foreach_* functions include("test_phase11_typed_with_control.jl") # foreach_record_with_control: typed data + Union control split include("test_issue23_unset_stype.jl") # regression: 0xFF unset stype in v3 SymbolMappingMsg (issue #23) + include("test_show.jl") # compact one-line Base.show for record types # Run compatibility tests if the Rust CLI is available dbn_cli_path = if Sys.iswindows() diff --git a/test/test_show.jl b/test/test_show.jl new file mode 100644 index 0000000..9b05f4f --- /dev/null +++ b/test/test_show.jl @@ -0,0 +1,142 @@ +# Tests for the compact one-line `Base.show` methods on DBN record types. +# These assert the human-facing rendering: correct labels/fields, full-precision +# timestamps, fixed-point price decoding, and — critically — that the various +# "unset" sentinels (UNDEF_PRICE, UNDEF_TIMESTAMP, UNDEF_ORDER_SIZE, the unset +# stat quantity, and the UInt64-max ts_recv) render as "-" instead of leaking +# NaN / huge integers / throwing on conversion. + +# `show` of a record returns its compact one-liner (no MIME specialization, so +# this is exactly what `println(rec)` and Vector display produce). +pp(x) = sprint(show, x) + +@testset "Record pretty-printing (show)" begin + # A fixed event timestamp: 2026-06-05T00:23:05.040049165 UTC. `datetime_to_ts` + # rounds to whole seconds, so add the full sub-second component as nanoseconds. + ts = DBN.datetime_to_ts(DateTime(2026, 6, 5, 0, 23, 5)) + 40_049_165 + ts_str = "2026-06-05T00:23:05.040049165" + hd = DBN.RecordHeader(0x0c, DBN.RType.MBP_0_MSG, 0x0001, UInt32(42), ts) + + @testset "TradeMsg" begin + r = DBN.TradeMsg(hd, 185_420_000_000, UInt32(100), DBN.Action.TRADE, + DBN.Side.ASK, 0x00, 0x00, ts, Int32(0), UInt32(88213)) + s = pp(r) + @test startswith(s, "Trade ") + @test occursin(ts_str, s) # full nanosecond precision + @test occursin("iid=42", s) + @test occursin("side=ASK", s) # bare enum name, not Mod.Side.ASK + @test occursin("px=185.42", s) # fixed-point decoded + @test occursin("sz=100", s) + @test occursin("seq=88213", s) + @test !occursin("DatabentoBinaryEncoding", s) # no qualified type noise + end + + @testset "MBOMsg with UNDEF sentinels" begin + # A CANCEL legitimately carries UNDEF_PRICE and UNDEF_ORDER_SIZE. + r = DBN.MBOMsg(hd, UInt64(99), DBN.UNDEF_PRICE, DBN.UNDEF_ORDER_SIZE, + 0x00, 0x00, DBN.Action.CANCEL, DBN.Side.BID, ts, Int32(0), UInt32(7)) + s = pp(r) + @test startswith(s, "MBO ") + @test occursin("act=CANCEL", s) + @test occursin("px=-", s) # UNDEF_PRICE -> dash, not NaN + @test occursin("sz=-", s) # UNDEF_ORDER_SIZE -> dash + @test !occursin("NaN", s) + @test occursin("oid=99", s) + end + + @testset "Book families show the BBO" begin + lv = DBN.BidAskPair(212_340_000_000, 212_350_000_000, UInt32(300), UInt32(150), + UInt32(3), UInt32(2)) + mbp1 = DBN.MBP1Msg(hd, 212_340_000_000, UInt32(100), DBN.Action.MODIFY, + DBN.Side.BID, 0x00, 0x01, ts, Int32(0), UInt32(1), lv) + s = pp(mbp1) + @test startswith(s, "MBP1 ") + @test occursin("bid=212.34 bsz=300 ask=212.35 asz=150", s) + @test occursin("act=MODIFY", s) + @test occursin("side=BID", s) # Side labeled `side=` consistently, not `sd=` + @test !occursin("sd=", s) + + # MBP10: top-of-book from levels[1] plus the depth hint. + levels = ntuple(_ -> lv, 10) + mbp10 = DBN.MBP10Msg(hd, 212_350_000_000, UInt32(50), DBN.Action.MODIFY, + DBN.Side.ASK, 0x00, 0x0a, ts, Int32(0), UInt32(2), levels) + s10 = pp(mbp10) + @test startswith(s10, "MBP10 ") + @test occursin("bid=212.34 bsz=300 ask=212.35 asz=150", s10) + @test occursin("(+9 lvls)", s10) + end + + @testset "OHLCVMsg" begin + r = DBN.OHLCVMsg(hd, 412_500_000_000, 413_100_000_000, 412_050_000_000, + 412_880_000_000, UInt64(18250)) + s = pp(r) + @test startswith(s, "OHLCV ") + @test occursin("O=412.5 H=413.1 L=412.05 C=412.88 V=18250", s) + end + + @testset "StatMsg sentinel safety (UInt64 ts_recv, unset quantity)" begin + # ts_recv left as the UInt64-max sentinel must NOT throw on the Int64 cast, + # and an unset quantity (typemax Int64) must render as a dash. + r = DBN.StatMsg(hd, typemax(UInt64), UInt64(0), 412_750_000_000, + typemax(Int64), UInt32(0), Int32(0), UInt16(3), UInt16(0), 0x00, 0x00) + local s + @test_nowarn (s = pp(r)) # no InexactError from Int64(typemax(UInt64)) + @test occursin("type=3", s) + @test occursin("px=412.75", s) + @test occursin("qty=-", s) # unset quantity -> dash + @test occursin("recv=-", s) # unset ts_recv -> dash + @test !occursin("9223372036854775807", s) + + # A populated stat renders the real values. + r2 = DBN.StatMsg(hd, UInt64(ts), UInt64(0), 412_750_000_000, Int64(500), + UInt32(0), Int32(0), UInt16(3), UInt16(0), 0x00, 0x00) + s2 = pp(r2) + @test occursin("qty=500", s2) + @test occursin("recv=$(ts_str)", s2) + end + + @testset "StatusMsg boolean flags" begin + r = DBN.StatusMsg(hd, UInt64(ts), UInt16(1), UInt16(0), UInt16(0), + 0x01, 0x01, 0x00) + s = pp(r) + @test startswith(s, "Status ") + @test occursin("action=1", s) + @test occursin("trading=true", s) + @test occursin("quoting=true", s) + @test occursin("ssr=false", s) + end + + @testset "Control records lead with their payload" begin + err = DBN.ErrorMsg(hd, "symbology resolution failed") + se = pp(err) + @test startswith(se, "Error ") + @test occursin("symbology resolution failed", se) + + sym = DBN.SymbolMappingMsg(hd, DBN.SType.RAW_SYMBOL, "ESM6", + DBN.SType.INSTRUMENT_ID, "42", Int64(0), DBN.UNDEF_TIMESTAMP) + ss = pp(sym) + @test startswith(ss, "SymMap ") + @test occursin("ESM6 [RAW_SYMBOL] -> 42 [INSTRUMENT_ID]", ss) + + sysm = DBN.SystemMsg(hd, "Heartbeat", "0") + sy = pp(sysm) + @test startswith(sy, "System ") + @test occursin("Heartbeat", sy) + end + + @testset "Substructures print compactly" begin + @test occursin("RecordHeader(", pp(hd)) + @test occursin("ts_event=$(ts_str)", pp(hd)) + lv = DBN.BidAskPair(212_340_000_000, 212_350_000_000, UInt32(300), UInt32(150), + UInt32(3), UInt32(2)) + @test occursin("BidAskPair(bid=212.34 bsz=300 ask=212.35 asz=150 bc=3 ac=2", pp(lv)) + end + + @testset "UNDEF_TIMESTAMP renders as dash" begin + hd_undef = DBN.RecordHeader(0x0c, DBN.RType.MBP_0_MSG, 0x0001, UInt32(1), + DBN.UNDEF_TIMESTAMP) + r = DBN.TradeMsg(hd_undef, 100_000_000_000, UInt32(1), DBN.Action.TRADE, + DBN.Side.ASK, 0x00, 0x00, DBN.UNDEF_TIMESTAMP, Int32(0), UInt32(1)) + s = pp(r) + @test occursin("Trade -", s) # leading ts is a dash + end +end From 6c5adb2ab9a18730c55f515c3114ea3b86cf56cf Mon Sep 17 00:00:00 2001 From: Tyler Beason Date: Thu, 4 Jun 2026 20:07:29 -0500 Subject: [PATCH 2/3] Fix StatusMsg flag rendering: c_char tri-state, not == 1 is_trading/is_quoting/is_short_sell_restricted decode as raw c_char bytes ('Y'=0x59, 'N'=0x4E, '~'/0x00 = not-available), per the StatusMsg round-trip in test/test_phase5.jl. Comparing against 1 rendered a genuinely-trading status as trading=false, inverting the exchange state for real decoded data. Add _pp_tristate ('Y'->true, 'N'->false, else "-") and update the test to use the canonical c_char values instead of the 0x01/0x00 representation. Reported by Codex review on PR #29. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/show.jl | 15 ++++++++++++--- test/test_show.jl | 12 +++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/show.jl b/src/show.jl index c97dd15..2a3e092 100644 --- a/src/show.jl +++ b/src/show.jl @@ -40,6 +40,15 @@ _pp_sz(s::Unsigned) = s == typemax(typeof(s)) ? _PP_UNDEF : string(s) # Signed quantity -> string; typemax(Int64) "unset" sentinel -> "-". _pp_qty(q::Integer) = q == typemax(Int64) ? _PP_UNDEF : string(q) +# DBN status flags are c_char tri-states, decoded as the raw byte: 'Y' (0x59), +# 'N' (0x4E), or "not available" ('~' / 0x00 / anything else). Comparing against +# 1 would mis-read the canonical 'Y'/'N' bytes, so match the characters. +function _pp_tristate(b::UInt8) + b == UInt8('Y') && return "true" + b == UInt8('N') && return "false" + return _PP_UNDEF +end + # Nanosecond timestamp (Int64 or UInt64) -> "YYYY-MM-DDThh:mm:ss.fffffffff". # Guards the typemax sentinel *before* the Int64 cast so a UInt64 `ts_recv` left # as typemax(UInt64) does not throw InexactError on conversion. @@ -136,12 +145,12 @@ function Base.show(io::IO, r::ImbalanceMsg) end # action/reason/trading_event are UInt16 venue codes (NOT the Action enum, despite -# the field name). The is_* flags are UInt8 tri-state c_chars; `== 1` -> clean bool. +# the field name). The is_* flags are UInt8 c_char tri-states ('Y'/'N'/not-avail). function Base.show(io::IO, r::StatusMsg) print(io, "Status ", _pp_ts(r.hd.ts_event), " iid=", r.hd.instrument_id, " action=", r.action, " reason=", r.reason, - " trading=", r.is_trading == 1, " quoting=", r.is_quoting == 1, - " ssr=", r.is_short_sell_restricted == 1) + " trading=", _pp_tristate(r.is_trading), " quoting=", _pp_tristate(r.is_quoting), + " ssr=", _pp_tristate(r.is_short_sell_restricted)) end # --- control / system (gateway-wide, not instrument-scoped) --- diff --git a/test/test_show.jl b/test/test_show.jl index 9b05f4f..3dd5826 100644 --- a/test/test_show.jl +++ b/test/test_show.jl @@ -94,15 +94,17 @@ pp(x) = sprint(show, x) @test occursin("recv=$(ts_str)", s2) end - @testset "StatusMsg boolean flags" begin + @testset "StatusMsg c_char tri-state flags" begin + # Flags decode as c_char bytes 'Y'/'N'/not-available (see test_phase5.jl), + # NOT 0x01/0x00 — render 'Y'->true, 'N'->false, anything else (0x00/'~')->"-". r = DBN.StatusMsg(hd, UInt64(ts), UInt16(1), UInt16(0), UInt16(0), - 0x01, 0x01, 0x00) + UInt8('Y'), UInt8('N'), 0x00) s = pp(r) @test startswith(s, "Status ") @test occursin("action=1", s) - @test occursin("trading=true", s) - @test occursin("quoting=true", s) - @test occursin("ssr=false", s) + @test occursin("trading=true", s) # 'Y' + @test occursin("quoting=false", s) # 'N' + @test occursin("ssr=-", s) # not available end @testset "Control records lead with their payload" begin From 9e0f93747215243a1abe59270dd1f7fee675b771 Mon Sep 17 00:00:00 2001 From: Tyler Beason Date: Fri, 5 Jun 2026 10:13:42 -0500 Subject: [PATCH 3/3] Release 0.1.3 (#30) Bump version 0.1.2 -> 0.1.3 and finalize the changelog for the compact Base.show record pretty-printing feature (PR #29). Additive / non-breaking, so a patch bump under SemVer for 0.x. Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 ++++- Project.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e605245..5670523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.3] - 2026-06-05 + ### Added - Compact, single-line `Base.show` for every DBN record type (and `RecordHeader` @@ -54,7 +56,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 conversion to JSON/Parquet/CSV; byte-for-byte compatibility with the official Rust implementation ([#18], [#19]). -[Unreleased]: https://github.com/tbeason/DatabentoBinaryEncoding.jl/compare/v0.1.2...HEAD +[Unreleased]: https://github.com/tbeason/DatabentoBinaryEncoding.jl/compare/v0.1.3...HEAD +[0.1.3]: https://github.com/tbeason/DatabentoBinaryEncoding.jl/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/tbeason/DatabentoBinaryEncoding.jl/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/tbeason/DatabentoBinaryEncoding.jl/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/tbeason/DatabentoBinaryEncoding.jl/releases/tag/v0.1.0 diff --git a/Project.toml b/Project.toml index 5a39ef0..011c0fe 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DatabentoBinaryEncoding" uuid = "90689371-c8cb-40d1-831f-18033db90f74" authors = ["Tyler Beason "] -version = "0.1.2" +version = "0.1.3" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"