"백엔드_핵심_로직_구현_프론트엔드_연동_및_도커_배포_최적화_완료"
This commit is contained in:
104
backend/app/services/sync_service.py
Normal file
104
backend/app/services/sync_service.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import logging
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete
|
||||
from datetime import datetime
|
||||
|
||||
from app.db.database import SessionLocal
|
||||
from app.db.models import AccountStatus, Holding
|
||||
from app.services.kis_client import kis_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SyncService:
|
||||
async def sync_account(self, db: AsyncSession):
|
||||
"""
|
||||
Fetches balance from KIS and updates DB (AccountStatus & Holdings).
|
||||
Currently supports Domestic only.
|
||||
"""
|
||||
logger.info("SyncService: Starting Account Sync...")
|
||||
try:
|
||||
# 1. Fetch Domestic Balance
|
||||
# kis_client.get_balance returns dict with 'output1', 'output2' or None
|
||||
res = await kis_client.get_balance("Domestic")
|
||||
if not res:
|
||||
logger.error("SyncService: Failed to fetch balance (API Error or No Data).")
|
||||
return
|
||||
|
||||
# output1: Holdings List
|
||||
# output2: Account Summary
|
||||
output1 = res.get('output1', [])
|
||||
output2 = res.get('output2', [])
|
||||
|
||||
# KIS API returns output2 as a LIST of 1 dict usually
|
||||
summary_data = output2[0] if output2 else {}
|
||||
|
||||
# --- Update AccountStatus ---
|
||||
# Map KIS fields to AccountStatus model
|
||||
# tot_evlu_amt: 총평가금액 (Total Assets)
|
||||
# dnca_tot_amt: 예수금총액 (Buying Power)
|
||||
# evlu_pfls_smt_tl: 평가손익합계 (Daily Profit - approximation)
|
||||
# evlu_pfls_rt: 수익률
|
||||
|
||||
total_assets = float(summary_data.get('tot_evlu_amt', 0) or 0)
|
||||
buying_power = float(summary_data.get('dnca_tot_amt', 0) or 0)
|
||||
daily_profit = float(summary_data.get('evlu_pfls_smt_tl', 0) or 0)
|
||||
|
||||
# Calculate daily profit rate if not provided directly
|
||||
# profit_rate = float(summary_data.get('evlu_pfls_rt', 0)) # Sometimes available
|
||||
daily_profit_rate = 0.0
|
||||
if total_assets > 0:
|
||||
daily_profit_rate = (daily_profit / total_assets) * 100
|
||||
|
||||
# Upsert AccountStatus (ID=1)
|
||||
stmt = select(AccountStatus).where(AccountStatus.id == 1)
|
||||
result = await db.execute(stmt)
|
||||
status = result.scalar_one_or_none()
|
||||
|
||||
if not status:
|
||||
status = AccountStatus(id=1)
|
||||
db.add(status)
|
||||
|
||||
status.totalAssets = total_assets
|
||||
status.buyingPower = buying_power
|
||||
status.dailyProfit = daily_profit
|
||||
status.dailyProfitRate = daily_profit_rate
|
||||
|
||||
# --- Update Holdings ---
|
||||
# Strategy: Delete all existing holdings (refresh) or Upsert?
|
||||
# Refresh is safer to remove sold items.
|
||||
await db.execute(delete(Holding))
|
||||
|
||||
for item in output1:
|
||||
# Map fields
|
||||
# pdno: 종목번호
|
||||
# prdt_name: 종목명
|
||||
# hldg_qty: 보유수량
|
||||
# pchs_avg_pric: 매입평균가격
|
||||
# prpr: 현재가
|
||||
# evlu_pfls_amt: 평가손익금액
|
||||
# evlu_pfls_rt: 평가손익율
|
||||
# evlu_amt: 평가금액
|
||||
|
||||
code = item.get('pdno')
|
||||
if not code: continue
|
||||
|
||||
h = Holding(
|
||||
stockCode=code,
|
||||
stockName=item.get('prdt_name', 'Unknown'),
|
||||
quantity=int(item.get('hldg_qty', 0) or 0),
|
||||
avgPrice=float(item.get('pchs_avg_pric', 0) or 0),
|
||||
currentPrice=float(item.get('prpr', 0) or 0),
|
||||
profit=float(item.get('evlu_pfls_amt', 0) or 0),
|
||||
profitRate=float(item.get('evlu_pfls_rt', 0) or 0),
|
||||
marketValue=float(item.get('evlu_amt', 0) or 0)
|
||||
)
|
||||
db.add(h)
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"SyncService: Account Sync Complete. Assets: {total_assets}, Holdings: {len(output1)}")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"SyncService: Error during sync: {e}")
|
||||
|
||||
sync_service = SyncService()
|
||||
Reference in New Issue
Block a user