1use 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#[derive(Debug, Clone)]
14pub struct FieldMetadata {
15 pub index: usize,
17 pub name_kr: String,
19 pub name_en: String,
21 pub data_type: FieldType,
23 pub description: String,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum FieldType {
30 Integer,
32 Decimal,
34 String,
36 Timestamp,
38 Boolean,
40}
41
42#[derive(Debug, Clone)]
44pub struct KrxA3B6G7Event {
45 pub fields: Vec<String>,
47 pub timestamp_ns: u64,
49 pub symbol: String,
51 pub event_type: KrxEventType,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
57pub enum KrxEventType {
58 OrderBook,
60 Trade,
62 MarketStats,
64 Auction,
66 CircuitBreaker,
68}
69
70pub struct KrxA3B6G7Adapter {
72 field_map: FxHashMap<String, FieldMetadata>,
74 symbol_map: FxHashMap<String, String>,
76 critical_indices: CriticalFieldIndices,
78}
79
80#[derive(Debug, Clone)]
82struct CriticalFieldIndices {
83 timestamp: usize,
84 symbol: usize,
85 event_type: usize,
86 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_price: usize,
95 trade_quantity: usize,
96 trade_side: usize,
97 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 Self {
109 timestamp: 0,
110 symbol: 1,
111 event_type: 2,
112 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_price: 50,
121 trade_quantity: 51,
122 trade_side: 52,
123 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 #[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 adapter.init_standard_fields();
151
152 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 fn init_standard_fields(&mut self) {
168 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 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 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 for i in 0..10 {
197 let base_idx = 10 + i * 2;
198
199 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 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 }
220
221 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 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 let timestamp_str = &fields[self.critical_indices.timestamp];
238 let timestamp_ns = self.parse_krx_timestamp(timestamp_str)?;
239
240 let symbol = fields[self.critical_indices.symbol].clone();
242 let symbol = self.symbol_map.get(&symbol).cloned().unwrap_or(symbol);
243
244 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
453pub struct MarketStats {
454 pub open: Decimal,
456 pub high: Decimal,
458 pub low: Decimal,
460 pub close: Decimal,
462 pub volume: Decimal,
464}
465
466#[derive(Debug, Clone)]
468pub enum ParseError {
469 InsufficientFields(usize),
471 InvalidTimestamp(String),
473 InvalidEventType(String),
475 InvalidField(String),
477 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
497pub struct KrxA3B6G7Iterator<R: Read> {
499 reader: BufReader<R>,
500 adapter: KrxA3B6G7Adapter,
501}
502
503impl<R: Read> KrxA3B6G7Iterator<R> {
504 #[must_use]
506 pub fn new(reader: R) -> Self {
507 Self {
508 reader: BufReader::new(reader),
509 adapter: KrxA3B6G7Adapter::new(),
510 }
511 }
512
513 #[must_use]
515 pub fn with_field_mapping(mut self, metadata: FieldMetadata) -> Self {
516 self.adapter.add_field(metadata);
517 self
518 }
519
520 #[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, Ok(_) => {
536 let line = line.trim();
537 if line.is_empty() || line.starts_with('#') {
538 return self.next(); }
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 let timestamp = "20231225093000123456";
558 let result = adapter.parse_krx_timestamp(timestamp);
559
560 assert!(result.is_ok());
561 }
564
565 #[test]
566 fn test_orderbook_parsing() {
567 let adapter = KrxA3B6G7Adapter::new();
568
569 let mut fields = vec![""; 160];
571 fields[0] = "20231225093000123456"; fields[1] = "005930"; fields[2] = "B1"; fields[10] = "70000"; fields[11] = "100"; fields[12] = "69900"; fields[13] = "200"; fields[30] = "70100"; fields[31] = "150"; fields[32] = "70200"; fields[33] = "250"; 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}