You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
662 lines
26 KiB
Python
662 lines
26 KiB
Python
"""
|
|
Financial data endpoints
|
|
"""
|
|
|
|
from datetime import datetime, timezone, date
|
|
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db, AsyncSessionLocal
|
|
from app.schemas.financial import (
|
|
FinancialDataRequest,
|
|
FinancialDataResponse,
|
|
BulkFinancialDataRequest,
|
|
BulkFinancialDataResponse,
|
|
BulkFinancialDataItem,
|
|
ErrorResponse,
|
|
ErrorType,
|
|
CompanyInfo,
|
|
FinancialDataPoint,
|
|
CalculatedMetricsData
|
|
)
|
|
from app.services.sec_data_service import SECDataService
|
|
from app.core.config import settings
|
|
from app.utils.date_utils import quarters_to_date_range
|
|
from app.utils.cache import (
|
|
build_cache_key,
|
|
get_cached_response,
|
|
set_cached_response,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
@router.post(
|
|
"/data",
|
|
response_model=FinancialDataResponse,
|
|
responses={
|
|
400: {"model": ErrorResponse, "description": "Invalid request parameters"},
|
|
404: {"model": ErrorResponse, "description": "Data not found"},
|
|
500: {"model": ErrorResponse, "description": "Internal server error"}
|
|
},
|
|
summary="Get SEC EDGAR financial data for a ticker",
|
|
description="""
|
|
Retrieve comprehensive financial data directly from SEC EDGAR filings for a specific ticker and time period.
|
|
|
|
**🔥 Three Ways to Specify Time Period (choose one):**
|
|
|
|
1. **Period String** (NEW! Most convenient):
|
|
- `period`: "1d", "7d", "30d", "1m", "3m", "6m", "1y", "2y", "5y", "max"
|
|
- Examples: `{"ticker": "AAPL", "period": "1y"}` - Last 1 year of data
|
|
- Example: `{"ticker": "TSLA", "period": "max"}` - All available data from listing date to SEC limits
|
|
|
|
2. **Date Range** (Traditional):
|
|
- `start_date` + `end_date`: Specific date range
|
|
- Example: `{"ticker": "AAPL", "start_date": "2024-01-01", "end_date": "2024-12-31"}`
|
|
|
|
3. **Quarters** (Quarter-based):
|
|
- `quarters`: List of quarters like ["2024Q1", "2024Q2"]
|
|
- Example: `{"ticker": "AAPL", "quarters": ["2024Q1", "2024Q2", "2024Q3"]}`
|
|
|
|
**Data Sources:**
|
|
- **Financial Data**: Direct SEC EDGAR API calls (revenue, income, assets, cash flow)
|
|
- **Price Data**: Available via separate price data endpoints using yfinance-plus
|
|
|
|
**This endpoint returns:**
|
|
- Company information (name, CIK, sector, industry)
|
|
- Financial statements data from SEC filings (income statement, balance sheet, cash flow)
|
|
- Calculated financial metrics (ratios, margins, growth rates)
|
|
- Period types: quarterly (10-Q) and annual (10-K) filings
|
|
|
|
**Performance Features:**
|
|
- Database caching to avoid repeated SEC API calls
|
|
- Historical data available from 1994-present
|
|
- 15+ years of data typically available for most companies
|
|
- Use `force_refresh=true` to fetch fresh data from SEC EDGAR
|
|
|
|
**Data Quality:**
|
|
- All financial data sourced directly from official SEC filings
|
|
- No estimated or synthetic data - only actual reported figures
|
|
- Automatic validation and error handling for missing periods
|
|
|
|
**Example Requests:**
|
|
```json
|
|
// Using period (simplest)
|
|
{
|
|
"ticker": "AAPL",
|
|
"period": "1y",
|
|
"include_metrics": true
|
|
}
|
|
|
|
// Using date range
|
|
{
|
|
"ticker": "MSFT",
|
|
"start_date": "2024-01-01",
|
|
"end_date": "2024-12-31",
|
|
"period_type": "quarterly"
|
|
}
|
|
|
|
// Using quarters
|
|
{
|
|
"ticker": "GOOGL",
|
|
"quarters": ["2024Q1", "2024Q2"],
|
|
"include_metrics": true
|
|
}
|
|
```
|
|
"""
|
|
)
|
|
async def get_financial_data(
|
|
request: FinancialDataRequest,
|
|
response: Response,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get financial data for a ticker using period, quarters, or date range"""
|
|
|
|
try:
|
|
# Use the updated service that handles period resolution
|
|
from app.services.financial_service import FinancialService
|
|
financial_service = FinancialService()
|
|
|
|
# Resolve time parameters for metadata
|
|
from app.utils.date_utils import resolve_time_parameters
|
|
resolved_start, resolved_end = resolve_time_parameters(
|
|
request.start_date, request.end_date, request.quarters, request.period, request.ticker,
|
|
ticker_max_range_fn=financial_service._get_ticker_max_range
|
|
)
|
|
|
|
# Build cache key using normalized inputs
|
|
cache_key = build_cache_key(
|
|
"financial:data",
|
|
request.ticker.upper(),
|
|
request.period_type.value if hasattr(request.period_type, 'value') else str(request.period_type),
|
|
"metrics" if request.include_metrics else "no-metrics",
|
|
(resolved_start.date().isoformat() if resolved_start else ""),
|
|
(resolved_end.date().isoformat() if resolved_end else ""),
|
|
)
|
|
|
|
# Try cache unless force_refresh
|
|
if not request.force_refresh:
|
|
cached = await get_cached_response(cache_key)
|
|
if cached:
|
|
cached_body, etag = cached
|
|
response.headers["X-Cache"] = "HIT"
|
|
response.headers["Cache-Control"] = f"public, max-age={settings.CACHE_TTL}"
|
|
response.headers["ETag"] = etag
|
|
return cached_body
|
|
|
|
data = await financial_service.get_or_create_company_data(
|
|
db,
|
|
request.ticker,
|
|
start_date=request.start_date,
|
|
end_date=request.end_date,
|
|
quarters=request.quarters,
|
|
period=request.period,
|
|
force_refresh=request.force_refresh
|
|
)
|
|
|
|
# Format response
|
|
company = data["company"]
|
|
financial_data = data["financial_data"]
|
|
calculated_metrics = data["calculated_metrics"]
|
|
|
|
# Convert to response models
|
|
company_info = CompanyInfo(
|
|
ticker=company.ticker,
|
|
name=company.name,
|
|
cik=company.cik,
|
|
sector=company.sector,
|
|
industry=company.industry,
|
|
business_description=company.business_description
|
|
)
|
|
|
|
# Filter financial data by period type
|
|
if request.period_type != "all":
|
|
financial_data = [fd for fd in financial_data if fd.period_type == request.period_type]
|
|
|
|
# Merge financial data with calculated metrics
|
|
financial_points = []
|
|
for fd in financial_data:
|
|
# Convert to dict for merging
|
|
fd_dict = fd.__dict__ if hasattr(fd, '__dict__') else {}
|
|
|
|
# Find matching calculated metrics for this period
|
|
matching_metrics = None
|
|
if request.include_metrics and calculated_metrics:
|
|
for cm in calculated_metrics:
|
|
if cm.period_date == fd.period_date:
|
|
matching_metrics = cm
|
|
break
|
|
|
|
# Merge metrics into financial data point
|
|
if matching_metrics:
|
|
fd_dict.update({
|
|
'pe_ratio': matching_metrics.pe_ratio,
|
|
'pb_ratio': matching_metrics.pb_ratio,
|
|
'ps_ratio': matching_metrics.ps_ratio,
|
|
'roe': matching_metrics.roe,
|
|
'roa': matching_metrics.roa,
|
|
'gross_margin': matching_metrics.gross_margin,
|
|
'operating_margin': matching_metrics.operating_margin,
|
|
'net_margin': matching_metrics.net_margin,
|
|
'debt_to_equity': matching_metrics.debt_to_equity,
|
|
'debt_to_assets': matching_metrics.debt_to_assets,
|
|
'ocf_margin': matching_metrics.ocf_margin,
|
|
'fcf_margin': matching_metrics.fcf_margin,
|
|
'market_cap': matching_metrics.market_cap
|
|
})
|
|
|
|
financial_points.append(FinancialDataPoint.model_validate(fd_dict))
|
|
|
|
# Calculate actual date range from returned data
|
|
actual_start_date = resolved_start
|
|
actual_end_date = resolved_end
|
|
|
|
if financial_points:
|
|
# Get actual start and end dates from the financial data
|
|
actual_start_date = min(point.period_date for point in financial_points)
|
|
actual_end_date = max(point.period_date for point in financial_points)
|
|
|
|
body = FinancialDataResponse(
|
|
company=company_info,
|
|
financial_data=financial_points,
|
|
metadata={
|
|
"request_id": str(request.ticker),
|
|
"data_points": len(financial_points),
|
|
"period_type": request.period_type.value,
|
|
"quarters_requested": request.quarters if request.quarters else None,
|
|
"date_range": {
|
|
"start": actual_start_date.isoformat(),
|
|
"end": actual_end_date.isoformat()
|
|
},
|
|
"last_updated": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
)
|
|
|
|
# Cache the response
|
|
etag = await set_cached_response(cache_key, body.model_dump(), ttl_seconds=settings.CACHE_TTL)
|
|
response.headers["X-Cache"] = "MISS"
|
|
response.headers["Cache-Control"] = f"public, max-age={settings.CACHE_TTL}"
|
|
response.headers["ETag"] = etag
|
|
return body
|
|
|
|
except ValueError as e:
|
|
if "No data returned" in str(e):
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={
|
|
"error_type": ErrorType.DATA_NOT_FOUND,
|
|
"message": f"No financial data found for ticker {request.ticker}",
|
|
"detail": {
|
|
"ticker": request.ticker,
|
|
"period": f"{request.start_date} to {request.end_date}"
|
|
}
|
|
}
|
|
)
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error_type": ErrorType.PARSING_ERROR,
|
|
"message": str(e)
|
|
}
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={
|
|
"error_type": ErrorType.DATABASE_ERROR,
|
|
"message": "An error occurred while processing your request",
|
|
"detail": {"error": str(e)}
|
|
}
|
|
)
|
|
|
|
@router.get(
|
|
"/data/{ticker}",
|
|
response_model=FinancialDataResponse,
|
|
summary="Get financial data by ticker (simplified)",
|
|
description="""
|
|
Simplified GET endpoint to retrieve financial data with query parameters.
|
|
|
|
**Time Period Options:**
|
|
- Use `period` for convenience: "1d", "7d", "1m", "3m", "6m", "1y", "2y", "5y", "max"
|
|
- OR use `start_date` and `end_date` for specific date range
|
|
- Cannot use both approaches simultaneously
|
|
|
|
**Examples:**
|
|
- `/api/v1/financial/data/AAPL?period=1y&include_metrics=true` - Last year of financial data
|
|
- `/api/v1/financial/data/AAPL?start_date=2024-01-01&end_date=2024-12-31&period_type=quarterly` - Specific date range
|
|
"""
|
|
)
|
|
async def get_financial_data_simple(
|
|
ticker: str,
|
|
response: Response,
|
|
period: Optional[str] = Query(None, description="Period like '1d', '7d', '1m', '3m', '6m', '1y', '2y', '5y', 'max'"),
|
|
start_date: Optional[date] = Query(None, description="Start date for data retrieval (use with end_date, not with period)"),
|
|
end_date: Optional[date] = Query(None, description="End date for data retrieval (use with start_date, not with period)"),
|
|
period_type: str = Query("all", description="Period type: quarterly, annual, or all"),
|
|
include_metrics: bool = Query(True, description="Include calculated metrics"),
|
|
force_refresh: bool = Query(False, description="Force refresh from SEC"),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Simplified GET endpoint for financial data"""
|
|
# Validate that either period OR date range is provided, not both
|
|
if period and (start_date or end_date):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error_type": ErrorType.VALIDATION_ERROR,
|
|
"message": "Cannot specify both period and date range. Use either period OR start_date+end_date."
|
|
}
|
|
)
|
|
|
|
if not period and not (start_date and end_date):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error_type": ErrorType.VALIDATION_ERROR,
|
|
"message": "Must specify either period OR both start_date and end_date."
|
|
}
|
|
)
|
|
|
|
# Create request based on provided parameters
|
|
if period:
|
|
request = FinancialDataRequest(
|
|
ticker=ticker,
|
|
period=period,
|
|
period_type=period_type,
|
|
include_metrics=include_metrics,
|
|
force_refresh=force_refresh
|
|
)
|
|
else:
|
|
request = FinancialDataRequest(
|
|
ticker=ticker,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
period_type=period_type,
|
|
include_metrics=include_metrics,
|
|
force_refresh=force_refresh
|
|
)
|
|
|
|
return await get_financial_data(request, response, db)
|
|
|
|
@router.post(
|
|
"/data/bulk",
|
|
response_model=BulkFinancialDataResponse,
|
|
responses={
|
|
400: {"model": ErrorResponse, "description": "Invalid request parameters"},
|
|
500: {"model": ErrorResponse, "description": "Internal server error"}
|
|
},
|
|
summary="Get SEC EDGAR financial data for multiple tickers",
|
|
description="""
|
|
Retrieve comprehensive financial data for multiple tickers in a single request directly from SEC EDGAR filings.
|
|
|
|
**🔥 Three Ways to Specify Time Period (choose one):**
|
|
|
|
1. **Period String** (NEW! Most convenient):
|
|
- `period`: "1d", "7d", "30d", "1m", "3m", "6m", "1y", "2y", "5y", "max"
|
|
- Example: Last 1 year for multiple tickers, or "max" for all available data
|
|
|
|
2. **Date Range** (Traditional):
|
|
- `start_date` + `end_date`: Specific date range
|
|
- Example: Specific date range for all tickers
|
|
|
|
3. **Quarters** (Quarter-based):
|
|
- `quarters`: List of quarters like ["2024Q1", "2024Q2"]
|
|
- Example: Specific quarters for all tickers
|
|
|
|
**Data Sources:**
|
|
- **Financial Data**: Direct SEC EDGAR API calls (revenue, income, assets, cash flow)
|
|
- **Price Data**: Available via separate price data endpoints using yfinance-plus
|
|
|
|
**Bulk Processing Features:**
|
|
- Processes up to 100 tickers in parallel for maximum efficiency
|
|
- Returns individual success/failure results for each ticker
|
|
- Handles partial failures gracefully (some tickers can fail while others succeed)
|
|
- Uses the same robust SEC data retrieval logic as single ticker endpoint
|
|
|
|
**SEC EDGAR Integration:**
|
|
- Direct API calls to official SEC EDGAR database
|
|
- All financial data sourced from actual SEC filings (10-K, 10-Q)
|
|
- No estimated or synthetic data - only actual reported figures
|
|
- Historical data available from 1994-present (15+ years for most companies)
|
|
- Automatic validation and error handling for missing periods
|
|
|
|
**Data Quality & Features:**
|
|
- Company information (name, CIK, sector, industry, business description)
|
|
- Comprehensive financial statements (income statement, balance sheet, cash flow)
|
|
- Calculated financial metrics (ratios, margins, growth rates)
|
|
- Period types: quarterly (10-Q) and annual (10-K) filings
|
|
- Database caching to avoid repeated SEC API calls
|
|
|
|
**Performance:**
|
|
- Parallel processing for bulk requests
|
|
- Intelligent caching and rate limiting
|
|
- Use `force_refresh=true` to fetch fresh data from SEC EDGAR
|
|
|
|
**Example Requests:**
|
|
```json
|
|
// Using period (simplest)
|
|
{
|
|
"tickers": ["AAPL", "MSFT", "GOOGL"],
|
|
"period": "1y",
|
|
"include_metrics": true
|
|
}
|
|
|
|
// Using date range
|
|
{
|
|
"tickers": ["NVDA", "AMD", "INTC"],
|
|
"start_date": "2024-01-01",
|
|
"end_date": "2024-12-31",
|
|
"period_type": "quarterly"
|
|
}
|
|
|
|
// Using quarters
|
|
{
|
|
"tickers": ["TSLA", "F", "GM"],
|
|
"quarters": ["2024Q1", "2024Q2"],
|
|
"include_metrics": true
|
|
}
|
|
```
|
|
|
|
Each ticker result includes the same comprehensive financial data structure as the single ticker endpoint.
|
|
Failed tickers will have detailed error messages while successful ones will have complete SEC filing data.
|
|
"""
|
|
)
|
|
async def get_bulk_financial_data(
|
|
request: BulkFinancialDataRequest,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get financial data for multiple tickers"""
|
|
|
|
# Convert time parameters to dates using the same logic as single endpoint
|
|
if request.period:
|
|
# Use period approach - import the parse_period function
|
|
from app.utils.date_utils import parse_period
|
|
try:
|
|
start_date, end_date = parse_period(request.period)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error_type": ErrorType.VALIDATION_ERROR,
|
|
"message": f"Invalid period format: {str(e)}"
|
|
}
|
|
)
|
|
elif request.quarters:
|
|
try:
|
|
start_date, end_date = quarters_to_date_range(request.quarters)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error_type": ErrorType.VALIDATION_ERROR,
|
|
"message": str(e)
|
|
}
|
|
)
|
|
else:
|
|
# Convert date to datetime for internal processing
|
|
start_date = datetime.combine(request.start_date, datetime.min.time()).replace(tzinfo=timezone.utc) if request.start_date else None
|
|
end_date = datetime.combine(request.end_date, datetime.max.time()).replace(tzinfo=timezone.utc) if request.end_date else None
|
|
|
|
# Validate date range
|
|
if start_date and end_date and start_date >= end_date:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error_type": ErrorType.VALIDATION_ERROR,
|
|
"message": "Start date must be before end date"
|
|
}
|
|
)
|
|
|
|
# Check if requested period is valid (SEC data available from 1994)
|
|
if start_date and start_date.year < settings.SEC_DATA_START_YEAR:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error_type": ErrorType.INVALID_PERIOD,
|
|
"message": f"SEC data is only available from {settings.SEC_DATA_START_YEAR}",
|
|
"detail": {
|
|
"requested_start": start_date.isoformat(),
|
|
"earliest_available": f"{settings.SEC_DATA_START_YEAR}-01-01"
|
|
}
|
|
}
|
|
)
|
|
|
|
# Future date check
|
|
current_time = datetime.now(timezone.utc)
|
|
if start_date and start_date > current_time:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error_type": ErrorType.INVALID_PERIOD,
|
|
"message": "Cannot request data for future dates"
|
|
}
|
|
)
|
|
|
|
import asyncio
|
|
|
|
results = []
|
|
successful_count = 0
|
|
failed_count = 0
|
|
|
|
async def process_ticker(ticker: str):
|
|
"""Process a single ticker and return result"""
|
|
try:
|
|
# Use an isolated DB session per task to avoid concurrent use of a single session
|
|
async with AsyncSessionLocal() as session:
|
|
# Get data using existing service
|
|
sec_service = SECDataService()
|
|
data = await sec_service.get_or_update_company_data(
|
|
session,
|
|
ticker,
|
|
start_date,
|
|
end_date,
|
|
request.force_refresh
|
|
)
|
|
|
|
# Format response
|
|
company = data["company"]
|
|
financial_data = data["financial_data"]
|
|
calculated_metrics = data["calculated_metrics"] # Always include metrics
|
|
|
|
# Convert to response models
|
|
company_info = CompanyInfo(
|
|
ticker=company.ticker,
|
|
name=company.name,
|
|
cik=company.cik,
|
|
sector=company.sector,
|
|
industry=company.industry,
|
|
business_description=company.business_description
|
|
)
|
|
|
|
# Filter financial data by period type
|
|
if request.period_type != "all":
|
|
financial_data = [fd for fd in financial_data if fd.period_type == request.period_type]
|
|
|
|
# Merge financial data with calculated metrics
|
|
financial_points = []
|
|
for fd in financial_data:
|
|
# Convert to dict for merging
|
|
fd_dict = fd.__dict__ if hasattr(fd, '__dict__') else {}
|
|
|
|
# Find matching calculated metrics for this period
|
|
matching_metrics = None
|
|
if request.include_metrics and calculated_metrics:
|
|
for cm in calculated_metrics:
|
|
if cm.period_date == fd.period_date:
|
|
matching_metrics = cm
|
|
break
|
|
|
|
# Merge metrics into financial data point
|
|
if matching_metrics:
|
|
fd_dict.update({
|
|
'pe_ratio': matching_metrics.pe_ratio,
|
|
'pb_ratio': matching_metrics.pb_ratio,
|
|
'ps_ratio': matching_metrics.ps_ratio,
|
|
'roe': matching_metrics.roe,
|
|
'roa': matching_metrics.roa,
|
|
'gross_margin': matching_metrics.gross_margin,
|
|
'operating_margin': matching_metrics.operating_margin,
|
|
'net_margin': matching_metrics.net_margin,
|
|
'debt_to_equity': matching_metrics.debt_to_equity,
|
|
'debt_to_assets': matching_metrics.debt_to_assets,
|
|
'ocf_margin': matching_metrics.ocf_margin,
|
|
'fcf_margin': matching_metrics.fcf_margin,
|
|
'market_cap': matching_metrics.market_cap
|
|
})
|
|
|
|
financial_points.append(FinancialDataPoint.model_validate(fd_dict))
|
|
|
|
# Calculate actual date range from returned data
|
|
actual_start_date = start_date
|
|
actual_end_date = end_date
|
|
|
|
if financial_points:
|
|
# Get actual start and end dates from the financial data
|
|
actual_start_date = min(point.period_date for point in financial_points)
|
|
actual_end_date = max(point.period_date for point in financial_points)
|
|
|
|
response = FinancialDataResponse(
|
|
company=company_info,
|
|
financial_data=financial_points,
|
|
metadata={
|
|
"request_id": str(ticker),
|
|
"data_points": len(financial_points),
|
|
"period_type": request.period_type.value,
|
|
"quarters_requested": request.quarters if request.quarters else None,
|
|
"date_range": {
|
|
"start": actual_start_date.isoformat(),
|
|
"end": actual_end_date.isoformat()
|
|
},
|
|
"last_updated": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
)
|
|
|
|
return BulkFinancialDataItem(
|
|
ticker=ticker,
|
|
success=True,
|
|
data=response,
|
|
error=None
|
|
)
|
|
|
|
except Exception as e:
|
|
# Handle individual ticker failure
|
|
error_message = str(e)
|
|
if "No data returned" in error_message:
|
|
error_message = f"No financial data found for ticker {ticker}"
|
|
elif "Invalid ticker" in error_message:
|
|
error_message = f"Invalid or unknown ticker: {ticker}"
|
|
|
|
return BulkFinancialDataItem(
|
|
ticker=ticker,
|
|
success=False,
|
|
data=None,
|
|
error=error_message
|
|
)
|
|
|
|
# Process all tickers in parallel with concurrency limit
|
|
semaphore = asyncio.Semaphore(10) # Limit concurrent operations to avoid overwhelming DB/APIs
|
|
|
|
async def process_with_limit(ticker: str):
|
|
async with semaphore:
|
|
return await process_ticker(ticker)
|
|
|
|
# Execute all tickers in parallel
|
|
tasks = [process_with_limit(ticker) for ticker in request.tickers]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# Count successful and failed results
|
|
successful_count = 0
|
|
failed_count = 0
|
|
|
|
for i, result in enumerate(results):
|
|
if isinstance(result, Exception):
|
|
# Handle unexpected exceptions
|
|
error_message = f"Unexpected error processing {request.tickers[i]}: {str(result)}"
|
|
results[i] = BulkFinancialDataItem(
|
|
ticker=request.tickers[i],
|
|
success=False,
|
|
data=None,
|
|
error=error_message
|
|
)
|
|
failed_count += 1
|
|
elif result.success:
|
|
successful_count += 1
|
|
else:
|
|
failed_count += 1
|
|
|
|
return BulkFinancialDataResponse(
|
|
results=results,
|
|
metadata={
|
|
"total_requested": len(request.tickers),
|
|
"successful": successful_count,
|
|
"failed": failed_count,
|
|
"period_type": request.period_type.value,
|
|
"quarters_requested": request.quarters if request.quarters else None,
|
|
"date_range": {
|
|
"start": start_date.isoformat(),
|
|
"end": end_date.isoformat()
|
|
},
|
|
"force_refresh": request.force_refresh,
|
|
"processed_at": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
) |