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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user