""" One-time script to import historical portfolio data. Builds portfolio from actual trade history with accurate average prices and cumulative holdings. Usage: cd backend && python -m scripts.seed_data Requires: DATABASE_URL environment variable or default dev connection. """ import sys import os from datetime import date, datetime from decimal import Decimal, ROUND_HALF_UP # Add backend to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from sqlalchemy.orm import Session from app.core.database import SessionLocal from app.models.portfolio import ( Portfolio, PortfolioType, Target, Holding, Transaction, TransactionType, ) from app.models.user import User # ETF name -> ticker mapping ETF_MAP = { "TIGER 200": "102110", "KIWOOM 국고채10년": "148070", "KODEX 200미국채혼합": "284430", "TIGER 미국S&P500": "360750", "ACE KRX금현물": "411060", } # Target ratios (percentage of total portfolio) TARGETS = { "102110": Decimal("0.83"), "148070": Decimal("25"), "284430": Decimal("41.67"), "360750": Decimal("17.5"), "411060": Decimal("15"), } # Actual trade history (date, name, quantity, price_per_unit) TRADES = [ # 2025-04-29: Initial purchases (date(2025, 4, 29), "ACE KRX금현물", 1, Decimal("21620")), (date(2025, 4, 29), "TIGER 미국S&P500", 329, Decimal("19770")), (date(2025, 4, 29), "KIWOOM 국고채10년", 1, Decimal("118000")), (date(2025, 4, 29), "TIGER 200", 16, Decimal("33815")), (date(2025, 4, 30), "KODEX 200미국채혼합", 355, Decimal("13235")), # 2025-05-13 ~ 05-16 (date(2025, 5, 13), "ACE KRX금현물", 260, Decimal("20820")), (date(2025, 5, 13), "KODEX 200미국채혼합", 14, Decimal("13165")), (date(2025, 5, 14), "ACE KRX금현물", 45, Decimal("20760")), (date(2025, 5, 14), "TIGER 미국S&P500", 45, Decimal("20690")), (date(2025, 5, 14), "KODEX 200미국채혼합", 733, Decimal("13220")), (date(2025, 5, 14), "KIWOOM 국고채10년", 90, Decimal("116939")), (date(2025, 5, 16), "KODEX 200미국채혼합", 169, Decimal("13125")), # 2025-06-12 (date(2025, 6, 12), "ACE KRX금현물", 14, Decimal("20855")), (date(2025, 6, 12), "TIGER 미국S&P500", 3, Decimal("20355")), (date(2025, 6, 12), "KODEX 200미국채혼합", 88, Decimal("13570")), (date(2025, 6, 12), "KIWOOM 국고채10년", 5, Decimal("115945")), # 2025-07-30 (date(2025, 7, 30), "KIWOOM 국고채10년", 6, Decimal("116760")), # 2025-08-14 ~ 08-19 (date(2025, 8, 14), "ACE KRX금현물", 6, Decimal("21095")), (date(2025, 8, 14), "TIGER 미국S&P500", 3, Decimal("22200")), (date(2025, 8, 14), "KODEX 200미국채혼합", 27, Decimal("14465")), (date(2025, 8, 14), "KIWOOM 국고채10년", 1, Decimal("117075")), (date(2025, 8, 19), "ACE KRX금현물", 2, Decimal("21030")), # 2025-10-13 (date(2025, 10, 13), "TIGER 미국S&P500", 3, Decimal("23480")), (date(2025, 10, 13), "KIWOOM 국고채10년", 12, Decimal("116465")), # 2025-12-05 (date(2025, 12, 5), "KIWOOM 국고채10년", 7, Decimal("112830")), # 2026-01-07 ~ 01-08 (date(2026, 1, 7), "TIGER 미국S&P500", 2, Decimal("25015")), (date(2026, 1, 8), "KIWOOM 국고채10년", 11, Decimal("109527")), # 2026-02-20 (date(2026, 2, 20), "TIGER 미국S&P500", 20, Decimal("24685")), (date(2026, 2, 20), "KIWOOM 국고채10년", 9, Decimal("108500")), # 2026-03-23 (date(2026, 3, 23), "ACE KRX금현물", 41, Decimal("30095")), (date(2026, 3, 23), "TIGER 미국S&P500", 128, Decimal("24290")), (date(2026, 3, 23), "KODEX 200미국채혼합", 188, Decimal("19579")), (date(2026, 3, 23), "KIWOOM 국고채10년", 10, Decimal("106780")), ] def _compute_holdings(trades: list) -> dict: """Compute current holdings with weighted average prices from trade history.""" holdings = {} for _, name, qty, price in trades: ticker = ETF_MAP[name] if ticker not in holdings: holdings[ticker] = {"qty": 0, "total_cost": Decimal("0")} holdings[ticker]["qty"] += qty holdings[ticker]["total_cost"] += qty * price return holdings def seed(db: Session): """Import historical data into database.""" # Find admin user (first user in DB) user = db.query(User).first() if not user: print("ERROR: No user found in database. Create a user first.") return # Delete existing portfolio if present (cascade deletes related records) existing = db.query(Portfolio).filter( Portfolio.user_id == user.id, Portfolio.name == "연금 포트폴리오", ).first() if existing: db.delete(existing) db.flush() print(f"Deleted existing portfolio (id={existing.id})") # Create portfolio portfolio = Portfolio( user_id=user.id, name="연금 포트폴리오", portfolio_type=PortfolioType.PENSION, ) db.add(portfolio) db.flush() print(f"Created portfolio id={portfolio.id}") # Set targets for ticker, ratio in TARGETS.items(): db.add(Target(portfolio_id=portfolio.id, ticker=ticker, target_ratio=ratio)) print(f"Set {len(TARGETS)} targets") # Create transactions from trade history tx_count = 0 for trade_date, name, qty, price in TRADES: ticker = ETF_MAP[name] db.add(Transaction( portfolio_id=portfolio.id, ticker=ticker, tx_type=TransactionType.BUY, quantity=qty, price=price, executed_at=datetime.combine(trade_date, datetime.min.time()), memo=f"{name} 매수", )) tx_count += 1 print(f"Created {tx_count} transactions") # Set current holdings from computed totals computed = _compute_holdings(TRADES) for ticker, data in computed.items(): qty = data["qty"] avg_price = (data["total_cost"] / qty).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) db.add(Holding( portfolio_id=portfolio.id, ticker=ticker, quantity=qty, avg_price=avg_price, )) print(f"Set {len(computed)} current holdings") # Print summary print("\n=== Holdings Summary ===") total_invested = Decimal("0") for ticker in sorted(computed.keys()): d = computed[ticker] avg = (d["total_cost"] / d["qty"]).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) name = [n for n, t in ETF_MAP.items() if t == ticker][0] print(f" {name:20s} ({ticker}) qty={d['qty']:>5d} avg={avg:>12} invested={d['total_cost']:>15}") total_invested += d["total_cost"] print(f" {'TOTAL':20s} invested={total_invested:>15}") db.commit() print("\nDone!") if __name__ == "__main__": db = SessionLocal() try: seed(db) finally: db.close()