feat: OpenBB Investment Analysis API
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
This commit is contained in:
150
models.py
Normal file
150
models.py
Normal file
@@ -0,0 +1,150 @@
|
||||
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
|
||||
Reference in New Issue
Block a user