diff options
Diffstat (limited to 'backend/app/services/data_service.py')
| -rw-r--r-- | backend/app/services/data_service.py | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/backend/app/services/data_service.py b/backend/app/services/data_service.py index 16e062e..bf75bc7 100644 --- a/backend/app/services/data_service.py +++ b/backend/app/services/data_service.py @@ -113,6 +113,173 @@ def _safe_ratio(num: float | None, den: float | None) -> float | None: return num / den +def _build_income(frame: pd.DataFrame, frame_q: pd.DataFrame, quarterly: bool) -> dict: + if frame is None or frame.empty: + return {"columns": [], "rows": []} + n = min(len(frame.columns), 8 if quarterly else 4) + col_labels = [_fmt_col(c, quarterly) for c in frame.columns[:n]] + if not quarterly: + col_labels.append("TTM") + + def v(label: str) -> list[float | None]: + base = _row_vals(frame, label, n) + return base + ([_statement_ttm(frame_q, label)] if not quarterly else []) + + def vm(*labels: str) -> list[float | None]: + base = _row_vals_multi(frame, n, *labels) + if not quarterly: + ttm = None + for lbl in labels: + ttm = _statement_ttm(frame_q, lbl) + if ttm is not None: + break + base = base + [ttm] + return base + + rev = v("Total Revenue") + gross = v("Gross Profit") + net = v("Net Income") + + return { + "columns": col_labels, + "rows": [ + _fin_row("Total Revenue", 0, True, rev), + _fin_row("Cost of Revenue", 1, False, v("Cost Of Revenue")), + _fin_row("Gross Profit", 0, True, gross), + _fin_margin("gross margin", [_safe_ratio(g, r) for g, r in zip(gross, rev)]), + _fin_row("Operating Expenses", 1, False, v("Operating Expense")), + _fin_row("Operating Income", 0, True, v("Operating Income")), + _fin_row("EBITDA", 1, False, vm("EBITDA", "Normalized EBITDA")), + _fin_row("Interest Expense", 1, False, v("Interest Expense")), + _fin_row("Pretax Income", 0, False, v("Pretax Income")), + _fin_row("Tax Provision", 1, False, v("Tax Provision")), + _fin_row("Net Income", 0, True, net), + _fin_margin("net margin", [_safe_ratio(ni, r) for ni, r in zip(net, rev)]), + _fin_row("EPS Basic", 1, False, v("Basic EPS")), + ], + } + + +def _build_balance(frame: pd.DataFrame, frame_q: pd.DataFrame, quarterly: bool) -> dict: + if frame is None or frame.empty: + return {"columns": [], "rows": []} + n = min(len(frame.columns), 8 if quarterly else 4) + col_labels = [_fmt_col(c, quarterly) for c in frame.columns[:n]] + if not quarterly: + col_labels.append("MRQ") + + def v(*labels: str) -> list[float | None]: + base = _row_vals_multi(frame, n, *labels) + if not quarterly: + val = None + for lbl in labels: + val = _balance_value(frame_q, lbl) + if val is not None: + break + base = base + [val] + return base + + return { + "columns": col_labels, + "rows": [ + _fin_section("ASSETS"), + _fin_row("Current Assets", 0, True, v("Current Assets")), + _fin_row("Cash & Equivalents", 1, False, v("Cash And Cash Equivalents", "Cash Cash Equivalents And Short Term Investments")), + _fin_row("Short Term Investments", 1, False, v("Other Short Term Investments", "Short Term Investments")), + _fin_row("Receivables", 1, False, v("Receivables", "Net Receivables")), + _fin_row("Inventory", 1, False, v("Inventory")), + _fin_row("Total Assets", 0, True, v("Total Assets")), + _fin_section("LIABILITIES"), + _fin_row("Current Liabilities", 0, True, v("Current Liabilities")), + _fin_row("Accounts Payable", 1, False, v("Payables And Accrued Expenses", "Accounts Payable")), + _fin_row("Short Term Debt", 1, False, v("Current Debt", "Short Term Debt And Capital Lease Obligation")), + _fin_row("Long Term Debt", 1, False, v("Long Term Debt", "Long Term Debt And Capital Lease Obligation")), + _fin_row("Total Liabilities", 0, True, v("Total Liabilities Net Minority Interest", "Total Liabilities")), + _fin_section("EQUITY"), + _fin_row("Stockholders Equity", 0, True, v("Stockholders Equity", "Common Stock Equity")), + ], + } + + +def _build_cash_flow(cf: pd.DataFrame, cf_q: pd.DataFrame, inc: pd.DataFrame, inc_q: pd.DataFrame, quarterly: bool) -> dict: + if cf is None or cf.empty: + return {"columns": [], "rows": []} + n = min(len(cf.columns), 8 if quarterly else 4) + col_labels = [_fmt_col(c, quarterly) for c in cf.columns[:n]] + if not quarterly: + col_labels.append("TTM") + + def cv(*labels: str) -> list[float | None]: + base = _row_vals_multi(cf, n, *labels) + if not quarterly: + ttm = None + for lbl in labels: + ttm = _statement_ttm(cf_q, lbl) + if ttm is not None: + break + base = base + [ttm] + return base + + def iv(*labels: str) -> list[float | None]: + base = _row_vals_multi(inc, n, *labels) + if not quarterly: + ttm = None + for lbl in labels: + ttm = _statement_ttm(inc_q, lbl) + if ttm is not None: + break + base = base + [ttm] + return base + + op_cf = cv("Operating Cash Flow", "Cash Flow From Continuing Operating Activities") + capex = cv("Capital Expenditure") + # CapEx is negative in yfinance; FCF = Operating CF + CapEx + fcf = [a + b if a is not None and b is not None else None for a, b in zip(op_cf, capex)] + rev = iv("Total Revenue") + + return { + "columns": col_labels, + "rows": [ + _fin_section("OPERATING"), + _fin_row("Net Income", 1, False, iv("Net Income")), + _fin_row("D&A", 1, False, cv("Depreciation And Amortization", "Reconciled Depreciation")), + _fin_row("Changes in Working Capital", 1, False, cv("Change In Working Capital")), + _fin_row("Operating Cash Flow", 0, True, op_cf), + _fin_section("INVESTING"), + _fin_row("CapEx", 1, False, capex), + _fin_row("Free Cash Flow", 0, True, fcf), + _fin_margin("FCF margin", [_safe_ratio(f, r) for f, r in zip(fcf, rev)]), + _fin_row("Investing Cash Flow", 0, True, cv("Investing Cash Flow", "Cash Flow From Continuing Investing Activities")), + _fin_section("FINANCING"), + _fin_row("Dividends Paid", 1, False, cv("Cash Dividends Paid", "Common Stock Dividend Paid")), + _fin_row("Buybacks", 1, False, cv("Repurchase Of Capital Stock", "Common Stock Repurchase")), + _fin_row("Financing Cash Flow", 0, True, cv("Financing Cash Flow", "Cash Flow From Continuing Financing Activities")), + _fin_row("Net Change in Cash", 0, True, cv("Changes In Cash", "End Cash Position")), + ], + } + + +@cached(FINANCIALS_CACHE) +def get_financials(symbol: str, period: str = "annual") -> dict: + sym = normalize_symbol(symbol) + quarterly = period == "quarterly" + + inc = get_income_statement(sym, quarterly=quarterly) + bal = get_balance_sheet(sym, quarterly=quarterly) + cf = get_cash_flow(sym, quarterly=quarterly) + + inc_q = get_income_statement(sym, quarterly=True) if not quarterly else inc + bal_q = get_balance_sheet(sym, quarterly=True) if not quarterly else bal + cf_q = get_cash_flow(sym, quarterly=True) if not quarterly else cf + + return { + "period": period, + "income": _build_income(inc, inc_q, quarterly), + "balance": _build_balance(bal, bal_q, quarterly), + "cash_flow": _build_cash_flow(cf, cf_q, inc, inc_q, quarterly), + } + + def _balance_value(frame: pd.DataFrame, *labels: str) -> float | None: if frame is None or frame.empty: return None |
