diff --git a/app/ai/status.py b/app/ai/status.py new file mode 100644 index 0000000..43c532b --- /dev/null +++ b/app/ai/status.py @@ -0,0 +1,131 @@ +""" +app/ai/status.py +현재 수익 현황 조회 — 실현손익 + 미실현손익 합산 +""" +import asyncio +import os +import sys +from datetime import datetime +from pathlib import Path + +ROOT = Path(__file__).parent.parent.parent +sys.path.insert(0, str(ROOT)) + +def load_env(): + env_path = ROOT / ".env" + if not env_path.exists(): + return + for line in env_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + k, v = k.strip(), 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() + +from app.db.models import get_conn +from app.execution.kis_client import KISClient + + +async def get_status(): + today = datetime.now().strftime("%Y-%m-%d") + now_str = datetime.now().strftime("%H:%M:%S") + + # ── 실현손익 (오늘 종료된 거래) ── + with get_conn() as conn: + rows = conn.execute(""" + SELECT ticker, name, entry_price, exit_price, quantity, pnl, exit_reason + FROM trades + WHERE date=? AND exit_time IS NOT NULL + ORDER BY exit_time DESC + """, (today,)).fetchall() + + open_rows = conn.execute(""" + SELECT ticker, name, entry_price, quantity + FROM positions + """).fetchall() + + realized_trades = rows + realized_pnl = sum(r[5] for r in rows if r[5] is not None) + wins = sum(1 for r in rows if r[5] and r[5] > 0) + losses = sum(1 for r in rows if r[5] and r[5] <= 0) + + # ── 미실현손익 (현재 포지션) ── + unrealized_pnl = 0.0 + position_details = [] + + if open_rows: + kis = KISClient() + await kis.ensure_token() + for ticker, name, entry_price, qty in open_rows: + try: + info = await kis.get_price(ticker) + current = info.get("current", 0) + if entry_price and current: + unreal = (current - entry_price) * qty + pct = (current - entry_price) / entry_price * 100 + unrealized_pnl += unreal + position_details.append({ + "ticker": ticker, "name": name, + "entry": entry_price, "current": current, + "qty": qty, "pnl": unreal, "pct": pct, + }) + except Exception as e: + position_details.append({ + "ticker": ticker, "name": name, + "entry": entry_price, "current": 0, + "qty": qty, "pnl": 0.0, "pct": 0.0, + "error": str(e), + }) + await asyncio.sleep(0.5) + + total_pnl = realized_pnl + unrealized_pnl + + # ── 출력 ── + print(f"\n{'='*50}") + print(f" StockBot 현황 [{today} {now_str}]") + print(f"{'='*50}") + + # 총 손익 + sign = "+" if total_pnl >= 0 else "" + print(f"\n 총 손익: {sign}{total_pnl:,.0f}원") + print(f" 실현손익: {'+' if realized_pnl >= 0 else ''}{realized_pnl:,.0f}원 ({wins}승 {losses}패)") + print(f" 미실현손익: {'+' if unrealized_pnl >= 0 else ''}{unrealized_pnl:,.0f}원 (보유 {len(position_details)}종목)") + + # 보유 포지션 + if position_details: + print(f"\n [보유 포지션]") + for p in position_details: + if "error" in p: + print(f" {p['name']}({p['ticker']}) {p['qty']}주 — 현재가 조회 실패") + else: + sign = "+" if p['pnl'] >= 0 else "" + print(f" {p['name']}({p['ticker']}) {p['qty']}주 | " + f"매수 {p['entry']:,.0f} → 현재 {p['current']:,.0f} | " + f"{sign}{p['pnl']:,.0f}원 ({sign}{p['pct']:.2f}%)") + + # 오늘 거래 내역 + if realized_trades: + print(f"\n [오늘 체결]") + for r in realized_trades[:10]: + ticker, name, ep, xp, qty, pnl, reason = r + pnl_str = f"{'+' if pnl and pnl >= 0 else ''}{pnl:,.0f}원" if pnl else "0원" + print(f" {name}({ticker}) {qty}주 | {ep:,.0f}→{xp:,.0f} | {pnl_str} [{reason}]") + if len(realized_trades) > 10: + print(f" ... 외 {len(realized_trades)-10}건") + else: + print(f"\n 오늘 체결 내역 없음") + + print(f"\n{'='*50}\n") + + return {"total_pnl": total_pnl, "realized": realized_pnl, "unrealized": unrealized_pnl} + + +if __name__ == "__main__": + asyncio.run(get_status())