Initial commit: invest-copilot app

This commit is contained in:
2026-05-30 11:29:20 -04:00
commit c70d6035cc
193 changed files with 22423 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
"""Pydantic v2 request/response schemas for the Invest Copilot API."""
from .stock import (
SearchQuery,
SearchResult,
StockProfile,
PriceEvent,
Peer,
PeerGroupResponse,
StockProfileResponse,
SecFiling,
InsiderTrade,
)
from .price import PriceHistoryResponse
from .watchlist import (
WatchlistCreate,
WatchlistUpdate,
WatchlistResponse,
WatchlistItemCreate,
WatchlistItem,
WatchlistWithPrices,
)
from .sec_filing import SecFiling as SecFilingSchema, SecFilingListResponse
from .insider_trade import InsiderTrade as InsiderTradeSchema
from .strategy import (
StrategyCreate,
StrategyUpdate,
StrategyResponse,
BacktestParams,
BacktestResult,
BacktestResponse,
)
from .alert import AlertResponse
from .sector_rotation import (
SectorRotation,
SectorRotationResponse,
RotationHistoryParams,
)
from .screener import (
ScreenerCreate,
ScreenerResponse,
ScreenerResult,
ScreenerRunResponse,
ScreenerResultsResponse,
)
__all__ = [
# Stock
"SearchQuery",
"SearchResult",
"StockProfile",
"PriceEvent",
"Peer",
"PeerGroupResponse",
"StockProfileResponse",
"SecFiling",
"InsiderTrade",
# Price
"PriceHistoryResponse",
# Watchlist
"WatchlistCreate",
"WatchlistUpdate",
"WatchlistResponse",
"WatchlistItemCreate",
"WatchlistItem",
"WatchlistWithPrices",
# SEC
"SecFilingSchema",
"SecFilingListResponse",
# Insider
"InsiderTradeSchema",
# Strategy
"StrategyCreate",
"StrategyUpdate",
"StrategyResponse",
"BacktestParams",
"BacktestResult",
"BacktestResponse",
# Alert
"AlertResponse",
# Sector
"SectorRotation",
"SectorRotationResponse",
"RotationHistoryParams",
# Screener
"ScreenerCreate",
"ScreenerResponse",
"ScreenerResult",
"ScreenerRunResponse",
"ScreenerResultsResponse",
]
Binary file not shown.
+49
View File
@@ -0,0 +1,49 @@
"""Pydantic schemas for alert endpoints."""
from datetime import datetime
from typing import Optional, Any
from pydantic import BaseModel
class AlertCreate(BaseModel):
"""Request schema for creating an alert."""
watchlist_id: str
type: str
trigger_type: Optional[str] = None
message: str
severity: Optional[str] = "info"
ticker: Optional[str] = None
metadata: dict[str, Any] = {}
class AlertUpdate(BaseModel):
"""Request schema for updating an alert."""
message: Optional[str] = None
severity: Optional[str] = None
trigger_type: Optional[str] = None
metadata: Optional[dict[str, Any]] = None
class AlertResponse(BaseModel):
"""Alert response."""
id: str
watchlist_id: str
type: str
trigger_type: Optional[str] = None
message: str
severity: str = "info"
ticker: Optional[str] = None
status: str = "active"
triggered_at: Optional[datetime] = None
resolved_at: Optional[datetime] = None
metadata: dict[str, Any] = {}
created_at: Optional[datetime] = None
class AlertListResponse(BaseModel):
"""Paginated alert list response."""
alerts: list[AlertResponse]
total: int
page: int
page_size: int
+65
View File
@@ -0,0 +1,65 @@
"""Authentication schemas."""
from typing import Optional
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
"""Request schema for user registration."""
email: EmailStr
password: str
name: Optional[str] = None
class UserLogin(BaseModel):
"""Request schema for user login."""
email: EmailStr
password: str
class TokenResponse(BaseModel):
"""Response schema for authentication tokens."""
access_token: str
token_type: str = "bearer"
user_id: str
email: str
name: Optional[str] = None
class CurrentUserResponse(BaseModel):
"""Response schema for current user profile."""
id: str
email: str
name: Optional[str] = None
timezone: str = "UTC"
class ForgotPasswordRequest(BaseModel):
"""Request schema for forgot password."""
email: EmailStr
class ResetPasswordRequest(BaseModel):
"""Request schema for reset password."""
token: str
password: str
confirm_password: str
def validate_passwords_match(self):
if self.password != self.confirm_password:
raise ValueError("Passwords do not match")
if len(self.password) < 6:
raise ValueError("Password must be at least 6 characters")
class PasswordResetResponse(BaseModel):
"""Response schema for password reset."""
message: str
success: bool = True
class ForgotPasswordResponse(BaseModel):
"""Response schema for forgot password (always returns success to prevent email enumeration)."""
message: str
success: bool = True
+20
View File
@@ -0,0 +1,20 @@
"""Pydantic schemas for insider trade endpoints."""
from datetime import date
from typing import Optional
from pydantic import BaseModel
class InsiderTrade(BaseModel):
"""Insider trade response."""
ticker: str
insiderName: Optional[str] = None
insiderTitle: Optional[str] = None
transactionDate: Optional[date] = None
transactionType: Optional[str] = None
shares: Optional[int] = None
pricePerShare: Optional[float] = None
totalValue: Optional[float] = None
sharesOwnedAfter: Optional[int] = None
filingDate: Optional[date] = None
+12
View File
@@ -0,0 +1,12 @@
"""Pydantic schemas for price endpoints."""
from typing import Optional
from pydantic import BaseModel
class PriceHistoryResponse(BaseModel):
"""Response for price history query."""
ticker: str
interval: str = "1d"
data: list[dict] = []
+65
View File
@@ -0,0 +1,65 @@
"""Pydantic schemas for screener endpoints."""
from datetime import datetime
from typing import Optional, Any
from pydantic import BaseModel, Field
class ScreenerFilter(BaseModel):
"""Single filter/condition for a screener."""
field: str
operator: str
value: Any
class ScreenerRunRequest(BaseModel):
"""Request schema for running a screener."""
background: bool = False
class ScreenerCreate(BaseModel):
"""Request schema for creating a screener."""
name: str
description: Optional[str] = None
filters: list[ScreenerFilter]
class ScreenerUpdate(BaseModel):
"""Request schema for updating a screener."""
name: Optional[str] = None
description: Optional[str] = None
filters: Optional[list[ScreenerFilter]] = None
class ScreenerResponse(BaseModel):
"""Screener response."""
id: str
name: str
description: Optional[str] = None
filters: Optional[list[ScreenerFilter]] = None
last_run_at: Optional[datetime] = None
results_count: int = 0
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class ScreenerResult(BaseModel):
"""Single screener result."""
ticker: str
match_score: Optional[float] = None
ranked_position: Optional[int] = None
result_data: Optional[dict] = None
class ScreenerRunResponse(BaseModel):
"""Response from running a screener."""
message: str
results: list[ScreenerResult] = []
class ScreenerResultsResponse(BaseModel):
"""Response from getting last screener results."""
screener_id: str
results: list[ScreenerResult] = []
run_at: Optional[datetime] = None
+25
View File
@@ -0,0 +1,25 @@
"""Pydantic schemas for SEC filing endpoints."""
from datetime import date
from typing import Optional
from pydantic import BaseModel
class SecFiling(BaseModel):
"""SEC filing response."""
ticker: str
cik: Optional[str] = None
formType: str
filingDate: Optional[date] = None
reportDate: Optional[date] = None
accessionNumber: Optional[str] = None
url: Optional[str] = None
contentSummary: Optional[str] = None
sentimentScore: Optional[float] = None
tags: Optional[list[str]] = []
class SecFilingListResponse(BaseModel):
"""Response for SEC filings list."""
filings: list[SecFiling] = []
+63
View File
@@ -0,0 +1,63 @@
"""Pydantic schemas for sector rotation endpoints."""
from datetime import date, datetime
from typing import Optional
from pydantic import BaseModel
class SectorRotation(BaseModel):
"""Single sector rotation record."""
detectionDate: Optional[date] = None
sectorTicker: str
sectorName: Optional[str] = None
rankNow: Optional[int] = None
rankPrevious: Optional[int] = None
rankChange: Optional[int] = None
momentum20d: Optional[float] = None
momentum50d: Optional[float] = None
momentum200d: Optional[float] = None
relativeStrength: Optional[float] = None
signal: Optional[str] = None
analysisSummary: Optional[str] = None
macroContext: Optional[dict] = None
class SectorRotationResponse(BaseModel):
"""Sector rotation response."""
date: Optional[date] = None
rotations: list[SectorRotation] = []
inRotation: list[str] = []
outOfRotation: list[str] = []
macroContext: Optional[dict] = None
class RotationHistoryParams(BaseModel):
"""Query params for rotation history."""
from_date: date
to: Optional[date] = None
sector: Optional[str] = None
class SectorMomentum(BaseModel):
"""Sector momentum data."""
sectorTicker: str
sectorName: Optional[str] = None
momentum20d: Optional[float] = None
relativeStrength: Optional[float] = None
class SectorRotationHistoryEntry(BaseModel):
"""Single entry in rotation history."""
id: str
sector: str
momentumScore: float
rank: int
date: date
createdAt: Optional[datetime] = None
class SectorRotationHistoryResponse(BaseModel):
"""Sector rotation history response."""
data: list[SectorRotationHistoryEntry] = []
total: int = 0
+123
View File
@@ -0,0 +1,123 @@
"""Pydantic schemas for stock endpoints."""
from datetime import datetime, date
from decimal import Decimal
from typing import Optional, Any
from pydantic import BaseModel, ConfigDict, Field
class SearchQuery(BaseModel):
"""Query parameters for stock search."""
q: str = Field(..., min_length=1, max_length=50)
limit: int = Field(default=10, ge=1, le=50)
class SearchResult(BaseModel):
"""Search result from stock search."""
ticker: str
name: Optional[str] = None
exchange: Optional[str] = None
sector: Optional[str] = None
marketCap: Optional[float] = None
class StockProfile(BaseModel):
"""Stock profile from stock_profiles table."""
ticker: str
name: Optional[str] = None
exchange: Optional[str] = None
sector: Optional[str] = None
industry: Optional[str] = None
marketCap: Optional[float] = None
description: Optional[str] = None
website: Optional[str] = None
ceo: Optional[str] = None
employees: Optional[int] = None
peRatio: Optional[float] = None
eps: Optional[float] = None
dividendYield: Optional[float] = None
beta: Optional[float] = None
class PriceEvent(BaseModel):
"""Single price event from prices table."""
ticker: Optional[str] = None
date: datetime
open: Optional[float] = None
high: Optional[float] = None
low: Optional[float] = None
close: Optional[float] = None
volume: Optional[int] = None
adjustedClose: Optional[float] = None
class Peer(BaseModel):
"""Peer company info."""
ticker: str
name: Optional[str] = None
similarityScore: Optional[float] = None
relativeStrength: Optional[float] = None
priceChange1d: Optional[float] = None
priceChange5d: Optional[float] = None
priceChange30d: Optional[float] = None
class PeerGroupResponse(BaseModel):
"""Peer group response."""
ticker: str
peers: list[Peer] = []
benchmark: Optional[str] = None
benchmarkChange1d: Optional[float] = None
class StockProfileResponse(BaseModel):
"""Full stock profile response."""
profile: StockProfile
price: Optional[PriceEvent] = None
peers: list[Peer] = []
sentiment: Optional["SentimentResponse"] = None
secFilings: list["SecFiling"] = []
insiderTrades: list["InsiderTrade"] = []
# Forward references for circular imports
class SecFiling(BaseModel):
ticker: str
cik: Optional[str] = None
formType: str
filingDate: Optional[date] = None
reportDate: Optional[date] = None
accessionNumber: Optional[str] = None
url: Optional[str] = None
contentSummary: Optional[str] = None
sentimentScore: Optional[float] = None
tags: Optional[list[str]] = []
class InsiderTrade(BaseModel):
ticker: str
insiderName: Optional[str] = None
insiderTitle: Optional[str] = None
transactionDate: Optional[date] = None
transactionType: Optional[str] = None
shares: Optional[int] = None
pricePerShare: Optional[float] = None
totalValue: Optional[float] = None
sharesOwnedAfter: Optional[int] = None
filingDate: Optional[date] = None
class SentimentSignal(BaseModel):
source: str
score: float
confidence: float
timestamp: Optional[datetime] = None
context: Optional[str] = None
class SentimentResponse(BaseModel):
ticker: str
signals: list[SentimentSignal] = []
averageScore: float
confidence: float
+71
View File
@@ -0,0 +1,71 @@
"""Pydantic schemas for strategy endpoints."""
from datetime import datetime, date
from typing import Optional, Any
from pydantic import BaseModel, Field
class StrategyCondition(BaseModel):
"""Single condition rule for a strategy."""
indicator: str
operator: str = Field(pattern=r"^(lt|gt|lte|gte|eq|neq|crossover|crossbelow)$")
value: Optional[float] = None
period: Optional[int] = None
source: Optional[str] = None
description: Optional[str] = None
class StrategyCreate(BaseModel):
"""Request schema for creating a strategy."""
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = Field(default=None, max_length=1000)
type: str = Field(..., pattern=r"^(technical|fundamental|hybrid)$")
conditions: dict = Field(..., description="Structured strategy conditions with rules and logic")
class StrategyUpdate(BaseModel):
"""Request schema for updating a strategy."""
name: Optional[str] = None
description: Optional[str] = None
conditions: Optional[dict] = None
class StrategyResponse(BaseModel):
"""Strategy response."""
id: str
name: str
description: Optional[str] = None
type: str
conditions: dict
backtestResults: Optional[dict] = None
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
class BacktestParams(BaseModel):
"""Parameters for running a backtest."""
start_date: Optional[date] = None
end_date: Optional[date] = None
watchlist_id: Optional[str] = None
class BacktestResult(BaseModel):
"""Backtest result response."""
startDate: Optional[date] = None
endDate: Optional[date] = None
totalReturn: Optional[float] = None
annualizedReturn: Optional[float] = None
maxDrawdown: Optional[float] = None
sharpeRatio: Optional[float] = None
winRate: Optional[float] = None
totalTrades: Optional[int] = None
avgHoldTime: Optional[str] = None
equityCurve: Optional[list[float]] = []
class BacktestResponse(BaseModel):
"""Response from strategy backtest."""
strategy_id: str
results: BacktestResult
runAt: datetime
+61
View File
@@ -0,0 +1,61 @@
"""Pydantic schemas for watchlist endpoints."""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
class WatchlistCreate(BaseModel):
"""Request schema for creating a watchlist."""
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = Field(default=None, max_length=1000)
class WatchlistUpdate(BaseModel):
"""Request schema for updating a watchlist."""
name: Optional[str] = Field(default=None, max_length=255)
description: Optional[str] = Field(default=None, max_length=1000)
class WatchlistResponse(BaseModel):
"""Response schema for a watchlist."""
id: str
name: str
description: Optional[str] = None
isDefault: bool = False
itemCount: int = 0
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
class WatchlistItemCreate(BaseModel):
"""Request schema for adding an item to a watchlist."""
ticker: str = Field(..., max_length=20)
type: str = Field(default="stock", pattern=r"^(stock|etf|index)$")
notes: Optional[str] = Field(default=None, max_length=500)
class WatchlistItem(BaseModel):
"""Watchlist item response."""
ticker: str
type: str
notes: Optional[str] = None
addedAt: Optional[datetime] = None
priceAtAddition: Optional[float] = None
currentPrice: Optional[float] = None
priceChange1d: Optional[float] = None
priceChangePercent: Optional[float] = None
marketCap: Optional[float] = None
class WatchlistWithPrices(BaseModel):
"""Watchlist with items and live prices."""
id: str
name: str
description: Optional[str] = None
isDefault: bool = False
itemCount: int = 0
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
items: list[WatchlistItem] = []