diff --git a/stock-lab/app/db.py b/stock-lab/app/db.py index 7411eba..9cadc33 100644 --- a/stock-lab/app/db.py +++ b/stock-lab/app/db.py @@ -46,6 +46,15 @@ def init_db(): ) """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS broker_cash ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + broker TEXT UNIQUE NOT NULL, + cash INTEGER NOT NULL DEFAULT 0, + updated_at TEXT DEFAULT (datetime('now','localtime')) + ) + """) + def save_articles(articles: List[Dict[str, str]]) -> int: count = 0 with _conn() as conn: @@ -120,3 +129,33 @@ def delete_portfolio_item(item_id: int) -> bool: with _conn() as conn: cur = conn.execute("DELETE FROM portfolio WHERE id = ?", (item_id,)) return cur.rowcount > 0 + + +# --- Broker Cash CRUD --- + +def upsert_broker_cash(broker: str, cash: int) -> None: + now = __import__("datetime").datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with _conn() as conn: + conn.execute(""" + INSERT INTO broker_cash (broker, cash, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(broker) DO UPDATE SET cash = excluded.cash, updated_at = excluded.updated_at + """, (broker, cash, now)) + + +def get_all_broker_cash() -> List[Dict[str, Any]]: + with _conn() as conn: + rows = conn.execute("SELECT * FROM broker_cash ORDER BY broker").fetchall() + return [dict(r) for r in rows] + + +def get_broker_cash(broker: str) -> Dict[str, Any] | None: + with _conn() as conn: + row = conn.execute("SELECT * FROM broker_cash WHERE broker = ?", (broker,)).fetchone() + return dict(row) if row else None + + +def delete_broker_cash(broker: str) -> bool: + with _conn() as conn: + cur = conn.execute("DELETE FROM broker_cash WHERE broker = ?", (broker,)) + return cur.rowcount > 0 diff --git a/stock-lab/app/main.py b/stock-lab/app/main.py index 442278f..e1ff57d 100644 --- a/stock-lab/app/main.py +++ b/stock-lab/app/main.py @@ -12,6 +12,7 @@ from .db import ( init_db, save_articles, get_latest_articles, add_portfolio_item, get_all_portfolio, get_portfolio_item, update_portfolio_item, delete_portfolio_item, + upsert_broker_cash, get_all_broker_cash, get_broker_cash, delete_broker_cash, ) from .scraper import fetch_market_news, fetch_major_indices, fetch_overseas_news from .price_fetcher import get_current_prices @@ -155,10 +156,24 @@ class PortfolioUpdateRequest(BaseModel): @app.get("/api/portfolio") def get_portfolio(): - """전체 포트폴리오 조회 (현재가 + 손익 포함)""" + """전체 포트폴리오 조회 (현재가 + 손익 + 예수금 포함)""" items = get_all_portfolio() + cash_rows = get_all_broker_cash() + total_cash = sum(r["cash"] for r in cash_rows) + if not items: - return {"holdings": [], "summary": {"total_buy": 0, "total_eval": 0, "total_profit": 0, "total_profit_rate": 0.0}} + return { + "holdings": [], + "cash": cash_rows, + "summary": { + "total_buy": 0, + "total_eval": 0, + "total_profit": 0, + "total_profit_rate": 0.0, + "total_cash": total_cash, + "total_assets": total_cash, + }, + } tickers = list({item["ticker"] for item in items}) prices = get_current_prices(tickers) @@ -196,11 +211,14 @@ def get_portfolio(): return { "holdings": holdings, + "cash": cash_rows, "summary": { "total_buy": total_buy, "total_eval": total_eval, "total_profit": total_profit, "total_profit_rate": total_profit_rate, + "total_cash": total_cash, + "total_assets": total_eval + total_cash, }, } @@ -229,3 +247,31 @@ def delete_portfolio(item_id: int): return {"ok": True} +# --- Broker Cash API --- + +class BrokerCashRequest(BaseModel): + broker: str + cash: int + + +@app.get("/api/portfolio/cash") +def list_broker_cash(): + """증권사별 예수금 전체 조회""" + return get_all_broker_cash() + + +@app.put("/api/portfolio/cash") +def set_broker_cash(req: BrokerCashRequest): + """증권사 예수금 등록 또는 수정 (upsert)""" + upsert_broker_cash(req.broker, req.cash) + return {"ok": True, "broker": req.broker, "cash": req.cash} + + +@app.delete("/api/portfolio/cash/{broker}") +def remove_broker_cash(broker: str): + """증권사 예수금 삭제""" + if not delete_broker_cash(broker): + return JSONResponse(status_code=404, content={"error": "Broker not found"}) + return {"ok": True} + +