From d671befb903b963959c979e4cadaa36f70a68300 Mon Sep 17 00:00:00 2001 From: zephyrdark Date: Tue, 3 Feb 2026 08:57:05 +0900 Subject: [PATCH] feat: add quant strategy Pydantic schemas Co-Authored-By: Claude Opus 4.5 --- backend/app/schemas/__init__.py | 10 +++ backend/app/schemas/strategy.py | 132 ++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 backend/app/schemas/strategy.py diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 69ed0e3..54b043c 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -9,6 +9,12 @@ from app.schemas.portfolio import ( RebalanceItem, RebalanceResponse, RebalanceSimulationRequest, RebalanceSimulationResponse, ) +from app.schemas.strategy import ( + FactorWeights, UniverseFilter, + StrategyRequest, MultiFactorRequest, QualityRequest, ValueMomentumRequest, + StockFactor, StrategyResult, + StockInfo, StockSearchResult, PriceData, +) __all__ = [ "UserBase", @@ -24,4 +30,8 @@ __all__ = [ "SnapshotResponse", "SnapshotHoldingResponse", "RebalanceItem", "RebalanceResponse", "RebalanceSimulationRequest", "RebalanceSimulationResponse", + "FactorWeights", "UniverseFilter", + "StrategyRequest", "MultiFactorRequest", "QualityRequest", "ValueMomentumRequest", + "StockFactor", "StrategyResult", + "StockInfo", "StockSearchResult", "PriceData", ] diff --git a/backend/app/schemas/strategy.py b/backend/app/schemas/strategy.py new file mode 100644 index 0000000..23de58f --- /dev/null +++ b/backend/app/schemas/strategy.py @@ -0,0 +1,132 @@ +""" +Quant strategy related Pydantic schemas. +""" +from datetime import date +from decimal import Decimal +from typing import Optional, List, Dict + +from pydantic import BaseModel, Field + + +class FactorWeights(BaseModel): + """Factor weights for multi-factor strategy.""" + value: Decimal = Field(default=Decimal("0.25"), ge=0, le=1) + quality: Decimal = Field(default=Decimal("0.25"), ge=0, le=1) + momentum: Decimal = Field(default=Decimal("0.25"), ge=0, le=1) + low_vol: Decimal = Field(default=Decimal("0.25"), ge=0, le=1) + + +class UniverseFilter(BaseModel): + """Stock universe filtering criteria.""" + markets: List[str] = ["KOSPI", "KOSDAQ"] + min_market_cap: Optional[int] = None # in 억원 + max_market_cap: Optional[int] = None + exclude_stock_types: List[str] = ["spac", "preferred", "reit"] + exclude_sectors: List[str] = [] + + +class StrategyRequest(BaseModel): + """Base request for running a strategy.""" + universe: UniverseFilter = UniverseFilter() + top_n: int = Field(default=30, ge=1, le=100) + base_date: Optional[date] = None + + +class MultiFactorRequest(StrategyRequest): + """Multi-factor strategy request.""" + weights: FactorWeights = FactorWeights() + + +class QualityRequest(StrategyRequest): + """Super Quality strategy request.""" + min_fscore: int = Field(default=7, ge=0, le=9) + + +class ValueMomentumRequest(StrategyRequest): + """Value-Momentum strategy request.""" + value_weight: Decimal = Field(default=Decimal("0.5"), ge=0, le=1) + momentum_weight: Decimal = Field(default=Decimal("0.5"), ge=0, le=1) + + +class StockFactor(BaseModel): + """Factor scores for a single stock.""" + ticker: str + name: str + market: str + sector_name: Optional[str] = None + market_cap: Optional[int] = None + close_price: Optional[Decimal] = None + + # Raw metrics + per: Optional[Decimal] = None + pbr: Optional[Decimal] = None + psr: Optional[Decimal] = None + pcr: Optional[Decimal] = None + dividend_yield: Optional[Decimal] = None + roe: Optional[Decimal] = None + + # Factor scores (z-scores) + value_score: Optional[Decimal] = None + quality_score: Optional[Decimal] = None + momentum_score: Optional[Decimal] = None + + # Composite + total_score: Optional[Decimal] = None + rank: Optional[int] = None + fscore: Optional[int] = None + + +class StrategyResult(BaseModel): + """Result from running a strategy.""" + strategy_name: str + base_date: date + universe_count: int + result_count: int + stocks: List[StockFactor] + + +class StockInfo(BaseModel): + """Detailed stock information.""" + ticker: str + name: str + market: str + sector_name: Optional[str] = None + stock_type: str + close_price: Optional[Decimal] = None + market_cap: Optional[int] = None + + # Valuation + per: Optional[Decimal] = None + pbr: Optional[Decimal] = None + psr: Optional[Decimal] = None + pcr: Optional[Decimal] = None + dividend_yield: Optional[Decimal] = None + + # Per-share data + eps: Optional[Decimal] = None + bps: Optional[Decimal] = None + + base_date: Optional[date] = None + + class Config: + from_attributes = True + + +class StockSearchResult(BaseModel): + """Stock search result.""" + ticker: str + name: str + market: str + + +class PriceData(BaseModel): + """Price data point.""" + date: date + open: Decimal + high: Decimal + low: Decimal + close: Decimal + volume: int + + class Config: + from_attributes = True