From 92823e30a3d7781885ee65b9e326199c5abcdeae Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sat, 13 Jun 2026 14:05:30 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20WBS-3.3=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EC=8B=9C=EB=AE=AC=EB=A0=88=EC=9D=B4=ED=84=B0=20tick=20?= =?UTF-8?q?=EC=A0=95=EA=B7=9C=ED=99=94=20=EC=99=84=EC=84=B1=20(ETF=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AF=B8=EA=B5=AD=20=EC=A3=BC=EC=8B=9D=20=ED=98=B8?= =?UTF-8?q?=EA=B0=80=20=EB=8B=A8=EC=9C=84=20=EC=84=B8=EB=B6=84=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/validate_execution_simulator_v1.py | 45 ++++++++++++++++--- .../validate_live_data_activation_gate_v1.py | 2 +- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/tools/validate_execution_simulator_v1.py b/tools/validate_execution_simulator_v1.py index 52a31dd..1d99543 100644 --- a/tools/validate_execution_simulator_v1.py +++ b/tools/validate_execution_simulator_v1.py @@ -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, diff --git a/tools/validate_live_data_activation_gate_v1.py b/tools/validate_live_data_activation_gate_v1.py index 5ac2fc7..542d811 100644 --- a/tools/validate_live_data_activation_gate_v1.py +++ b/tools/validate_live_data_activation_gate_v1.py @@ -148,7 +148,7 @@ def run() -> dict: } OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) - OUTPUT_PATH.write_text(json.dumps(result, ensure_ascii=False, indent=2)) + OUTPUT_PATH.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") return result