galaxis-po/backend/scripts/seed_data.py

199 lines
6.8 KiB
Python
Raw Normal View History

"""
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()