feat: WBS-3.3 주문 시뮬레이터 tick 정규화 완성 (ETF 및 미국 주식 호가 단위 세분화)
This commit is contained in:
@@ -36,6 +36,32 @@ TICK_TABLE = [
|
||||
]
|
||||
TICK_ABOVE_2M = 1_000
|
||||
|
||||
# Load names from GatherTradingData.json to detect ETF or US Stocks
|
||||
TICKER_NAMES: dict[str, str] = {}
|
||||
gtd_path = ROOT / "GatherTradingData.json"
|
||||
if gtd_path.exists():
|
||||
try:
|
||||
gtd = json.loads(gtd_path.read_text(encoding="utf-8"))
|
||||
df_rows = gtd.get("data", {}).get("data_feed") or []
|
||||
for r in df_rows:
|
||||
if isinstance(r, dict) and r.get("Ticker"):
|
||||
TICKER_NAMES[str(r["Ticker"])] = str(r.get("Name") or "")
|
||||
univ_rows = gtd.get("data", {}).get("universe") or []
|
||||
for r in univ_rows:
|
||||
if isinstance(r, dict) and r.get("Ticker"):
|
||||
TICKER_NAMES[str(r["Ticker"])] = str(r.get("Name") or "")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _is_etf(ticker: str) -> bool:
|
||||
name = TICKER_NAMES.get(ticker, "").upper()
|
||||
return any(x in name for x in ["KODEX", "TIGER", "KBSTAR", "ARIRANG", "HANARO", "TIMEFOLIO", "SOL", "ACE", "PLUS"])
|
||||
|
||||
|
||||
def _is_us_stock(ticker: str) -> bool:
|
||||
return str(ticker).isalpha()
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict:
|
||||
if not path.exists():
|
||||
@@ -46,15 +72,19 @@ def _load_json(path: Path) -> dict:
|
||||
return {"_error": str(e), "_path": str(path)}
|
||||
|
||||
|
||||
def _tick_size(price: float) -> int:
|
||||
def _tick_size(price: float, ticker: str) -> float:
|
||||
if _is_us_stock(ticker):
|
||||
return 0.01
|
||||
if _is_etf(ticker):
|
||||
return 5.0
|
||||
for threshold, tick in TICK_TABLE:
|
||||
if price < threshold:
|
||||
return tick
|
||||
return TICK_ABOVE_2M
|
||||
|
||||
|
||||
def _normalize_tick(price: float) -> float:
|
||||
tick = _tick_size(price)
|
||||
def _normalize_tick(price: float, ticker: str) -> float:
|
||||
tick = _tick_size(price, ticker)
|
||||
return math.floor(price / tick) * tick
|
||||
|
||||
|
||||
@@ -95,10 +125,11 @@ def _simulate_order(order: dict) -> dict:
|
||||
qty = int(order.get("quantity") or order.get("qty") or 0)
|
||||
action = str(order.get("action") or order.get("order_type") or "").upper()
|
||||
side = "BUY" if "BUY" in action else "SELL"
|
||||
ticker = str(order.get("ticker") or "")
|
||||
|
||||
# Tick normalization check
|
||||
if price > 0:
|
||||
normalized = _normalize_tick(price)
|
||||
normalized = _normalize_tick(price, ticker)
|
||||
if abs(normalized - price) > 0.01:
|
||||
errors.append(f"TICK_INVALID: price={price} → normalized={normalized}")
|
||||
else:
|
||||
@@ -113,11 +144,11 @@ def _simulate_order(order: dict) -> dict:
|
||||
order_amount = slippage_price * qty
|
||||
|
||||
return {
|
||||
"ticker": order.get("ticker"),
|
||||
"ticker": ticker,
|
||||
"action": action,
|
||||
"original_price": price,
|
||||
"normalized_price": _normalize_tick(price) if price > 0 else 0,
|
||||
"slippage_adjusted_price": round(slippage_price, 0),
|
||||
"normalized_price": _normalize_tick(price, ticker) if price > 0 else 0,
|
||||
"slippage_adjusted_price": round(slippage_price, 2) if _is_us_stock(ticker) else round(slippage_price, 0),
|
||||
"quantity": qty,
|
||||
"order_amount_krw": round(order_amount),
|
||||
"errors": errors,
|
||||
|
||||
Reference in New Issue
Block a user