REST API wrapping OpenBB SDK for stock data, sentiment analysis, technical indicators, macro data, and rule-based portfolio analysis. - Stock data via yfinance (quote, profile, metrics, financials, historical, news) - News sentiment via Alpha Vantage (per-article, per-ticker scores) - Analyst data via Finnhub (recommendations, insider trades, upgrades) - Macro data via FRED (Fed rate, CPI, GDP, unemployment, treasury yields) - Technical indicators via openbb-technical (RSI, MACD, SMA, EMA, Bollinger) - Rule-based portfolio analysis engine (BUY_MORE/HOLD/SELL) - Stock discovery (gainers, losers, active, undervalued, growth) - 102 tests, all passing
151 lines
3.6 KiB
Python
151 lines
3.6 KiB
Python
import re
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from pydantic import AwareDatetime, BaseModel, Field, field_validator
|
|
|
|
|
|
# --- Constants ---
|
|
|
|
SYMBOL_PATTERN = re.compile(r"^[A-Za-z0-9.\-]{1,20}$")
|
|
|
|
|
|
# --- Request Models ---
|
|
|
|
|
|
class Holding(BaseModel):
|
|
symbol: str = Field(..., description="Stock symbol, e.g. AAPL or VOLV-B.ST")
|
|
shares: float = Field(..., gt=0, description="Number of shares held")
|
|
buy_in_price: float = Field(..., gt=0, description="Average cost basis per share")
|
|
|
|
@field_validator("symbol")
|
|
@classmethod
|
|
def validate_symbol(cls, v: str) -> str:
|
|
if not SYMBOL_PATTERN.match(v):
|
|
raise ValueError("Invalid symbol format. Use 1-20 alphanumeric chars, dots, or hyphens.")
|
|
return v.upper()
|
|
|
|
|
|
class PortfolioRequest(BaseModel):
|
|
holdings: list[Holding] = Field(..., min_length=1, max_length=50)
|
|
|
|
|
|
# --- Response Models ---
|
|
|
|
|
|
class ActionEnum(str, Enum):
|
|
BUY_MORE = "BUY_MORE"
|
|
HOLD = "HOLD"
|
|
SELL = "SELL"
|
|
|
|
|
|
class ConfidenceEnum(str, Enum):
|
|
HIGH = "HIGH"
|
|
MEDIUM = "MEDIUM"
|
|
LOW = "LOW"
|
|
|
|
|
|
class QuoteResponse(BaseModel):
|
|
symbol: str
|
|
name: str | None = None
|
|
price: float | None = None
|
|
change: float | None = None
|
|
change_percent: float | None = None
|
|
volume: int | None = None
|
|
market_cap: float | None = None
|
|
currency: str | None = None
|
|
|
|
|
|
class ProfileResponse(BaseModel):
|
|
symbol: str
|
|
name: str | None = None
|
|
sector: str | None = None
|
|
industry: str | None = None
|
|
country: str | None = None
|
|
description: str | None = None
|
|
website: str | None = None
|
|
employees: int | None = None
|
|
|
|
|
|
class MetricsResponse(BaseModel):
|
|
symbol: str
|
|
pe_ratio: float | None = None
|
|
pb_ratio: float | None = None
|
|
ps_ratio: float | None = None
|
|
peg_ratio: float | None = None
|
|
roe: float | None = None
|
|
roa: float | None = None
|
|
dividend_yield: float | None = None
|
|
beta: float | None = None
|
|
eps: float | None = None
|
|
revenue_growth: float | None = None
|
|
earnings_growth: float | None = None
|
|
|
|
|
|
class HistoricalBar(BaseModel):
|
|
date: str
|
|
open: float | None = None
|
|
high: float | None = None
|
|
low: float | None = None
|
|
close: float | None = None
|
|
volume: int | None = None
|
|
|
|
|
|
class FinancialsResponse(BaseModel):
|
|
symbol: str
|
|
income: list[dict] = Field(default_factory=list)
|
|
balance: list[dict] = Field(default_factory=list)
|
|
cash_flow: list[dict] = Field(default_factory=list)
|
|
|
|
|
|
class NewsItem(BaseModel):
|
|
title: str | None = None
|
|
url: str | None = None
|
|
date: str | None = None
|
|
source: str | None = None
|
|
|
|
|
|
class SummaryResponse(BaseModel):
|
|
quote: QuoteResponse | None = None
|
|
profile: ProfileResponse | None = None
|
|
metrics: MetricsResponse | None = None
|
|
financials: FinancialsResponse | None = None
|
|
|
|
|
|
class AnalysisResult(BaseModel):
|
|
action: ActionEnum
|
|
confidence: ConfidenceEnum
|
|
reasons: list[str]
|
|
|
|
|
|
class HoldingAnalysis(BaseModel):
|
|
symbol: str
|
|
current_price: float | None = None
|
|
buy_in_price: float
|
|
shares: float
|
|
pnl: float | None = None
|
|
pnl_percent: float | None = None
|
|
metrics: MetricsResponse | None = None
|
|
target_price: float | None = None
|
|
analysis: AnalysisResult
|
|
|
|
|
|
class PortfolioResponse(BaseModel):
|
|
holdings: list[HoldingAnalysis]
|
|
analyzed_at: AwareDatetime
|
|
|
|
|
|
class DiscoverItem(BaseModel):
|
|
symbol: str | None = None
|
|
name: str | None = None
|
|
price: float | None = None
|
|
change_percent: float | None = None
|
|
volume: int | None = None
|
|
market_cap: float | None = None
|
|
|
|
|
|
class ApiResponse(BaseModel):
|
|
success: bool = True
|
|
data: dict[str, Any] | list[Any] | None = None
|
|
error: str | None = None
|