"""sim 엔진 설정 — 상수·경로. 기본값은 여기, 런타임 튜닝은 state/sim/params.json 오버라이드."""
from __future__ import annotations

import json
from pathlib import Path
from zoneinfo import ZoneInfo

KST = ZoneInfo('Asia/Seoul')

WORKSPACE = Path(__file__).resolve().parent.parent          # agents/stock/workspace
SCRIPTS = WORKSPACE / 'scripts'
STATE_DIR = WORKSPACE / 'state' / 'sim'
HOLIDAYS_PATH = WORKSPACE / 'state' / 'market_holidays.json'
MARKET_HISTORY = WORKSPACE / 'state' / 'market_indicators_history.jsonl'

# sim state 파일
PORTFOLIO_PATH = STATE_DIR / 'portfolio.json'
TRADES_PATH = STATE_DIR / 'trades.jsonl'
LAST_SCAN_PATH = STATE_DIR / 'last_scan.json'

# 보유종목 universe 수집 대상 계좌 (본인만)
OWNER_ACCOUNT_LABELS = ['일반', 'ISA']

# ---- 자본·체결·비용 ----
INITIAL_CAPITAL = 100_000_000     # 가상 자본 (기초 예수금 1억)
COMMISSION_RATE = 0.00015         # 매매 수수료 (편도, 키움 ~0.015%)
SELL_TAX_RATE = 0.0018            # 매도 증권거래세 (~0.18%)
BT_SLIPPAGE = 0.002               # 백테스트 슬리피지 (편도 0.2% — 매수는 비싸게, 매도는 싸게. 라이브는 호가 체결이라 미적용)

# ---- 포지션 사이징 ----
MAX_POSITIONS = 8                 # 동시 보유 최대 종목수
# 종목당 목표 비중 = 자산 / MAX_POSITIONS (= 균등비중 상한)
RISK_PER_TRADE_PCT = 0.01         # 리스크 기반 사이징: 종목당 손실 위험 = 자산 × 이 값. 수량 = 자산×이값 ÷ (진입가−손절가), 균등비중 상한으로 캡. 0=끔(순수 균등비중)

# ---- 지표 기간 ----
SMA_SHORT = 5
SMA_LONG = 20
SMA_MID = 60                      # 중기 추세선 — 정배열 확인(5>20>60). 데이터 부족 시 게이트 미적용
TREND_SLOPE_DAYS = 5             # 20일선 기울기 판정: 현재 20일선 vs N일 전 20일선(우상향 여부)
REL_STRENGTH_DAYS = 20           # 상대강도: 종목 N일 수익률 − 시장지수 N일 수익률 (라이브 전용)
TREND_SLOPE_GATE = 1             # 추세 게이트 ① 장기선 우상향 요구 (1=요구/0=완화). 보수성 완화 변이 실험용
MID_ARRAY_GATE = 1               # 추세 게이트 ② 5일선>중기선(60) 요구 (1=요구/0=완화)
IMMEDIATE_ENTRY = 0              # 즉시매수: 조건 통과 종목이 돌파·눌림목 둘 다 아니어도 강세장이면 현재가 즉시 매수 (1=켜기). 꾸준한 상승 종목 놓침 방지 실험용
AVERAGING_ENTRY = 0             # 물타기(애버리징): 보유 중 직전매수가 대비 하락 시 손절 대신 추가매수로 평단↓ (1=켜기). 종목당 10% 한도 내, 급락·약세장 제외
AVERAGING_DROP_PCT = 0.10       # 물타기 트리거 — 직전 매수가 대비 이 비율 하락 시 추가 (0.10=-10%마다)
CRASH_DROP_PCT = 0.10           # 당일 단일 하락이 이 비율 이상이면 물타기 말고 손절 (그림 #3, 0.10=-10%↓)
ATR_PERIOD = 14
BREAKOUT_LOOKBACK = 20            # 돌파 판정용 최근 고점 구간
SWING_LOW_LOOKBACK = 10          # 손절선 후보 스윙 저점 구간
VOLUME_AVG_PERIOD = 20
FLOW_DAYS = 5                     # 수급 합산 일수 (외국인·기관 N일 순매수)

# ---- 매수 ----
VOLUME_BREAKOUT_MULT = 1.5        # 돌파 매수: 거래량 ≥ 평균 × 이 배수
TURNOVER_BREAKOUT_MULT = 1.5      # 돌파 확인: 오늘 회전율 ≥ 20일 평균 회전율 × 이 배수 (데이터 없으면 통과)
TURNOVER_OVERHEAT_MULT = 3.0      # 과열 보류: 오늘 회전율 ≥ 평균 × 이 배수면 신규매수 보류 (돌파 배수보다 높게)
PULLBACK_ATR_MULT = 1.0           # 눌림목 지정가 = 전일종가 − ATR × 이 배수 (20일선과 비교 후 위쪽)
RSI_PERIOD = 14                   # RSI 계산 기간 (고정 상수 — 튜닝 비노출)
RSI_OVERBOUGHT = 78.0             # 돌파 매수 시 RSI ≥ 이 값이면 과열 추격으로 보고 보류
RSI_OVERSOLD = 25.0               # 눌림목 매수 시 RSI < 이 값이면 낙하 중(떨어지는 칼)으로 보고 보류

# ---- 분할매수·추격매수 ----
ENTRY_TRANCHES = 2                # 종목당 목표비중을 이 횟수로 나눠 진입 (1=전량 1회). 1차 = 목표/이 값
ADD_ATR_MULT = 0.5                # 추격매수: 직전 진입가 + ATR × 이 배수 돌파 & 추세 지속 시 다음 트랜치 추가

# ---- 매도 ----
STOP_ATR_MULT = 2.0               # 손절 = 진입가 − ATR × 이 배수 (스윙저점과 비교 후 위쪽)
RR_RATIO = 2.0                    # 목표 = 진입가 + (진입가−손절) × 이 배수
SCALE_OUT_FRAC = 0.0              # 분할매도: 목표 도달 시 보유량의 이 비율 익절 후 잔량 트레일링 (0=분할매도 끔, 목표 도달 시 전량 트레일링 전환)
TRAIL_ATR_MULT = 1.5              # 트레일링: 고점 − ATR × 이 배수
TRAIL_ATR_MULT_TIGHT = 1.0        # 컨센 목표가 부근에서 트레일링 강화
MAX_HOLD_DAYS = 20                # 시간손절: 보유 N일(달력) 경과 & 미진전(트레일링 전 + 진입가 이하)이면 청산해 자본 회전. 0=끔

# ---- 애널리스트 게이트 (약한 게이트) ----
ANALYST_MIN_UPSIDE_PCT = 5.0      # 컨센 목표주가 상승여력 < 이 값이면 신규매수 제외
ANALYST_SELL_OPINION_MAX = 2.5    # 투자의견(1매도~5매수) 평균이 이 값 이하면 매도 의견으로 보고 제외

# ---- 시장 필터 (소프트) ----
MARKET_BREADTH_MIN = 0.15         # 해당 시장 당일 상승비율 < 이 값이면 그 시장 신규매수 보류
MARKET_BREADTH_STRONG = 0.60      # 상승비율 ≥ 이 값이면 지수가 20일선 아래여도 강세 인정 (지수추세 소프트 완화)
BEAR_ENTRY_MODE = 1               # 약세 국면(혼조·약세·급락) 신규매수: 0=완전 현금화(돌파도 보류) / 1=돌파만 허용 / 2=눌림목도 허용

# ---- 운영 시간 (KST) ----
MARKET_OPEN = (9, 0)
MARKET_CLOSE = (15, 30)


# ==== 런타임 튜닝 ====
# 웹 튜닝 화면이 노출·수정하는 파라미터. state/sim/params.json 에 저장되며 config import 시 위 기본값을 덮어쓴다.
# 엔진은 매 스캔마다 새 프로세스라 저장 즉시(다음 스캔부터) 반영된다.
PARAMS_PATH = STATE_DIR / 'params.json'

TUNABLE = [
    {'key': 'SMA_SHORT', 'label': '단기 이동평균(일)', 'type': 'int', 'min': 2, 'max': 60, 'step': 1, 'group': '추세'},
    {'key': 'SMA_LONG', 'label': '장기 이동평균(일)', 'type': 'int', 'min': 5, 'max': 200, 'step': 1, 'group': '추세'},
    {'key': 'TREND_SLOPE_GATE', 'label': '장기선 우상향 요구', 'type': 'int', 'min': 0, 'max': 1, 'step': 1, 'group': '추세'},
    {'key': 'MID_ARRAY_GATE', 'label': '중기 정배열(5>60) 요구', 'type': 'int', 'min': 0, 'max': 1, 'step': 1, 'group': '추세'},
    {'key': 'IMMEDIATE_ENTRY', 'label': '즉시매수(눌림 안 기다림)', 'type': 'int', 'min': 0, 'max': 1, 'step': 1, 'group': '매수'},
    {'key': 'AVERAGING_ENTRY', 'label': '물타기(하락 시 추가매수)', 'type': 'int', 'min': 0, 'max': 1, 'step': 1, 'group': '매도'},
    {'key': 'AVERAGING_DROP_PCT', 'label': '물타기 하락 트리거', 'type': 'float', 'min': 0.03, 'max': 0.3, 'step': 0.01, 'group': '매도'},
    {'key': 'ATR_PERIOD', 'label': 'ATR 기간(일)', 'type': 'int', 'min': 5, 'max': 60, 'step': 1, 'group': '변동성·기간'},
    {'key': 'BREAKOUT_LOOKBACK', 'label': '돌파 고점 구간(일)', 'type': 'int', 'min': 5, 'max': 120, 'step': 1, 'group': '변동성·기간'},
    {'key': 'SWING_LOW_LOOKBACK', 'label': '스윙 저점 구간(일)', 'type': 'int', 'min': 3, 'max': 60, 'step': 1, 'group': '변동성·기간'},
    {'key': 'VOLUME_AVG_PERIOD', 'label': '거래량 평균 구간(일)', 'type': 'int', 'min': 5, 'max': 120, 'step': 1, 'group': '변동성·기간'},
    {'key': 'FLOW_DAYS', 'label': '수급 합산(일)', 'type': 'int', 'min': 1, 'max': 20, 'step': 1, 'group': '변동성·기간'},
    {'key': 'VOLUME_BREAKOUT_MULT', 'label': '돌파 거래량 배수', 'type': 'float', 'min': 1.0, 'max': 5.0, 'step': 0.1, 'group': '매수'},
    {'key': 'TURNOVER_BREAKOUT_MULT', 'label': '돌파 회전율 배수', 'type': 'float', 'min': 1.0, 'max': 5.0, 'step': 0.1, 'group': '매수'},
    {'key': 'TURNOVER_OVERHEAT_MULT', 'label': '과열 보류 회전율 배수', 'type': 'float', 'min': 1.5, 'max': 10.0, 'step': 0.1, 'group': '매수'},
    {'key': 'PULLBACK_ATR_MULT', 'label': '눌림목 ATR 배수', 'type': 'float', 'min': 0.0, 'max': 3.0, 'step': 0.1, 'group': '매수'},
    {'key': 'ENTRY_TRANCHES', 'label': '분할매수 횟수', 'type': 'int', 'min': 1, 'max': 5, 'step': 1, 'group': '매수'},
    {'key': 'ADD_ATR_MULT', 'label': '추격매수 ATR 배수', 'type': 'float', 'min': 0.3, 'max': 3.0, 'step': 0.1, 'group': '매수'},
    {'key': 'RSI_OVERBOUGHT', 'label': 'RSI 과열 컷(돌파)', 'type': 'float', 'min': 50.0, 'max': 95.0, 'step': 1.0, 'group': '매수'},
    {'key': 'RSI_OVERSOLD', 'label': 'RSI 과매도 컷(눌림목)', 'type': 'float', 'min': 5.0, 'max': 50.0, 'step': 1.0, 'group': '매수'},
    {'key': 'STOP_ATR_MULT', 'label': '손절 ATR 배수', 'type': 'float', 'min': 0.5, 'max': 5.0, 'step': 0.1, 'group': '매도'},
    {'key': 'RR_RATIO', 'label': '손익비(목표/손절)', 'type': 'float', 'min': 1.0, 'max': 5.0, 'step': 0.1, 'group': '매도'},
    {'key': 'SCALE_OUT_FRAC', 'label': '분할매도 익절 비율', 'type': 'float', 'min': 0.0, 'max': 1.0, 'step': 0.05, 'group': '매도'},
    {'key': 'TRAIL_ATR_MULT', 'label': '트레일링 ATR 배수', 'type': 'float', 'min': 0.5, 'max': 5.0, 'step': 0.1, 'group': '매도'},
    {'key': 'TRAIL_ATR_MULT_TIGHT', 'label': '트레일링 강화 배수', 'type': 'float', 'min': 0.3, 'max': 3.0, 'step': 0.1, 'group': '매도'},
    {'key': 'MAX_HOLD_DAYS', 'label': '시간손절(보유 최대 일)', 'type': 'int', 'min': 0, 'max': 120, 'step': 1, 'group': '매도'},
    {'key': 'ANALYST_MIN_UPSIDE_PCT', 'label': '최소 상승여력(%)', 'type': 'float', 'min': -50.0, 'max': 100.0, 'step': 1.0, 'group': '애널·시장'},
    {'key': 'ANALYST_SELL_OPINION_MAX', 'label': '매도의견 컷(1~5)', 'type': 'float', 'min': 1.0, 'max': 5.0, 'step': 0.1, 'group': '애널·시장'},
    {'key': 'MARKET_BREADTH_MIN', 'label': '시장 상승비율 하한', 'type': 'float', 'min': 0.0, 'max': 1.0, 'step': 0.01, 'group': '애널·시장'},
    {'key': 'BEAR_ENTRY_MODE', 'label': '약세장 매수(0현금/1돌파/2눌림)', 'type': 'int', 'min': 0, 'max': 2, 'step': 1, 'group': '애널·시장'},
    {'key': 'MAX_POSITIONS', 'label': '동시 보유 최대 종목', 'type': 'int', 'min': 1, 'max': 20, 'step': 1, 'group': '포지션'},
    {'key': 'RISK_PER_TRADE_PCT', 'label': '종목당 리스크 비율', 'type': 'float', 'min': 0.0, 'max': 0.1, 'step': 0.005, 'group': '포지션'},
]

# 오버라이드 적용 전 원본 기본값 캡처 (튜닝 화면의 '기본값' 표시·복원용)
_DEFAULTS = {spec['key']: globals()[spec['key']] for spec in TUNABLE}


def _coerce(spec: dict, v):
    """문자열/숫자를 spec 타입으로 변환 + min/max clamp. 실패 시 None."""
    try:
        v = int(float(v)) if spec['type'] == 'int' else float(v)
    except (TypeError, ValueError):
        return None
    return max(spec['min'], min(spec['max'], v))


def load_params() -> dict:
    """현재 유효 파라미터 {key: value} — 저장값(clamp) 우선, 없으면 기본값."""
    try:
        saved = json.loads(PARAMS_PATH.read_text())
    except Exception:
        saved = {}
    out = {}
    for spec in TUNABLE:
        k = spec['key']
        cv = _coerce(spec, saved[k]) if k in saved else None
        out[k] = cv if cv is not None else _DEFAULTS[k]
    return out


def save_params(raw: dict) -> dict:
    """raw(폼 문자열 dict) 검증·clamp 후 params.json 병합 저장."""
    try:
        cur = json.loads(PARAMS_PATH.read_text())
    except Exception:
        cur = {}
    for spec in TUNABLE:
        k = spec['key']
        if k in raw and str(raw[k]).strip() != '':
            cv = _coerce(spec, raw[k])
            if cv is not None:
                cur[k] = cv
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    PARAMS_PATH.write_text(json.dumps(cur, ensure_ascii=False, indent=2))
    return cur


def reset_params():
    """저장된 오버라이드 삭제 → 전부 기본값."""
    if PARAMS_PATH.exists():
        PARAMS_PATH.unlink()


def _apply_overrides():
    """import 시 유효 파라미터를 모듈 전역에 반영 (엔진·신호가 config.X 로 참조)."""
    g = globals()
    for k, v in load_params().items():
        g[k] = v


_apply_overrides()
