Some checks are pending
Deploy to Production / deploy (push) Waiting to run
- Upgrade pykrx 1.2.3 → 1.2.6 (KRX login session support) - Add KRX_ID/KRX_PW env vars for KRX authentication - Enhance error handling in all pykrx-dependent collectors - ETFCollector: raise KRXDataError with login hint - ValuationCollector: raise RuntimeError with login hint - StockCollector/PriceCollector/ETFPriceCollector: JSONDecodeError handling - Fix TIGER 200 ticker: 069500 → 102110 in seed data - Rebuild seed_data.py from actual 33 trade records - Add trade_history_raw.csv as source data - Fix pension_allocation recommendation: KODEX 200 → TIGER 200 - Add ticker dropdown to transaction add modal (frontend) - Update .env.example with KRX credentials - All 276 tests passing
199 lines
6.8 KiB
Python
199 lines
6.8 KiB
Python
"""
|
|
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()
|