tdbe 0.13.0

ThetaData Binary Encoding -- market data types, FIT/FIE codecs, Black-Scholes Greeks
Documentation
//! 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");
    }
}