Compare commits

...

3 Commits

Author SHA1 Message Date
hyowons 5cd3ba9f4e auto: 일일 백업 2026-06-16 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 02:00:06 +09:00
hyowons a5e52dc69b feat(sim): 관찰목록 자동편입/제외 — 키움 순위정보로 주도주 매일 편입
- kiwoom_client: get_volume_surge(ka10023)·get_foreign_inst_netbuy(ka90009) 순위 조회 추가 (rkinfo endpoint)
- universe: auto_import(외인·기관 순매수+거래량급증 상위, ETF·하락·급증률이상치 제외, 각15·총120 상한, origin=auto) + auto_prune(auto·미보유·10거래일 무신호만, 보유/수동/비하이브 불가침) + mark_signals
- engine.scan_all: 변이 watching 종목을 mark_signals로 신호 추적(auto_seen.json)
- CLI: python3 -m sim universe {import|prune|all}
- trade-journal plist 평일21:00에 'sim universe all' 4번째 명령 추가
- 첫 실행: 후보38 → 32편입 (59→91종목)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:03:35 +09:00
hyowons e3c54d62ed 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>
2026-06-15 18:57:30 +09:00
283 changed files with 128619 additions and 93087 deletions
+7 -3
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -49,7 +49,7 @@
| `ipo-calendar-sync` | 매주 금요일 17:00 | `ipo_calendar_sync.py` | IPO 청약·상장 일정 → Google Calendar |
| `holiday-sync` | 매주 일요일 03:00 | `holiday_sync.py` | investing.com → `state/market_holidays.json`. 실패 시 기존 파일 보존 |
| `behive-web` | 상시 (`KeepAlive`) | `behive_web.py serve` | 3-탭 워치리스트 웹뷰. Tailscale `100.75.148.12:18790` → `https://stock.hyowons.net/`(Synology reverse proxy). 코드 변경 후 bootout/bootstrap 필요 |
| `trade-journal` | 평일 21:00 (스크립트가 휴장일 self-skip) | `trade_journal.py collect` | ka10170 당일매매일지 4계좌 → `state/trade_journal.jsonl` 누적. NXT 야간 마감 후 발화. 키움이 기간 거래내역 API 미제공이라 일자별 적재만이 유일. `briefing-fallback-2100`(21:00)과 같은 시각이지만 둘 다 read-only 충돌 없음. 재실행 시 (date,account) 단위 idempotent. **시드**: 2026-05-13 1회 `trade_journal.py seed` 로 적재 시작일 이전 보유분을 현재 평단가×(qty - tdy_buyq + tdy_sellq)로 단일 시드 행 압축(28건, seed=true 플래그, `*` 마커). 시드는 가중평균이라 과거 매수 단가와 정확히 일치 X. CLI: `collect`/`seed`/`show <code>`/`query --from --to --account --code` |
| `trade-journal` | 평일 21:00 (스크립트가 휴장일 self-skip) | `trade_journal.py collect` | ka10170 당일매매일지 4계좌 → `state/trade_journal.jsonl` 누적. NXT 야간 마감 후 발화. 키움이 기간 거래내역 API 미제공이라 일자별 적재만이 유일. `briefing-fallback-2100`(21:00)과 같은 시각이지만 둘 다 read-only 충돌 없음. 재실행 시 (date,account) 단위 idempotent. **시드**: 2026-05-13 1회 `trade_journal.py seed` 로 적재 시작일 이전 보유분을 현재 평단가×(qty - tdy_buyq + tdy_sellq)로 단일 시드 행 압축(28건, seed=true 플래그, `*` 마커). 시드는 가중평균이라 과거 매수 단가와 정확히 일치 X. CLI: `collect`/`seed`/`show <code>`/`query --from --to --account --code`. **2026-06-15 4번째 명령 추가**: `python3 -m sim universe all` — sim 관찰목록 자동편입/제외. 키움 순위정보(ka90009 외인·기관순매수 + ka10023 거래량급증)에서 ETF·하락·급증률 이상치 거르고 각 15·총 120 상한 편입(origin='auto'), origin=auto·미보유·10거래일 무신호만 자동제외(보유/watch/interest/manual 불가침). 신호추적=scan_all `universe.mark_signals`→`auto_seen.json` |
| `sim-scan` | 평일 09:00–15:30 **매 5분** (`StartCalendarInterval` 79엔트리, 엔진이 장외/휴장 self-skip) ⚠️2026-06-09 15분→5분 단축 + `StartInterval` 900s→`StartCalendarInterval` 전환(StartInterval은 GUI세션 idle/디스플레이 sleep 시 timer coalescing으로 발화 보류돼 장중 안 돎. calendar는 git-autopush가 새벽 02:00 정상발화로 검증됨) | `python3 -m sim scan` (내부 `scan_all` — 메인+병렬변이 동시) | **자동매매 시뮬(페이퍼)** 1회 스캔. 가상자본 1억(2026-06-10 1천만→1억), 규칙 엔진(LLM 미경유). universe=비하이브 워치+관심+보유(**ETF/ETN은 2026-06-09부터 자동매수 제외** — 애널 방향성 부재로 전략 부정합, `universe.is_etf()` 프리픽스 판별, 보유분은 청산까지 추적). 방향=애널 약한게이트+가점, 타이밍=수급·기술(2026-06-09 시장강약 필터: 하드차단→소프트게이트+장중 실시간 상승비율 + **지수추세 이중확인**). **2026-06-09 4종 추가**: 리스크 기반 사이징(`signals.target_value` 수량=자산×`RISK_PER_TRADE_PCT`÷손절폭, 균등비중 캡)·RSI 필터(돌파 과열/눌림목 과매도 보류)·**시장 국면 6단계**(`market_regime_map`→`engine.REGIME`: 🔥강세·📈반등·🌤완만상승=매수허용 / ➖혼조·📉약세·❄️급락=보류. 상승비율+지수추세+당일등락 조합. market_ok·시장탭 공용. 강세/약세 2단계가 84% 반등을 약세로 오판해 세분화)·**약세장 매수모드**(`BEAR_ENTRY_MODE` 0현금화/1돌파만/2눌림허용, market_ok False일 때 signals 분기. backtest는 시장중립이라 무영향→병렬변이로 라이브 비교. 2026-06-10 **유형36×모드3=108변이 풀매트릭스**로 확장, 첫 라이브 2026-06-11)·시간손절(`MAX_HOLD_DAYS` 미진전 청산)·**추세 질 보강**(2026-06-09 trend_ok 게이트에 ①20일선 우상향 기울기 ②5>60 중기정배열 추가〔종목데이터, backtest 반영〕 + ③상대강도〔종목−지수 N일수익률, 라이브 전용 체크〕). 사이징·시간손절은 엔진·백테스트 공용 헬퍼. TUNABLE 27개. **2026-06-10 코드리뷰 보강**: ①수급DB 일일증분(trade-journal 21시 묶음 3번째 명령 `-m sim.backfill_flow --pages 1 --force` — stale시 백테스트 수급게이트 중립화됨) ②일별 성과이력 `state/sim/equity_history.json`(EOD 덮어쓰기, ph=파라미터해시) ③당일 재진입 쿨다운(`Portfolio.last_exit`, backtest 동일) ④장중 거래량 하루치 환산(`vol_time_frac` — 이전엔 오전 돌파 불가). ⑤백테스트 체결 현실화(당일종가 판단→다음날 시가 체결 pending큐 + `BT_SLIPPAGE` 0.2% — 낙관분 ~5%p 제거) ⑥3분할 꾸준함 순위(`worst_ret` 1순위 — 한 구간 운빨 배제) ⑧**기준전략(M) 폐지**(2026-06-10 — scan_all 메인 패스 execute=False 판단전용·계좌 동결, 비교/매매는 변이 36개만, 메인 제외 규칙 삭제, 마킹 아이콘 6종 `favorites.json {아이콘:vid}`+`/fav_set`〔2026-06-11 ☆토글→아이콘 피커, 아이콘당 전략 1개〕, 관심종목 기본 시각=첫 마킹→1위) ⑦비교군 동기화는 **수동 전용**(2026-06-10 자동 동기화 끔 — sweep은 결과 파일만 갱신, 비교군 변경은 관리자님 명시 요청 시만 `python3 -m sim variants sync`. `sync_label_tops`=유형(~36종)별 1위 동기화, 동일params 계좌 유지. 백테스트 탭은 유형별 대표 읽기전용 요약, 수동 추가/제거·튜닝적용 버튼 제거). 잔여 한계: 검증 데이터가 여전히 ~1년 강세 위주(진짜 하락장 검증은 라이브 누적)·생존편향 universe(의도된 설계). 매수=돌파 즉시/눌림목 지정가, 매도=손절·2:1 후 트레일링·추세수급 이탈. 결과 `state/sim/`(portfolio.json·trades.jsonl·last_scan.json). 실주문 `orders/` 불가침. 일봉은 캐시 우선 읽기(ka10081 rate limit 회피). cwd=`agents/stock/workspace` |
| `sim-web` | 상시 (`KeepAlive`) | `python3 -m sim.sim_web serve` | 시뮬 대시보드 **별도 서버 포트 18792** (behive-web 18790과 분리). `sim.hyowons.net` → mac:18792 (Synology reverse proxy, **관리자님 수동 등록 대기**). Tailnet 한정 무인증. ⚠️ 18791은 node 점유라 18792. **3탭**(2026-06-10~11 개편, 내계좌·튜닝 탭 삭제): **전략**(기본=비교만, 서브탭 버튼 없음 — 백테스트 순위표는 [📊 모의결과보기] 토글)·**관심종목**(전 전략 합산 상태그룹 + 시각 드롭다운 + 👁토스트 맨 위 '▶ 시각' 판단 표시)·**시장**(지수/ADR/투자자). 비교 행 표기: 순위배지 `N위` → 약세장대처 칩(회피/안정/적극 — 2026-06-11 '돌파'→'안정' 개명, 칩 클릭=설명 토스트) → 고유번호 `#N` → 짧은 유형명(`상승장·빠른·공격·추세추종`) → 마킹 아이콘(6종 ⭐🔥💎🎯👀🚀, **아이콘당 전략 1개**, `favorites.json {아이콘:vid}`, `/fav_set`). 펼침 body: 유형(ⓘ=시장적합)→약세장대처(ⓘ)→성적→모의평가(`btN위/36` — 약세모드 3형제는 같은 bt등수 공유). **108변이 = 유형36 × 약세모드3**(2026-06-10 풀매트릭스, v37/38=bt1위의 회피/적극 쌍 순서). 새로고침 탭 보존(라디오↔URL 동기화 + `Cache-Control: no-store`, 2026-06-11). 읽기전용 원칙, 쓰기는 `do_POST` save_params/run_sweep/variants_*/watch_*/fav_set만. 시장 탭은 장중엔 네이버 m.stock 실시간(지수+상승하락·ADR·투자자 순매수, `_get_live_market` 60s 캐시), 장외엔 EOD jsonl 폴백 (2026-06-09: 이전엔 상승하락·투자자가 EOD 고정이라 장중 안 바뀌던 것 수정) |
| (트리거 없음) | on-demand | `python3 -m sim {sweep\|backtest}` | 튜닝 비교용 백테스트 스윕. signals.py 재사용·과거 일봉 되감기. 9파라미터(RR·STOP·SMA·PULLBACK·VOLUME + RSI과열/과매도·시간손절·리스크비율) 1152조합 학습/검증 분리 → `state/sim/backtest_results.json`. ⚡멀티프로세싱(2026-06-10 spawn, 워커=코어−2)+일봉/수급 프로세스 메모이즈 → 1152조합 44분→~5-7분. 데이터는 sqlite캐시 전용(키움API 0), 병목은 지표 재계산이라 캐싱 아닌 병렬화가 답. GRID 곱셈 폭발 주의. ⚠️ 종가체결·생존편향 → **상대순위**로만. 2026-06-09 스윕: 핵심 레버 `VOLUME_BREAKOUT=1.3`, 신규4종은 강세창선 미미(하락장 보험). 균형값 params.json 적용(RISK 0.01 유지). 수급은 `backfill_flow.py`(ka10059 다년치 → `flow_history.sqlite`, 2026-06-08 54종목 3만행 적재)로 반영됨(`flow_used:true`). 런타임 튜닝은 `params.json`이 config 기본값 오버라이드(엔진 매 스캔 새프로세스라 즉시 반영). **병렬 페이퍼**: `variants.json`+`state/sim/variants/<id>/` — 스윕 상위 튜닝을 각자 가상계좌로 실시간 동시 운영, 같은 라이브 시세에 다른 파라미터(애널 게이트까지 실반영), `variants_compare.json`→웹 비교 탭. `python3 -m sim variants seed N`. sim-scan이 매 스캔 메인+변이 함께 갱신 |
@@ -32,6 +32,7 @@ ENDPOINT_ACNT = '/api/dostk/acnt'
ENDPOINT_STKINFO = '/api/dostk/stkinfo'
ENDPOINT_MRKCOND = '/api/dostk/mrkcond' # 시세(market condition) — ka10004 호가 등
ENDPOINT_CHART = '/api/dostk/chart' # 차트 — ka10081 일봉
ENDPOINT_RKINFO = '/api/dostk/rkinfo' # 순위정보 — ka10023 거래량급증, ka90009 외인기관매매상위
TR_DEPOSIT = 'kt00001' # 예수금상세현황요청
TR_ACNT_EVAL = 'kt00004' # 계좌평가현황요청 (acnt_nm·brch_nm 포함)
TR_TRUST_TRADE = 'kt00015' # 위탁종합거래내역요청 (tp='1' 입출금 — 당일 추가입금/인출 추출용)
@@ -1051,6 +1052,49 @@ def _load_code_cache() -> dict:
return {}
def get_volume_surge(account_label: str | None = None, market: str = '000',
min_qty: str = '50') -> list[dict]:
"""거래량 급증 종목 (ka10023). 급증률 정렬·ETF+ETN+스팩 제외·5천원↑·KRX.
반환 [{code, name, price, flu_rt(등락률%), surge_rt(급증률%)}] — 조회 전용."""
label = account_label or _default_account_label()
body = {'mrkt_tp': market, 'sort_tp': '2', 'tm_tp': '2', 'trde_qty_tp': min_qty,
'tm': '', 'stk_cnd': '20', 'pric_tp': '6', 'stex_tp': '1'} # stk_cnd 20=ETF+ETN+스팩제외
resp = _call(label, 'ka10023', body, ENDPOINT_RKINFO)
out = []
for x in resp.get('trde_qty_sdnin', []) or []:
code = _clean_code(x.get('stk_cd', ''))
if not code:
continue
out.append({'code': code, 'name': x.get('stk_nm', ''),
'price': abs(_to_int(x.get('cur_prc'))),
'flu_rt': _to_float(x.get('flu_rt')),
'surge_rt': _to_float(x.get('sdnin_rt'))})
return out
def get_foreign_inst_netbuy(account_label: str | None = None, market: str = '000',
date: str | None = None) -> dict:
"""외국인·기관 순매수 상위 (ka90009). 금액(천만) 기준.
반환 {'foreign': [{code,name,amt}], 'inst': [{code,name,amt}]} — 조회 전용."""
label = account_label or _default_account_label()
if not date:
date = datetime.now(KST).strftime('%Y%m%d')
body = {'mrkt_tp': market, 'amt_qty_tp': '1', 'qry_dt_tp': '1', 'date': date, 'stex_tp': '1'}
resp = _call(label, 'ka90009', body, ENDPOINT_RKINFO)
rows = resp.get('frgnr_orgn_trde_upper', []) or []
foreign, inst = [], []
for x in rows:
fc = _clean_code(x.get('for_netprps_stk_cd', ''))
if fc:
foreign.append({'code': fc, 'name': x.get('for_netprps_stk_nm', ''),
'amt': _to_int(x.get('for_netprps_amt'))})
ic = _clean_code(x.get('orgn_netprps_stk_cd', ''))
if ic:
inst.append({'code': ic, 'name': x.get('orgn_netprps_stk_nm', ''),
'amt': _to_int(x.get('orgn_netprps_amt'))})
return {'foreign': foreign, 'inst': inst}
def refresh_stock_codes(account_label: str | None = None) -> dict:
"""ka10099로 KOSPI+KOSDAQ 전체 상장종목 리스트 캐싱. 페이지네이션 처리.
+11 -1
View File
@@ -136,13 +136,23 @@ def _cmd_reset(argv):
print(f'초기화 완료 — 가상자본 {config.INITIAL_CAPITAL:,}원')
def _cmd_universe(argv):
"""관찰목록 자동편입/제외 — 매일 장 마감 후 1회. 'import'=주도주 편입, 'prune'=무신호 auto 제외, 둘 다 기본."""
from . import universe
sub = argv[0] if argv else 'all'
if sub in ('import', 'all'):
print('자동편입:', universe.auto_import())
if sub in ('prune', 'all'):
print('자동제외:', universe.auto_prune())
def main():
argv = sys.argv[1:]
cmd = argv[0] if argv else 'status'
rest = argv[1:]
{'scan': _cmd_scan, 'status': _cmd_status, 'report': _cmd_report,
'sweep': _cmd_sweep, 'backtest': _cmd_backtest, 'backfill': _cmd_backfill,
'backfill-index': _cmd_backfill_index,
'backfill-index': _cmd_backfill_index, 'universe': _cmd_universe,
'variants': _cmd_variants, 'reset': _cmd_reset}.get(cmd, lambda a: print(__doc__))(rest)
+6
View File
@@ -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': '변동성·기간'},
+11 -6
View File
@@ -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
@@ -477,6 +472,16 @@ def scan_all(force: bool = False) -> dict:
'held': sorted(vpf.positions)})
compare.sort(key=lambda c: c['summary'].get('total_return_pct', 0) or 0, reverse=True)
# 자동편입(auto) 종목 신호 추적 — 한 변이라도 노리는(매수/대기) 종목은 '살아있음' 기록 (auto_prune 판단용)
try:
from . import universe as _um
sig = {it['code'] for c in compare for it in (c.get('watching') or []) if it.get('code')}
if sig:
_um.mark_signals(sorted(sig))
except Exception:
pass
variants_mod.COMPARE_PATH.write_text(json.dumps({
'scanned_at': now.isoformat(), 'session': is_market_session(now),
'variants': compare,
+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,
+150 -31
View File
@@ -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'<div class="kv"><span class="k">{cat}</span>'
f'<span><b>{_esc(name)}</b> <span class="muted">— {_esc(val)}</span>'
f'<button type="button" class="chk-i" data-title="{_esc(title)}" '
f'data-detail="{_esc(desc)}">ⓘ</button></span></div>')
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]}</span>')
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'<span class="bic bic-relax" data-title="추세 게이트 완화" '
f'data-detail="{_esc(desc)}">완화</span>')
def _immediate_chip_html(p) -> str:
"""즉시매수 칩 — 돌파·눌림목 안 기다리고 조건 통과 시 현재가 즉시 매수하는 변이. 아니면 빈 문자열."""
if not int((p or {}).get('IMMEDIATE_ENTRY', 0)):
return ''
desc = ('즉시매수 변이 — 추세·정배열·수급·애널을 다 통과한 종목이면, 돌파나 눌림목을 안 기다리고 '
'강세장에서 현재가에 바로 사요. 꾸준히 오르기만 하는 좋은 종목을 놓치지 않으려는 실험 전략이에요. '
'(기다렸다 사는 기존 전략과 라이브 비교 중)')
return (f'<span class="bic bic-now" data-title="즉시매수" '
f'data-detail="{_esc(desc)}">즉시</span>')
def _backtest_panel() -> str:
"""백테스트 결과 — 유형별(세부 라벨) 대표(1위) 조합 요약. 비교군은 이 대표들로 자동 구성되므로
수동 추가/제거 없이 읽기 전용 요약만 보여준다."""
@@ -1028,12 +1123,14 @@ def _compare_panel() -> str:
rows += (
f'<details class="row" data-reg="{i}" data-rank="{rank_of[v["id"]]}" data-bt="{bt_attr}" '
f'data-held="{s.get("open_positions", 0)}" data-trades="{s.get("closed_trades", 0)}"><summary>'
f'<span class="vid">{rank_of[v["id"]]}위</span>{_bear_chip_html(mp)} <span class="muted">{_vid_disp(vid)}</span> '
f'<span class="nm">{_esc(mf_w)}</span>{tag}'
f'<span class="vid">{rank_of[v["id"]]}위</span>{_bear_chip_html(mp)}{_relax_chip_html(mp)}{_immediate_chip_html(mp)} <span class="muted">{_vid_disp(vid)}</span> '
f'<span class="nm">{_esc(_cat_name(mp))}</span>{tag}'
f'<span class="rt"><span class="muted">🛒{s.get("open_positions", 0)}</span> '
f'<b style="color:{rc}">{ret:+g}%</b></span></summary>'
f'<div class="body">'
f'<div class="kv"><span class="k">유형</span><span>{_esc(mf_sum)}'
f'<div class="kv"><span class="k">전략 4요소</span><span class="muted">추세·진입·매도·베팅</span></div>'
f'{_cat_detail_html(mp)}'
f'<div class="kv"><span class="k">시장적합</span><span>{_esc(mf_sum)}'
f'<button type="button" class="chk-i" data-title="{_esc(mf_t)}" '
f'data-detail="{_esc(mf_b)}">ⓘ</button></span></div>'
f'<div class="kv"><span class="k">약세장대처</span><span><b>{_BEAR_NAME[bm]}</b>'
@@ -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}
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{_esc(vid)} 전략 상세</title>{style}</head><body><div id="page">
{back}
<h1><span class="vid">{rank or "—"}위</span> <span class="muted">{_vid_disp(vid)}</span> {_esc(_market_fit_words(params))}
<span class="bic bic{bm}">{_BEAR_WORD[bm]}</span> {icon}</h1>
<h1><span class="vid">{rank or "—"}위</span> <span class="muted">{_vid_disp(vid)}</span> {_esc(_cat_name(params))}
<span class="bic bic{bm}">{_BEAR_WORD[bm]}</span>{_relax_chip_html(params)}{_immediate_chip_html(params)} {icon}</h1>
<div class="sub">{_esc(_market_fit_summary(params))} · 약세장대처: {_BEAR_NAME[bm]}</div>
<div class="card">{notes_html}</div>
<h2>🧩 전략 4요소 (이름 풀이)</h2>
<div class="card">{_cat_detail_html(params)}</div>
<h2>📈 수익률 추이</h2>
<div class="card">{_ret_chart_svg(series, idx_series)}</div>
@@ -1380,7 +1481,9 @@ _DAY_CAL_JS = """<script>(function(){
function render(){
var p=cur.split('-'), y=+p[0], m=+p[1];
var startDow=new Date(y,m-1,1).getDay(), ndays=new Date(y,m,0).getDate();
var minYM=(TD[TD.length-1]||'').slice(0,7), maxYM=(TD[0]||'').slice(0,7);
var allYM=TD.concat([TODAY]); // 월 이동 범위에 오늘 포함 (거래 없어도 오늘 월까지 이동 가능)
var minYM=allYM.reduce(function(a,b){return a<b?a:b;}).slice(0,7);
var maxYM=allYM.reduce(function(a,b){return a>b?a:b;}).slice(0,7);
var h='<div class="cal-hd">'
+'<button type="button" data-nav="-1"'+(cur<=minYM?' disabled':'')+'>‹</button>'
+'<span>'+y+'.'+pad(m)+'</span>'
@@ -1390,9 +1493,12 @@ _DAY_CAL_JS = """<script>(function(){
for(var i=0;i<startDow;i++)h+='<div></div>';
for(var d=1;d<=ndays;d++){
var ds=y+'-'+pad(m)+'-'+pad(d);
var cls='cal-d'+(ds===TODAY?' tdy':'');
if(tset[ds]){h+='<a class="'+cls+' on'+(ds===SEL?' sel':'')+'" data-d="'+ds+'" href="/?t=lab&s=day&d='+ds+'">'+d+'</a>';}
else{h+='<span class="'+cls+' off">'+d+'</span>';}
var isToday=(ds===TODAY), hasTr=!!tset[ds];
var cls='cal-d'+(isToday?' tdy':'');
if(hasTr||isToday){ // 거래일=밝게(on) / 오늘은 거래없어도 클릭가능(today-open)
cls+=hasTr?' on':' today-open';
h+='<a class="'+cls+(ds===SEL?' sel':'')+'" data-d="'+ds+'" href="/?t=lab&s=day&d='+ds+'">'+d+'</a>';
} else { h+='<span class="'+cls+' off">'+d+'</span>'; }
}
h+='</div>';
box.innerHTML=h;
@@ -1404,7 +1510,7 @@ _DAY_CAL_JS = """<script>(function(){
cur=ny+'-'+pad(nm); render();
});
});
box.querySelectorAll('.cal-d.on').forEach(function(a){
box.querySelectorAll('.cal-d[data-d]').forEach(function(a){
a.addEventListener('click',function(e){ e.preventDefault(); pick(a.getAttribute('data-d')); });
});
}
@@ -1468,19 +1574,18 @@ def _day_trades_panel(sel_date: str | None = None) -> str:
if d:
all_rows.append((d, v['id'], r))
dates = sorted({d for d, _, _ in all_rows}, reverse=True) # 최신순
dates = sorted({d for d, _, _ in all_rows}, reverse=True) # 거래일만, 최신순
today = datetime.now(config.KST).strftime('%Y-%m-%d')
selectable = set(dates) | {today} # 오늘은 거래 없어도 선택 가능
back = '<label for="sub-cmp" class="cmp-btlink" role="button" tabindex="0">← 비교로</label>'
if not dates:
return (f'<h2 class="cmp-title">📋 거래목록 {back}</h2>'
'<div class="empty">아직 거래 기록이 없어요 — 스캔이 진입 조건을 기다리는 중</div>')
if sel_date not in dates:
sel_date = dates[0] # 기본 = 최근 거래일
if sel_date not in selectable:
sel_date = today # 기본 = 오늘 (거래 없으면 '이 날은 거래 없어요' 표시)
# 달력형 날짜 선택 — 거래일만 밝게·클릭가능, 거래 없는 날은 어둡게. 월 이동은 JS(새로고침 X).
is_today = sel_date == datetime.now(config.KST).strftime('%Y-%m-%d')
is_today = sel_date == today
cal_js = (_DAY_CAL_JS.replace('__DATES__', json.dumps(dates))
.replace('__SEL__', sel_date)
.replace('__TODAY__', datetime.now(config.KST).strftime('%Y-%m-%d')))
.replace('__TODAY__', today))
picker = (f'<details class="day-cal-wrap" id="day-cal-wrap"><summary>'
f'📅 <b id="day-sel-label">{sel_date}{" (오늘)" if is_today else ""}</b> '
f'<span class="muted">▾ 날짜 선택</span>'
@@ -1513,7 +1618,9 @@ def _day_trades_panel(sel_date: str | None = None) -> str:
a['n'] = len(a['vids'])
if not agg:
return head + '<div class="empty">이 날은 거래가 없어요</div>'
# 빈 날도 #day-sub·#day-rows 컨테이너 유지 — 다른 날로 fetch 전환 시 갱신되게
return (head + '<div class="sub" id="day-sub">이 날은 거래가 없어요</div>'
+ '<div id="day-rows"><div class="empty">이 날은 거래가 없어요 — 스캔이 진입 조건을 기다렸어요</div></div>')
# (종목, 방향) 단위로 한 아이템 — 같은 종목에 매수·매도 동시면 2개 아이템으로 분리.
# 정렬: 매도(청산) 먼저, 그다음 전략 수 많은 순.
@@ -1686,14 +1793,18 @@ def _trades_table(trades) -> str:
return tr_rows or '<div class="empty">거래 없음</div>'
def _decision_card(d, watch_n: int = 0, held_n: int = 0, acc_detail: str = '') -> str:
"""관심종목 탭 종목 카드 (일반·제외목록 공용). watch_n/held_n = 이 종목을 관심/보유 중인 계좌 수.
acc_detail 이 있으면 👁🛒 클릭 시 '어느 전략이 보는지' 목록 토스트."""
def _decision_card(d, watch_n: int = 0, held_n: int = 0, acc_detail: str = '', buy_n: int = 0) -> str:
"""관심종목 탭 종목 카드 (일반·제외목록 공용). watch_n=관심(대기+매수준비) 총합, buy_n=매수준비중, held_n=보유.
acc_detail 이 있으면 배지 클릭 시 '어느 전략이 보는지' 목록 토스트."""
chips = _chips(d.get('checks', []))
src = '/'.join(ORIGIN_LABEL.get(s, s) for s in d.get('sources', []))
acc = ''
wait_n = max(0, watch_n - buy_n) # 대기중 = 관심 총합 − 매수준비중
if watch_n or held_n:
parts = ([f'👁{watch_n}'] if watch_n else []) + ([f'🛒{held_n}'] if held_n else [])
# 🟢 매수준비중 · 🟡 대기중 · 🛒 보유 — 0인 항목은 생략
parts = ([f'<span style="color:#22c55e">🟢{buy_n}</span>'] if buy_n else []) \
+ ([f'<span style="color:#eab308">🟡{wait_n}</span>'] if wait_n else []) \
+ ([f'🛒{held_n}'] if held_n else [])
attrs = (f' data-title="{_esc(d.get("name"))} — 보는 전략" data-detail="{_esc(acc_detail)}"'
if acc_detail else '')
acc = f' <span class="muted acc-n"{attrs}>{" ".join(parts)}</span>'
@@ -1706,7 +1817,7 @@ def _decision_card(d, watch_n: int = 0, held_n: int = 0, acc_detail: str = '') -
f'<button type="submit" title="관찰목록에서 삭제">✕</button></form>')
return (
f'<div class="card"><div class="card-h">'
f'<b>{_esc(d.get("name"))}</b> <span class="muted">{_esc(d.get("code"))} · {_esc(src)}</span>{acc}'
f'<b>{_esc(d.get("name"))}</b> <span class="muted">{_esc(src)}</span>{acc}'
f'<span class="price">{price_h}{pnl_h}</span>{del_btn}</div>'
f'<div class="reason">{_esc(d.get("reason",""))}</div>'
f'<div class="chks">{chips}</div></div>'
@@ -1792,7 +1903,9 @@ def render_html(view: str | None = None, day_date: str | None = None,
if ws:
if lines:
lines.append('')
lines.append(f'👁 관심 {len(ws)}개 전략:')
_nb = sum(1 for _, st, _ in ws if st in ('BUY', 'ADD'))
_nw = len(ws) - _nb
lines.append(f'관심 {len(ws)}개 — 🟢매수준비 {_nb} · 🟡대기 {_nw}:')
for vid, st, rsn in ws:
em = '🟢' if st in ('BUY', 'ADD') else '🟡'
rk = rank_map.get(vid)
@@ -1838,7 +1951,7 @@ def render_html(view: str | None = None, day_date: str | None = None,
judge_html += f'<h3 class="grp"><span class="badge" style="background:{color}">{label}</span> {len(items)}</h3>'
for d in items:
judge_html += _decision_card(d, watch_n.get(d.get('code'), 0), held_n.get(d.get('code'), 0),
_acc_detail(d.get('code')))
_acc_detail(d.get('code')), buy_n.get(d.get('code'), 0))
# 관찰목록 중 아직 스캔 안 된(방금 추가 등) 종목 → '스캔 대기'로 즉시 표시
wl = umod._load_watchlist()
@@ -1854,7 +1967,7 @@ def render_html(view: str | None = None, day_date: str | None = None,
f'<button type="submit" title="관찰목록에서 삭제">✕</button></form>')
judge_html += (
f'<div class="card"><div class="card-h">'
f'<b>{_esc(e.get("name") or code)}</b> <span class="muted">{_esc(code)} · {_esc(src)}</span>'
f'<b>{_esc(e.get("name") or code)}</b> <span class="muted">{_esc(src)}</span>'
f'<span class="price muted">다음 스캔 대기</span>{del_btn}</div></div>'
)
@@ -1866,7 +1979,7 @@ def render_html(view: str | None = None, day_date: str | None = None,
f'<span class="bt-type-d" style="display:inline;margin-left:6px">스캔은 하되 매수 안 함</span></summary>'
f'<div class="body">' + ''.join(
_decision_card(d, watch_n.get(d.get('code'), 0), held_n.get(d.get('code'), 0),
_acc_detail(d.get('code')))
_acc_detail(d.get('code')), buy_n.get(d.get('code'), 0))
for d in excluded) + '</div></details>'
)
@@ -1879,8 +1992,10 @@ def render_html(view: str | None = None, day_date: str | None = None,
_sel = ' selected' if view == vv['id'] else ''
_fav = f'{icon_of[vv["id"]]} ' if vv['id'] in icon_of else ''
_vp = vv.get('params') or {}
_rx = ' 완화' if (not int(_vp.get('TREND_SLOPE_GATE', 1)) or not int(_vp.get('MID_ARRAY_GATE', 1))) else ''
_im = ' 즉시' if int(_vp.get('IMMEDIATE_ENTRY', 0)) else ''
opts += (f'<option value="{_esc(vv["id"])}"{_sel}>{_fav}{_rk(vv["id"])}{_vid_disp(vv["id"])} '
f'[{_BEAR_WORD[_bear_mode(_vp)]}] · {_esc(_market_fit_words(_vp))}</option>')
f'[{_BEAR_WORD[_bear_mode(_vp)]}{_rx}{_im}] · {_esc(_cat_name(_vp))}</option>')
view_sel = (f'<div class="sub" style="margin:4px 0 8px">체크리스트 시각: '
f'<select id="judge-view" '
f'onchange="location.href=\'/?t=watch&view=\'+encodeURIComponent(this.value)">{opts}</select></div>')
@@ -1935,6 +2050,7 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
.chks{{display:flex;flex-wrap:wrap;gap:5px}}
.chk{{background:#0f151c;border:1px solid #1f2937;border-radius:6px;padding:2px 7px;font-size:11px}}
.chk .cd{{color:#6b7280;margin-left:5px}}
.acc-n{{font-size:9px}}
.acc-n[data-detail]{{cursor:pointer;text-decoration:underline dotted #475569;text-underline-offset:3px}}
#chk-toast .ct-d{{max-height:55vh;overflow-y:auto}}
.chk-i{{background:none;border:none;color:#60a5fa;font-size:11px;cursor:pointer;padding:0 0 0 4px;line-height:1;vertical-align:baseline}}
@@ -1976,6 +2092,7 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
.cal-wd{{text-align:center;font-size:9px;color:#64748b;padding:1px 0}}
.cal-d{{position:relative;text-align:center;padding:5px 0;border-radius:5px;font-size:11px;font-variant-numeric:tabular-nums}}
.cal-d.on{{background:#0f2a3f;color:#7dd3fc;border:1px solid #1e3a5f;font-weight:700;text-decoration:none;cursor:pointer}}
.cal-d.today-open{{color:#7dd3fc;border:1px dashed #334155;text-decoration:none;cursor:pointer}}
.cal-d.off{{color:#2c3848}}
.cal-d.sel{{background:#2563eb;color:#fff;border-color:#60a5fa;box-shadow:0 0 0 1px #60a5fa}}
.cal-d.tdy::after{{content:'';position:absolute;bottom:2px;left:50%;transform:translateX(-50%);width:3px;height:3px;border-radius:50%;background:#7dd3fc}}
@@ -1996,6 +2113,8 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
.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-sortbar{{display:flex;gap:6px;margin:0 0 10px}}
.cmp-sortbar button{{flex:1;background:#141b24;border:1px solid #1f2937;border-radius:8px;color:#9ca3af;font-size:11px;font-weight:600;padding:7px 4px;cursor:pointer}}
.cmp-sortbar button.active{{background:#15406e;color:#fff;border-color:#2952cc}}
@@ -2100,7 +2219,7 @@ input[name="tab"],input[name="subtab"]{{display:none}}
<input type="text" id="wq" placeholder="종목명 또는 6자리 코드 검색" aria-label="종목 검색" autocomplete="off">
<button type="button" id="wq-btn" class="btn-addcmp">검색</button></div>
<div id="wq-results" class="wsearch-results hidden"></div>
<div class="sub">상태 그룹은 전체 전략(메인+변이) 합산 — 한 전략이라도 보유/매수대기면 그 그룹으로 · 체크리스트는 기준전략(M) 시각 · 👁관심 🛒보유 전략 수</div>
<div class="sub">상태 그룹은 전체 전략(메인+변이) 합산 — 한 전략이라도 보유/매수대기면 그 그룹으로 · 배지: 🟢매수준비 · 🟡대기 · 🛒보유 전략 수 (탭하면 어떤 전략인지)</div>
<div class="sub">종목명 일부만 입력해도 후보가 떠요 · 후보를 누르면 추가 · 자산(비하이브 워치·관심·보유)은 자동 편입 · 삭제는 카드의 ✕ (수동만) · 보유 중이면 삭제해도 청산까지 추적</div>
{view_sel}
{judge_html}
+2 -2
View File
@@ -35,9 +35,9 @@ def _eval_combo(args):
# 백테스트에서 실제 영향 있는 파라미터만 스윕 (애널 게이트·시장필터는 backtest 중립이라 제외).
# 기존 5 + 신규 4(RSI·시간손절·리스크) 탐색. 조합폭발 억제 위해 기존 그리드는 핵심값으로 축소.
# base 3×3×2×2×2 = 72 × 신규 2×2×2×2 = 16 → 1152 조합.
# base 4×3×2×2×2 = 96 × 신규 2×2×2×2 = 16 → 1536 조합. (2026-06-15 SMA_LONG 60 추가)
GRID = {
'SMA_LONG': [10, 20, 40], # 추세 속도 — 빠른/느린 (다양성 핵심)
'SMA_LONG': [10, 20, 60], # 추세선 길이 — 10/20/60 (2026-06-15 40 제거: 어중간·관리자님 지시. 60은 중기선과 겹침)
'RR_RATIO': [1.5, 2.0, 3.0], # 손익비
'STOP_ATR_MULT': [1.5, 2.0], # 손절 폭
'PULLBACK_ATR_MULT': [1.0, 1.5], # 눌림 깊이
+102
View File
@@ -151,6 +151,108 @@ def remove(code: str):
_save_watchlist(wl)
# ── 자동편입(auto) — 키움 순위정보로 주도주를 매일 후보에 편입 ──────────────
# 소스: ka90009 외인·기관 순매수(수급 좋은 종목, 우리 전략 수급게이트와 궁합) + ka10023 거래량급증(관심 폭발).
# 규칙: ETF/ETN·급증률 이상치·하락 제외, 각 소스 상위 N, 총 상한. 자동제외는 origin=auto·미보유·N거래일 무신호만.
AUTO_TOP_N = 15 # 각 소스 상위 N 편입
AUTO_TOTAL_CAP = 120 # 관찰목록 총 상한 (auto 편입은 이 한도 내에서만)
AUTO_SURGE_CAP = 1000.0 # 거래량 급증률 이 % 초과는 노이즈로 제외
AUTO_PRUNE_DAYS = 10 # auto 종목이 이 거래일 수 동안 무신호면 자동 제외 (보유·수동 등은 제외 안 함)
def _held_codes() -> set:
try:
held = kc.get_positions_all(labels=config.OWNER_ACCOUNT_LABELS)
return {(p.get('code') or '').strip().zfill(6)
for poss in held.values() for p in poss if p.get('qty', 0) > 0}
except Exception:
return set()
def auto_import() -> dict:
"""키움 순위정보로 주도주 자동 편입(origin='auto'). 총 상한 내에서만. 결과 요약 반환."""
cands: dict[str, str] = {} # code -> name (중복 자동 dedup)
def take(rows, key_name='name', n=AUTO_TOP_N):
c = 0
for x in rows:
if c >= n:
break
code = (x.get('code') or '').strip().zfill(6)
nm = x.get(key_name, '')
if not code or is_etf(nm) or code in cands:
continue
cands[code] = nm
c += 1
try:
nb = kc.get_foreign_inst_netbuy()
take([x for x in nb['foreign'] if x['amt'] > 0]) # 외인 순매수
take([x for x in nb['inst'] if x['amt'] > 0]) # 기관 순매수
except Exception as e:
sys.stderr.write(f'[universe] 외인기관 순매수 조회 실패: {e}\n')
try:
vs = kc.get_volume_surge()
take([x for x in vs if x['flu_rt'] > 0 and 0 < x['surge_rt'] <= AUTO_SURGE_CAP]) # 거래량 급증
except Exception as e:
sys.stderr.write(f'[universe] 거래량급증 조회 실패: {e}\n')
wl = _load_watchlist()
now = datetime.now(config.KST).isoformat()
added = 0
for code, nm in cands.items():
if code in wl:
continue
if len(wl) >= AUTO_TOTAL_CAP: # 상한 도달 시 신규 auto 편입 중단
break
wl[code] = {'name': nm, 'origin': 'auto', 'added_at': now}
added += 1
if added:
_save_watchlist(wl)
return {'candidates': len(cands), 'added': added, 'total': len(wl)}
def auto_prune(no_signal_days: int = AUTO_PRUNE_DAYS) -> dict:
"""origin='auto'·미보유 종목 중 최근 no_signal_days 거래일 동안 한 번도 매수신호(BUY/ADD/WAIT-watch) 없던 것 제거.
보유·수동·비하이브(watch/interest/held/manual)는 절대 건드리지 않는다."""
wl = _load_watchlist()
auto_codes = {c for c, e in wl.items() if e.get('origin') == 'auto'}
if not auto_codes:
return {'pruned': 0, 'total': len(wl)}
held = _held_codes()
# 최근 신호 추적: state/sim/auto_seen.json {code: last_signal_iso}. 매 import 시 갱신.
seen = _load_json(config.STATE_DIR / 'auto_seen.json', {})
now = datetime.now(config.KST)
pruned = 0
for code in list(auto_codes):
if code in held:
continue
last = seen.get(code)
ent_added = wl[code].get('added_at', '')
ref = last or ent_added
try:
days = (now - datetime.fromisoformat(ref)).days
except Exception:
days = 0
if days >= no_signal_days:
del wl[code]
pruned += 1
if pruned:
_save_watchlist(wl)
return {'pruned': pruned, 'total': len(wl)}
def mark_signals(codes_with_signal: list[str]):
"""이번 스캔에서 매수신호(BUY/ADD/대기) 든 종목의 '마지막 신호 시각' 기록 — auto_prune 판단용."""
path = config.STATE_DIR / 'auto_seen.json'
seen = _load_json(path, {})
now = datetime.now(config.KST).isoformat()
for c in codes_with_signal:
seen[(c or '').strip().zfill(6)] = now
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(seen, ensure_ascii=False))
def build_universe() -> list[dict]:
"""누적 관찰목록을 동기화 후 [{code, name, sources:[origin]}]로 반환. code 6자리 정규화."""
wl = sync_watchlist()
@@ -30,3 +30,5 @@
{"date": "2026-06-11", "market": "KOSPI", "market_label": "코스피", "rise": 576, "upper": 3, "fall": 320, "lower": 0, "steady": 25, "adr": 180.94, "personal": 20803, "foreign": -14640, "institutional": -7561, "captured_at": "2026-06-11T21:00:01.845336+09:00"}
{"date": "2026-06-12", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1318, "upper": 9, "fall": 371, "lower": 0, "steady": 47, "adr": 357.68, "personal": -9416, "foreign": 2769, "institutional": 6315, "captured_at": "2026-06-12T21:00:03.422609+09:00"}
{"date": "2026-06-12", "market": "KOSPI", "market_label": "코스피", "rise": 756, "upper": 3, "fall": 144, "lower": 0, "steady": 18, "adr": 527.08, "personal": -43174, "foreign": 22041, "institutional": 22870, "captured_at": "2026-06-12T21:00:03.422609+09:00"}
{"date": "2026-06-15", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1010, "upper": 10, "fall": 662, "lower": 0, "steady": 62, "adr": 154.08, "personal": 6162, "foreign": -8164, "institutional": 2166, "captured_at": "2026-06-15T21:00:02.416077+09:00"}
{"date": "2026-06-15", "market": "KOSPI", "market_label": "코스피", "rise": 676, "upper": 3, "fall": 207, "lower": 0, "steady": 35, "adr": 328.02, "personal": -15124, "foreign": 10774, "institutional": 4462, "captured_at": "2026-06-15T21:00:02.416077+09:00"}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943184+09:00",
"updated_at": "2026-06-12T15:25:02.233392+09:00"
"updated_at": "2026-06-15T15:25:02.113024+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943289+09:00",
"updated_at": "2026-06-12T15:25:03.011659+09:00"
"updated_at": "2026-06-15T15:25:02.148135+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944222+09:00",
"updated_at": "2026-06-12T15:25:03.143491+09:00"
"updated_at": "2026-06-15T15:25:02.276302+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944232+09:00",
"updated_at": "2026-06-12T15:25:03.144663+09:00"
"updated_at": "2026-06-15T15:25:02.277525+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944241+09:00",
"updated_at": "2026-06-12T15:25:03.145850+09:00"
"updated_at": "2026-06-15T15:25:02.278741+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944251+09:00",
"updated_at": "2026-06-12T15:25:03.147039+09:00"
"updated_at": "2026-06-15T15:25:02.279953+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944261+09:00",
"updated_at": "2026-06-12T15:25:03.148171+09:00"
"updated_at": "2026-06-15T15:25:02.281173+09:00"
}
@@ -18,7 +18,7 @@
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 233,744 도달",
"cur_price": 247500,
"cur_price": 248750,
"tranches": 2,
"tranche_value": 3332473.3402132783,
"last_add_price": 244500
@@ -31,5 +31,5 @@
"max_drawdown": 0.005230139292375102,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944270+09:00",
"updated_at": "2026-06-12T15:25:03.149359+09:00"
"updated_at": "2026-06-15T15:25:02.282444+09:00"
}
@@ -18,7 +18,7 @@
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 232,280 도달",
"cur_price": 247500,
"cur_price": 248750,
"tranches": 2,
"tranche_value": 3741721.8543046354,
"last_add_price": 238500
@@ -31,5 +31,5 @@
"max_drawdown": 0.0059876134567287895,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944280+09:00",
"updated_at": "2026-06-12T15:25:03.150563+09:00"
"updated_at": "2026-06-15T15:25:02.283725+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944290+09:00",
"updated_at": "2026-06-12T15:25:03.151726+09:00"
"updated_at": "2026-06-15T15:25:02.284988+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944300+09:00",
"updated_at": "2026-06-12T15:25:03.152868+09:00"
"updated_at": "2026-06-15T15:25:02.286210+09:00"
}
@@ -8,6 +8,6 @@
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943320+09:00",
"updated_at": "2026-06-12T15:25:03.024987+09:00"
"created_at": "2026-06-15T14:55:00.582443+09:00",
"updated_at": "2026-06-15T15:25:02.287442+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943299+09:00",
"updated_at": "2026-06-12T15:25:03.014412+09:00"
"updated_at": "2026-06-15T15:25:02.150231+09:00"
}
@@ -8,6 +8,6 @@
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943331+09:00",
"updated_at": "2026-06-12T15:25:03.027412+09:00"
"created_at": "2026-06-15T14:55:00.582452+09:00",
"updated_at": "2026-06-15T15:25:02.288736+09:00"
}
@@ -8,6 +8,6 @@
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943342+09:00",
"updated_at": "2026-06-12T15:25:03.029560+09:00"
"created_at": "2026-06-15T14:55:00.582461+09:00",
"updated_at": "2026-06-15T15:25:02.290034+09:00"
}
@@ -8,6 +8,6 @@
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943352+09:00",
"updated_at": "2026-06-12T15:25:03.031721+09:00"
"created_at": "2026-06-15T14:55:00.582469+09:00",
"updated_at": "2026-06-15T15:25:02.291322+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582478+09:00",
"updated_at": "2026-06-15T15:25:02.292542+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582490+09:00",
"updated_at": "2026-06-15T15:25:02.293831+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582499+09:00",
"updated_at": "2026-06-15T15:25:02.295144+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582507+09:00",
"updated_at": "2026-06-15T15:25:02.296435+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582515+09:00",
"updated_at": "2026-06-15T15:25:02.297662+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582546+09:00",
"updated_at": "2026-06-15T15:25:02.298875+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582554+09:00",
"updated_at": "2026-06-15T15:25:02.300087+09:00"
}
@@ -9,5 +9,5 @@
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943310+09:00",
"updated_at": "2026-06-12T15:25:03.016933+09:00"
"updated_at": "2026-06-15T15:25:02.152146+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582561+09:00",
"updated_at": "2026-06-15T15:25:02.301307+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582647+09:00",
"updated_at": "2026-06-15T15:25:02.315746+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582670+09:00",
"updated_at": "2026-06-15T15:25:02.319615+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582678+09:00",
"updated_at": "2026-06-15T15:25:02.320890+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582685+09:00",
"updated_at": "2026-06-15T15:25:02.322172+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582693+09:00",
"updated_at": "2026-06-15T15:25:02.323394+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582701+09:00",
"updated_at": "2026-06-15T15:25:02.324728+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582708+09:00",
"updated_at": "2026-06-15T15:25:02.326019+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582717+09:00",
"updated_at": "2026-06-15T15:25:02.327240+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582725+09:00",
"updated_at": "2026-06-15T15:25:02.328455+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582733+09:00",
"updated_at": "2026-06-15T15:25:02.329672+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582740+09:00",
"updated_at": "2026-06-15T15:25:02.330947+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582748+09:00",
"updated_at": "2026-06-15T15:25:02.332191+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582755+09:00",
"updated_at": "2026-06-15T15:25:02.333422+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582763+09:00",
"updated_at": "2026-06-15T15:25:02.334642+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582771+09:00",
"updated_at": "2026-06-15T15:25:02.335934+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582779+09:00",
"updated_at": "2026-06-15T15:25:02.337220+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582787+09:00",
"updated_at": "2026-06-15T15:25:02.338496+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582795+09:00",
"updated_at": "2026-06-15T15:25:02.339790+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582803+09:00",
"updated_at": "2026-06-15T15:25:02.341070+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582811+09:00",
"updated_at": "2026-06-15T15:25:02.342372+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582819+09:00",
"updated_at": "2026-06-15T15:25:02.343597+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582826+09:00",
"updated_at": "2026-06-15T15:25:02.344810+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582834+09:00",
"updated_at": "2026-06-15T15:25:02.346088+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582841+09:00",
"updated_at": "2026-06-15T15:25:02.347368+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582849+09:00",
"updated_at": "2026-06-15T15:25:02.348667+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582857+09:00",
"updated_at": "2026-06-15T15:25:02.349953+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582864+09:00",
"updated_at": "2026-06-15T15:25:02.351247+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582872+09:00",
"updated_at": "2026-06-15T15:25:02.352529+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582879+09:00",
"updated_at": "2026-06-15T15:25:02.353755+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582887+09:00",
"updated_at": "2026-06-15T15:25:02.354966+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582894+09:00",
"updated_at": "2026-06-15T15:25:02.356166+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582902+09:00",
"updated_at": "2026-06-15T15:25:02.357379+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582910+09:00",
"updated_at": "2026-06-15T15:25:02.358617+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582918+09:00",
"updated_at": "2026-06-15T15:25:02.359838+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582926+09:00",
"updated_at": "2026-06-15T15:25:02.361064+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.582933+09:00",
"updated_at": "2026-06-15T15:25:02.362282+09:00"
}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943361+09:00",
"updated_at": "2026-06-12T15:25:03.033694+09:00"
}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943371+09:00",
"updated_at": "2026-06-12T15:25:03.035650+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583136+09:00",
"updated_at": "2026-06-15T15:25:02.390047+09:00"
}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943381+09:00",
"updated_at": "2026-06-12T15:25:03.037555+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583144+09:00",
"updated_at": "2026-06-15T15:25:02.391331+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583184+09:00",
"updated_at": "2026-06-15T15:25:02.397768+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583191+09:00",
"updated_at": "2026-06-15T15:25:02.398982+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583198+09:00",
"updated_at": "2026-06-15T15:25:02.400251+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583205+09:00",
"updated_at": "2026-06-15T15:25:02.401542+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583212+09:00",
"updated_at": "2026-06-15T15:25:02.402814+09:00"
}
@@ -18,7 +18,7 @@
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 233,744 도달",
"cur_price": 247500,
"cur_price": 248750,
"tranches": 2,
"tranche_value": 6250000.0,
"last_add_price": 244500
@@ -31,5 +31,5 @@
"max_drawdown": 0.009821865591495171,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943203+09:00",
"updated_at": "2026-06-12T15:25:02.979059+09:00"
"updated_at": "2026-06-15T15:25:02.126478+09:00"
}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943391+09:00",
"updated_at": "2026-06-12T15:25:03.039344+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583220+09:00",
"updated_at": "2026-06-15T15:25:02.404094+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583227+09:00",
"updated_at": "2026-06-15T15:25:02.405315+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583234+09:00",
"updated_at": "2026-06-15T15:25:02.406536+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583241+09:00",
"updated_at": "2026-06-15T15:25:02.407826+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583249+09:00",
"updated_at": "2026-06-15T15:25:02.409101+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583273+09:00",
"updated_at": "2026-06-15T15:25:02.410386+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583281+09:00",
"updated_at": "2026-06-15T15:25:02.411696+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583289+09:00",
"updated_at": "2026-06-15T15:25:02.412998+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583297+09:00",
"updated_at": "2026-06-15T15:25:02.414287+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583304+09:00",
"updated_at": "2026-06-15T15:25:02.415576+09:00"
}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943400+09:00",
"updated_at": "2026-06-12T15:25:03.041123+09:00"
}
@@ -0,0 +1,13 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-15T14:55:00.583311+09:00",
"updated_at": "2026-06-15T15:25:02.416804+09:00"
}

Some files were not shown because too many files have changed in this diff Show More