feat(sim): 추세선 4종·완화/즉시매수 토글·전략 4요소 이름·거래목록 달력 등 대개편

전략 차원/변이:
- 추세선 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>
This commit is contained in:
hyowons
2026-06-15 18:57:30 +09:00
parent ef477d878c
commit e3c54d62ed
278 changed files with 128243 additions and 93084 deletions
+46 -25
View File
@@ -74,11 +74,11 @@ def trend_ok(ind: dict) -> bool:
기본: 현재가>20일선 & 정배열(5>20). 보강: ①20일선 우상향 ②5>중기선(60) — 데이터 있을 때만."""
if not (ind['price'] > ind['sma_long'] and ind['sma_short'] > ind['sma_long']):
return False
if ind.get('sma_long_slope_up') is False: # ① 20일선이 꺾여 내려오면 제외
return False
if getattr(config, 'TREND_SLOPE_GATE', 1) and ind.get('sma_long_slope_up') is False:
return False # ① 장기선이 꺾여 내려오면 제외 (게이트 ON일 때만)
sm = ind.get('sma_mid')
if sm is not None and ind['sma_short'] <= sm: # ② 단기선이 중기선 아래면(역배열) 제외
return False
if getattr(config, 'MID_ARRAY_GATE', 1) and sm is not None and ind['sma_short'] <= sm:
return False # ② 단기선이 중기선 아래면(역배열) 제외 (게이트 ON일 때만)
return True
@@ -122,29 +122,44 @@ def _analyst_checks(analyst: dict | None, price: float) -> tuple[bool, list[dict
return gate_ok, checks, bonus
def trend_checks(ind: dict) -> list[dict]:
"""추세 게이트 체크리스트(trend_ok와 동일 4조건 + 상대강도) — 매수 판단·제외 카드 공용.
라벨은 실제 적용된 이평 일수(config)로 표기 (변이마다 SMA_LONG 10/20/40 다름)."""
price = ind['price']
_S, _L, _M = config.SMA_SHORT, config.SMA_LONG, config.SMA_MID
slope = ind.get('sma_long_slope_up')
sm = ind.get('sma_mid')
rel = ind.get('rel_strength')
slope_gate = getattr(config, 'TREND_SLOPE_GATE', 1)
mid_gate = getattr(config, 'MID_ARRAY_GATE', 1)
# 게이트 꺼진 조건은 ok=None(참고) — 통과/탈락에 영향 없음을 표시
if not slope_gate:
slope_check = {'label': f'추세 기울기({_L}일선↑)', 'ok': None,
'detail': '게이트 끔(완화)' + ('' if slope is None else f" · 실제 {'우상향' if slope else '하향'}")}
else:
slope_check = {'label': f'추세 기울기({_L}일선↑)', 'ok': None if slope is None else slope,
'detail': '데이터 없음(통과)' if slope is None else ('우상향' if slope else '하향')}
if not mid_gate:
mid_check = {'label': f'중기 정배열({_S}>{_M})', 'ok': None,
'detail': '게이트 끔(완화)' + ('' if sm is None else f" · {ind['sma_short']:,.0f} / {sm:,.0f}")}
else:
mid_check = {'label': f'중기 정배열({_S}>{_M})', 'ok': None if sm is None else ind['sma_short'] > sm,
'detail': '데이터 없음(통과)' if sm is None else f"{ind['sma_short']:,.0f} / {sm:,.0f}"}
return [
{'label': f'추세({_L}일선 위)', 'ok': price > ind['sma_long'],
'detail': f"{price:,} vs {ind['sma_long']:,.0f}"},
{'label': f'정배열({_S}>{_L})', 'ok': ind['sma_short'] > ind['sma_long'],
'detail': f"{ind['sma_short']:,.0f} / {ind['sma_long']:,.0f}"},
slope_check, mid_check,
{'label': '상대강도(시장대비)', 'ok': None if rel is None else rel > 0,
'detail': '데이터 없음(통과)' if rel is None else f'{rel:+.1f}%p'},
]
def evaluate_candidate(code, name, sources, ind, flow, analyst, market_ok) -> dict:
"""미보유 종목 매수 판단. flow/analyst 는 engine 이 trend 통과 후 주입."""
price = ind['price']
checks: list[dict] = []
# 1. 추세
checks.append({'label': '추세(20일선 위)', 'ok': price > ind['sma_long'],
'detail': f"{price:,} vs {ind['sma_long']:,.0f}"})
checks.append({'label': '정배열(5>20)', 'ok': ind['sma_short'] > ind['sma_long'],
'detail': f"{ind['sma_short']:,.0f} / {ind['sma_long']:,.0f}"})
# 1-b. 추세 질 보강 — 기울기 / 중기선 / 상대강도 (데이터 없으면 통과 표시)
slope = ind.get('sma_long_slope_up')
checks.append({'label': '추세 기울기(20일선↑)',
'ok': None if slope is None else slope,
'detail': '데이터 없음(통과)' if slope is None else ('우상향' if slope else '하향')})
sm = ind.get('sma_mid')
checks.append({'label': '중기 정배열(5>60)',
'ok': None if sm is None else ind['sma_short'] > sm,
'detail': '데이터 없음(통과)' if sm is None else f"{ind['sma_short']:,.0f} / {sm:,.0f}"})
rel = ind.get('rel_strength')
checks.append({'label': '상대강도(시장대비)',
'ok': None if rel is None else rel > 0,
'detail': '데이터 없음(통과)' if rel is None else f'{rel:+.1f}%p'})
checks: list[dict] = list(trend_checks(ind)) # 1. 추세 게이트 (제외 카드와 공유)
# 2. 수급
fnet = flow.get('foreign', 0) if flow else 0
@@ -241,6 +256,12 @@ def evaluate_candidate(code, name, sources, ind, flow, analyst, market_ok) -> di
'buy_price': price, 'watch_price': limit,
'reason': f'눌림목 지정가 {limit:,} 도달'}
# 6-b. 즉시매수 — 추세·정배열·수급·애널 다 통과했는데 돌파·눌림목 둘 다 아니면(=안 내려오는 상승 종목),
# 강세장·RSI 정상이면 현재가에 바로 매수. 꾸준히 오르기만 하는 좋은 종목 놓침 방지 (실험 토글).
if getattr(config, 'IMMEDIATE_ENTRY', 0) and market_ok and not rsi_overbought:
return {**base, 'state': 'BUY', 'action': 'buy', 'buy_path': 'immediate',
'buy_price': price, 'reason': '조건 충족 — 즉시매수(눌림 안 기다림)'}
return {**base, 'state': 'WAIT', 'watch_price': limit,
'reason': f'눌림목 대기 — 지정가 {limit:,}'}
@@ -279,7 +300,7 @@ def evaluate_holding(position, ind, flow, analyst, held_days=None) -> dict:
{'label': '목표/트레일링', 'ok': None,
'detail': ('트레일링 ON' if trailing_on else
('일부익절 완료' if scaled_out else f'목표 {target:,}'))},
{'label': '추세(20일선)', 'ok': price >= ind['sma_long'],
{'label': f'추세({config.SMA_LONG}일선)', 'ok': price >= ind['sma_long'],
'detail': f"{ind['sma_long']:,.0f}"},
{'label': '수급(외/기)', 'ok': supply_ok, 'detail': f'{fnet:+,} · 기 {inet:+,}'},
{'label': f'분할({tranches}/{config.ENTRY_TRANCHES})', 'ok': None,