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.
643 lines
26 KiB
Python
643 lines
26 KiB
Python
"""
|
|
Real financial data service that combines price data with calculations
|
|
"""
|
|
|
|
from datetime import datetime, timezone, timedelta, date
|
|
from typing import Dict, List, Optional, Tuple, Union
|
|
import logging
|
|
import asyncio
|
|
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.utils.date_utils import parse_period, quarters_to_date_range, resolve_time_parameters
|
|
from app.core.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class FinancialService:
|
|
"""Real financial service that uses actual price data for calculations"""
|
|
|
|
def __init__(self):
|
|
self.price_service = PriceDataService()
|
|
|
|
async def _get_ticker_max_range(self, ticker: str) -> Tuple[datetime, datetime]:
|
|
"""
|
|
Get maximum date range for a ticker by checking its listing date via yfinance_plus.
|
|
Runs the blocking yfinance call in a thread pool with a 20s timeout.
|
|
|
|
Returns:
|
|
Tuple of (listing_date, current_date) or fallback to 20 years if yfinance unavailable
|
|
"""
|
|
try:
|
|
import yfinance_plus as yf
|
|
|
|
ticker_obj = yf.Ticker(ticker)
|
|
loop = asyncio.get_event_loop()
|
|
hist = await asyncio.wait_for(
|
|
loop.run_in_executor(
|
|
None,
|
|
lambda: ticker_obj.history(period="max", interval="1mo")
|
|
),
|
|
timeout=20,
|
|
)
|
|
|
|
if not hist.empty:
|
|
earliest_date = hist.index[0].to_pydatetime()
|
|
if earliest_date.tzinfo is None:
|
|
earliest_date = earliest_date.replace(tzinfo=timezone.utc)
|
|
|
|
end_date = datetime.now(timezone.utc).replace(hour=23, minute=59, second=59, microsecond=0)
|
|
sec_start = datetime(settings.SEC_DATA_START_YEAR, 1, 1, tzinfo=timezone.utc)
|
|
actual_start = max(earliest_date, sec_start)
|
|
|
|
logger.info(f"Found actual listing date for {ticker}: {actual_start.date()}")
|
|
return actual_start, end_date
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Could not get ticker info for {ticker}: {e}")
|
|
|
|
# Fallback to 20-year max if yfinance_plus fails
|
|
logger.info(f"Using fallback 20-year range for {ticker}")
|
|
end_date = datetime.now(timezone.utc).replace(hour=23, minute=59, second=59, microsecond=0)
|
|
start_date = end_date - timedelta(days=20 * 365.25) # 20 years
|
|
|
|
sec_start = datetime(settings.SEC_DATA_START_YEAR, 1, 1, tzinfo=timezone.utc)
|
|
actual_start = max(start_date, sec_start)
|
|
|
|
return actual_start, end_date
|
|
|
|
async def get_or_create_company_data(
|
|
self,
|
|
db: AsyncSession,
|
|
ticker: str,
|
|
start_date: Optional[Union[date, datetime]] = None,
|
|
end_date: Optional[Union[date, datetime]] = None,
|
|
quarters: Optional[List[str]] = None,
|
|
period: Optional[str] = None,
|
|
force_refresh: bool = False
|
|
) -> Dict:
|
|
"""
|
|
Get company data with real price-based calculations
|
|
"""
|
|
ticker = ticker.upper()
|
|
|
|
# Resolve time parameters to standard datetime range.
|
|
# _get_ticker_max_range is async, so handle "max" period before calling
|
|
# the sync resolve_time_parameters helper.
|
|
if period and period.lower() == "max":
|
|
resolved_start, resolved_end = await self._get_ticker_max_range(ticker)
|
|
else:
|
|
resolved_start, resolved_end = resolve_time_parameters(
|
|
start_date, end_date, quarters, period, ticker
|
|
)
|
|
|
|
# Get or create company — always succeeds (may have NULL sector for truly unknown tickers)
|
|
company = await self._get_or_create_company(db, ticker)
|
|
|
|
# Fetch financials and price data independently; failures return empty lists.
|
|
try:
|
|
financial_data = await self._get_or_generate_financial_data(
|
|
db, ticker, resolved_start, resolved_end, force_refresh
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Financial data fetch failed for %s: %s", ticker, e)
|
|
financial_data = []
|
|
|
|
try:
|
|
price_data = await self._get_price_data_for_period(
|
|
db, ticker, resolved_start, resolved_end
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Price data fetch failed for %s: %s", ticker, e)
|
|
price_data = []
|
|
|
|
# Calculate metrics using real price 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
|
|
}
|
|
|
|
@staticmethod
|
|
def _is_placeholder(company: Company) -> bool:
|
|
"""True when the row was created by the old hardcoded-defaults path."""
|
|
if not (company.sector == "Technology" and company.industry == "Software"):
|
|
return False
|
|
# Explicit placeholder: synthetic name
|
|
if company.name and company.name.endswith(" Corporation"):
|
|
return True
|
|
# Implicit placeholder: Technology/Software assigned but no exchange means
|
|
# yfinance never actually enriched this row (old defaults path).
|
|
if not getattr(company, "exchange", None):
|
|
return True
|
|
return False
|
|
|
|
async def _get_or_create_company(self, db: AsyncSession, ticker: str) -> Company:
|
|
"""Get or create company record, enriching via CompanyMetadataService."""
|
|
result = await db.execute(select(Company).where(Company.ticker == ticker))
|
|
company = result.scalar_one_or_none()
|
|
# Fast path: row exists with real sector (not a hardcoded placeholder)
|
|
if company and company.sector and not self._is_placeholder(company):
|
|
return company
|
|
|
|
# Enrich via registry + yfinance
|
|
try:
|
|
from app.services import company_metadata_service as cms
|
|
meta = await cms.get_metadata(db, ticker)
|
|
except ValueError:
|
|
# Invalid ticker — still create a minimal placeholder so the rest of the
|
|
# financial pipeline doesn't break.
|
|
meta = {
|
|
"name": f"{ticker} Corporation",
|
|
"cik": None,
|
|
"exchange": None,
|
|
"sector": None,
|
|
"industry": None,
|
|
"country": None,
|
|
"market_cap": None,
|
|
"business_description": None,
|
|
}
|
|
except Exception as e:
|
|
logger.warning("CompanyMetadataService failed for %s: %s — using placeholder", ticker, e)
|
|
meta = {
|
|
"name": f"{ticker} Corporation",
|
|
"cik": None,
|
|
"exchange": None,
|
|
"sector": None,
|
|
"industry": None,
|
|
"country": None,
|
|
"market_cap": None,
|
|
"business_description": None,
|
|
}
|
|
|
|
# cms.get_metadata already upserts the Company row; re-fetch or create if missing.
|
|
result = await db.execute(select(Company).where(Company.ticker == ticker))
|
|
company = result.scalar_one_or_none()
|
|
if not company:
|
|
now = datetime.now(timezone.utc)
|
|
company = Company(
|
|
ticker=ticker,
|
|
name=meta.get("name") or f"{ticker} Corporation",
|
|
cik=meta.get("cik"),
|
|
exchange=meta.get("exchange"),
|
|
sector=meta.get("sector"),
|
|
industry=meta.get("industry"),
|
|
country=meta.get("country"),
|
|
market_cap=meta.get("market_cap"),
|
|
business_description=meta.get("business_description"),
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(company)
|
|
await db.commit()
|
|
await db.refresh(company)
|
|
|
|
return company
|
|
|
|
async def _get_or_generate_financial_data(
|
|
self,
|
|
db: AsyncSession,
|
|
ticker: str,
|
|
start_date: datetime,
|
|
end_date: datetime,
|
|
force_refresh: bool = False
|
|
) -> List[FinancialData]:
|
|
"""Get financial data from database or return empty list if no real data exists"""
|
|
|
|
# First, try to get real data from SEC EDGAR if not force refresh
|
|
if not force_refresh:
|
|
try:
|
|
from app.services.sec_edgar_service import SECEdgarService
|
|
sec_service = SECEdgarService()
|
|
real_financial_data = await sec_service.get_financial_data(
|
|
db, ticker, start_date, end_date, force_refresh
|
|
)
|
|
|
|
# If we got real data, return it
|
|
if real_financial_data:
|
|
logger.info(f"Found {len(real_financial_data)} real financial records for {ticker}")
|
|
return real_financial_data
|
|
else:
|
|
logger.info(f"No real financial data found for {ticker}, returning empty list")
|
|
return []
|
|
except Exception as e:
|
|
logger.error(f"Error fetching real financial data for {ticker}: {e}")
|
|
# Fall back to database check if SEC service fails
|
|
|
|
# Check existing data in database (both real and estimated)
|
|
result = await db.execute(
|
|
select(FinancialData)
|
|
.where(
|
|
and_(
|
|
FinancialData.ticker == ticker,
|
|
FinancialData.period_date >= start_date,
|
|
FinancialData.period_date <= end_date
|
|
)
|
|
)
|
|
.order_by(FinancialData.period_date)
|
|
)
|
|
existing_data = result.scalars().all()
|
|
|
|
if existing_data and not force_refresh:
|
|
return existing_data
|
|
|
|
# If no real data and no existing data, return empty list instead of generating estimated data
|
|
logger.info(f"No financial data available for {ticker} in the requested period")
|
|
return []
|
|
|
|
async def _generate_realistic_financial_data(
|
|
self,
|
|
db: AsyncSession,
|
|
ticker: str,
|
|
start_date: datetime,
|
|
end_date: datetime,
|
|
force_refresh: bool = False
|
|
) -> List[FinancialData]:
|
|
"""Generate realistic financial data based on company size and sector"""
|
|
|
|
# Get company base metrics from real market data if available
|
|
base_metrics = await self._estimate_company_size(db, ticker)
|
|
|
|
# Generate quarterly periods
|
|
quarters = self._generate_quarterly_periods(start_date, end_date)
|
|
|
|
financial_records = []
|
|
|
|
for i, quarter_end in enumerate(quarters):
|
|
# Calculate growth factor based on time progression
|
|
years_from_start = (quarter_end - start_date).days / 365.25
|
|
growth_factor = (1.05) ** years_from_start # 5% annual growth baseline
|
|
|
|
# Add some realistic volatility
|
|
volatility = np.random.normal(1.0, 0.08) # 8% volatility
|
|
|
|
total_factor = growth_factor * volatility
|
|
|
|
# Generate realistic financial metrics
|
|
revenue = base_metrics['revenue'] * total_factor
|
|
gross_profit = revenue * base_metrics['gross_margin']
|
|
operating_income = revenue * base_metrics['operating_margin']
|
|
net_income = revenue * base_metrics['net_margin']
|
|
|
|
# Balance sheet items
|
|
total_assets = base_metrics['total_assets'] * total_factor
|
|
total_equity = total_assets * base_metrics['equity_ratio']
|
|
total_debt = total_assets * base_metrics['debt_ratio']
|
|
cash = total_assets * base_metrics['cash_ratio']
|
|
shares_outstanding = base_metrics['shares_outstanding']
|
|
|
|
# Cash flow items
|
|
operating_cash_flow = net_income * 1.15 # OCF typically higher than net income
|
|
capex = revenue * 0.04 # 4% of revenue
|
|
free_cash_flow = operating_cash_flow - capex
|
|
|
|
eps = net_income / shares_outstanding if shares_outstanding > 0 else 0
|
|
|
|
# Check if record exists
|
|
existing = await db.execute(
|
|
select(FinancialData).where(
|
|
and_(
|
|
FinancialData.ticker == ticker,
|
|
FinancialData.period_date == quarter_end,
|
|
FinancialData.period_type == "quarterly"
|
|
)
|
|
)
|
|
)
|
|
|
|
if existing.scalar_one_or_none() and not force_refresh:
|
|
continue
|
|
|
|
# Create or update financial data record
|
|
financial_record = FinancialData(
|
|
ticker=ticker,
|
|
period_date=quarter_end,
|
|
period_type="quarterly",
|
|
filing_type="10-Q",
|
|
revenue=revenue,
|
|
gross_profit=gross_profit,
|
|
operating_income=operating_income,
|
|
net_income=net_income,
|
|
eps=eps,
|
|
total_assets=total_assets,
|
|
total_equity=total_equity,
|
|
total_debt=total_debt,
|
|
cash=cash,
|
|
shares_outstanding=shares_outstanding,
|
|
operating_cash_flow=operating_cash_flow,
|
|
free_cash_flow=free_cash_flow,
|
|
capex=capex,
|
|
data_source=DataSource.SEC_EDGAR.value,
|
|
is_estimated=True, # Mark as estimated since we're generating it
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
# Delete existing if force refresh
|
|
if force_refresh:
|
|
await db.execute(
|
|
select(FinancialData).where(
|
|
and_(
|
|
FinancialData.ticker == ticker,
|
|
FinancialData.period_date == quarter_end,
|
|
FinancialData.period_type == "quarterly"
|
|
)
|
|
)
|
|
)
|
|
|
|
db.add(financial_record)
|
|
financial_records.append(financial_record)
|
|
|
|
await db.commit()
|
|
|
|
# Refresh all records to get IDs
|
|
for record in financial_records:
|
|
await db.refresh(record)
|
|
|
|
return financial_records
|
|
|
|
async def _estimate_company_size(self, db: AsyncSession, ticker: str) -> Dict:
|
|
"""Estimate company size based on recent price data and industry"""
|
|
|
|
# Get recent price data to estimate market cap
|
|
recent_date = datetime.now(timezone.utc) - timedelta(days=30)
|
|
result = await db.execute(
|
|
select(PriceData)
|
|
.where(
|
|
and_(
|
|
PriceData.ticker == ticker,
|
|
PriceData.date >= recent_date
|
|
)
|
|
)
|
|
.order_by(desc(PriceData.date))
|
|
.limit(1)
|
|
)
|
|
|
|
recent_price = result.scalar_one_or_none()
|
|
|
|
# Default metrics based on typical companies
|
|
default_metrics = {
|
|
'revenue': 50_000_000_000, # $50B
|
|
'total_assets': 75_000_000_000, # $75B
|
|
'shares_outstanding': 1_000_000_000, # 1B shares
|
|
'gross_margin': 0.45, # 45%
|
|
'operating_margin': 0.15, # 15%
|
|
'net_margin': 0.12, # 12%
|
|
'equity_ratio': 0.40, # 40%
|
|
'debt_ratio': 0.25, # 25%
|
|
'cash_ratio': 0.10, # 10%
|
|
}
|
|
|
|
if recent_price:
|
|
# Estimate company size based on current price
|
|
estimated_market_cap = recent_price.close * default_metrics['shares_outstanding']
|
|
|
|
# Adjust metrics based on estimated market cap
|
|
if estimated_market_cap > 1_000_000_000_000: # $1T+ (mega cap)
|
|
scale_factor = 5.0
|
|
elif estimated_market_cap > 200_000_000_000: # $200B+ (large cap)
|
|
scale_factor = 3.0
|
|
elif estimated_market_cap > 10_000_000_000: # $10B+ (mid cap)
|
|
scale_factor = 1.5
|
|
else: # Small cap
|
|
scale_factor = 0.5
|
|
|
|
default_metrics['revenue'] *= scale_factor
|
|
default_metrics['total_assets'] *= scale_factor
|
|
|
|
return default_metrics
|
|
|
|
def _generate_quarterly_periods(self, start_date: datetime, end_date: datetime) -> List[datetime]:
|
|
"""Generate quarterly period end dates"""
|
|
quarters = []
|
|
|
|
# Start from the first quarter end after start_date
|
|
current_year = start_date.year
|
|
quarter_ends = [
|
|
datetime(current_year, 3, 31, tzinfo=timezone.utc),
|
|
datetime(current_year, 6, 30, tzinfo=timezone.utc),
|
|
datetime(current_year, 9, 30, tzinfo=timezone.utc),
|
|
datetime(current_year, 12, 31, tzinfo=timezone.utc),
|
|
]
|
|
|
|
# Find first quarter end >= start_date
|
|
for qe in quarter_ends:
|
|
if qe >= start_date:
|
|
quarters.append(qe)
|
|
|
|
# Add subsequent years
|
|
year = current_year + 1
|
|
while True:
|
|
year_quarters = [
|
|
datetime(year, 3, 31, tzinfo=timezone.utc),
|
|
datetime(year, 6, 30, tzinfo=timezone.utc),
|
|
datetime(year, 9, 30, tzinfo=timezone.utc),
|
|
datetime(year, 12, 31, tzinfo=timezone.utc),
|
|
]
|
|
|
|
added_any = False
|
|
for qe in year_quarters:
|
|
if qe <= end_date:
|
|
quarters.append(qe)
|
|
added_any = True
|
|
else:
|
|
break
|
|
|
|
if not added_any:
|
|
break
|
|
|
|
year += 1
|
|
|
|
return quarters
|
|
|
|
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"""
|
|
|
|
# Try to get from database first
|
|
result = await db.execute(
|
|
select(PriceData)
|
|
.where(
|
|
and_(
|
|
PriceData.ticker == ticker,
|
|
PriceData.date >= start_date,
|
|
PriceData.date <= end_date
|
|
)
|
|
)
|
|
.order_by(PriceData.date)
|
|
)
|
|
|
|
price_data = result.scalars().all()
|
|
|
|
# If no price data, try to fetch it
|
|
if not price_data:
|
|
try:
|
|
price_data = await self.price_service.get_or_update_price_data(
|
|
ticker, start_date, end_date, "1d", force_refresh=False
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Could not fetch price data for {ticker}: {e}")
|
|
price_data = []
|
|
|
|
return price_data
|
|
|
|
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 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
|
|
)
|
|
)
|
|
)
|
|
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
|
|
market_cap = price_at_period.close * financial_record.shares_outstanding if financial_record.shares_outstanding else None
|
|
|
|
pe_ratio = None
|
|
if financial_record.eps and financial_record.eps > 0:
|
|
pe_ratio = price_at_period.close / financial_record.eps
|
|
|
|
pb_ratio = None
|
|
if financial_record.total_equity and financial_record.shares_outstanding:
|
|
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:
|
|
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 |