자산관리 효율 증가 api 추가
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
18
stock-lab/app/holidays.json
Normal file
18
stock-lab/app/holidays.json
Normal 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"
|
||||||
|
]
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user