e3c54d62ed
전략 차원/변이: - 추세선 10/20/40→10/20/60 (40 제거, 60 추가; 60은 중기게이트와 겹침) - 기울기 게이트 완화(완화=기울기만 면제, 중기정배열 유지) - 즉시매수 토글 — 백테스트로 불리 확인 → 라이브 확인용 3개만 유지 - 최종 219개 (비즉시 216 + 즉시 3) UI: - 전략 이름 = 4카테고리 두글자(추세·진입·매도·베팅) + 카테고리별 ⓘ 설명 - 거래목록 달력형(오늘 자동선택·거래없어도 클릭), 매수/매도 분리·익절/손절 표시 - 라벨 동적화(실제 SMA_LONG), 제외카드 4조건 표시, 약세모드/완화/즉시 칩 - 매수준비/대기 배지 분리, 탭 깜빡임 제거(서버 checked), 손절/목표 평단% 등 문서: CLAUDE.md sim 섹션 갱신 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
181 lines
12 KiB
Python
181 lines
12 KiB
Python
"""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=켜기). 꾸준한 상승 종목 놓침 방지 실험용
|
|
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': '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()
|