""" One-time script to import historical portfolio data from data.txt. 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 # 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, PortfolioSnapshot, SnapshotHolding, Transaction, TransactionType, ) from app.models.user import User # ETF name -> ticker mapping ETF_MAP = { "TIGER 200": "069500", "KIWOOM 국고채10년": "148070", "KODEX 200미국채혼합": "284430", "TIGER 미국S&P500": "360750", "ACE KRX금현물": "411060", } # Target ratios TARGETS = { "069500": Decimal("0.83"), "148070": Decimal("25"), "284430": Decimal("41.67"), "360750": Decimal("17.5"), "411060": Decimal("15"), } # Actual total invested amounts per ticker (from brokerage records) TOTAL_INVESTED = { "069500": Decimal("541040"), "148070": Decimal("15432133"), "284430": Decimal("18375975"), "360750": Decimal("7683515"), "411060": Decimal("6829620"), } # Historical snapshots from data.txt SNAPSHOTS = [ { "date": date(2025, 4, 28), "total_assets": Decimal("42485834"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("33815"), "value": Decimal("541040")}, {"ticker": "148070", "qty": 1, "price": Decimal("118000"), "value": Decimal("118000")}, {"ticker": "284430", "qty": 355, "price": Decimal("13235"), "value": Decimal("4698435")}, {"ticker": "360750", "qty": 329, "price": Decimal("19770"), "value": Decimal("6504330")}, {"ticker": "411060", "qty": 1, "price": Decimal("21620"), "value": Decimal("21620")}, ], }, { "date": date(2025, 5, 13), "total_assets": Decimal("42485834"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("34805"), "value": Decimal("556880")}, {"ticker": "148070", "qty": 1, "price": Decimal("117010"), "value": Decimal("117010")}, {"ticker": "284430", "qty": 369, "price": Decimal("13175"), "value": Decimal("4861575")}, {"ticker": "360750", "qty": 329, "price": Decimal("20490"), "value": Decimal("6741210")}, {"ticker": "411060", "qty": 261, "price": Decimal("20840"), "value": Decimal("5439240")}, ], }, { "date": date(2025, 6, 11), "total_assets": Decimal("44263097"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("39110"), "value": Decimal("625760")}, {"ticker": "148070", "qty": 91, "price": Decimal("115790"), "value": Decimal("10536890")}, {"ticker": "284430", "qty": 1271, "price": Decimal("13570"), "value": Decimal("17247470")}, {"ticker": "360750", "qty": 374, "price": Decimal("20570"), "value": Decimal("7693180")}, {"ticker": "411060", "qty": 306, "price": Decimal("20670"), "value": Decimal("6325020")}, ], }, { "date": date(2025, 7, 30), "total_assets": Decimal("47395573"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("43680"), "value": Decimal("698880")}, {"ticker": "148070", "qty": 96, "price": Decimal("116470"), "value": Decimal("11181120")}, {"ticker": "284430", "qty": 1359, "price": Decimal("14550"), "value": Decimal("19773450")}, {"ticker": "360750", "qty": 377, "price": Decimal("22085"), "value": Decimal("8326045")}, {"ticker": "411060", "qty": 320, "price": Decimal("20870"), "value": Decimal("6678400")}, ], }, { "date": date(2025, 8, 13), "total_assets": Decimal("47997732"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("43795"), "value": Decimal("700720")}, {"ticker": "148070", "qty": 102, "price": Decimal("116800"), "value": Decimal("11913600")}, {"ticker": "284430", "qty": 1359, "price": Decimal("14435"), "value": Decimal("19617165")}, {"ticker": "360750", "qty": 377, "price": Decimal("22090"), "value": Decimal("8327930")}, {"ticker": "411060", "qty": 320, "price": Decimal("20995"), "value": Decimal("6718400")}, ], }, { "date": date(2025, 10, 12), "total_assets": Decimal("54188966"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("50850"), "value": Decimal("813600")}, {"ticker": "148070", "qty": 103, "price": Decimal("116070"), "value": Decimal("11955210")}, {"ticker": "284430", "qty": 1386, "price": Decimal("15665"), "value": Decimal("21711690")}, {"ticker": "360750", "qty": 380, "price": Decimal("23830"), "value": Decimal("9055400")}, {"ticker": "411060", "qty": 328, "price": Decimal("27945"), "value": Decimal("9165960")}, ], }, { "date": date(2025, 12, 4), "total_assets": Decimal("56860460"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("57190"), "value": Decimal("915040")}, {"ticker": "148070", "qty": 115, "price": Decimal("112900"), "value": Decimal("12983500")}, {"ticker": "284430", "qty": 1386, "price": Decimal("16825"), "value": Decimal("23319450")}, {"ticker": "360750", "qty": 383, "price": Decimal("25080"), "value": Decimal("9605640")}, {"ticker": "411060", "qty": 328, "price": Decimal("27990"), "value": Decimal("9180720")}, ], }, { "date": date(2026, 1, 6), "total_assets": Decimal("58949962"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("66255"), "value": Decimal("1060080")}, {"ticker": "148070", "qty": 122, "price": Decimal("108985"), "value": Decimal("13296170")}, {"ticker": "284430", "qty": 1386, "price": Decimal("17595"), "value": Decimal("24386670")}, {"ticker": "360750", "qty": 383, "price": Decimal("24840"), "value": Decimal("9513720")}, {"ticker": "411060", "qty": 328, "price": Decimal("29605"), "value": Decimal("9710440")}, ], }, { "date": date(2026, 2, 16), "total_assets": Decimal("62433665"), "holdings": [ {"ticker": "069500", "qty": 16, "price": Decimal("81835"), "value": Decimal("1309360")}, {"ticker": "148070", "qty": 133, "price": Decimal("108290"), "value": Decimal("14402570")}, {"ticker": "284430", "qty": 1386, "price": Decimal("19250"), "value": Decimal("26680500")}, {"ticker": "360750", "qty": 385, "price": Decimal("24435"), "value": Decimal("9407475")}, {"ticker": "411060", "qty": 328, "price": Decimal("32420"), "value": Decimal("10633760")}, ], }, ] 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 snapshots for snap in SNAPSHOTS: snapshot = PortfolioSnapshot( portfolio_id=portfolio.id, total_value=snap["total_assets"], snapshot_date=snap["date"], ) db.add(snapshot) db.flush() total = snap["total_assets"] for h in snap["holdings"]: ratio = (h["value"] / total * 100).quantize(Decimal("0.01")) if total > 0 else Decimal("0") db.add(SnapshotHolding( snapshot_id=snapshot.id, ticker=h["ticker"], quantity=h["qty"], price=h["price"], value=h["value"], current_ratio=ratio, )) print(f" Snapshot {snap['date']}: {len(snap['holdings'])} holdings") # Create transactions by comparing consecutive snapshots tx_count = 0 for i, snap in enumerate(SNAPSHOTS): current_holdings = {h["ticker"]: h for h in snap["holdings"]} if i == 0: # First snapshot: all holdings are initial buys prev_holdings = {} else: prev_holdings = {h["ticker"]: h for h in SNAPSHOTS[i - 1]["holdings"]} all_tickers = set(current_holdings.keys()) | set(prev_holdings.keys()) for ticker in all_tickers: cur_qty = current_holdings[ticker]["qty"] if ticker in current_holdings else 0 prev_qty = prev_holdings[ticker]["qty"] if ticker in prev_holdings else 0 diff = cur_qty - prev_qty if diff == 0: continue if diff > 0: tx_type = TransactionType.BUY price = current_holdings[ticker]["price"] else: tx_type = TransactionType.SELL price = prev_holdings[ticker]["price"] db.add(Transaction( portfolio_id=portfolio.id, ticker=ticker, tx_type=tx_type, quantity=abs(diff), price=price, executed_at=datetime.combine(snap["date"], datetime.min.time()), )) tx_count += 1 print(f"Created {tx_count} transactions from snapshot diffs") # Set current holdings from latest snapshot # avg_price = total invested amount / quantity (from actual brokerage records) latest = SNAPSHOTS[-1] for h in latest["holdings"]: ticker = h["ticker"] qty = h["qty"] invested = TOTAL_INVESTED[ticker] avg_price = (invested / qty).quantize(Decimal("0.01")) db.add(Holding( portfolio_id=portfolio.id, ticker=ticker, quantity=qty, avg_price=avg_price, )) print(f"Set {len(latest['holdings'])} current holdings from {latest['date']}") db.commit() print("Done!") if __name__ == "__main__": db = SessionLocal() try: seed(db) finally: db.close()