All checks were successful
Deploy to Production / deploy (push) Successful in 1m48s
The seed script was incorrectly using the latest snapshot's market price as avg_price, resulting in inflated average costs. Now computes avg_price from actual total invested amounts per ticker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
279 lines
11 KiB
Python
279 lines
11 KiB
Python
"""
|
|
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()
|