diff --git a/CLAUDE.md b/CLAUDE.md index 823f9ed..b4c4bf9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,11 +108,15 @@ Stock agent (`agents/stock/workspace/scripts/`), run with `python3`: - `trade_journal.py` — 종목별 매매 기록 누적. 키움 REST에 기간 거래내역 API 부재 → 평일 21:00 launchd(NXT 야간 마감 후)로 ka10170 4계좌 호출해 `state/trade_journal.jsonl`에 적재. `(date, account)` 단위 idempotent, 휴장일/주말 self-skip(`--force`로 우회). 적재 시작일 2026-05-13 이전 보유분은 `seed` 명령(1회)으로 현재 평단가×(보유-당일매수+당일매도) 단일 행으로 압축 적재됨(`seed=true` 플래그·`*` 마커). CLI: `collect [--date YYYYMMDD]` / `seed` / `show ` / `query [--from --to --account --code]` - `market_indicators_sync.py` — 시장 단위 ADR·투자자별 매매 누적. 네이버 m.stock `/api/index/{KOSPI|KOSDAQ}/integration` 한 콜로 dealTrendInfo·upDownStockInfo 수집 → `state/market_indicators_history.jsonl`. `(date, market)` 단위 idempotent, 휴장/주말 self-skip(`--force`). 평일 21:00 stock.trade-journal launchd에 통합 발화 (별도 plist 없음). KRX 정보데이터시스템은 응답 패턴 변경으로 백필 보류 — 매일 누적만 시작. behive_web 자산정보 탭의 시장정보 sub-tab이 sparkline 데이터원으로 사용 - `sim/` (스크립트 아닌 **별도 패키지** — `agents/stock/workspace/sim/`) — **자동매매 시뮬레이션(페이퍼 트레이딩) 엔진**. 실주문 `orders/`와 완전 분리, 가상자본 1억으로만 동작(`INITIAL_CAPITAL`, 2026-06-10 1천만→1억). 구성: `config.py`(파라미터)·`indicators.py`(SMA/RSI/ATR/고저/거래량 순수함수)·`universe.py`(**누적 관찰목록** `state/sim/watchlist.json` — 비하이브 워치+관심+본인보유를 매 스캔 **자동 편입**하되 **자동 삭제 X**(수동만). `sync_watchlist`/`add_manual`/`remove`)·`data.py`(시세 batch·일봉 **캐시 우선**·수급 60m캐시·애널〔fnguide/wisereport 12h 캐시, **캐시 미스 때만 0.4s 페이싱**으로 연속 크롤링 차단 회피〕)·`signals.py`(진입/청산 규칙 + 손절·목표 계산 + dashboard용 체크리스트)·`portfolio.py`(가상 체결·비용·손익·MDD)·`engine.py`(스캔 오케스트레이션 — `_gather(extra_codes)`로 **보유 종목은 관찰목록에서 빠져도 청산 추적**)·`benchmark.py`(**기준지수 대비 알파** — 네이버 m.stock 지수 일별종가를 `state/sim/index_history.json`에 누적〔KOSPI/KOSDAQ, pageSize≤50〕, 구간 등락률·초과수익 계산. 스캔마다 `update_today` 1회 갱신, 실패 무시)·`__main__.py`(CLI)·`sim_web.py`(별도 대시보드 서버 18792). 전략: 방향=애널 약한게이트+가점, 타이밍=수급·기술. **ETF/ETN은 자동매수 제외**(2026-06-09 — 방향성 엔진인 애널 컨센서스가 ETF엔 None이라 전략 부정합. `universe.is_etf()`가 운용사 프리픽스(KODEX/TIGER/ACE…)로 판별, engine 미보유 매수 분기 직전에 `ETF — 자동매매 제외` SKIP. **보유 ETF는 청산까지 추적**(매수 분기 전 처리라 무관)). 매수=거래량+전고점 돌파 즉시 / 평소 눌림목(max(20일선, 전일종가−ATR)) 지정가. **시장 국면 필터(6단계)**: `market_regime_map()`이 상승비율(폭)+지수추세(방향, 20일선)+당일등락(강도)을 조합해 `engine.REGIME` 6단계로 분류 — 🔥강세·📈반등(상승비율↑ & 지수 20일선 아래=약세탈출)·🌤완만상승 / ➖혼조·📉약세·❄️급락. **앞 3개=매수허용, 뒤 3개=보류**(`market_ok_map`이 `REGIME[g][2]`로 파생). 상승비율은 **항상 네이버 실시간**(`_live_breadth` 60s 캐시, 실패 시 EOD). 약세장이어도 **돌파 진입은 허용, 눌림목만 보류**(soft gate). 시장탭에 국면 배지+설명 표시(매수판단과 동일 소스). `code_market_map()`이 종목→KOSPI/KOSDAQ 매핑. (2026-06-09 강세/약세 2단계→6단계 국면으로 세분화) **2026-06-09 4종 추가**: ⓐ **리스크 기반 사이징**(`signals.target_value` — 수량=자산×`RISK_PER_TRADE_PCT` ÷ 손절폭, 균등비중 자산/`MAX_POSITIONS` 상한 캡, 0=끔. 변동성 큰 종목 자동 축소·종목당 리스크 균등화. 엔진·백테스트 공용) ⓑ **RSI 필터**(`indicators.rsi` 활성화 — 돌파 시 `RSI_OVERBOUGHT`↑ 과열 추격 보류 / 눌림목 시 `RSI_OVERSOLD`↓ 낙하 중 보류, 데이터 없으면 통과) ⓒ **시장 국면 6단계**(`market_regime_map`→`engine.REGIME`, 위 시장 국면 필터 참조. `_index_trend_ok`·`_index_daily_change` 보조. ⚠️2026-06-09 강세/약세 2단계가 84% 반등일을 약세로 오판해 6단계 국면으로 세분화) ⓓ **시간손절**(`MAX_HOLD_DAYS` 경과 & 미진전〔트레일링 전+진입가 이하〕 시 청산해 자본 회전, `evaluate_holding(held_days)` — 엔진=달력일·백테스트=`entry_date` 달력일). ⓖ **스캔 메모 4종**(2026-06-10): 변이 수가 늘어도 스캔이 선형 폭증하지 않게 — 호가(`data._BOOK_MEMO`, 체결 공정성 겸용)·일봉(`engine._SCAN_CANDLES`)·지표(`engine._IND_MEMO`, (code,지표파라미터) 키)·수급/애널(`data._FLOW_MEMO_SCAN`/`_ANL_MEMO_SCAN`)을 스캔 1회 동안 공유, `_gather`가 매 스캔 리셋. 핵심은 애널 캐시 stale 시 0.4s 페이싱이 변이 수만큼 반복되던 것 — 108계좌 판단 38s→2s. ⓕ **2026-06-10 코드리뷰 3종**: 재진입 쿨다운(전량청산 종목 **당일 재매수 금지** — `Portfolio.last_exit`/`exited_today`, backtest 동일 규칙. 휩쏘 churn 방지)·장중 거래량 환산(`_session_elapsed_frac` 0.15~1.0을 `ind['vol_time_frac']`로 주입 → signals가 돌파 거래량·회전율을 하루치로 환산 비교, '(환산)' 표기. 이전엔 오전 돌파가 구조적으로 불가했음. backtest는 1.0=무영향)·일별 성과이력(`state/sim/equity_history.json` — 매 스캔 당일 항목 덮어써 EOD값 보존, `{date:[{id,ph(파라미터해시),equity,ret}]}`. 변이 id 재사용돼도 ph로 식별. 국면별 사후분석용) ⓔ **추세 질 보강**(2026-06-09 `trend_ok` 게이트 강화: 기존 현재가>20일선+정배열(5>20)에 더해 ①**20일선 우상향 기울기**〔`sma_long_slope_up`, `TREND_SLOPE_DAYS`전 대비〕 ②**중기 정배열 5>`SMA_MID`(60)** — 둘 다 종목데이터라 **백테스트 반영**, 데이터 부족 시 통과. ③**상대강도**〔`rel_strength`=종목−지수 `REL_STRENGTH_DAYS`(20)일 수익률, engine이 주입하는 **라이브 전용**, 백테스트 None=중립, 체크표시만〕). **분할매수·추격매수**: 목표비중을 `ENTRY_TRANCHES`(기본 3)로 나눠 1/3씩 진입, 직전 진입가+ATR×`ADD_ATR_MULT` 돌파 & 추세 지속 시 다음 트랜치 추가매수(평단 가중평균, 손절 위로만 래칫)·트레일링/일부익절 시작 후엔 추가 종료. 매도=손절(max(진입−ATR×2, 10일저점))·**분할매도**(목표 도달 시 `SCALE_OUT_FRAC`(기본 50%) 익절 후 잔량 트레일링(고점−ATR×1.5))·추세수급 이탈 시 전량 청산. 부분매도는 closed/win 미카운트(전량 청산 시만 카운트). 결정론적(LLM 미경유). **기준지수 대비(알파)**: 가상계좌/백테스트 수익률을 같은 기간 KOSPI·KOSDAQ '매수후보유' 대비로 평가(수익률만으론 지수 못 이기면 무의미하므로). 라이브는 계좌 시작일~오늘, 백테스트는 윈도우 구간 대비. KPI '코스피/코스닥 대비(알파)' + 백테스트·비교 탭 알파 행. 경과기간 0(시작 당일)·캐시 부재 시 None('데이터 없음'). 상태 `state/sim/{portfolio.json,trades.jsonl,last_scan.json,index_history.json}`. CLI: `python3 -m sim {scan [--force]|status|report|reset --yes|sweep [frac]|backtest|backfill [--pages N] [--force]|backfill-index [--pages N]|variants {seed N|list|reset|clear}}` (cwd=workspace). launchd `stock.sim-scan`(15분)·`stock.sim-web`(상시). - - **런타임 튜닝**: `config.TUNABLE` 27개 파라미터(분할매수 횟수·추격 ATR 배수·분할매도 익절 비율·종목당 리스크 비율·RSI 과열/과매도 컷·시간손절 일수·**약세장 매수모드**〔`BEAR_ENTRY_MODE` 0현금화/1돌파만/2눌림허용, signals에서 `market_ok` False일 때 분기, backtest는 market중립이라 무영향, 병렬변이로 라이브 비교〕 포함)를 `state/sim/params.json`로 오버라이드(웹 튜닝 탭에서 저장). config import 시 기본값 위에 덮어써 엔진이 다음 스캔부터 반영. `config._DEFAULTS`가 원본 기본값. `load_params/save_params/reset_params`. + - **체크리스트 라벨 동적화**(2026-06-15): `추세(N일선 위)`·`정배열(5>N)`의 N은 **변이별 `config.SMA_LONG`(10/20/40)**을 읽어 표기. 이전엔 "20"으로 하드코딩돼 SMA_LONG=10/40 변이도 "20일선"으로 거짓 표기됨(차트의 5>20 정배열과 어긋나 보이던 원인). 하드코딩이 `signals.py`(buy_signal·evaluate_holding·trend 보강)와 **`engine.py` 추세미충족 분기** 두 곳에 있었음. ⚠️ 과거 `trades.jsonl` 판단근거는 거래 시점 baked라 옛 라벨 잔존(새 거래부터 정상). 라이브 판단은 `judge_view`가 변이 params 적용 후 계산하므로 즉시 정확. 추세 체크리스트는 `signals.trend_checks(ind)`로 추출해 매수판단·제외(SKIP)카드 공유(2026-06-15 — 제외 카드가 추세/정배열 2개만 보여 ①②통과인데 왜 SKIP인지 헷갈리던 것 → 4조건〔현재가>장기선·정배열·장기선 기울기·중기정배열〕+상대강도 전부 표시). + - **즉시매수 토글 + 40일선 제거**(2026-06-15): `IMMEDIATE_ENTRY`(기본 0) — 추세·정배열·수급·애널 다 통과한 종목이 돌파·눌림목 둘 다 아니어도(=안 내려오는 상승 종목) 강세장·RSI정상이면 현재가 즉시 매수(`signals` 눌림목 WAIT 직전 분기, buy_path='immediate'). 꾸준한 상승 놓침 방지. 40일선 추세선 제거(어중간·관리자님 지시, 스윕 GRID도 `[10,20,60]`). 변이: 40 −72 후, **백테스트 검증 결과 즉시매수가 명확히 불리**(전체기간 10/20/60 모두 즉시ON이 수익률 1/3·승률 −10~25%p·PF 반토막·MDD↑ — 눌림목/돌파 기다림 규칙이 핵심 알파). 그래서 즉시매수는 라이브 강세장 한정 확인용 **3개만**(10/20/60 안정·보수게이트) 유지 → **총 219개**(비즉시216+즉시3). 즉시매수는 GRID 미포함이라 백테스트 기본은 OFF(검증은 위 수동 비교로 수행). 웹 초록 `즉시` 칩(`_immediate_chip_html`)·드롭다운 `[안정 즉시]`. ⚠️ 기존 216은 IMMEDIATE_ENTRY 기본 0이라 불변. + - **추세선 60일 추가**(2026-06-15): 스윕 GRID `SMA_LONG`에 60 추가(`[10,20,40,60]`, 1152→1536조합). 60일선 상위 12조합 × 약세대처3 × 진입게이트2 = 72변이 추가 → **총 288개**(10/20/40/60 각 72). ⚠️ 60일선은 base 정배열(5>60)이 중기게이트(`SMA_MID=60`, 5>60)와 동일 — 중기게이트가 추가 필터링 없음(무해, 관리자님 인지·승인). ⚠️ 스윕 첫 실행이 워커 조용히 죽는 일회성 deadlock(메인 SN 0%CPU·워커0) → kill 후 재실행으로 정상화. 장중엔 sim-scan/web과 CPU 경쟁으로 20분+ 소요. + - **추세 게이트 완화 토글**(2026-06-15): `TREND_SLOPE_GATE`·`MID_ARRAY_GATE`(기본 1=현행). `trend_ok`이 `getattr`로 참조 — 0이면 장기선 우상향/중기정배열(5>60) 게이트 미적용. 보수성(3거래일 매수 2건, 추세미충족이 비ETF의 93% 차단) 완화 실험용. **완화 변이**(2026-06-15 최종: **기울기 게이트만 OFF, 중기정배열(5>60)은 유지** — `TREND_SLOPE_GATE=0, MID_ARRAY_GATE=1`. 관리자님: "중기정배열은 살려야지". 144개 완화변이 전부 이 규칙, 계좌 초기화). 웹: 보라 `완화` 칩(`_relax_chip_html`)·드롭다운 `[안정 완화]`. `trend_checks`가 게이트 OFF 조건을 ok=None('게이트 끔')로 표기. ⚠️ TUNABLE에 추가돼 기존 108개는 apply_params에서 기본 1(현행)로 적용돼 불변. + - **런타임 튜닝**: `config.TUNABLE` 29개 파라미터(분할매수 횟수·추격 ATR 배수·분할매도 익절 비율·종목당 리스크 비율·RSI 과열/과매도 컷·시간손절 일수·**약세장 매수모드**〔`BEAR_ENTRY_MODE` 0현금화/1돌파만/2눌림허용, signals에서 `market_ok` False일 때 분기, backtest는 market중립이라 무영향, 병렬변이로 라이브 비교〕 포함)를 `state/sim/params.json`로 오버라이드(웹 튜닝 탭에서 저장). config import 시 기본값 위에 덮어써 엔진이 다음 스캔부터 반영. `config._DEFAULTS`가 원본 기본값. `load_params/save_params/reset_params`. - **백테스트·스윕** (`backtest.py`/`sweep.py`): signals.py 그대로 재사용해 과거 일봉 되감기(지표 t까지·종가체결·애널 중립·수급은 flow_history 사용). `sweep`은 9파라미터(SMA_LONG·RR·STOP_ATR·PULLBACK_ATR·VOLUME_BREAKOUT + 신규 RSI_OVERBOUGHT·RSI_OVERSOLD·MAX_HOLD_DAYS·RISK_PER_TRADE_PCT) **1152조합**을 학습/검증(out-of-sample 0.7) 분리 비교 (애널·시장필터는 backtest 중립이라 스윕 제외) → `state/sim/backtest_results.json`. **체결 현실화**(2026-06-10): backtest가 당일 종가 판단→**다음날 시가 체결**(pending 주문 큐, look-ahead 제거) + `BT_SLIPPAGE` 0.2%(매수 비싸게/매도 싸게). 체결 낙관분 ~5%p 제거 확인. 거래 없는 날 주문은 소멸(신호 지속 시 재큐잉). **3분할 꾸준함 순위**(2026-06-10): 전체기간 3분할 구간별 수익(`windows`/`worst_ret`)을 측정, 순위 1순위=최악구간 수익(한 구간 운빨 배제)·2순위=검증수익. 웹 행에 '구간별 검증(3분할)' 표시. **멀티프로세싱**(2026-06-10): `sweep`이 조합을 코어에 분배(`get_context('spawn')`, 워커=코어−2). 일봉·수급은 `_hist_cached`/`_flow_cached` 프로세스 메모이즈로 종목당 sqlite 1회만 읽음. **44분 → 약 5~7분**(10코어 기준). 데이터는 이미 sqlite 캐시 전용(키움 API 0회) — 병목은 네트워크가 아니라 지표 재계산(파이썬). spawn이라 워커가 메인 재import하지만 `sim/__main__.py` 가드로 안전. ⚠️ `GRID` 늘릴 때 곱셈 폭발 주의. ⚠️ 종가체결·현 워치리스트(생존편향)라 절대수익 아닌 상대순위로만 해석. 2026-06-09 스윕: 핵심 레버는 `VOLUME_BREAKOUT=1.3`(거래량 문턱 낮춤, 중앙 +19%) — 신규 4종은 강세 검증창에선 미미(RSI·시간손절=하락장 보험). 적용 균형값 params.json 저장(RISK 0.01 보수 유지, 0.02 고수익은 강세장 레버리지라 채택 X). - **수급 백필** (`backfill_flow.py`): ka10059 연속조회로 종목별 투자자 순매수 다년치(기본 6페이지≈2.4년) → `state/sim/flow_history.sqlite`(code,date,foreign_net,inst_net,indiv_net 천주). idempotent·rate limit 페이싱+429 백오프. backtest가 이 DB로 수급(외/기) 반영 (없으면 중립). 2026-06-08 54종목 3만행 1회 적재 완료. - **병렬 페이퍼** (`variants.py` + `engine.scan_all`): 스윕 상위 튜닝들을 각자 가상계좌(`state/sim/variants//`)로 실시간 동시 운영 → 비교(`variants_compare.json`). `scan_all`이 라이브 데이터 1회 수집 후 메인+모든 변이를 같은 시세로 평가(`_gather`+`_run_scan` 분리, Portfolio가 경로 인자 받음). 변이는 애널 게이트까지 실반영(백테스트가 못 보는 부분). `variants seed N`은 backtest_results 상위 N개 등록. **launchd `sim-scan`은 이제 `scan`→내부적으로 `scan_all` 실행**(메인+변이 매 스캔 갱신, 트리거·명령 동일). - - **웹 3메인탭**(sim_web — **전략/관심종목/시장** 순, 기본=전략·비교. **기준전략(M) 폐지**(2026-06-10): scan_all의 메인 패스는 `execute=False` 판단 전용(last_scan=화면·상태점용, 메인 계좌 매매 동결), 실매매·비교는 변이 36개만. `sync_label_tops` 메인 제외 규칙 삭제(유형별 1위 전부 변이). 비교탭 행마다 **마킹 아이콘 피커**(2026-06-10 ☆토글→6종 아이콘 ⭐🔥💎🎯👀🚀 선택, `FAV_ICONS`/`favorites.json`은 `{아이콘: vid}` — **아이콘 하나당 전략 1개**(다른 전략에 쓰면 이동)·전략당 아이콘 1개. 칩 클릭→바텀시트 피커→`POST /fav_set`(fetch). 구버전 list는 자동 마이그레이션), 관심종목 탭 기본 시각 = 첫 마킹(FAV_ICONS 순) → 없으면 1위 변이. **약세모드 단어 칩**(2026-06-10): 비교 행 vid 옆 + 드롭다운 + 펼침 body '약세장대처' 행에 회피(0 현금화)/안정(1 돌파만·기본, 2026-06-11 '돌파'→'안정' 개명 — 적극과 혼동)/적극(2 눌림허용) 색 칩(`_BEAR_WORD`/`_bear_chip_html`, 클릭 시 설명 토스트. 아이콘→단어는 관리자님 요청). `_market_fit_summary`의 긴 접미사는 제거. **비교 행·드롭다운 이름은 짧은 단어 나열**(`_market_fit_words` — '상승장·빠른·공격·추세추종', `_fit_tokens` 공용 토큰. 긴 문장형 `_market_fit_summary`는 백테스트 탭 라벨·비교 펼침 '유형' 행에 유지). **bt매칭은 `_btkey`(swept_params만)** — BEAR_ENTRY_MODE 등 백테스트 중립 키가 params에 있어도 같은 bt행에 매칭(모드 3형제는 bt순위 공유, 백테스트 탭은 36종만 표시가 정상). 헤더 상태점 자산줄은 '전략 N개 · 평균/1위 수익률'. 2026-06-10 내계좌 탭 삭제〔계좌 정보는 비교탭 메인 행으로〕·튜닝 보조탭 삭제〔params 변경은 백테스트 "이 튜닝 적용"으로만, `/save_params` 리다이렉트도 s=bt〕. 탭 라우팅 `?t=`+보조탭 `?s=`, 레거시 t=acct/tune→lab): 헤더 타이틀 옆 **시뮬 상태점**(`_engine_status`→`#health` 점: 🟢 장중 가동(최근 스캔 ≤12분)·🟡 장외 대기·🔴 장중 멈춤 의심, 클릭 시 상태 토스트). **관심종목**(종목별 신호 체크리스트 + 각 평가항목 **ⓘ 버튼 → 쉬운 설명 토스트**〔`_CHK_HELP`/`_chk_key`/`_CHK_HELP_JS`, 바텀시트 `#chk-toast`, 열려있으면 자동새로고침 멈춤〕 + 상단 **종목 추가 폼**〔코드/이름〕 + 카드별 **✕ 삭제**〔수동만, 보유 중이면 청산 추적〕 + origin 라벨〔워치/관심/보유/수동〕 + 미스캔분 '스캔 대기' 표시) · **시장**(지수·ADR·상승하락·투자자 순매수 — **항상 네이버 m.stock 실시간**〔`_get_live_market` 60s 캐시, `market_indicators_sync.fetch_market` 재사용. 네이버가 장 마감 후에도 당일 최종값 제공하므로 세션 무관 최신 사용〕, **조회 실패 시에만** `market_indicators_history.jsonl` EOD 폴백. note는 장중='장중 실시간'·장외='직전 장 마감 기준'. 2026-06-09 초기엔 장중만 실시간이라 마감 후 어제 EOD를 보이던 빈틈을 항상-실시간으로 수정) · **전략**(2026-06-11 보조탭 버튼 제거 — 기본=비교만 표시, 백테스트는 비교 타이틀 우측 **[📊 모의결과보기]** 라벨(라디오 `sub-bt` 토글)로 진입·bt 패널 상단 [← 비교로]로 복귀. 그 옆 **[📋 거래목록]**(`sub-day`/`_day_trades_panel(sel_date)` — 전 전략 거래를 **달력형 날짜 선택**(`_DAY_CAL_JS` — 거래일만 밝게·클릭가능, 비거래일 어둡게, 월 이동 JS 클라이언트〔범위 밖 ‹›disabled〕, 오늘 강조. 컴팩트 max-width 300px. 날짜 클릭 시 **달력 안 닫고 fetch로 #day-rows·#day-sub·라벨만 갱신**〔history.replaceState로 `?d=` 보존〕, 별도 [닫기] 버튼. 접이식 details, 기본=최근 거래일)으로 **(종목,방향)별 아이템**(같은 종목 매수·매도 동시면 2아이템 분리, 요약=종목명+🟢매수/🔴매도 배지+전략수)으로 집계: 전략수·전략번호칩(상세링크)·가격대·평균손익·사유. 날짜는 simURL이 `&d=`로 보존). `?s=bt` 라우팅·60s 새로고침 라디오 보존은 유지. 구성 2: **백테스트**〔**유형별(세부 라벨 ~36종) 대표 요약** — 2026-06-10 단순화: 시장 성격별 그룹(상승장/추세장/조정장/출렁이는 장/보통 장에 강한 유형) 안에 각 유형 1위만 표시(꾸준함=worst_ret 순), 행=제목+최악·검증수익, 펼치면 구간별 검증·MDD·승률·PF·알파·파라미터·🌐. **비교군 추가/제거·이 튜닝 적용 버튼 제거**(읽기전용) — `variants.sync_label_tops()`는 **수동 전용**(2026-06-10 자동 동기화 끔 — sweep은 결과 파일만 갱신, 비교군 변경은 관리자님 명시 요청 시 `python3 -m sim variants sync` 또는 코디가 수동 등록)(같은 params 변이는 계좌 유지·신규 추가·탈락 삭제, 메인 조합 제외). 메인 변경은 코디/CLI로만. + **'↻ 다시 계산'**〔백그라운드 sweep — 결과 파일만 갱신, 비교군은 불변〕〕/**비교**〔병렬 변이 실시간 성적, 변이별 성적초기화/삭제 + **전체 삭제(메인 제외)** `/variants_clear_all`→`vmod.clear()`. 펼침 body 첫 행=**유형**(긴 문장형+ⓘ→🌐 시장적합 토스트, 2026-06-10 하단 🌐 버튼 대체)→둘째 행 **약세장대처**(`돌파 (기본)` 모드명만+ⓘ 토스트). bt 등록근거 행은 **모의평가**로 개명(1행 `N위/36`+2행 최악구간·검증, 모드 3형제는 `_btkey`로 같은 bt행 공유). **전략 상세 페이지**(2026-06-11): 행 펼침 [📈 자세히보기] → `GET /strategy?id=vN` 독립 페이지 — 일별 수익률 SVG 차트(`equity_history` id+ph 매칭, KOSPI 같은 구간 점선 오버레이) + 결정론 종합 코멘트(순위·알파·과최적 경고) + 현재 성적·모의평가·전략 성격·약세장대처·파라미터 표(`_TUNE_HELP` 라벨)·보유/관심/거래. 평가순 정렬은 고유번호 오름차순(#1→#108). 행 표기(2026-06-10): **순위 배지 `N위`가 앞, 고유번호는 `#N`**(`_vid_disp` — 'v37'→'#37' 표시 전용, 내부 id·URL은 raw. 드롭다운·👁토스트·사유 라벨 동일 표기)〕). 🌐는 `data-detail` 통합 토스트 핸들러로 헤더 상태점(`#health`)과 공유. sticky 헤더, 당겨서 새로고침. **새로고침 상태 보존**(2026-06-10): 60s 자동새로고침이 전체 리로드지만 펼침(details open)·스크롤을 sessionStorage(`simOpen`/`simScroll`, key=패널id+summary텍스트)로 저장·복원해 보던 화면 유지. 입력 중·토스트 열림엔 새로고침 멈춤. **탭 보존**(2026-06-11): 탭/서브탭 라디오 변경 시 URL 즉시 동기화(`window._simURL`+history.replaceState) — 당겨서·브라우저·마킹저장 등 모든 리로드가 보던 탭에 착지. HTML 응답에 `Cache-Control: no-store`(Safari 휴리스틱 캐싱이 옛 JS를 재사용해 탭 복원이 안 먹던 문제). **깜빡임 제거**(2026-06-12): 활성 탭/서브탭을 `render_html(tab,sub)`이 라디오 `checked`로 서버에서 미리 박음(GET이 `?t/?s` 파싱+레거시 보정) — 이전엔 기본탭 렌더→JS가 로드 후 전환하며 깜빡였음. `do_POST`: `/save_params`·`/run_sweep`·`/variants_{add,reset_one,remove,clear_all}`·`/watch_{add,remove}`·`/fav_set`(마킹 아이콘 지정/이동/해제 → `state/sim/favorites.json`) (그 외 읽기전용) + - **웹 3메인탭**(sim_web — **전략/관심종목/시장** 순, 기본=전략·비교. **기준전략(M) 폐지**(2026-06-10): scan_all의 메인 패스는 `execute=False` 판단 전용(last_scan=화면·상태점용, 메인 계좌 매매 동결), 실매매·비교는 변이 36개만. `sync_label_tops` 메인 제외 규칙 삭제(유형별 1위 전부 변이). 비교탭 행마다 **마킹 아이콘 피커**(2026-06-10 ☆토글→6종 아이콘 ⭐🔥💎🎯👀🚀 선택, `FAV_ICONS`/`favorites.json`은 `{아이콘: vid}` — **아이콘 하나당 전략 1개**(다른 전략에 쓰면 이동)·전략당 아이콘 1개. 칩 클릭→바텀시트 피커→`POST /fav_set`(fetch). 구버전 list는 자동 마이그레이션), 관심종목 탭 기본 시각 = 첫 마킹(FAV_ICONS 순) → 없으면 1위 변이. **약세모드 단어 칩**(2026-06-10): 비교 행 vid 옆 + 드롭다운 + 펼침 body '약세장대처' 행에 회피(0 현금화)/안정(1 돌파만·기본, 2026-06-11 '돌파'→'안정' 개명 — 적극과 혼동)/적극(2 눌림허용) 색 칩(`_BEAR_WORD`/`_bear_chip_html`, 클릭 시 설명 토스트. 아이콘→단어는 관리자님 요청). **전략 이름 = 4카테고리 두글자**(2026-06-15 `_cat_name`/`_cat_labels` — 추세〔민감10/표준20/둔감40/장기60〕·진입〔적극 vol≤1.5/신중〕·매도〔한방 RR≥2.5/균형/단타 RR≤1.5〕·베팅〔집중 risk≥2%/분산〕. 예 '표준·적극·균형·분산'). 비교 행·드롭다운·상세 헤더에 적용. 펼침/상세에 **`_cat_detail_html`** 4요소 풀이(라벨+실제값+쉬운설명). 긴 문장형 `_market_fit_summary`(상승장에 강한…)는 '시장적합' 행 ⓘ로 유지. **bt매칭은 `_btkey`(swept_params만)** — BEAR_ENTRY_MODE 등 백테스트 중립 키가 params에 있어도 같은 bt행에 매칭(모드 3형제는 bt순위 공유, 백테스트 탭은 36종만 표시가 정상). 헤더 상태점 자산줄은 '전략 N개 · 평균/1위 수익률'. 2026-06-10 내계좌 탭 삭제〔계좌 정보는 비교탭 메인 행으로〕·튜닝 보조탭 삭제〔params 변경은 백테스트 "이 튜닝 적용"으로만, `/save_params` 리다이렉트도 s=bt〕. 탭 라우팅 `?t=`+보조탭 `?s=`, 레거시 t=acct/tune→lab): 헤더 타이틀 옆 **시뮬 상태점**(`_engine_status`→`#health` 점: 🟢 장중 가동(최근 스캔 ≤12분)·🟡 장외 대기·🔴 장중 멈춤 의심, 클릭 시 상태 토스트). **관심종목**(종목별 신호 체크리스트 + 각 평가항목 **ⓘ 버튼 → 쉬운 설명 토스트**〔`_CHK_HELP`/`_chk_key`/`_CHK_HELP_JS`, 바텀시트 `#chk-toast`, 열려있으면 자동새로고침 멈춤〕 + 상단 **종목 추가 폼**〔코드/이름〕 + 카드별 **✕ 삭제**〔수동만, 보유 중이면 청산 추적〕 + origin 라벨〔워치/관심/보유/수동〕 + 미스캔분 '스캔 대기' 표시) · **시장**(지수·ADR·상승하락·투자자 순매수 — **항상 네이버 m.stock 실시간**〔`_get_live_market` 60s 캐시, `market_indicators_sync.fetch_market` 재사용. 네이버가 장 마감 후에도 당일 최종값 제공하므로 세션 무관 최신 사용〕, **조회 실패 시에만** `market_indicators_history.jsonl` EOD 폴백. note는 장중='장중 실시간'·장외='직전 장 마감 기준'. 2026-06-09 초기엔 장중만 실시간이라 마감 후 어제 EOD를 보이던 빈틈을 항상-실시간으로 수정) · **전략**(2026-06-11 보조탭 버튼 제거 — 기본=비교만 표시, 백테스트는 비교 타이틀 우측 **[📊 모의결과보기]** 라벨(라디오 `sub-bt` 토글)로 진입·bt 패널 상단 [← 비교로]로 복귀. 그 옆 **[📋 거래목록]**(`sub-day`/`_day_trades_panel(sel_date)` — 전 전략 거래를 **달력형 날짜 선택**(`_DAY_CAL_JS` — 거래일만 밝게·클릭가능, 비거래일 어둡게, 월 이동 JS 클라이언트〔범위 밖 ‹›disabled〕, 오늘 강조. 컴팩트 max-width 300px. 날짜 클릭 시 **달력 안 닫고 fetch로 #day-rows·#day-sub·라벨만 갱신**〔history.replaceState로 `?d=` 보존〕, 별도 [닫기] 버튼. 접이식 details, **기본=오늘(거래 없어도 자동선택·클릭가능, `today-open` 점선 셀)**, 2026-06-15. 빈 날도 #day-rows 컨테이너 유지해 fetch 전환 정상)으로 **(종목,방향)별 아이템**(같은 종목 매수·매도 동시면 2아이템 분리, 요약=종목명+🟢매수/🔴매도 배지+전략수)으로 집계: 전략수·전략번호칩(상세링크)·가격대·평균손익·사유. 날짜는 simURL이 `&d=`로 보존). `?s=bt` 라우팅·60s 새로고침 라디오 보존은 유지. 구성 2: **백테스트**〔**유형별(세부 라벨 ~36종) 대표 요약** — 2026-06-10 단순화: 시장 성격별 그룹(상승장/추세장/조정장/출렁이는 장/보통 장에 강한 유형) 안에 각 유형 1위만 표시(꾸준함=worst_ret 순), 행=제목+최악·검증수익, 펼치면 구간별 검증·MDD·승률·PF·알파·파라미터·🌐. **비교군 추가/제거·이 튜닝 적용 버튼 제거**(읽기전용) — `variants.sync_label_tops()`는 **수동 전용**(2026-06-10 자동 동기화 끔 — sweep은 결과 파일만 갱신, 비교군 변경은 관리자님 명시 요청 시 `python3 -m sim variants sync` 또는 코디가 수동 등록)(같은 params 변이는 계좌 유지·신규 추가·탈락 삭제, 메인 조합 제외). 메인 변경은 코디/CLI로만. + **'↻ 다시 계산'**〔백그라운드 sweep — 결과 파일만 갱신, 비교군은 불변〕〕/**비교**〔병렬 변이 실시간 성적, 변이별 성적초기화/삭제 + **전체 삭제(메인 제외)** `/variants_clear_all`→`vmod.clear()`. 펼침 body 첫 행=**유형**(긴 문장형+ⓘ→🌐 시장적합 토스트, 2026-06-10 하단 🌐 버튼 대체)→둘째 행 **약세장대처**(`돌파 (기본)` 모드명만+ⓘ 토스트). bt 등록근거 행은 **모의평가**로 개명(1행 `N위/36`+2행 최악구간·검증, 모드 3형제는 `_btkey`로 같은 bt행 공유). **전략 상세 페이지**(2026-06-11): 행 펼침 [📈 자세히보기] → `GET /strategy?id=vN` 독립 페이지 — 일별 수익률 SVG 차트(`equity_history` id+ph 매칭, KOSPI 같은 구간 점선 오버레이) + 결정론 종합 코멘트(순위·알파·과최적 경고) + 현재 성적·모의평가·전략 성격·약세장대처·파라미터 표(`_TUNE_HELP` 라벨)·보유/관심/거래. 평가순 정렬은 고유번호 오름차순(#1→#108). 행 표기(2026-06-10): **순위 배지 `N위`가 앞, 고유번호는 `#N`**(`_vid_disp` — 'v37'→'#37' 표시 전용, 내부 id·URL은 raw. 드롭다운·👁토스트·사유 라벨 동일 표기)〕). 🌐는 `data-detail` 통합 토스트 핸들러로 헤더 상태점(`#health`)과 공유. sticky 헤더, 당겨서 새로고침. **새로고침 상태 보존**(2026-06-10): 60s 자동새로고침이 전체 리로드지만 펼침(details open)·스크롤을 sessionStorage(`simOpen`/`simScroll`, key=패널id+summary텍스트)로 저장·복원해 보던 화면 유지. 입력 중·토스트 열림엔 새로고침 멈춤. **탭 보존**(2026-06-11): 탭/서브탭 라디오 변경 시 URL 즉시 동기화(`window._simURL`+history.replaceState) — 당겨서·브라우저·마킹저장 등 모든 리로드가 보던 탭에 착지. HTML 응답에 `Cache-Control: no-store`(Safari 휴리스틱 캐싱이 옛 JS를 재사용해 탭 복원이 안 먹던 문제). **깜빡임 제거**(2026-06-12): 활성 탭/서브탭을 `render_html(tab,sub)`이 라디오 `checked`로 서버에서 미리 박음(GET이 `?t/?s` 파싱+레거시 보정) — 이전엔 기본탭 렌더→JS가 로드 후 전환하며 깜빡였음. `do_POST`: `/save_params`·`/run_sweep`·`/variants_{add,reset_one,remove,clear_all}`·`/watch_{add,remove}`·`/fav_set`(마킹 아이콘 지정/이동/해제 → `state/sim/favorites.json`) (그 외 읽기전용) - Portfolio data: `memory/portfolio.json` (v2 스키마 참고용 스냅샷, `accounts.{일반,ISA}.positions`), `state/portfolio_daily_snapshot.json`, `state/kiwoom_tokens/{일반,ISA}.json`, `state/stock_codes.json`(키움 ka10099 lazy 캐시), `state/watchlist_alerts.json`(알림 중복 방지), `state/ipo_calendar_sync.json`, `state/behive_*.json`, `state/fnguide_cache/{code}.json`(FnGuide 펀더멘털 12h), `state/wisereport_cache/{code}.json`·`{code}_reports.json`(컨센서스 12h·리포트 6h), `state/stock_reports//`(분석 보고서 HTML) ## Scheduled Jobs diff --git a/agents/stock/workspace/sim/config.py b/agents/stock/workspace/sim/config.py index 45bcabf..08c402d 100644 --- a/agents/stock/workspace/sim/config.py +++ b/agents/stock/workspace/sim/config.py @@ -38,6 +38,9 @@ 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 # 손절선 후보 스윙 저점 구간 @@ -87,6 +90,9 @@ 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': '변동성·기간'}, diff --git a/agents/stock/workspace/sim/engine.py b/agents/stock/workspace/sim/engine.py index 79e6f05..6f36423 100644 --- a/agents/stock/workspace/sim/engine.py +++ b/agents/stock/workspace/sim/engine.py @@ -326,12 +326,7 @@ def _run_scan(pf, uni, quotes, cmkt, now, execute: bool = True) -> dict: decisions.append({ 'code': code, 'name': name, 'sources': sources, 'price': cur_price, 'state': 'SKIP', 'reason': '추세 미충족', 'action': None, - 'checks': [ - {'label': '추세(20일선 위)', 'ok': cur_price > ind['sma_long'], - 'detail': f"{cur_price:,} vs {ind['sma_long']:,.0f}"}, - {'label': '정배열(5>20)', 'ok': ind['sma_short'] > ind['sma_long'], - 'detail': f"{ind['sma_short']:,.0f} / {ind['sma_long']:,.0f}"}, - ], + 'checks': signals.trend_checks(ind), # 4조건+상대강도 모두 표시 (왜 제외인지 보이게) }) continue diff --git a/agents/stock/workspace/sim/signals.py b/agents/stock/workspace/sim/signals.py index ca0baa9..9906620 100644 --- a/agents/stock/workspace/sim/signals.py +++ b/agents/stock/workspace/sim/signals.py @@ -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, diff --git a/agents/stock/workspace/sim/sim_web.py b/agents/stock/workspace/sim/sim_web.py index 9878845..7c7f920 100644 --- a/agents/stock/workspace/sim/sim_web.py +++ b/agents/stock/workspace/sim/sim_web.py @@ -27,7 +27,7 @@ BIND_HOST = '' BIND_PORT = 18792 STATE_BADGE = { - 'SELL': ('매도', '#ef4444'), 'ADD': ('추격매수', '#16a34a'), 'BUY': ('매수', '#22c55e'), + 'SELL': ('매도', '#ef4444'), 'ADD': ('추격매수', '#0f7a34'), 'BUY': ('매수', '#22c55e'), 'HOLD': ('보유', '#3b82f6'), 'WAIT': ('대기', '#eab308'), 'SKIP': ('제외', '#6b7280'), 'NODATA': ('데이터없음', '#374151'), } @@ -745,6 +745,56 @@ def _market_fit_words(p: dict) -> str: return '·'.join([m] + adj + [style]) +def _cat_labels(p: dict) -> tuple[str, str, str, str]: + """전략을 4카테고리 두글자 이름으로 — (추세, 진입, 매도, 베팅).""" + p = p or {} + sma = p.get('SMA_LONG', 20) + trend = {10: '민감', 20: '표준', 40: '둔감', 60: '장기'}.get(sma, '표준') + entry = '적극' if p.get('VOLUME_BREAKOUT_MULT', 1.5) <= 1.5 else '신중' + rr = p.get('RR_RATIO', 2.0) + sell = '한방' if rr >= 2.5 else '단타' if rr <= 1.5 else '균형' + bet = '집중' if p.get('RISK_PER_TRADE_PCT', 0.01) >= 0.02 else '분산' + return trend, entry, sell, bet + + +def _cat_name(p: dict) -> str: + """4카테고리 이름 한 줄 — 예 '표준·적극·균형·분산'.""" + return '·'.join(_cat_labels(p)) + + +def _cat_detail_html(p: dict) -> str: + """펼침용 — 4카테고리 각각의 선택값·쉬운 설명.""" + p = p or {} + trend, entry, sell, bet = _cat_labels(p) + sma = p.get('SMA_LONG', 20); vol = p.get('VOLUME_BREAKOUT_MULT', 1.5) + rr = p.get('RR_RATIO', 2.0); risk = p.get('RISK_PER_TRADE_PCT', 0.01) + stop = p.get('STOP_ATR_MULT', 2.0); hold = p.get('MAX_HOLD_DAYS', 0) + rows = [ + ('🟦 추세', trend, f'{sma:g}일선 기준', '추세 — 며칠짜리 평균선으로 보나', + f'추세를 며칠짜리 평균선으로 보는지예요. 이 전략은 {sma:g}일선 기준({trend}).\n\n' + '· 민감(10일선): 빨리 반응 — 기회 빨리 잡지만 속임수(휩쏘)도 많음\n' + '· 표준(20일선): 무난한 중간\n· 둔감(40일선): 천천히 — 늦지만 안정적\n· 장기(60일선): 큰 흐름만'), + ('🟩 진입', entry, f'돌파 거래량 {vol:g}배', '진입 — 얼마나 쉽게 사나', + f'돌파로 살 때 거래량이 평소의 몇 배는 터져야 사는지예요. 이 전략은 {vol:g}배({entry}).\n\n' + '· 적극(1.3배): 문턱 낮음 — 자주 삼\n· 신중(2.0배): 확실히 터질 때만 삼'), + ('🟥 매도', sell, f'손익비 {rr:g} · 손절 ATR{stop:g}배' + (f' · 시간손절 {hold}일' if hold else ''), + '매도 — 얼마나 길게 먹나', + f'목표 수익을 손절폭의 몇 배로 잡는지예요. 이 전략은 손익비 {rr:g}({sell}).\n\n' + '· 한방(3.0): 길게 들고 크게 먹기\n· 균형(2.0): 중간\n· 단타(1.5): 짧게 자주 먹기\n\n' + f'손절은 변동성(ATR)의 {stop:g}배 빠지면 실행' + (f', {hold}일 지나도 안 오르면 정리해요.' if hold else '해요.')), + ('🟨 베팅', bet, f'종목당 리스크 {risk * 100:g}%', '베팅 — 한 종목에 얼마나', + f'한 종목에 자산의 몇 %까지 거는지예요. 이 전략은 {risk * 100:g}%({bet}).\n\n' + '· 집중(2%): 크게 베팅 — 수익·손실 둘 다 큼\n· 분산(1%): 작게 — 안정적'), + ] + out = '' + for cat, name, val, title, desc in rows: + out += (f'
{cat}' + f'{_esc(name)} — {_esc(val)}' + f'
') + return out + + # 약세장 모드 단어 칩 — 비교군 행·드롭다운에서 한눈에 구분 (긴 접미사 대신). _BEAR_WORD = {0: '회피', 1: '안정', 2: '적극'} _BEAR_NAME = {0: '회피 (현금화)', 1: '안정 (돌파만·기본)', 2: '적극 (눌림허용)'} @@ -771,6 +821,51 @@ def _bear_chip_html(p) -> str: f'data-detail="{_esc(_BEAR_DESC[m] + _BEAR_LEGEND)}">{_BEAR_WORD[m]}') +def _gate_info(p): + """진입 게이트 차원 — (이름, 설명). 유형·약세장대처와 독립된 3번째 축.""" + p = p or {} + off = [] + if not int(p.get('TREND_SLOPE_GATE', 1)): + off.append('장기선 우상향 요구') + if not int(p.get('MID_ARRAY_GATE', 1)): + off.append('중기 정배열(5>60) 요구') + if not off: + return ('보수 (현행)', + '추세 조건을 모두 요구해요 — 현재가>장기선·정배열·장기선 우상향·중기 정배열(5>60). ' + '안전하지만 매수 기회가 적어요(보수적).') + return ('완화 — ' + '·'.join('장기선 기울기' if '우상향' in o else '중기 정배열' for o in off) + ' 면제', + '추세 게이트를 완화한 변이예요. 꺼진 조건:\n· ' + '\n· '.join(off) + + '\n\n기본 조건(현재가>장기선·정배열)은 그대로라, 단기 반등 초입 종목도 잡아 더 적극적으로 매수해요. ' + '보수형(현행)과 라이브로 비교 중입니다.') + + +def _relax_chip_html(p) -> str: + """추세 게이트 완화 칩 — 장기선 기울기/중기정배열 게이트를 끈 변이 표시. 현행(둘다 ON)이면 빈 문자열.""" + p = p or {} + off = [] + if not int(p.get('TREND_SLOPE_GATE', 1)): + off.append('장기선 우상향 요구') + if not int(p.get('MID_ARRAY_GATE', 1)): + off.append('중기 정배열(5>60) 요구') + if not off: + return '' + desc = ('추세 게이트 완화 변이 — 다음 조건을 꺼서 더 적극적으로 매수해요:\n· ' + + '\n· '.join(off) + '\n\n보수형(현행)과 라이브로 비교 중인 실험 전략입니다.') + return (f'완화') + + +def _immediate_chip_html(p) -> str: + """즉시매수 칩 — 돌파·눌림목 안 기다리고 조건 통과 시 현재가 즉시 매수하는 변이. 아니면 빈 문자열.""" + if not int((p or {}).get('IMMEDIATE_ENTRY', 0)): + return '' + desc = ('즉시매수 변이 — 추세·정배열·수급·애널을 다 통과한 종목이면, 돌파나 눌림목을 안 기다리고 ' + '강세장에서 현재가에 바로 사요. 꾸준히 오르기만 하는 좋은 종목을 놓치지 않으려는 실험 전략이에요. ' + '(기다렸다 사는 기존 전략과 라이브 비교 중)') + return (f'즉시') + + def _backtest_panel() -> str: """백테스트 결과 — 유형별(세부 라벨) 대표(1위) 조합 요약. 비교군은 이 대표들로 자동 구성되므로 수동 추가/제거 없이 읽기 전용 요약만 보여준다.""" @@ -1028,12 +1123,14 @@ def _compare_panel() -> str: rows += ( f'
' - f'{rank_of[v["id"]]}위{_bear_chip_html(mp)} {_vid_disp(vid)} ' - f'{_esc(mf_w)}{tag}' + f'{rank_of[v["id"]]}위{_bear_chip_html(mp)}{_relax_chip_html(mp)}{_immediate_chip_html(mp)} {_vid_disp(vid)} ' + f'{_esc(_cat_name(mp))}{tag}' f'🛒{s.get("open_positions", 0)} ' f'{ret:+g}%' f'
' - f'
유형{_esc(mf_sum)}' + f'
전략 4요소추세·진입·매도·베팅
' + f'{_cat_detail_html(mp)}' + f'
시장적합{_esc(mf_sum)}' f'
' f'
약세장대처{_BEAR_NAME[bm]}' @@ -1197,6 +1294,7 @@ h1{font-size:17px;margin:10px 0 2px}h2{font-size:14px;margin:22px 0 8px;color:#c .vid{background:#1e293b;color:#7dd3fc;border-radius:5px;padding:1px 6px;font-size:11px;font-weight:700} .bic{border-radius:5px;padding:1px 5px;font-size:10px;font-weight:700;margin-left:4px} .bic0{background:#334155;color:#cbd5e1}.bic1{background:#1e3a5f;color:#7dd3fc}.bic2{background:#4a2e12;color:#fbbf24} +.bic-relax{background:#3b1d4a;color:#d8b4fe}.bic-now{background:#0c3a2e;color:#6ee7b7} .cmp-btlink{background:#0f2a3f;color:#7dd3fc;border:1px solid #1e3a5f;border-radius:6px;padding:3px 8px;font-size:11px;font-weight:600;display:inline-block} .note{white-space:pre-wrap;line-height:1.65;font-size:13px;color:#cbd5e1} ul.an{margin:6px 0;padding-left:18px;line-height:1.7;font-size:13px} @@ -1326,12 +1424,15 @@ td:first-child,th:first-child{text-align:left} {_esc(vid)} 전략 상세{style}
{back} -

{rank or "—"}위 {_vid_disp(vid)} {_esc(_market_fit_words(params))} -{_BEAR_WORD[bm]} {icon}

+

{rank or "—"}위 {_vid_disp(vid)} {_esc(_cat_name(params))} +{_BEAR_WORD[bm]}{_relax_chip_html(params)}{_immediate_chip_html(params)} {icon}

{_esc(_market_fit_summary(params))} · 약세장대처: {_BEAR_NAME[bm]}
{notes_html}
+

🧩 전략 4요소 (이름 풀이)

+
{_cat_detail_html(params)}
+

📈 수익률 추이

{_ret_chart_svg(series, idx_series)}
@@ -1380,7 +1481,9 @@ _DAY_CAL_JS = """