백엔드 전체 구현 완료: 내부 서비스(Auth, Client, Realtime), API 엔드포인트 및 스케줄러 구현
This commit is contained in:
28
backend/app/db/database.py
Normal file
28
backend/app/db/database.py
Normal 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
24
backend/app/db/init_db.py
Normal 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
222
backend/app/db/models.py
Normal 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)
|
||||
Reference in New Issue
Block a user