From 6d7cf340ea454d56015c8c0412e24e3feb56edd1 Mon Sep 17 00:00:00 2001 From: zephyrdark Date: Wed, 11 Feb 2026 23:27:36 +0900 Subject: [PATCH] feat: add historical data import script from data.txt Co-Authored-By: Claude Opus 4.6 --- backend/scripts/__init__.py | 0 backend/scripts/seed_data.py | 212 +++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 backend/scripts/__init__.py create mode 100644 backend/scripts/seed_data.py diff --git a/backend/scripts/__init__.py b/backend/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/scripts/seed_data.py b/backend/scripts/seed_data.py new file mode 100644 index 0000000..572a1bd --- /dev/null +++ b/backend/scripts/seed_data.py @@ -0,0 +1,212 @@ +""" +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 +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, +) +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"), +} + +# 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")}, + ], + }, +] + + +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 + + # Check if portfolio already exists + existing = db.query(Portfolio).filter( + Portfolio.user_id == user.id, + Portfolio.name == "연금 포트폴리오", + ).first() + if existing: + print(f"Portfolio '연금 포트폴리오' already exists (id={existing.id}). Skipping.") + return + + # 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") + + # Set current holdings from latest snapshot + latest = SNAPSHOTS[-1] + for h in latest["holdings"]: + db.add(Holding( + portfolio_id=portfolio.id, + ticker=h["ticker"], + quantity=h["qty"], + avg_price=h["price"], # Using current price as avg (best available) + )) + 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()