Files
Stock-trading-programming/app/ai/midday.py
T
whdwo798 edafeb7c79 [2026-05-19] 세션 분리 + L3→B안 전환 + /midday 장중 분석 추가
- L3 하드 중단 제거 → B안(연속 손절별 포지션 축소) 적용
  0회×1.0 / 1회×0.7 / 2회×0.5 / 3+회×0.3, 익절 시 한 단계 회복
- 아침·점심 세션 분리: 11:00 이후 midday_context.json 감지 시 점심 세션 자동 시작
  (12:00 고정 시작 제거 → 이벤트 기반)
- app/ai/midday.py: 장중 데이터 수집 스크립트 신규 작성
- .claude/commands/midday.md: /midday 슬래시 커맨드 신규 작성
- scripts/run_midday.ps1: 11:20 스케줄러 스크립트 신규 작성
- setup_scheduler.ps1: StockBot_Midday 태스크 추가
- CLAUDE.md: 전체 문서 업데이트

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:07:27 +09:00

230 lines
7.4 KiB
Python

"""
app/ai/midday.py
장중 데이터 수집 스크립트 (claude_midday 헬퍼)
Claude Code headless가 이 스크립트를 실행해 장중 스냅샷을 수집한 뒤,
오전 daily_context와 비교 분석해 midday_context.json을 작성한다.
실행:
python app/ai/midday.py --print
"""
import asyncio
import json
import logging
import os
import sys
from datetime import datetime
from pathlib import Path
def _load_env():
env_path = Path(".env")
if not env_path.exists():
return
with open(env_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
k = k.strip()
v = v.strip()
if " #" in v:
v = v[: v.index(" #")]
v = v.strip().strip('"').strip("'")
if k and v and k not in os.environ:
os.environ[k] = v
_load_env()
ROOT = Path(__file__).parent.parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
os.makedirs("logs", exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[
logging.StreamHandler(sys.stderr),
logging.FileHandler("logs/stockbot.log", encoding="utf-8"),
],
)
logger = logging.getLogger(__name__)
from app.execution.kis_client import KISClient
from app.db.models import get_conn
TODAY = datetime.now().strftime("%Y-%m-%d")
DAILY_CONTEXT_PATH = Path("data/daily_context.json")
MIDDAY_CONTEXT_PATH = Path("data/midday_context.json")
# ── DB 조회 ────────────────────────────────────────────────────────────────────
def get_today_trades() -> list:
"""오늘 체결된 거래 내역 (오전 결과)"""
try:
with get_conn() as conn:
rows = conn.execute(
"""SELECT ticker, name, entry_time, exit_time,
entry_price, exit_price, quantity,
exit_reason, pnl
FROM trades
WHERE date=? AND exit_time IS NOT NULL
ORDER BY exit_time""",
(TODAY,),
).fetchall()
return [
{
"ticker" : r[0],
"name" : r[1],
"entry_time" : r[2],
"exit_time" : r[3],
"entry_price": r[4],
"exit_price" : r[5],
"qty" : r[6],
"reason" : r[7],
"pnl" : r[8],
}
for r in rows
]
except Exception as e:
logger.warning(f"거래 내역 조회 실패: {e}")
return []
def get_current_positions() -> list:
"""현재 보유 포지션"""
try:
with get_conn() as conn:
rows = conn.execute(
"""SELECT ticker, name, entry_time, entry_price, quantity,
target_price, stop_price
FROM positions""",
).fetchall()
return [
{
"ticker" : r[0],
"name" : r[1],
"entry_time" : r[2],
"entry_price" : r[3],
"qty" : r[4],
"target_price": r[5],
"stop_price" : r[6],
}
for r in rows
]
except Exception as e:
logger.warning(f"포지션 조회 실패: {e}")
return []
def get_morning_context() -> dict:
"""아침 daily_context.json 로드"""
if not DAILY_CONTEXT_PATH.exists():
return {}
try:
return json.loads(DAILY_CONTEXT_PATH.read_text(encoding="utf-8"))
except Exception:
return {}
# ── KIS 시장 스냅샷 ────────────────────────────────────────────────────────────
async def fetch_market_snapshot(kis: KISClient) -> dict:
"""거래량 순위 + 업종 동향 현재 시점 스냅샷"""
data: dict = {"volume_rank": [], "sectors": []}
try:
data["volume_rank"] = await kis.get_volume_rank(top_n=20)
logger.info(f"장중 거래량 순위 {len(data['volume_rank'])}종목")
await asyncio.sleep(1.1)
except Exception as e:
logger.warning(f"거래량 순위 실패: {e}")
try:
data["sectors"] = await kis.get_sector_trend()
logger.info(f"업종 동향 {len(data['sectors'])}")
await asyncio.sleep(1.1)
except Exception as e:
logger.warning(f"업종 동향 실패: {e}")
return data
# ── 통계 계산 ──────────────────────────────────────────────────────────────────
def _calc_session_stats(trades: list) -> dict:
closed = [t for t in trades if t["pnl"] is not None]
wins = [t for t in closed if t["pnl"] > 0]
losses = [t for t in closed if t["pnl"] <= 0]
net = sum(t["pnl"] for t in closed)
# 현재 연속 손절 수 계산 (역순으로 순회)
consec = 0
for t in reversed(closed):
if t["pnl"] < 0:
consec += 1
else:
break
return {
"total" : len(closed),
"wins" : len(wins),
"losses" : len(losses),
"net_pnl" : round(net, 0),
"win_rate_pct": round(len(wins) / len(closed) * 100, 1) if closed else 0,
"consec_loss" : consec,
}
# ── 메인 ──────────────────────────────────────────────────────────────────────
async def main(print_mode: bool = False):
logger.info(f"장중 데이터 수집 시작 [{TODAY}]")
# 실거래 API로 KISClient 생성
_orig = os.environ.get("KIS_MOCK", "true")
os.environ["KIS_MOCK"] = "false"
kis = KISClient()
os.environ["KIS_MOCK"] = _orig
market: dict = {"volume_rank": [], "sectors": []}
try:
await kis.get_access_token()
market = await fetch_market_snapshot(kis)
except Exception as e:
logger.warning(f"KIS 장중 수집 실패: {e}")
trades = get_today_trades()
positions = get_current_positions()
morning = get_morning_context()
stats = _calc_session_stats(trades)
logger.info(f"오전 결과: {stats['total']}건 / 승{stats['wins']}{stats['losses']} "
f"/ 연속손절 {stats['consec_loss']}")
if print_mode:
print(json.dumps(
{
"date" : TODAY,
"generated_at" : datetime.now().strftime("%H:%M:%S"),
"morning_context" : morning,
"session_stats" : stats,
"trades" : trades,
"current_positions": positions,
"volume_rank" : market["volume_rank"],
"sectors" : market["sectors"],
},
ensure_ascii=False,
indent=2,
))
if __name__ == "__main__":
print_mode = "--print" in sys.argv
asyncio.run(main(print_mode=print_mode))