"""KRX 호가단위(tick-size) 산출 — 순수 도메인 함수 (REQ-083, dcy-030). slippage 보호 지정가(REQ-070)가 KIS에 제출되기 전에 유효한 KRX 호가단위 배수로 스냅되도록 가격→tick 산출과 배수 내림/올림 헬퍼를 제공한다. 주식 전용이다. ETF/ETN 등 비주식 호가체계는 범위 밖이며(dcy-030 OQ-1), 2,000원 미만 비주식은 미지원(known limitation). 종목 유형 분기는 두지 않는다. 호가단위 표(KRX 주식, 2023-01-25~). 구간은 하한 포함/상한 미만 `[lo, hi)`: 가격(원) tick(원) < 2,000 1 2,000 ~ 5,000 5 5,000 ~ 20,000 10 20,000 ~ 50,000 50 50,000 ~ 200,000 100 200,000 ~ 500,000 500 >= 500,000 1,000 """ from __future__ import annotations import math # (lower_bound_inclusive, tick). 내림차순으로 평가한다. 단일 변경 지점(dcy-030 R-3). _KRX_STOCK_TICKS: tuple[tuple[int, int], ...] = ( (500_000, 1_000), (200_000, 500), (50_000, 100), (20_000, 50), (5_000, 10), (2_000, 5), (0, 1), ) def krx_stock_tick(price: float) -> int: """`price`(원)가 속한 KRX 주식 호가단위(원)를 반환한다. 구간 경계는 하한 포함(예: 정확히 50,000원 → 100원 tick). """ p = float(price) for lower, tick in _KRX_STOCK_TICKS: if p >= lower: return tick return 1 # p < 0 방어 (도달하지 않음) # 부동소수 잡음 흡수용 epsilon. slippage raw = base*(1+pct/100)는 수학적으로 # 정수여도 float로는 1004.9999999999999처럼 나올 수 있다(잡음 ~ q*1e-16). # q(=price/tick)는 유계라 1e-9면 잡음을 안전하게 흡수하면서 실제 값은 안 넘긴다. _EPS = 1e-9 def floor_to_tick(price: float) -> int: """`price`를 해당 가격대 호가단위의 **내림 배수**로 정규화한다 (≤ price).""" tick = krx_stock_tick(price) return int(math.floor(float(price) / tick + _EPS) * tick) def ceil_to_tick(price: float) -> int: """`price`를 해당 가격대 호가단위의 **올림 배수**로 정규화한다 (≥ price).""" tick = krx_stock_tick(price) return int(math.ceil(float(price) / tick - _EPS) * tick)