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>
This commit is contained in:
hyowons
2026-06-15 19:03:35 +09:00
parent e3c54d62ed
commit a5e52dc69b
7 changed files with 329 additions and 3 deletions
@@ -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 전체 상장종목 리스트 캐싱. 페이지네이션 처리.