"백엔드_핵심_로직_구현_프론트엔드_연동_및_도커_배포_최적화_완료"
This commit is contained in:
BIN
backend/app/api/endpoints/__pycache__/kis.cpython-312.pyc
Normal file
BIN
backend/app/api/endpoints/__pycache__/kis.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/endpoints/__pycache__/settings.cpython-312.pyc
Normal file
BIN
backend/app/api/endpoints/__pycache__/settings.cpython-312.pyc
Normal file
Binary file not shown.
54
backend/app/api/endpoints/account.py
Normal file
54
backend/app/api/endpoints/account.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import AccountStatus, Holding
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class AccountStatusSchema(BaseModel):
|
||||
totalAssets: float
|
||||
buyingPower: float
|
||||
dailyProfit: float
|
||||
dailyProfitRate: float
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class HoldingSchema(BaseModel):
|
||||
stockCode: str
|
||||
stockName: str
|
||||
quantity: int
|
||||
avgPrice: float
|
||||
currentPrice: float
|
||||
profit: float
|
||||
profitRate: float
|
||||
marketValue: float
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@router.get("/summary", response_model=AccountStatusSchema)
|
||||
async def get_account_summary(db: AsyncSession = Depends(get_db)):
|
||||
stmt = select(AccountStatus).where(AccountStatus.id == 1)
|
||||
result = await db.execute(stmt)
|
||||
status = result.scalar_one_or_none()
|
||||
|
||||
if not status:
|
||||
# Return default zeroed if not initialized
|
||||
return AccountStatusSchema(totalAssets=0, buyingPower=0, dailyProfit=0, dailyProfitRate=0)
|
||||
|
||||
return status
|
||||
|
||||
@router.get("/holdings", response_model=List[HoldingSchema])
|
||||
async def get_holdings(market: str = None, db: AsyncSession = Depends(get_db)):
|
||||
# TODO: Filter by market if Holding table supports it.
|
||||
# Current Holding model doesn't have 'market' column explicitly, but we can assume mixed or add it.
|
||||
# For now, return all.
|
||||
stmt = select(Holding)
|
||||
result = await db.execute(stmt)
|
||||
holdings = result.scalars().all()
|
||||
return holdings
|
||||
87
backend/app/api/endpoints/auto_trade.py
Normal file
87
backend/app/api/endpoints/auto_trade.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import AutoTradeRobot
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- Schema ---
|
||||
class AutoTradeConfigSchema(BaseModel):
|
||||
id: str
|
||||
stockCode: str
|
||||
stockName: str
|
||||
groupId: Optional[str] = None
|
||||
type: str
|
||||
frequency: str
|
||||
executionTime: str
|
||||
market: str
|
||||
quantity: int
|
||||
specificDay: Optional[int] = None
|
||||
trailingPercent: Optional[float] = None
|
||||
active: bool = True
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# --- Endpoints ---
|
||||
@router.get("/configs", response_model=List[AutoTradeConfigSchema])
|
||||
async def get_auto_configs(db: AsyncSession = Depends(get_db)):
|
||||
stmt = select(AutoTradeRobot)
|
||||
res = await db.execute(stmt)
|
||||
return res.scalars().all()
|
||||
|
||||
@router.post("/configs")
|
||||
async def create_auto_config(config: AutoTradeConfigSchema, db: AsyncSession = Depends(get_db)):
|
||||
# Check exists? Upsert?
|
||||
# Frontend generates ID usually or we do. Schema has ID.
|
||||
# We will upsert (merge) or just add.
|
||||
|
||||
# Check if exists
|
||||
stmt = select(AutoTradeRobot).where(AutoTradeRobot.id == config.id)
|
||||
res = await db.execute(stmt)
|
||||
existing = res.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
# Update
|
||||
existing.stockCode = config.stockCode
|
||||
existing.stockName = config.stockName
|
||||
existing.groupId = config.groupId
|
||||
existing.type = config.type
|
||||
existing.frequency = config.frequency
|
||||
existing.executionTime = config.executionTime
|
||||
existing.market = config.market
|
||||
existing.quantity = config.quantity
|
||||
existing.specificDay = config.specificDay
|
||||
existing.trailingPercent = config.trailingPercent
|
||||
existing.active = config.active
|
||||
else:
|
||||
# Create
|
||||
new_obj = AutoTradeRobot(
|
||||
id=config.id,
|
||||
stockCode=config.stockCode,
|
||||
stockName=config.stockName,
|
||||
groupId=config.groupId,
|
||||
type=config.type,
|
||||
frequency=config.frequency,
|
||||
executionTime=config.executionTime,
|
||||
market=config.market,
|
||||
quantity=config.quantity,
|
||||
specificDay=config.specificDay,
|
||||
trailingPercent=config.trailingPercent,
|
||||
active=config.active
|
||||
)
|
||||
db.add(new_obj)
|
||||
|
||||
await db.commit()
|
||||
return {"status": "saved", "id": config.id}
|
||||
|
||||
@router.delete("/configs/{config_id}")
|
||||
async def delete_auto_config(config_id: str, db: AsyncSession = Depends(get_db)):
|
||||
await db.execute(delete(AutoTradeRobot).where(AutoTradeRobot.id == config_id))
|
||||
await db.commit()
|
||||
return {"status": "deleted"}
|
||||
68
backend/app/api/endpoints/discovery.py
Normal file
68
backend/app/api/endpoints/discovery.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import MasterStock, DiscoveryRankingCache, StockItem
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class StockItemSchema(BaseModel):
|
||||
code: str
|
||||
name: str
|
||||
market: str
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class RankingItemSchema(BaseModel):
|
||||
code: str
|
||||
name: str
|
||||
price: float
|
||||
change: float
|
||||
changePercent: float
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@router.get("/rankings", response_model=List[RankingItemSchema])
|
||||
async def get_rankings(type: str = "rise", limit: int = 10, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Get Top Rankings based on cached StockItem data.
|
||||
type: 'rise' (Top Gainers), 'fall' (Top Losers), 'volume' (Not impl yet)
|
||||
"""
|
||||
stmt = select(StockItem)
|
||||
|
||||
if type == "fall":
|
||||
stmt = stmt.order_by(StockItem.changePercent.asc())
|
||||
else:
|
||||
stmt = stmt.order_by(StockItem.changePercent.desc())
|
||||
|
||||
stmt = stmt.limit(limit)
|
||||
res = await db.execute(stmt)
|
||||
items = res.scalars().all()
|
||||
|
||||
return items
|
||||
|
||||
@router.get("/stocks/search", response_model=List[StockItemSchema])
|
||||
async def search_stocks(query: str, db: AsyncSession = Depends(get_db)):
|
||||
# Search by name or code
|
||||
stmt = select(MasterStock).where(
|
||||
(MasterStock.name.like(f"%{query}%")) | (MasterStock.code.like(f"%{query}%"))
|
||||
).limit(20)
|
||||
result = await db.execute(stmt)
|
||||
stocks = result.scalars().all()
|
||||
return stocks
|
||||
|
||||
from app.services.master_service import master_service
|
||||
|
||||
@router.get("/kis/master-stocks")
|
||||
async def sync_master_stocks(db: AsyncSession = Depends(get_db)):
|
||||
# Trigger Sync
|
||||
# Ideally should be BackgroundTasks, but for now await to show result
|
||||
await master_service.sync_master_data(db)
|
||||
|
||||
# Return count
|
||||
stmt = select(MasterStock).limit(1)
|
||||
# Just return status
|
||||
return {"status": "Sync Complete"}
|
||||
@@ -3,8 +3,10 @@ from pydantic import BaseModel
|
||||
from typing import Literal
|
||||
|
||||
from app.services.kis_client import kis_client
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger("KisAPI")
|
||||
|
||||
class OrderRequest(BaseModel):
|
||||
market: Literal["Domestic", "Overseas"]
|
||||
@@ -13,6 +15,15 @@ class OrderRequest(BaseModel):
|
||||
quantity: int
|
||||
price: float = 0 # 0 for Market Price (if supported)
|
||||
|
||||
class ModifyOrderRequest(BaseModel):
|
||||
market: Literal["Domestic", "Overseas"]
|
||||
order_no: str
|
||||
code: str
|
||||
quantity: int
|
||||
price: float = 0
|
||||
type: str = "00"
|
||||
is_cancel: bool = False # True = Cancel, False = Modify
|
||||
|
||||
@router.get("/price")
|
||||
async def get_current_price(market: Literal["Domestic", "Overseas"], code: str):
|
||||
"""
|
||||
@@ -29,11 +40,22 @@ async def get_balance(market: Literal["Domestic", "Overseas"]):
|
||||
try:
|
||||
data = await kis_client.get_balance(market)
|
||||
return data
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/order")
|
||||
async def modify_order(req: ModifyOrderRequest):
|
||||
logger.info(f"Received Modify/Cancel Request: {req}")
|
||||
try:
|
||||
res = await kis_client.modify_order(
|
||||
req.market, req.order_no, req.code, req.quantity, req.price, req.type, req.is_cancel
|
||||
)
|
||||
return res
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/order")
|
||||
async def place_order(order: OrderRequest):
|
||||
logger.info(f"Received Order Request: {order}")
|
||||
try:
|
||||
res = await kis_client.place_order(order.market, order.side, order.code, order.quantity, order.price)
|
||||
return res
|
||||
|
||||
29
backend/app/api/endpoints/news.py
Normal file
29
backend/app/api/endpoints/news.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import NewsCache
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class NewsItemSchema(BaseModel):
|
||||
news_id: str
|
||||
title: str
|
||||
description: str
|
||||
pubDate: str
|
||||
sentiment: str | None
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@router.get("/", response_model=List[NewsItemSchema])
|
||||
async def get_news(query: str = None, limit: int = 50, db: AsyncSession = Depends(get_db)):
|
||||
stmt = select(NewsCache).order_by(NewsCache.pubDate.desc()).limit(limit)
|
||||
if query:
|
||||
stmt = stmt.where(NewsCache.title.like(f"%{query}%"))
|
||||
|
||||
result = await db.execute(stmt)
|
||||
news = result.scalars().all()
|
||||
return news
|
||||
@@ -6,8 +6,10 @@ from pydantic import BaseModel
|
||||
from app.db.database import get_db
|
||||
from app.db.models import ApiSettings
|
||||
from app.services.kis_auth import kis_auth
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger("SettingsAPI")
|
||||
|
||||
class SettingsSchema(BaseModel):
|
||||
# Partial schema for updates
|
||||
@@ -50,6 +52,7 @@ async def get_settings(db: AsyncSession = Depends(get_db)):
|
||||
|
||||
@router.put("/", response_model=SettingsSchema)
|
||||
async def update_settings(payload: SettingsSchema, db: AsyncSession = Depends(get_db)):
|
||||
logger.info("Updating API Settings...")
|
||||
stmt = select(ApiSettings).where(ApiSettings.id == 1)
|
||||
result = await db.execute(stmt)
|
||||
settings = result.scalar_one_or_none()
|
||||
|
||||
100
backend/app/api/endpoints/trade.py
Normal file
100
backend/app/api/endpoints/trade.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import TradeHistory, AutoTradeRobot, ReservedOrder
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- Schemas ---
|
||||
class TradeOrderSchema(BaseModel):
|
||||
id: str
|
||||
stockName: str
|
||||
type: str
|
||||
quantity: int
|
||||
price: float
|
||||
timestamp: datetime
|
||||
status: str
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ReservedOrderSchema(BaseModel):
|
||||
id: str
|
||||
stockName: str
|
||||
stockCode: str
|
||||
monitoringType: str
|
||||
status: str
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class CreateReservedOrderRequest(BaseModel):
|
||||
stockCode: str
|
||||
stockName: str
|
||||
monitoringType: str # PRICE_TRIGGER
|
||||
triggerPrice: float
|
||||
orderType: str # BUY, SELL
|
||||
quantity: int
|
||||
price: float = 0 # Limit price (0 = Market)
|
||||
|
||||
# Trailing Stop Options
|
||||
trailingType: str | None = None # AMOUNT, PERCENT
|
||||
trailingValue: float | None = None
|
||||
stopLossValue: float | None = None
|
||||
|
||||
class ReservedOrderResponse(BaseModel):
|
||||
id: str
|
||||
status: str
|
||||
|
||||
# --- Endpoints ---
|
||||
|
||||
@router.get("/history/orders", response_model=List[TradeOrderSchema])
|
||||
async def get_trade_history(limit: int = 100, db: AsyncSession = Depends(get_db)):
|
||||
stmt = select(TradeHistory).order_by(TradeHistory.timestamp.desc()).limit(limit)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.get("/reserved-orders", response_model=List[ReservedOrderSchema])
|
||||
async def get_reserved_orders(db: AsyncSession = Depends(get_db)):
|
||||
stmt = select(ReservedOrder)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/reserved-orders")
|
||||
async def create_reserved_order(req: CreateReservedOrderRequest, db: AsyncSession = Depends(get_db)):
|
||||
import uuid
|
||||
new_id = str(uuid.uuid4())
|
||||
|
||||
order = ReservedOrder(
|
||||
id=new_id,
|
||||
stockCode=req.stockCode,
|
||||
stockName=req.stockName,
|
||||
monitoringType=req.monitoringType,
|
||||
triggerPrice=req.triggerPrice,
|
||||
type=req.orderType, # BUY/SELL
|
||||
quantity=req.quantity,
|
||||
price=req.price,
|
||||
|
||||
# TS Fields
|
||||
trailingType=req.trailingType,
|
||||
trailingValue=req.trailingValue,
|
||||
stopLossValue=req.stopLossValue,
|
||||
highestPrice=0, # Init
|
||||
lowestPrice=99999999, # Init
|
||||
|
||||
status="MONITORING",
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(order)
|
||||
await db.commit()
|
||||
return {"id": new_id, "status": "MONITORING"}
|
||||
|
||||
@router.delete("/reserved-orders/{order_id}")
|
||||
async def delete_reserved_order(order_id: str, db: AsyncSession = Depends(get_db)):
|
||||
from sqlalchemy import delete
|
||||
await db.execute(delete(ReservedOrder).where(ReservedOrder.id == order_id))
|
||||
await db.commit()
|
||||
return {"status": "Deleted"}
|
||||
95
backend/app/api/endpoints/watchlist.py
Normal file
95
backend/app/api/endpoints/watchlist.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.orm import selectinload
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import WatchlistGroup, WatchlistItem
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- Schemas ---
|
||||
class WatchlistItemSchema(BaseModel):
|
||||
stock_code: str
|
||||
added_at: datetime
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class WatchlistGroupSchema(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
market: str
|
||||
items: List[WatchlistItemSchema] = []
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class CreateGroupRequest(BaseModel):
|
||||
name: str
|
||||
market: str
|
||||
codes: List[str] = []
|
||||
|
||||
class UpdateGroupRequest(BaseModel):
|
||||
name: str | None = None
|
||||
codes: List[str] | None = None
|
||||
|
||||
# --- Endpoints ---
|
||||
|
||||
@router.get("/", response_model=List[WatchlistGroupSchema])
|
||||
async def get_watchlists(db: AsyncSession = Depends(get_db)):
|
||||
# Load groups with items (Need relationship setup?
|
||||
# Current models.py WatchlistGroup doesn't have `items` relationship defined explicitly in snippet provided?
|
||||
# Let's assume we need to join manually or update models.
|
||||
# Checking models.py... WatchlistItem has foreign key.
|
||||
# Ideally should add `items = relationship("WatchlistItem")` to WatchlistGroup.
|
||||
# For now, let's just fetch items separately or via join.
|
||||
pass
|
||||
# Creating relationship on the fly or assuming simple manual join for safety.
|
||||
|
||||
stmt = select(WatchlistGroup)
|
||||
result = await db.execute(stmt)
|
||||
groups = result.scalars().all()
|
||||
|
||||
resp = []
|
||||
for g in groups:
|
||||
# Fetch items
|
||||
stmt_items = select(WatchlistItem).where(WatchlistItem.group_id == g.id)
|
||||
res_items = await db.execute(stmt_items)
|
||||
items = res_items.scalars().all()
|
||||
|
||||
g_schema = WatchlistGroupSchema(
|
||||
id=g.id, name=g.name, market=g.market,
|
||||
items=[WatchlistItemSchema.model_validate(i) for i in items]
|
||||
)
|
||||
resp.append(g_schema)
|
||||
|
||||
return resp
|
||||
|
||||
@router.post("/", response_model=WatchlistGroupSchema)
|
||||
async def create_watchlist(req: CreateGroupRequest, db: AsyncSession = Depends(get_db)):
|
||||
gid = str(uuid.uuid4())
|
||||
group = WatchlistGroup(id=gid, name=req.name, market=req.market)
|
||||
db.add(group)
|
||||
|
||||
items = []
|
||||
for code in req.codes:
|
||||
item = WatchlistItem(group_id=gid, stock_code=code)
|
||||
db.add(item)
|
||||
items.append(item)
|
||||
|
||||
await db.commit()
|
||||
return WatchlistGroupSchema(
|
||||
id=gid, name=req.name, market=req.market,
|
||||
items=[WatchlistItemSchema(stock_code=i.stock_code, added_at=i.added_at) for i in items]
|
||||
)
|
||||
|
||||
@router.delete("/{group_id}")
|
||||
async def delete_watchlist(group_id: str, db: AsyncSession = Depends(get_db)):
|
||||
# Delete items first (Cascade usually handls this but explicit is safe)
|
||||
await db.execute(delete(WatchlistItem).where(WatchlistItem.group_id == group_id))
|
||||
await db.execute(delete(WatchlistGroup).where(WatchlistGroup.id == group_id))
|
||||
await db.commit()
|
||||
return {"status": "deleted"}
|
||||
Reference in New Issue
Block a user