Initial commit: invest-copilot app
This commit is contained in:
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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] = []
|
||||
@@ -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
|
||||
@@ -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] = []
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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] = []
|
||||
Reference in New Issue
Block a user