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.
349 lines
14 KiB
Python
349 lines
14 KiB
Python
"""
|
|
Real SEC Financial Data Service
|
|
Uses direct SEC EDGAR API to fetch actual SEC filing data and yfinance-plus for price data only
|
|
"""
|
|
|
|
from datetime import datetime, timezone, timedelta
|
|
from typing import Dict, List, Optional, Tuple
|
|
import logging
|
|
import numpy as np
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, and_, or_, desc
|
|
|
|
from app.models.financial import Company, FinancialData, CalculatedMetrics, PriceData
|
|
from app.schemas.financial import DataSource
|
|
from app.services.price_data_service import PriceDataService
|
|
from app.services.sec_edgar_service import SECEdgarService
|
|
from app.core.config import settings
|
|
|
|
# Import yfinance-plus only for price data
|
|
try:
|
|
import yfinance_plus as yf
|
|
YFINANCE_AVAILABLE = True
|
|
logger = logging.getLogger(__name__)
|
|
logger.info("yfinance-plus imported for price data only")
|
|
except ImportError:
|
|
logger = logging.getLogger(__name__)
|
|
logger.error("yfinance-plus not available for price data")
|
|
YFINANCE_AVAILABLE = False
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class RealSECFinancialService:
|
|
"""Real financial service that uses actual SEC EDGAR API"""
|
|
|
|
def __init__(self):
|
|
self.price_service = PriceDataService()
|
|
self.sec_service = SECEdgarService()
|
|
logger.info("SEC EDGAR service initialized")
|
|
|
|
async def get_or_create_company_data(
|
|
self,
|
|
db: AsyncSession,
|
|
ticker: str,
|
|
start_date: datetime,
|
|
end_date: datetime,
|
|
force_refresh: bool = False
|
|
) -> Dict:
|
|
"""
|
|
Get company data with real SEC financial data
|
|
"""
|
|
ticker = ticker.upper()
|
|
|
|
# Get or create company
|
|
company = await self._get_or_create_company(db, ticker)
|
|
|
|
# Get real financial data from SEC EDGAR API
|
|
financial_data = await self.sec_service.get_financial_data(
|
|
db, ticker, start_date, end_date, force_refresh
|
|
)
|
|
|
|
# Get price data for calculations
|
|
price_data = await self._get_price_data_for_period(
|
|
db, ticker, start_date, end_date
|
|
)
|
|
|
|
# Calculate metrics using real data
|
|
calculated_metrics = await self._calculate_real_metrics(
|
|
db, ticker, financial_data, price_data, force_refresh
|
|
)
|
|
|
|
return {
|
|
"company": company,
|
|
"financial_data": financial_data,
|
|
"calculated_metrics": calculated_metrics
|
|
}
|
|
|
|
async def _get_or_create_company(self, db: AsyncSession, ticker: str) -> Company:
|
|
"""Get or create company record using real SEC data"""
|
|
result = await db.execute(
|
|
select(Company).where(Company.ticker == ticker)
|
|
)
|
|
company = result.scalar_one_or_none()
|
|
|
|
if not company:
|
|
# Get real company info from SEC
|
|
company_info = await self._fetch_company_info_from_sec(ticker)
|
|
company = Company(
|
|
ticker=ticker,
|
|
name=company_info["name"],
|
|
cik=company_info["cik"],
|
|
sector=company_info["sector"],
|
|
industry=company_info["industry"],
|
|
business_description=company_info["business_description"],
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
db.add(company)
|
|
await db.commit()
|
|
await db.refresh(company)
|
|
|
|
return company
|
|
|
|
async def _fetch_company_info_from_sec(self, ticker: str) -> Dict:
|
|
"""Fetch real company information from SEC EDGAR API"""
|
|
try:
|
|
return await self.sec_service.get_company_info(ticker)
|
|
except Exception as e:
|
|
logger.error(f"Error fetching SEC company info for {ticker}: {e}")
|
|
return self._get_fallback_company_info(ticker)
|
|
|
|
def _get_fallback_company_info(self, ticker: str) -> Dict:
|
|
"""Fallback company info if SEC is not available"""
|
|
company_defaults = {
|
|
'AAPL': {
|
|
'name': 'Apple Inc.',
|
|
'cik': '0000320193',
|
|
'sector': 'Technology',
|
|
'industry': 'Consumer Electronics',
|
|
'business_description': 'Technology company designing and manufacturing consumer electronics'
|
|
},
|
|
'MSFT': {
|
|
'name': 'Microsoft Corporation',
|
|
'cik': '0000789019',
|
|
'sector': 'Technology',
|
|
'industry': 'Software—Infrastructure',
|
|
'business_description': 'Software and cloud services company'
|
|
},
|
|
'TSLA': {
|
|
'name': 'Tesla Inc.',
|
|
'cik': '0001318605',
|
|
'sector': 'Consumer Cyclical',
|
|
'industry': 'Auto Manufacturers',
|
|
'business_description': 'Electric vehicle and clean energy company'
|
|
},
|
|
'NVDA': {
|
|
'name': 'NVIDIA Corporation',
|
|
'cik': '0001045810',
|
|
'sector': 'Technology',
|
|
'industry': 'Semiconductors',
|
|
'business_description': 'Semiconductor company specializing in graphics processing units'
|
|
}
|
|
}
|
|
|
|
return company_defaults.get(ticker, {
|
|
'name': f'{ticker} Corporation',
|
|
'cik': f'000{hash(ticker) % 1000000:06d}',
|
|
'sector': 'Technology',
|
|
'industry': 'Software',
|
|
'business_description': f'{ticker} technology company'
|
|
})
|
|
|
|
# SEC financial data fetching is now handled by SECEdgarService
|
|
|
|
# Financial data fetching is now handled by SECEdgarService only
|
|
# yfinance is only used for price data via PriceDataService
|
|
|
|
# All financial data processing is now handled by SECEdgarService
|
|
|
|
async def _get_price_data_for_period(
|
|
self,
|
|
db: AsyncSession,
|
|
ticker: str,
|
|
start_date: datetime,
|
|
end_date: datetime
|
|
) -> List[PriceData]:
|
|
"""Get price data for the specified period.
|
|
|
|
Failures (e.g. unknown ticker, yfinance error) are swallowed so that a
|
|
price-fetch problem never causes the financial endpoint to return 500.
|
|
"""
|
|
try:
|
|
return await self.price_service.get_or_update_price_data(
|
|
db, ticker, start_date, end_date, "1d", force_refresh=False
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"Could not fetch price data for {ticker} during financial data processing: {e}"
|
|
)
|
|
return []
|
|
|
|
async def _calculate_real_metrics(
|
|
self,
|
|
db: AsyncSession,
|
|
ticker: str,
|
|
financial_data: List[FinancialData],
|
|
price_data: List[PriceData],
|
|
force_refresh: bool = False
|
|
) -> List[CalculatedMetrics]:
|
|
"""Calculate metrics using real financial and price data"""
|
|
|
|
calculated_metrics = []
|
|
|
|
for financial_record in financial_data:
|
|
period_date = financial_record.period_date
|
|
|
|
# Check if metrics already exist
|
|
existing_metrics = None
|
|
if not force_refresh:
|
|
existing = await db.execute(
|
|
select(CalculatedMetrics).where(
|
|
and_(
|
|
CalculatedMetrics.ticker == ticker,
|
|
CalculatedMetrics.period_date == period_date
|
|
)
|
|
).limit(1)
|
|
)
|
|
existing_metrics = existing.scalar_one_or_none()
|
|
if existing_metrics:
|
|
calculated_metrics.append(existing_metrics)
|
|
continue
|
|
|
|
# Find price data close to the period date
|
|
price_at_period = self._find_price_near_date(price_data, period_date)
|
|
|
|
if not price_at_period:
|
|
logger.warning(f"No price data found for {ticker} near {period_date}")
|
|
continue
|
|
|
|
# Calculate valuation metrics using real price and real financial data
|
|
market_cap = None
|
|
if price_at_period.close and financial_record.shares_outstanding:
|
|
market_cap = price_at_period.close * financial_record.shares_outstanding
|
|
|
|
pe_ratio = None
|
|
if financial_record.eps and financial_record.eps > 0 and price_at_period.close:
|
|
pe_ratio = price_at_period.close / financial_record.eps
|
|
|
|
pb_ratio = None
|
|
if (financial_record.total_equity and financial_record.shares_outstanding and
|
|
financial_record.shares_outstanding > 0 and price_at_period.close):
|
|
book_value_per_share = financial_record.total_equity / financial_record.shares_outstanding
|
|
if book_value_per_share > 0:
|
|
pb_ratio = price_at_period.close / book_value_per_share
|
|
|
|
ps_ratio = None
|
|
if (financial_record.revenue and financial_record.shares_outstanding and
|
|
financial_record.shares_outstanding > 0 and price_at_period.close):
|
|
revenue_per_share = financial_record.revenue / financial_record.shares_outstanding
|
|
if revenue_per_share > 0:
|
|
ps_ratio = price_at_period.close / revenue_per_share
|
|
|
|
# Calculate profitability metrics
|
|
roe = None
|
|
if (financial_record.net_income and financial_record.total_equity and
|
|
financial_record.total_equity > 0):
|
|
roe = financial_record.net_income / financial_record.total_equity
|
|
|
|
roa = None
|
|
if (financial_record.net_income and financial_record.total_assets and
|
|
financial_record.total_assets > 0):
|
|
roa = financial_record.net_income / financial_record.total_assets
|
|
|
|
gross_margin = None
|
|
if (financial_record.gross_profit and financial_record.revenue and
|
|
financial_record.revenue > 0):
|
|
gross_margin = financial_record.gross_profit / financial_record.revenue
|
|
|
|
operating_margin = None
|
|
if (financial_record.operating_income and financial_record.revenue and
|
|
financial_record.revenue > 0):
|
|
operating_margin = financial_record.operating_income / financial_record.revenue
|
|
|
|
net_margin = None
|
|
if (financial_record.net_income and financial_record.revenue and
|
|
financial_record.revenue > 0):
|
|
net_margin = financial_record.net_income / financial_record.revenue
|
|
|
|
# Calculate debt ratios
|
|
debt_to_equity = None
|
|
if (financial_record.total_debt and financial_record.total_equity and
|
|
financial_record.total_equity > 0):
|
|
debt_to_equity = financial_record.total_debt / financial_record.total_equity
|
|
|
|
debt_to_assets = None
|
|
if (financial_record.total_debt and financial_record.total_assets and
|
|
financial_record.total_assets > 0):
|
|
debt_to_assets = financial_record.total_debt / financial_record.total_assets
|
|
|
|
# Calculate cash flow metrics
|
|
ocf_margin = None
|
|
if (financial_record.operating_cash_flow and financial_record.revenue and
|
|
financial_record.revenue > 0):
|
|
ocf_margin = financial_record.operating_cash_flow / financial_record.revenue
|
|
|
|
fcf_margin = None
|
|
if (financial_record.free_cash_flow and financial_record.revenue and
|
|
financial_record.revenue > 0):
|
|
fcf_margin = financial_record.free_cash_flow / financial_record.revenue
|
|
|
|
# Create calculated metrics record
|
|
metrics = CalculatedMetrics(
|
|
ticker=ticker,
|
|
calculation_date=datetime.now(timezone.utc),
|
|
period_date=period_date,
|
|
pe_ratio=pe_ratio,
|
|
pb_ratio=pb_ratio,
|
|
ps_ratio=ps_ratio,
|
|
roe=roe,
|
|
roa=roa,
|
|
gross_margin=gross_margin,
|
|
operating_margin=operating_margin,
|
|
net_margin=net_margin,
|
|
debt_to_equity=debt_to_equity,
|
|
debt_to_assets=debt_to_assets,
|
|
ocf_margin=ocf_margin,
|
|
fcf_margin=fcf_margin,
|
|
market_cap=market_cap,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(metrics)
|
|
calculated_metrics.append(metrics)
|
|
|
|
if calculated_metrics:
|
|
await db.commit()
|
|
for metrics in calculated_metrics:
|
|
await db.refresh(metrics)
|
|
|
|
return calculated_metrics
|
|
|
|
def _find_price_near_date(self, price_data: List[PriceData], target_date: datetime) -> Optional[PriceData]:
|
|
"""Find price data closest to the target date"""
|
|
if not price_data:
|
|
return None
|
|
|
|
# Convert target_date to date for comparison
|
|
target_date_only = target_date.date()
|
|
|
|
closest_price = None
|
|
min_diff = float('inf')
|
|
|
|
for price in price_data:
|
|
price_date = price.date.date() if hasattr(price.date, 'date') else price.date
|
|
diff = abs((price_date - target_date_only).days)
|
|
|
|
if diff < min_diff:
|
|
min_diff = diff
|
|
closest_price = price
|
|
|
|
return closest_price
|
|
|
|
|
|
# Import pandas for data processing
|
|
try:
|
|
import pandas as pd
|
|
except ImportError:
|
|
logger.error("pandas not available - real SEC financial service will not work")
|
|
pd = None |