# -*- coding: utf-8 -*-
"""
KIS strategy_builder 동작 확인 하네스 (알고랩, 2026-11-08)
https://algolab.co.kr/blog/kis-strategy-builder-limits-real-order-2026

- 한국투자증권 공식 저장소 koreainvestment/open-trading-api 의 strategy_builder 를
  네트워크·API 키 없이 돌려 본다. kis_auth 를 가짜 모듈로 바꿔 끼우므로 주문은 나가지 않는다.
- 기준 커밋을 고정한다: main 3a9be59 (2026-10-27). 저장소가 바뀌면 결과도 달라질 수 있다.
- 가짜 일봉은 '한 번 호출에 최대 100건'(공식 API 설명)만 흉내 낸 합성 데이터다. 휴장일은 넣지 않았다.

사용법:
  1) https://github.com/koreainvestment/open-trading-api/archive/3a9be59.zip 을 받아 푼다
  2) python kis_strategy_builder_check.py <압축푼폴더>/strategy_builder
저장소는 Python 3.11+ 기준이다. 3.10 에서는 아래 ExceptionGroup 대체 줄이 필요하다.
"""
import sys, types, datetime as dt, math, json, builtins
if not hasattr(builtins, "ExceptionGroup"): builtins.ExceptionGroup = builtins.BaseExceptionGroup = type("EG",(Exception,),{})  # py3.10 shim (repo targets 3.11+)
SB = sys.argv[1]
sys.path.insert(0, SB)
calls = []
ka = types.ModuleType("kis_auth")
class Body:
    def __init__(s, o2): s.output2 = o2; s.output = {}
class Res:
    def __init__(s, o2): s._b = Body(o2)
    def isOK(s): return True
    def getBody(s): return s._b
    def printError(s, *a, **k): pass
def _url_fetch(url, tr_id, tr_cont, params, postFlag=False):
    calls.append((url, tr_id, dict(params)))
    if "daily-itemchartprice" in url:
        d1 = dt.datetime.strptime(params["FID_INPUT_DATE_1"], "%Y%m%d")
        d2 = dt.datetime.strptime(params["FID_INPUT_DATE_2"], "%Y%m%d")
        rows, d, i = [], d2, 0
        while d >= d1 and len(rows) < 100:          # KIS: max 100 rows per call (official description)
            if d.weekday() < 5:
                p = 50000 + int(3000 * math.sin(i / 9.0)) + i * 10
                rows.append({"stck_bsop_date": d.strftime("%Y%m%d"), "stck_oprc": p, "stck_hgpr": p + 300,
                             "stck_lwpr": p - 300, "stck_clpr": p + 50, "acml_vol": 100000})
                i += 1
            d -= dt.timedelta(days=1)
        return Res(rows)
    return Res([])
ka._url_fetch = _url_fetch
ka.isPaperTrading = lambda: True
ka.getTREnv = lambda: types.SimpleNamespace(my_acct="00000000", my_prod="01")
sys.modules["kis_auth"] = ka
from core import data_fetcher
data_fetcher._assert_trenv_ready = lambda *a, **k: True
from strategy.strategy_01_golden_cross import GoldenCrossStrategy
for long in (20, 60, 90, 95, 100, 120, 200):
    calls.clear()
    s = GoldenCrossStrategy(short_period=5, long_period=long)
    sig = s.generate_signal("005930", "TEST")
    p = calls[0][2]
    df = data_fetcher.get_daily_prices("005930", s.required_days)
    print(f"long={long:3d} required_days={s.required_days:3d} window={p['FID_INPUT_DATE_1']}~{p['FID_INPUT_DATE_2']} rows={len(df):3d} -> {sig.action.value.upper()} / {sig.reason}")
# builder path: default sell condition when exit is empty
from strategy_core.dsl.converter import builder_state_to_dsl
import strategy_core.preset
from strategy_core import StrategyRegistry
bs = json.loads(json.dumps([x for x in StrategyRegistry.get_list() if x.get("id")=="golden_cross"][0]["builder_state"]))
print("DSL (preset):", builder_state_to_dsl(bs))
bs["exit"]["conditions"] = []
print("DSL (exit empty):", builder_state_to_dsl(bs))
print("risk in DSL? ->", "risk" in str(builder_state_to_dsl(bs)))
# builder path with SMA(120) and empty exit
from strategy_core.executor import execute_from_builder_state
bs2 = json.loads(json.dumps(bs)); bs2["indicators"][1]["params"]["period"] = 120
logs = []
res = execute_from_builder_state(bs2, "my_sma120", ["005930"], lambda t, m: logs.append(m), lambda c: c, lambda: None)
print("builder SMA120:", res[0]["action"], res[0]["reason"]); print([l for l in logs if "조건" in l])
# order path: what actually goes to KIS
from core import order_executor as oe
from core.signal import Signal, Action
class PM:
    def __init__(s, *a): pass
    def get_holding_quantity(s, c): return 10
    def check_duplicate(s, c): return False
    def refresh(s): pass
oe.PositionManager = PM
calls.clear()
ex = oe.OrderExecutor(env_dv="demo")
ex.execute_signal(Signal(stock_code="005930", stock_name="TEST", action=Action.BUY, strength=1.0, reason="t", quantity=3))
ex.execute_signal(Signal(stock_code="005930", stock_name="TEST", action=Action.SELL, strength=0.7, reason="t", target_price=50070))
for c in calls: print("ORDER", c[1], json.dumps(c[2], ensure_ascii=False))
print("risk_manager.stop_loss_pct =", ex.risk_manager.stop_loss_pct, "(builder preset said 5%)")
# real-mode: backend passes trading_state.current_mode ("vps" | "prod")
ka.isPaperTrading = lambda: False
calls.clear()
for mode in ("prod", "real"):
    ex = oe.OrderExecutor(env_dv=mode)
    ex.execute_signal(Signal(stock_code="005930", stock_name="TEST", action=Action.BUY, strength=1.0, reason="t", quantity=1))
for c, mode in zip(calls, ("prod", "real")): print(f"env_dv={mode!r:7} -> header tr_id sent: {c[1]}")
