finance_enums 0.6.0

Standard financial enumerations
Documentation
#![allow(non_upper_case_globals)]

use std::ffi::{c_char, CString};
use std::sync::OnceLock;

const EXCHANGE_RECORD_PARTS: &[&str] = &[
    include_str!("exchange_records/exchange_records_part1.tsv"),
    include_str!("exchange_records/exchange_records_part2.tsv"),
    include_str!("exchange_records/exchange_records_part3.tsv"),
    include_str!("exchange_records/exchange_records_part4.tsv"),
    include_str!("exchange_records/exchange_records_part5.tsv"),
    include_str!("exchange_records/exchange_records_part6.tsv"),
];

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ExchangeRecord {
    pub mic: &'static str,
    pub market_name: &'static str,
    pub legal_entity_name: &'static str,
    pub operating_mic: &'static str,
    pub parent_mic: &'static str,
    pub market_category_code: &'static str,
    pub acronym: &'static str,
    pub iso_country_code: &'static str,
    pub city: &'static str,
    pub website: &'static str,
    pub status: &'static str,
    pub region: &'static str,
    pub subregion: &'static str,
    pub is_segment: bool,
    pub is_official: bool,
}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct ExchangeRecordRaw {
    pub mic: *const c_char,
    pub market_name: *const c_char,
    pub legal_entity_name: *const c_char,
    pub operating_mic: *const c_char,
    pub parent_mic: *const c_char,
    pub market_category_code: *const c_char,
    pub acronym: *const c_char,
    pub iso_country_code: *const c_char,
    pub city: *const c_char,
    pub website: *const c_char,
    pub status: *const c_char,
    pub region: *const c_char,
    pub subregion: *const c_char,
    pub is_segment: bool,
    pub is_official: bool,
}

unsafe impl Sync for ExchangeRecordRaw {}
unsafe impl Send for ExchangeRecordRaw {}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct ExchangeDataExportV1 {
    pub abi_version: u32,
    pub export_struct_size: usize,
    pub exchange_record_size: usize,
    pub records: *const ExchangeRecordRaw,
    pub records_len: usize,
}

unsafe impl Sync for ExchangeDataExportV1 {}
unsafe impl Send for ExchangeDataExportV1 {}

pub const EXCHANGE_EXPORT_ABI_VERSION: u32 = 1;

struct ExchangeDataExportBacking {
    _records: Box<[ExchangeRecordRaw]>,
    export: ExchangeDataExportV1,
}

unsafe impl Sync for ExchangeDataExportBacking {}
unsafe impl Send for ExchangeDataExportBacking {}

fn leak_c_string(value: &'static str) -> *const c_char {
    CString::new(value)
        .expect("exchange field contained interior NUL")
        .into_raw()
        .cast_const()
}

fn build_exchange_export_v1() -> ExchangeDataExportBacking {
    let records = exchange_records()
        .iter()
        .map(|record| ExchangeRecordRaw {
            mic: leak_c_string(record.mic),
            market_name: leak_c_string(record.market_name),
            legal_entity_name: leak_c_string(record.legal_entity_name),
            operating_mic: leak_c_string(record.operating_mic),
            parent_mic: leak_c_string(record.parent_mic),
            market_category_code: leak_c_string(record.market_category_code),
            acronym: leak_c_string(record.acronym),
            iso_country_code: leak_c_string(record.iso_country_code),
            city: leak_c_string(record.city),
            website: leak_c_string(record.website),
            status: leak_c_string(record.status),
            region: leak_c_string(record.region),
            subregion: leak_c_string(record.subregion),
            is_segment: record.is_segment,
            is_official: record.is_official,
        })
        .collect::<Vec<_>>()
        .into_boxed_slice();

    let export = ExchangeDataExportV1 {
        abi_version: EXCHANGE_EXPORT_ABI_VERSION,
        export_struct_size: std::mem::size_of::<ExchangeDataExportV1>(),
        exchange_record_size: std::mem::size_of::<ExchangeRecordRaw>(),
        records: records.as_ptr(),
        records_len: records.len(),
    };

    ExchangeDataExportBacking {
        _records: records,
        export,
    }
}

fn parse_bool(value: &str, field_name: &str, mic: &str) -> bool {
    match value {
        "0" => false,
        "1" => true,
        _ => panic!("invalid {field_name} flag for exchange {mic}: {value}"),
    }
}

fn parse_exchange_record(line: &'static str) -> ExchangeRecord {
    let fields: Vec<_> = line.split('\t').collect();
    assert_eq!(
        fields.len(),
        15,
        "expected 15 tab-delimited fields for exchange record: {line}"
    );

    ExchangeRecord {
        mic: fields[0],
        market_name: fields[1],
        legal_entity_name: fields[2],
        operating_mic: fields[3],
        parent_mic: fields[4],
        market_category_code: fields[5],
        acronym: fields[6],
        iso_country_code: fields[7],
        city: fields[8],
        website: fields[9],
        status: fields[10],
        region: fields[11],
        subregion: fields[12],
        is_segment: parse_bool(fields[13], "is_segment", fields[0]),
        is_official: parse_bool(fields[14], "is_official", fields[0]),
    }
}

fn build_exchange_records() -> Box<[ExchangeRecord]> {
    let mut records = Vec::new();

    for part in EXCHANGE_RECORD_PARTS {
        for line in part.lines() {
            if line.is_empty() {
                continue;
            }
            records.push(parse_exchange_record(line));
        }
    }

    records.into_boxed_slice()
}

static EXCHANGE_RECORDS_INNER: OnceLock<Box<[ExchangeRecord]>> = OnceLock::new();
static EXCHANGE_EXPORT_V1_INNER: OnceLock<ExchangeDataExportBacking> = OnceLock::new();

pub fn exchange_records() -> &'static [ExchangeRecord] {
    EXCHANGE_RECORDS_INNER
        .get_or_init(build_exchange_records)
        .as_ref()
}

pub fn exchange_record(mic: &str) -> Option<&'static ExchangeRecord> {
    exchange_records().iter().find(|record| record.mic == mic)
}

pub fn exchange_export_v1() -> &'static ExchangeDataExportV1 {
    &EXCHANGE_EXPORT_V1_INNER
        .get_or_init(build_exchange_export_v1)
        .export
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::exchange_codes::ExchangeCode_VARIANTS;
    use std::collections::HashSet;

    #[test]
    fn test_exchange_records_cover_variant_table() {
        assert_eq!(exchange_records().len(), ExchangeCode_VARIANTS.len());

        for mic in ExchangeCode_VARIANTS {
            assert!(
                exchange_record(mic).is_some(),
                "missing exchange record for {mic}"
            );
        }
    }

    #[test]
    fn test_exchange_records_are_unique() {
        let mut seen = HashSet::new();

        for record in exchange_records() {
            assert!(
                seen.insert(record.mic),
                "duplicate exchange MIC: {}",
                record.mic
            );
        }
    }

    #[test]
    fn test_project_defined_exchange_records_are_flagged() {
        let forex = exchange_record("FOREX").unwrap();
        let crypto = exchange_record("CRYPTO").unwrap();

        assert_eq!(forex.status, "PROJECT");
        assert!(!forex.is_official);
        assert_eq!(crypto.region, "Global");
        assert!(!crypto.is_official);
    }
}