Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [8.0.36] - 2026-05-07

### Changed

- **`crates/thetadatadx/src/decode.rs` (2177 LoC) split into 7 modules**
under `mdds/decode/{error,headers,transport,extract,cell,v3}`. Pure
structural refactor; public API unchanged via `mdds::decode::*` re-exports.
- **Eastern-time + DST primitives lifted to `tdbe::time`.**
`eastern_offset_ms`, `march_second_sunday_utc`, `november_first_sunday_utc`,
`april_first_sunday_utc`, `october_last_sunday_utc`, `civil_to_epoch_days`,
`timestamp_to_ms_of_day`, `timestamp_to_date` — single canonical module
reused by mdds, fpss, flatfiles. tdbe 0.12.9 → 0.12.10.
- **`crates/thetadatadx/src/fpss/protocol.rs` (1613 LoC) split into 4 modules**
under `fpss/protocol/`. `mod.rs` keeps constants and re-exports;
`contract.rs` holds `Contract` + 6 constructors + `Display` + `FromStr` +
OCC-21 parser; `wire.rs` holds payload builders / parsers; `subscription.rs`
holds `SubscriptionKind`.
- **`crates/thetadatadx/src/config.rs` (1396 LoC, 30 flat fields) refactored
into 7 nested typed sub-configs.** `DirectConfig` now contains `mdds`,
`fpss`, `reconnect`, `retry`, `auth`, `metrics`, `runtime`. Field-read
accessors preserved on `DirectConfig` for back-compat (`config.mdds_host()`
etc still work). Field-write callers must migrate to nested form
(`config.fpss.queue_depth = ...`). Adds `mdds.connect_timeout_secs`
(default 10s, covers prior LOW finding).
- **`crates/tdbe/src/conditions.rs` (2749 LoC) refactored to TOML-driven
codegen.** Source-of-truth at `crates/tdbe/data/{trade,quote}_conditions.toml`
(149 + 75 entries). `crates/tdbe/build.rs` reads the TOMLs and emits
`crates/tdbe/src/conditions/tables_generated.rs` with compile-time
const arrays. Public surface unchanged; new `condition_tables_pin`
test pins 12 known entries against the const arrays for round-trip
protection.

Refs #500.

## [8.0.35] - 2026-05-07

### Documentation
Expand Down
10 changes: 6 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion crates/tdbe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tdbe"
version = "0.12.9"
version = "0.12.10"
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
Expand All @@ -11,6 +11,14 @@ repository.workspace = true
license.workspace = true
keywords = ["thetadata", "encoding", "market-data", "codec", "greeks"]
categories = ["encoding", "finance"]
build = "build.rs"
include = [
"src/**/*.rs",
"data/*.toml",
"build.rs",
"Cargo.toml",
"README.md",
]

[lints]
workspace = true
Expand All @@ -19,6 +27,13 @@ workspace = true
sonic-rs = "0.5.8"
thiserror = "2.0.18"

[build-dependencies]
# Build-time only: parses `data/{trade,quote}_conditions.toml` into the
# committed `src/conditions/tables_generated.rs`. Does not ship to
# runtime consumers -- `tdbe`'s runtime dep graph is unchanged.
serde = { version = "1.0", features = ["derive"] }
toml = "1.1"

[dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] }

Expand Down
186 changes: 186 additions & 0 deletions crates/tdbe/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
//! Build-time codegen for trade and quote condition tables.
//!
//! Reads `data/trade_conditions.toml` (149 entries) and
//! `data/quote_conditions.toml` (75 entries) and emits
//! `src/conditions/tables_generated.rs` containing the
//! `TRADE_CONDITIONS` and `QUOTE_CONDITIONS` const arrays.
//!
//! The generated file is committed so downstream consumers building
//! `tdbe` from crates.io don't have to re-run codegen unless they edit
//! the TOML source-of-truth files locally.

use std::env;
use std::fs;
use std::path::PathBuf;

use serde::Deserialize;
use toml::Value;

#[derive(Deserialize)]
struct TradeFile {
trade: Vec<TradeRow>,
}

#[derive(Deserialize)]
struct QuoteFile {
quote: Vec<QuoteRow>,
}

#[derive(Deserialize)]
struct TradeRow {
code: i32,
name: String,
description: String,
cancel: bool,
late_report: bool,
auto_executed: bool,
open_report: bool,
volume: bool,
high: bool,
low: bool,
last: bool,
}

#[derive(Deserialize)]
struct QuoteRow {
code: i32,
name: String,
description: String,
firm: bool,
halted: bool,
}

fn rust_string_literal(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
use std::fmt::Write;
let _ = write!(out, "\\u{{{:x}}}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}

fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let trade_toml = manifest_dir.join("data/trade_conditions.toml");
let quote_toml = manifest_dir.join("data/quote_conditions.toml");
let out = manifest_dir.join("src/conditions/tables_generated.rs");

println!("cargo:rerun-if-changed=data/trade_conditions.toml");
println!("cargo:rerun-if-changed=data/quote_conditions.toml");
println!("cargo:rerun-if-changed=build.rs");

let trade_src = fs::read_to_string(&trade_toml)
.unwrap_or_else(|e| panic!("read {}: {e}", trade_toml.display()));
let quote_src = fs::read_to_string(&quote_toml)
.unwrap_or_else(|e| panic!("read {}: {e}", quote_toml.display()));

// Parse via `toml::Value` first to give precise spec errors, then re-deserialize
// into typed rows.
let _: Value = toml::from_str(&trade_src).expect("trade_conditions.toml: invalid TOML");
let _: Value = toml::from_str(&quote_src).expect("quote_conditions.toml: invalid TOML");

let trades: TradeFile =
toml::from_str(&trade_src).expect("trade_conditions.toml: schema mismatch");
let quotes: QuoteFile =
toml::from_str(&quote_src).expect("quote_conditions.toml: schema mismatch");

assert_eq!(
trades.trade.len(),
149,
"trade_conditions.toml must have exactly 149 entries"
);
assert_eq!(
quotes.quote.len(),
75,
"quote_conditions.toml must have exactly 75 entries"
);
for (i, t) in trades.trade.iter().enumerate() {
assert_eq!(
t.code as usize, i,
"trade_conditions.toml[{i}] has code {} (must equal index)",
t.code
);
}
for (i, q) in quotes.quote.iter().enumerate() {
assert_eq!(
q.code as usize, i,
"quote_conditions.toml[{i}] has code {} (must equal index)",
q.code
);
}

let mut s = String::new();
s.push_str("// @generated DO NOT EDIT — regenerated by build.rs from data/*.toml\n");
s.push_str("//\n");
s.push_str("// Source-of-truth: crates/tdbe/data/trade_conditions.toml\n");
s.push_str("// crates/tdbe/data/quote_conditions.toml\n\n");
s.push_str("use super::{QuoteCondition, TradeCondition};\n\n");

s.push_str("/// All 149 trade condition codes (0..148).\n");
s.push_str("pub const TRADE_CONDITIONS: [TradeCondition; 149] = [\n");
for t in &trades.trade {
s.push_str(" TradeCondition {\n");
s.push_str(&format!(" code: {},\n", t.code));
s.push_str(&format!(
" name: {},\n",
rust_string_literal(&t.name)
));
s.push_str(&format!(
" description: {},\n",
rust_string_literal(&t.description)
));
s.push_str(&format!(" cancel: {},\n", t.cancel));
s.push_str(&format!(" late_report: {},\n", t.late_report));
s.push_str(&format!(" auto_executed: {},\n", t.auto_executed));
s.push_str(&format!(" open_report: {},\n", t.open_report));
s.push_str(&format!(" volume: {},\n", t.volume));
s.push_str(&format!(" high: {},\n", t.high));
s.push_str(&format!(" low: {},\n", t.low));
s.push_str(&format!(" last: {},\n", t.last));
s.push_str(" },\n");
}
s.push_str("];\n\n");

s.push_str("/// All 75 quote condition codes (0..74).\n");
s.push_str("pub const QUOTE_CONDITIONS: [QuoteCondition; 75] = [\n");
for q in &quotes.quote {
s.push_str(" QuoteCondition {\n");
s.push_str(&format!(" code: {},\n", q.code));
s.push_str(&format!(
" name: {},\n",
rust_string_literal(&q.name)
));
s.push_str(&format!(
" description: {},\n",
rust_string_literal(&q.description)
));
s.push_str(&format!(" firm: {},\n", q.firm));
s.push_str(&format!(" halted: {},\n", q.halted));
s.push_str(" },\n");
}
s.push_str("];\n");

// Write only if changed, to avoid touching mtime and triggering downstream rebuilds.
let needs_write = match fs::read_to_string(&out) {
Ok(existing) => existing != s,
Err(_) => true,
};
if needs_write {
if let Some(parent) = out.parent() {
fs::create_dir_all(parent).expect("create conditions dir");
}
fs::write(&out, s).expect("write tables_generated.rs");
}
}
Loading
Loading