rusty_backtest/adapters/
krx_a3b6g7.rs

1//! KRX A3B6G7 Data Adapter for Korean Exchange Data
2//!
3//! Handles the complex KRX A3B6G7 format with 160+ fields including
4//! Korean market-specific metadata and multi-language support.
5
6use crate::features::{Level, OrderBookSnapshot, TradeSide, TradeTick};
7use rust_decimal::Decimal;
8use rusty_common::collections::FxHashMap;
9use smallvec::SmallVec;
10use std::io::{BufRead, BufReader, Read};
11
12/// KRX A3B6G7 field metadata
13#[derive(Debug, Clone)]
14pub struct FieldMetadata {
15    /// Field index (0-based)
16    pub index: usize,
17    /// Korean field name
18    pub name_kr: String,
19    /// English field name
20    pub name_en: String,
21    /// Field data type
22    pub data_type: FieldType,
23    /// Field description
24    pub description: String,
25}
26
27/// Field data types in A3B6G7
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum FieldType {
30    /// Integer field
31    Integer,
32    /// Decimal/Price field
33    Decimal,
34    /// String field
35    String,
36    /// Timestamp field (YYYYMMDDHHMMSSNNNNNN format)
37    Timestamp,
38    /// Boolean/Flag field
39    Boolean,
40}
41
42/// KRX A3B6G7 event - contains all 160+ fields
43#[derive(Debug, Clone)]
44pub struct KrxA3B6G7Event {
45    /// Raw field values indexed by position
46    pub fields: Vec<String>,
47    /// Parsed timestamp in nanoseconds
48    pub timestamp_ns: u64,
49    /// Symbol code
50    pub symbol: String,
51    /// Event type
52    pub event_type: KrxEventType,
53}
54
55/// KRX event types
56#[derive(Debug, Clone, Copy, PartialEq)]
57pub enum KrxEventType {
58    /// L2 orderbook update
59    OrderBook,
60    /// Trade execution
61    Trade,
62    /// Market statistics
63    MarketStats,
64    /// Auction data
65    Auction,
66    /// Circuit breaker event
67    CircuitBreaker,
68}
69
70/// KRX A3B6G7 adapter with field mapping
71pub struct KrxA3B6G7Adapter {
72    /// Field metadata mapping
73    field_map: FxHashMap<String, FieldMetadata>,
74    /// Symbol mapping (KRX code to internal format)
75    symbol_map: FxHashMap<String, String>,
76    /// Critical field indices for fast access
77    critical_indices: CriticalFieldIndices,
78}
79
80/// Pre-computed indices for critical fields
81#[derive(Debug, Clone)]
82struct CriticalFieldIndices {
83    timestamp: usize,
84    symbol: usize,
85    event_type: usize,
86    // Order book fields
87    bid_prices: Vec<usize>,
88    bid_quantities: Vec<usize>,
89    bid_counts: Vec<usize>,
90    ask_prices: Vec<usize>,
91    ask_quantities: Vec<usize>,
92    ask_counts: Vec<usize>,
93    // Trade fields
94    trade_price: usize,
95    trade_quantity: usize,
96    trade_side: usize,
97    // Market stats
98    open_price: usize,
99    high_price: usize,
100    low_price: usize,
101    close_price: usize,
102    volume: usize,
103}
104
105impl Default for CriticalFieldIndices {
106    fn default() -> Self {
107        // Standard A3B6G7 field positions
108        Self {
109            timestamp: 0,
110            symbol: 1,
111            event_type: 2,
112            // 10 levels of order book (indices 10-69)
113            bid_prices: (10..30).step_by(2).collect(),
114            bid_quantities: (11..30).step_by(2).collect(),
115            bid_counts: (70..80).collect(),
116            ask_prices: (30..50).step_by(2).collect(),
117            ask_quantities: (31..50).step_by(2).collect(),
118            ask_counts: (80..90).collect(),
119            // Trade fields
120            trade_price: 50,
121            trade_quantity: 51,
122            trade_side: 52,
123            // Market stats
124            open_price: 90,
125            high_price: 91,
126            low_price: 92,
127            close_price: 93,
128            volume: 94,
129        }
130    }
131}
132
133impl Default for KrxA3B6G7Adapter {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl KrxA3B6G7Adapter {
140    /// Create a new adapter with default field mappings
141    #[must_use]
142    pub fn new() -> Self {
143        let mut adapter = Self {
144            field_map: FxHashMap::default(),
145            symbol_map: FxHashMap::default(),
146            critical_indices: CriticalFieldIndices::default(),
147        };
148
149        // Initialize standard field mappings
150        adapter.init_standard_fields();
151
152        // Common symbol mappings
153        adapter
154            .symbol_map
155            .insert("005930".to_string(), "SAMSUNG-KRW".to_string());
156        adapter
157            .symbol_map
158            .insert("000660".to_string(), "SK-HYNIX-KRW".to_string());
159        adapter
160            .symbol_map
161            .insert("035420".to_string(), "NAVER-KRW".to_string());
162
163        adapter
164    }
165
166    /// Initialize standard A3B6G7 field mappings
167    fn init_standard_fields(&mut self) {
168        // Timestamp field
169        self.add_field(FieldMetadata {
170            index: 0,
171            name_kr: "시간".to_string(),
172            name_en: "timestamp".to_string(),
173            data_type: FieldType::Timestamp,
174            description: "Event timestamp in YYYYMMDDHHMMSSNNNNNN format".to_string(),
175        });
176
177        // Symbol field
178        self.add_field(FieldMetadata {
179            index: 1,
180            name_kr: "종목코드".to_string(),
181            name_en: "symbol_code".to_string(),
182            data_type: FieldType::String,
183            description: "6-digit KRX symbol code".to_string(),
184        });
185
186        // Event type field
187        self.add_field(FieldMetadata {
188            index: 2,
189            name_kr: "이벤트구분".to_string(),
190            name_en: "event_type".to_string(),
191            data_type: FieldType::String,
192            description: "Event type code".to_string(),
193        });
194
195        // Add order book fields (10 levels)
196        for i in 0..10 {
197            let base_idx = 10 + i * 2;
198
199            // Bid price
200            self.add_field(FieldMetadata {
201                index: base_idx,
202                name_kr: format!("매수호가{}", i + 1),
203                name_en: format!("bid_price_{}", i + 1),
204                data_type: FieldType::Decimal,
205                description: format!("Bid price level {}", i + 1),
206            });
207
208            // Bid quantity
209            self.add_field(FieldMetadata {
210                index: base_idx + 1,
211                name_kr: format!("매수잔량{}", i + 1),
212                name_en: format!("bid_quantity_{}", i + 1),
213                data_type: FieldType::Decimal,
214                description: format!("Bid quantity level {}", i + 1),
215            });
216        }
217
218        // Add more fields as needed...
219    }
220
221    /// Add a field metadata entry
222    pub fn add_field(&mut self, metadata: FieldMetadata) {
223        self.field_map
224            .insert(metadata.name_en.clone(), metadata.clone());
225        self.field_map.insert(metadata.name_kr.clone(), metadata);
226    }
227
228    /// Parse A3B6G7 line into event
229    pub fn parse_line(&self, line: &str) -> Result<KrxA3B6G7Event, ParseError> {
230        let fields: Vec<String> = line.split('|').map(|s| s.trim().to_string()).collect();
231
232        if fields.len() < 95 {
233            return Err(ParseError::InsufficientFields(fields.len()));
234        }
235
236        // Parse timestamp (YYYYMMDDHHMMSSNNNNNN to nanoseconds)
237        let timestamp_str = &fields[self.critical_indices.timestamp];
238        let timestamp_ns = self.parse_krx_timestamp(timestamp_str)?;
239
240        // Parse symbol
241        let symbol = fields[self.critical_indices.symbol].clone();
242        let symbol = self.symbol_map.get(&symbol).cloned().unwrap_or(symbol);
243
244        // Parse event type
245        let event_type = match fields[self.critical_indices.event_type].as_str() {
246            "B1" | "S1" => KrxEventType::OrderBook,
247            "T1" => KrxEventType::Trade,
248            "M1" => KrxEventType::MarketStats,
249            "A1" => KrxEventType::Auction,
250            "C1" => KrxEventType::CircuitBreaker,
251            _ => {
252                return Err(ParseError::InvalidEventType(
253                    fields[self.critical_indices.event_type].clone(),
254                ));
255            }
256        };
257
258        Ok(KrxA3B6G7Event {
259            fields,
260            timestamp_ns,
261            symbol,
262            event_type,
263        })
264    }
265
266    /// Parse KRX timestamp format to nanoseconds
267    fn parse_krx_timestamp(&self, timestamp: &str) -> Result<u64, ParseError> {
268        if timestamp.len() != 20 {
269            return Err(ParseError::InvalidTimestamp(timestamp.to_string()));
270        }
271
272        // Parse components
273        let year = timestamp[0..4]
274            .parse::<u64>()
275            .map_err(|_| ParseError::InvalidTimestamp(timestamp.to_string()))?;
276        let month = timestamp[4..6]
277            .parse::<u64>()
278            .map_err(|_| ParseError::InvalidTimestamp(timestamp.to_string()))?;
279        let day = timestamp[6..8]
280            .parse::<u64>()
281            .map_err(|_| ParseError::InvalidTimestamp(timestamp.to_string()))?;
282        let hour = timestamp[8..10]
283            .parse::<u64>()
284            .map_err(|_| ParseError::InvalidTimestamp(timestamp.to_string()))?;
285        let minute = timestamp[10..12]
286            .parse::<u64>()
287            .map_err(|_| ParseError::InvalidTimestamp(timestamp.to_string()))?;
288        let second = timestamp[12..14]
289            .parse::<u64>()
290            .map_err(|_| ParseError::InvalidTimestamp(timestamp.to_string()))?;
291        let microsecond = timestamp[14..20]
292            .parse::<u64>()
293            .map_err(|_| ParseError::InvalidTimestamp(timestamp.to_string()))?;
294
295        // Convert to Unix timestamp (simplified - doesn't handle leap years perfectly)
296        let days_since_epoch = (year - 1970) * 365 + (month - 1) * 30 + (day - 1);
297        let seconds_since_epoch = days_since_epoch * 86400 + hour * 3600 + minute * 60 + second;
298        let nanoseconds = seconds_since_epoch * 1_000_000_000 + microsecond * 1000;
299
300        Ok(nanoseconds)
301    }
302}
303
304impl KrxA3B6G7Event {
305    /// Convert to OrderBookSnapshot if this is an orderbook event
306    pub fn to_orderbook_snapshot(&self, adapter: &KrxA3B6G7Adapter) -> Option<OrderBookSnapshot> {
307        if self.event_type != KrxEventType::OrderBook {
308            return None;
309        }
310
311        let indices = &adapter.critical_indices;
312        let mut bids = SmallVec::with_capacity(10);
313        let mut asks = SmallVec::with_capacity(10);
314
315        // Parse bid levels
316        for i in 0..10 {
317            let price = self
318                .fields
319                .get(indices.bid_prices[i])
320                .and_then(|s| s.parse::<Decimal>().ok());
321            let quantity = self
322                .fields
323                .get(indices.bid_quantities[i])
324                .and_then(|s| s.parse::<Decimal>().ok());
325            let order_count = self
326                .fields
327                .get(indices.bid_counts.get(i).copied().unwrap_or(0))
328                .and_then(|s| s.parse::<u32>().ok())
329                .unwrap_or(0);
330
331            if let (Some(price), Some(quantity)) = (price, quantity)
332                && price > Decimal::ZERO
333                && quantity > Decimal::ZERO
334            {
335                bids.push(Level {
336                    price,
337                    quantity,
338                    order_count,
339                });
340            }
341        }
342
343        // Parse ask levels
344        for i in 0..10 {
345            let price = self
346                .fields
347                .get(indices.ask_prices[i])
348                .and_then(|s| s.parse::<Decimal>().ok());
349            let quantity = self
350                .fields
351                .get(indices.ask_quantities[i])
352                .and_then(|s| s.parse::<Decimal>().ok());
353            let order_count = self
354                .fields
355                .get(indices.ask_counts.get(i).copied().unwrap_or(0))
356                .and_then(|s| s.parse::<u32>().ok())
357                .unwrap_or(0);
358
359            if let (Some(price), Some(quantity)) = (price, quantity)
360                && price > Decimal::ZERO
361                && quantity > Decimal::ZERO
362            {
363                asks.push(Level {
364                    price,
365                    quantity,
366                    order_count,
367                });
368            }
369        }
370
371        Some(OrderBookSnapshot {
372            timestamp_ns: self.timestamp_ns,
373            symbol: self.symbol.clone(),
374            bids,
375            asks,
376        })
377    }
378
379    /// Convert to TradeTick if this is a trade event
380    pub fn to_trade_tick(&self, adapter: &KrxA3B6G7Adapter) -> Option<TradeTick> {
381        if self.event_type != KrxEventType::Trade {
382            return None;
383        }
384
385        let indices = &adapter.critical_indices;
386
387        let price = self
388            .fields
389            .get(indices.trade_price)
390            .and_then(|s| s.parse::<Decimal>().ok())?;
391        let quantity = self
392            .fields
393            .get(indices.trade_quantity)
394            .and_then(|s| s.parse::<Decimal>().ok())?;
395        let side_str = self.fields.get(indices.trade_side)?;
396
397        let side = match side_str.as_str() {
398            "B" | "매수" => TradeSide::Buy,
399            "S" | "매도" => TradeSide::Sell,
400            _ => return None,
401        };
402
403        Some(TradeTick {
404            timestamp_ns: self.timestamp_ns,
405            symbol: self.symbol.clone(),
406            side,
407            price,
408            quantity,
409        })
410    }
411
412    /// Get a specific field value by name (Korean or English)
413    pub fn get_field(&self, field_name: &str, adapter: &KrxA3B6G7Adapter) -> Option<&str> {
414        let metadata = adapter.field_map.get(field_name)?;
415        self.fields.get(metadata.index).map(|s| s.as_str())
416    }
417
418    /// Get market statistics if available
419    pub fn get_market_stats(&self, adapter: &KrxA3B6G7Adapter) -> Option<MarketStats> {
420        if self.event_type != KrxEventType::MarketStats {
421            return None;
422        }
423
424        let indices = &adapter.critical_indices;
425
426        Some(MarketStats {
427            open: self
428                .fields
429                .get(indices.open_price)
430                .and_then(|s| s.parse::<Decimal>().ok())?,
431            high: self
432                .fields
433                .get(indices.high_price)
434                .and_then(|s| s.parse::<Decimal>().ok())?,
435            low: self
436                .fields
437                .get(indices.low_price)
438                .and_then(|s| s.parse::<Decimal>().ok())?,
439            close: self
440                .fields
441                .get(indices.close_price)
442                .and_then(|s| s.parse::<Decimal>().ok())?,
443            volume: self
444                .fields
445                .get(indices.volume)
446                .and_then(|s| s.parse::<Decimal>().ok())?,
447        })
448    }
449}
450
451/// Market statistics data.
452#[derive(Debug, Clone)]
453pub struct MarketStats {
454    /// The opening price.
455    pub open: Decimal,
456    /// The highest price.
457    pub high: Decimal,
458    /// The lowest price.
459    pub low: Decimal,
460    /// The closing price.
461    pub close: Decimal,
462    /// The trading volume.
463    pub volume: Decimal,
464}
465
466/// Error types for parsing KRX A3B6G7 data.
467#[derive(Debug, Clone)]
468pub enum ParseError {
469    /// The input has an insufficient number of fields.
470    InsufficientFields(usize),
471    /// The timestamp in the input is invalid.
472    InvalidTimestamp(String),
473    /// The event type in the input is invalid.
474    InvalidEventType(String),
475    /// A field in the input is invalid.
476    InvalidField(String),
477    /// An I/O error occurred.
478    IoError(String),
479}
480
481impl std::fmt::Display for ParseError {
482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483        match self {
484            ParseError::InsufficientFields(count) => {
485                write!(f, "Insufficient fields: expected at least 95, got {count}")
486            }
487            ParseError::InvalidTimestamp(ts) => write!(f, "Invalid timestamp: {ts}"),
488            ParseError::InvalidEventType(et) => write!(f, "Invalid event type: {et}"),
489            ParseError::InvalidField(field) => write!(f, "Invalid field: {field}"),
490            ParseError::IoError(e) => write!(f, "IO error: {e}"),
491        }
492    }
493}
494
495impl std::error::Error for ParseError {}
496
497/// Iterator for streaming KRX A3B6G7 data
498pub struct KrxA3B6G7Iterator<R: Read> {
499    reader: BufReader<R>,
500    adapter: KrxA3B6G7Adapter,
501}
502
503impl<R: Read> KrxA3B6G7Iterator<R> {
504    /// Create a new iterator
505    #[must_use]
506    pub fn new(reader: R) -> Self {
507        Self {
508            reader: BufReader::new(reader),
509            adapter: KrxA3B6G7Adapter::new(),
510        }
511    }
512
513    /// Set custom field mapping
514    #[must_use]
515    pub fn with_field_mapping(mut self, metadata: FieldMetadata) -> Self {
516        self.adapter.add_field(metadata);
517        self
518    }
519
520    /// Set custom symbol mapping
521    #[must_use]
522    pub fn with_symbol_mapping(mut self, krx_code: String, internal_symbol: String) -> Self {
523        self.adapter.symbol_map.insert(krx_code, internal_symbol);
524        self
525    }
526}
527
528impl<R: Read> Iterator for KrxA3B6G7Iterator<R> {
529    type Item = Result<KrxA3B6G7Event, ParseError>;
530
531    fn next(&mut self) -> Option<Self::Item> {
532        let mut line = String::new();
533        match self.reader.read_line(&mut line) {
534            Ok(0) => None, // EOF
535            Ok(_) => {
536                let line = line.trim();
537                if line.is_empty() || line.starts_with('#') {
538                    return self.next(); // Skip empty lines and comments
539                }
540                Some(self.adapter.parse_line(line))
541            }
542            Err(e) => Some(Err(ParseError::IoError(e.to_string()))),
543        }
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use rust_decimal_macros::dec;
551
552    #[test]
553    fn test_krx_timestamp_parsing() {
554        let adapter = KrxA3B6G7Adapter::new();
555
556        // Test timestamp: 2023-12-25 09:30:00.123456
557        let timestamp = "20231225093000123456";
558        let result = adapter.parse_krx_timestamp(timestamp);
559
560        assert!(result.is_ok());
561        // The actual value would need proper date calculation
562        // This is a simplified test
563    }
564
565    #[test]
566    fn test_orderbook_parsing() {
567        let adapter = KrxA3B6G7Adapter::new();
568
569        // Create a sample A3B6G7 line with orderbook data
570        let mut fields = vec![""; 160];
571        fields[0] = "20231225093000123456"; // timestamp
572        fields[1] = "005930"; // Samsung Electronics
573        fields[2] = "B1"; // Orderbook event
574
575        // Add some bid levels
576        fields[10] = "70000"; // bid price 1
577        fields[11] = "100"; // bid quantity 1
578        fields[12] = "69900"; // bid price 2
579        fields[13] = "200"; // bid quantity 2
580
581        // Add some ask levels
582        fields[30] = "70100"; // ask price 1
583        fields[31] = "150"; // ask quantity 1
584        fields[32] = "70200"; // ask price 2
585        fields[33] = "250"; // ask quantity 2
586
587        let line = fields.join("|");
588        let event = adapter.parse_line(&line).unwrap();
589
590        assert_eq!(event.event_type, KrxEventType::OrderBook);
591        assert_eq!(event.symbol, "SAMSUNG-KRW");
592
593        let snapshot = event.to_orderbook_snapshot(&adapter).unwrap();
594        assert_eq!(snapshot.bids.len(), 2);
595        assert_eq!(snapshot.asks.len(), 2);
596        assert_eq!(snapshot.bids[0].price, dec!(70000));
597        assert_eq!(snapshot.asks[0].price, dec!(70100));
598    }
599}