자산관리 효율 증가 api 추가

This commit is contained in:
2026-03-07 03:44:14 +09:00
parent 11423e5106
commit 483963b463
3 changed files with 146 additions and 1 deletions

View File

@@ -55,6 +55,17 @@ def init_db():
) )
""") """)
conn.execute("""
CREATE TABLE IF NOT EXISTS asset_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT UNIQUE NOT NULL,
total_eval INTEGER NOT NULL,
total_cash INTEGER NOT NULL,
total_assets INTEGER NOT NULL,
created_at TEXT DEFAULT (datetime('now','localtime'))
)
""")
def save_articles(articles: List[Dict[str, str]]) -> int: def save_articles(articles: List[Dict[str, str]]) -> int:
count = 0 count = 0
with _conn() as conn: with _conn() as conn:
@@ -159,3 +170,34 @@ def delete_broker_cash(broker: str) -> bool:
with _conn() as conn: with _conn() as conn:
cur = conn.execute("DELETE FROM broker_cash WHERE broker = ?", (broker,)) cur = conn.execute("DELETE FROM broker_cash WHERE broker = ?", (broker,))
return cur.rowcount > 0 return cur.rowcount > 0
# --- Asset Snapshot CRUD ---
def upsert_asset_snapshot(date: str, total_eval: int, total_cash: int, total_assets: int) -> None:
now = __import__("datetime").datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with _conn() as conn:
conn.execute("""
INSERT INTO asset_snapshots (date, total_eval, total_cash, total_assets, created_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(date) DO UPDATE SET
total_eval = excluded.total_eval,
total_cash = excluded.total_cash,
total_assets = excluded.total_assets,
created_at = excluded.created_at
""", (date, total_eval, total_cash, total_assets, now))
def get_asset_snapshots(days: int = 30) -> List[Dict[str, Any]]:
with _conn() as conn:
if days == 0:
rows = conn.execute(
"SELECT date, total_eval, total_cash, total_assets FROM asset_snapshots ORDER BY date ASC"
).fetchall()
else:
rows = conn.execute(
"SELECT date, total_eval, total_cash, total_assets FROM asset_snapshots ORDER BY date DESC LIMIT ?",
(days,)
).fetchall()
rows = list(reversed(rows))
return [dict(r) for r in rows]

View File

@@ -0,0 +1,18 @@
[
"2026-01-01",
"2026-01-28",
"2026-01-29",
"2026-01-30",
"2026-03-01",
"2026-05-05",
"2026-05-25",
"2026-06-06",
"2026-08-15",
"2026-09-24",
"2026-09-25",
"2026-09-26",
"2026-10-03",
"2026-10-09",
"2026-12-25",
"2026-12-31"
]

View File

@@ -1,7 +1,8 @@
import os import os
import json import json
from datetime import date as date_type
from typing import Optional from typing import Optional
from fastapi import FastAPI from fastapi import FastAPI, Query
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import requests import requests
@@ -13,6 +14,7 @@ from .db import (
add_portfolio_item, get_all_portfolio, get_portfolio_item, add_portfolio_item, get_all_portfolio, get_portfolio_item,
update_portfolio_item, delete_portfolio_item, update_portfolio_item, delete_portfolio_item,
upsert_broker_cash, get_all_broker_cash, get_broker_cash, delete_broker_cash, upsert_broker_cash, get_all_broker_cash, get_broker_cash, delete_broker_cash,
upsert_asset_snapshot, get_asset_snapshots,
) )
from .scraper import fetch_market_news, fetch_major_indices, fetch_overseas_news from .scraper import fetch_market_news, fetch_major_indices, fetch_overseas_news
from .price_fetcher import get_current_prices from .price_fetcher import get_current_prices
@@ -33,6 +35,43 @@ scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul"))
# Windows AI Server URL (NAS .env에서 설정) # Windows AI Server URL (NAS .env에서 설정)
WINDOWS_AI_SERVER_URL = os.getenv("WINDOWS_AI_SERVER_URL", "http://192.168.0.5:8000") WINDOWS_AI_SERVER_URL = os.getenv("WINDOWS_AI_SERVER_URL", "http://192.168.0.5:8000")
# 공휴일 목록 로드
_HOLIDAYS_PATH = os.path.join(os.path.dirname(__file__), "holidays.json")
try:
with open(_HOLIDAYS_PATH, "r") as f:
_HOLIDAYS: set = set(json.load(f))
except Exception:
_HOLIDAYS = set()
def is_market_open(d: date_type) -> bool:
return d.weekday() < 5 and d.strftime("%Y-%m-%d") not in _HOLIDAYS
def save_daily_snapshot():
today = date_type.today()
if not is_market_open(today):
print(f"[Snapshot] {today} 휴장일 — 스킵")
return
today_str = today.strftime("%Y-%m-%d")
items = get_all_portfolio()
cash_rows = get_all_broker_cash()
total_cash = sum(r["cash"] for r in cash_rows)
if items:
tickers = list({item["ticker"] for item in items})
prices = get_current_prices(tickers)
total_eval = sum(
prices.get(item["ticker"], item["avg_price"]) * item["quantity"]
for item in items
)
else:
total_eval = 0
total_assets = total_eval + total_cash
upsert_asset_snapshot(today_str, total_eval, total_cash, total_assets)
print(f"[Snapshot] {today_str} 저장 완료: eval={total_eval}, cash={total_cash}, total={total_assets}")
@app.on_event("startup") @app.on_event("startup")
def on_startup(): def on_startup():
init_db() init_db()
@@ -40,6 +79,9 @@ def on_startup():
# 매일 아침 8시 뉴스 스크랩 (NAS 자체 수행) # 매일 아침 8시 뉴스 스크랩 (NAS 자체 수행)
scheduler.add_job(run_scraping_job, "cron", hour="8", minute="0") scheduler.add_job(run_scraping_job, "cron", hour="8", minute="0")
# 평일 15:40 총 자산 스냅샷 저장
scheduler.add_job(save_daily_snapshot, "cron", day_of_week="mon-fri", hour=15, minute=40)
# 앱 시작 시에도 한 번 실행 (데이터 없으면) # 앱 시작 시에도 한 번 실행 (데이터 없으면)
if not get_latest_articles(1): if not get_latest_articles(1):
run_scraping_job() run_scraping_job()
@@ -276,3 +318,46 @@ def delete_portfolio(item_id: int):
return {"ok": True} return {"ok": True}
# --- Asset Snapshot API ---
@app.post("/api/portfolio/snapshot")
def create_snapshot():
"""총 자산 스냅샷 수동 저장 (오늘 날짜 기준)"""
today = date_type.today()
today_str = today.strftime("%Y-%m-%d")
items = get_all_portfolio()
cash_rows = get_all_broker_cash()
total_cash = sum(r["cash"] for r in cash_rows)
if items:
tickers = list({item["ticker"] for item in items})
prices = get_current_prices(tickers)
total_eval = sum(
prices.get(item["ticker"], item["avg_price"]) * item["quantity"]
for item in items
)
else:
total_eval = 0
total_assets = total_eval + total_cash
upsert_asset_snapshot(today_str, total_eval, total_cash, total_assets)
return {
"ok": True,
"snapshot": {
"date": today_str,
"total_eval": total_eval,
"total_cash": total_cash,
"total_assets": total_assets,
},
}
@app.get("/api/portfolio/snapshot/history")
def get_snapshot_history(days: int = Query(30, ge=0)):
"""총 자산 스냅샷 이력 조회 (days=0: 전체, days=N: 최근 N일)"""
snapshots = get_asset_snapshots(days)
return {"snapshots": snapshots}