백엔드 전체 구현 완료: 내부 서비스(Auth, Client, Realtime), API 엔드포인트 및 스케줄러 구현

This commit is contained in:
2026-02-02 23:55:07 +09:00
parent 03027d2206
commit 4f0cc05f39
22 changed files with 1279 additions and 23 deletions

View File

@@ -0,0 +1,28 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
# 1. Async Engine
# "check_same_thread": False is required for SQLite
engine = create_async_engine(
settings.DATABASE_URL,
echo=False,
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}
)
# 2. Async Session Factory
SessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False
)
# 3. Base Model
class Base(DeclarativeBase):
pass
# 4. Dependency Injection for FastAPI
async def get_db():
async with SessionLocal() as session:
yield session

24
backend/app/db/init_db.py Normal file
View File

@@ -0,0 +1,24 @@
from sqlalchemy import select
from app.db.database import engine, Base, SessionLocal
from app.db.models import ApiSettings
# Must import all models to ensure they are registered in Base.metadata
from app.db import models
async def init_db():
async with engine.begin() as conn:
# Create all tables
await conn.run_sync(Base.metadata.create_all)
# Seed Data
async with SessionLocal() as session:
# Check if ApiSettings(id=1) exists
stmt = select(ApiSettings).where(ApiSettings.id == 1)
result = await session.execute(stmt)
settings_entry = result.scalar_one_or_none()
if not settings_entry:
# Create default settings
default_settings = ApiSettings(id=1)
session.add(default_settings)
await session.commit()
print("Initialized default ApiSettings(id=1)")

222
backend/app/db/models.py Normal file
View File

@@ -0,0 +1,222 @@
from datetime import datetime
from typing import List, Optional
from sqlalchemy import Integer, String, Boolean, Float, DateTime, ForeignKey, Text, JSON
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.database import Base
# -----------------
# 1. System & Config
# -----------------
class AiConfig(Base):
__tablename__ = "ai_configs"
id: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String)
providerType: Mapped[str] = mapped_column(String) # Gemini, Ollama, OpenAI
modelName: Mapped[str] = mapped_column(String)
baseUrl: Mapped[Optional[str]] = mapped_column(String, nullable=True)
class ApiSettings(Base):
__tablename__ = "api_settings"
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) # Always 1
# Credentials
appKey: Mapped[Optional[str]] = mapped_column(String, nullable=True)
appSecret: Mapped[Optional[str]] = mapped_column(String, nullable=True)
accountNumber: Mapped[Optional[str]] = mapped_column(String, nullable=True)
# Integrations
useTelegram: Mapped[bool] = mapped_column(Boolean, default=False)
telegramToken: Mapped[Optional[str]] = mapped_column(String, nullable=True)
telegramChatId: Mapped[Optional[str]] = mapped_column(String, nullable=True)
useNaverNews: Mapped[bool] = mapped_column(Boolean, default=False)
naverClientId: Mapped[Optional[str]] = mapped_column(String, nullable=True)
naverClientSecret: Mapped[Optional[str]] = mapped_column(String, nullable=True)
# Configs
kisApiDelayMs: Mapped[int] = mapped_column(Integer, default=250)
newsScrapIntervalMin: Mapped[int] = mapped_column(Integer, default=10)
# Token Storage (Runtime)
accessToken: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
tokenExpiry: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
websocketApprovalKey: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
# AI Config Relations (Foreign Keys)
preferredNewsAiId: Mapped[Optional[str]] = mapped_column(ForeignKey("ai_configs.id"), nullable=True)
preferredStockAiId: Mapped[Optional[str]] = mapped_column(ForeignKey("ai_configs.id"), nullable=True)
preferredNewsJudgementAiId: Mapped[Optional[str]] = mapped_column(ForeignKey("ai_configs.id"), nullable=True)
preferredAutoBuyAiId: Mapped[Optional[str]] = mapped_column(ForeignKey("ai_configs.id"), nullable=True)
preferredAutoSellAiId: Mapped[Optional[str]] = mapped_column(ForeignKey("ai_configs.id"), nullable=True)
# -----------------
# 2. Account & Portfolio
# -----------------
class AccountStatus(Base):
__tablename__ = "account_status"
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
totalAssets: Mapped[float] = mapped_column(Float, default=0.0)
buyingPower: Mapped[float] = mapped_column(Float, default=0.0)
dailyProfit: Mapped[float] = mapped_column(Float, default=0.0)
dailyProfitRate: Mapped[float] = mapped_column(Float, default=0.0)
class Holding(Base):
__tablename__ = "holdings"
stockCode: Mapped[str] = mapped_column(String, primary_key=True)
stockName: Mapped[str] = mapped_column(String)
quantity: Mapped[int] = mapped_column(Integer)
avgPrice: Mapped[float] = mapped_column(Float)
currentPrice: Mapped[float] = mapped_column(Float) # Real-time updated
profit: Mapped[float] = mapped_column(Float)
profitRate: Mapped[float] = mapped_column(Float)
marketValue: Mapped[float] = mapped_column(Float)
# -----------------
# 3. Market & Discovery
# -----------------
class MasterStock(Base):
__tablename__ = "master_stocks"
code: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String)
market: Mapped[str] = mapped_column(String) # Domestic, Overseas
# Stats
per: Mapped[float] = mapped_column(Float, default=0.0)
pbr: Mapped[float] = mapped_column(Float, default=0.0)
roe: Mapped[float] = mapped_column(Float, default=0.0)
marketCap: Mapped[float] = mapped_column(Float, default=0.0)
dividendYield: Mapped[float] = mapped_column(Float, default=0.0)
# User Data
memo: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
isHidden: Mapped[bool] = mapped_column(Boolean, default=False)
class NewsCache(Base):
__tablename__ = "news_cache"
news_id: Mapped[str] = mapped_column(String, primary_key=True) # Hashed ID
title: Mapped[str] = mapped_column(Text)
description: Mapped[str] = mapped_column(Text)
link: Mapped[str] = mapped_column(Text)
pubDate: Mapped[str] = mapped_column(String)
sentiment: Mapped[Optional[str]] = mapped_column(String, nullable=True)
relatedThemes: Mapped[Optional[List]] = mapped_column(JSON, nullable=True)
relatedStocks: Mapped[Optional[List]] = mapped_column(JSON, nullable=True)
class DiscoveryRankingCache(Base):
__tablename__ = "discovery_ranking_cache"
# Composite Key simulated (category_market string or separate cols)
# Using composite PK
category: Mapped[str] = mapped_column(String, primary_key=True)
market: Mapped[str] = mapped_column(String, primary_key=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
items_json: Mapped[str] = mapped_column(Text) # JSON String of StockItem[]
class StockStat(Base):
__tablename__ = "stock_stats"
code: Mapped[str] = mapped_column(String, primary_key=True)
tradingValue: Mapped[float] = mapped_column(Float, default=0.0)
buyRatio: Mapped[int] = mapped_column(Integer, default=0)
sellRatio: Mapped[int] = mapped_column(Integer, default=0)
foreignNetBuy: Mapped[int] = mapped_column(Integer, default=0)
institutionalNetBuy: Mapped[int] = mapped_column(Integer, default=0)
aiScoreBuy: Mapped[int] = mapped_column(Integer, default=0)
aiScoreSell: Mapped[int] = mapped_column(Integer, default=0)
# -----------------
# 4. Watchlist
# -----------------
class WatchlistGroup(Base):
__tablename__ = "watchlist_groups"
id: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String)
market: Mapped[str] = mapped_column(String) # Domestic, Overseas
class WatchlistItem(Base):
__tablename__ = "watchlist_items"
group_id: Mapped[str] = mapped_column(ForeignKey("watchlist_groups.id"), primary_key=True)
stock_code: Mapped[str] = mapped_column(String, primary_key=True)
added_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
# -----------------
# 5. Trading & Automation
# -----------------
class TradeHistory(Base):
__tablename__ = "trade_history"
id: Mapped[str] = mapped_column(String, primary_key=True)
stockCode: Mapped[str] = mapped_column(String)
stockName: Mapped[str] = mapped_column(String)
type: Mapped[str] = mapped_column(String) # BUY, SELL
quantity: Mapped[int] = mapped_column(Integer)
price: Mapped[float] = mapped_column(Float)
timestamp: Mapped[datetime] = mapped_column(DateTime)
status: Mapped[str] = mapped_column(String) # FILLED, CANCELLED
class AutoTradeRobot(Base):
__tablename__ = "auto_trade_robots"
id: Mapped[str] = mapped_column(String, primary_key=True)
stockCode: Mapped[str] = mapped_column(String)
stockName: Mapped[str] = mapped_column(String)
groupId: Mapped[Optional[str]] = mapped_column(String, nullable=True)
type: Mapped[str] = mapped_column(String) # ACCUMULATION, TRAILING, etc.
frequency: Mapped[str] = mapped_column(String) # DAILY, WEEKLY
executionTime: Mapped[str] = mapped_column(String) # HH:MM
market: Mapped[str] = mapped_column(String)
quantity: Mapped[int] = mapped_column(Integer)
specificDay: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0=Monday
trailingPercent: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
active: Mapped[bool] = mapped_column(Boolean, default=True)
class ReservedOrder(Base):
__tablename__ = "reserved_orders"
id: Mapped[str] = mapped_column(String, primary_key=True)
stockCode: Mapped[str] = mapped_column(String)
stockName: Mapped[str] = mapped_column(String)
type: Mapped[str] = mapped_column(String) # BUY, SELL
market: Mapped[str] = mapped_column(String)
monitoringType: Mapped[str] = mapped_column(String) # TARGET, TRAILING
trailingType: Mapped[Optional[str]] = mapped_column(String, nullable=True) # AMOUNT, PERCENT
status: Mapped[str] = mapped_column(String) # MONITORING, TRIGGERED, EXPIRED
quantity: Mapped[int] = mapped_column(Integer)
triggerPrice: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
trailingValue: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
stopLossValue: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
highestPrice: Mapped[Optional[float]] = mapped_column(Float, nullable=True) # For Trailing
lowestPrice: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
useStopLoss: Mapped[bool] = mapped_column(Boolean, default=False)
sellAll: Mapped[bool] = mapped_column(Boolean, default=False)
stopLossType: Mapped[Optional[str]] = mapped_column(String, nullable=True)
createdAt: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
expiryDate: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)