Add year-over-year trends + expanded ratios (EBITDA, turnover, FCF conversion)
Browse files- README.md +8 -6
- src/financials.py +148 -91
- src/rag.py +1 -1
- vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/data_level0.bin +2 -2
- vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/header.bin +1 -1
- vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/index_metadata.pickle +2 -2
- vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/length.bin +2 -2
- vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/link_lists.bin +2 -2
- vectorstore/chroma.sqlite3 +2 -2
README.md
CHANGED
|
@@ -28,12 +28,14 @@ citations** — instead of making things up.
|
|
| 28 |
|
| 29 |
- **Grounded answers with citations** — every response is backed by excerpts
|
| 30 |
from real 10-K filings, shown in an expandable *Sources* panel.
|
| 31 |
-
- **Answers financial figures (hybrid RAG)** — plain text RAG
|
| 32 |
-
out of financial-statement tables. FinChat
|
| 33 |
-
**XBRL** structured financials
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
- **Query routing ("knows where to look")** — FinChat detects which company a
|
| 38 |
question is about and searches *only* that company's filings via metadata
|
| 39 |
filtering, with graceful semantic fallback when the company is ambiguous.
|
|
|
|
| 28 |
|
| 29 |
- **Grounded answers with citations** — every response is backed by excerpts
|
| 30 |
from real 10-K filings, shown in an expandable *Sources* panel.
|
| 31 |
+
- **Answers financial figures, ratios & trends (hybrid RAG)** — plain text RAG
|
| 32 |
+
can't read numbers out of financial-statement tables. FinChat extracts each
|
| 33 |
+
filing's **XBRL** structured financials, **computes standard ratios**
|
| 34 |
+
(margins, liquidity, returns, EBITDA, turnover, free cash flow)
|
| 35 |
+
deterministically in Python, and builds **year-over-year trend** facts — then
|
| 36 |
+
a hybrid retriever *guarantees* these are in context for numeric questions.
|
| 37 |
+
So *"Apple's FY2023 revenue?"* → **$383.29B**, *"quick ratio?"* → **0.94**,
|
| 38 |
+
*"did its margin improve YoY?"* → answered straight from the data.
|
| 39 |
- **Query routing ("knows where to look")** — FinChat detects which company a
|
| 40 |
question is about and searches *only* that company's filings via metadata
|
| 41 |
filtering, with graceful semantic fallback when the company is ambiguous.
|
src/financials.py
CHANGED
|
@@ -1,15 +1,14 @@
|
|
| 1 |
-
"""Extract structured
|
| 2 |
-
|
| 3 |
-
SEC filings carry machine-readable XBRL financials.
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
free cash flow) deterministically in Python,
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
This lets FinChat answer numeric questions --
|
| 10 |
-
and
|
| 11 |
-
|
| 12 |
-
LLMs are unreliable at arithmetic.
|
| 13 |
"""
|
| 14 |
from __future__ import annotations
|
| 15 |
|
|
@@ -26,9 +25,25 @@ _STATEMENTS = [
|
|
| 26 |
]
|
| 27 |
_KEYWORDS = ("revenue, sales, income, earnings, profit, margin, assets, "
|
| 28 |
"liabilities, equity, cash flow, expenses, EPS")
|
| 29 |
-
_RATIO_KEYWORDS = ("margin, ratio, return, ROE, ROA, liquidity, leverage, "
|
| 30 |
-
"profitability, quick ratio, current ratio, debt,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
|
|
|
| 32 |
|
| 33 |
def _fmt(value, label: str = "") -> str | None:
|
| 34 |
try:
|
|
@@ -44,13 +59,7 @@ def _fmt(value, label: str = "") -> str | None:
|
|
| 44 |
if a >= 1e6:
|
| 45 |
return f"{v / 1e6:,.1f} million shares"
|
| 46 |
return f"{v:,.0f} shares"
|
| 47 |
-
if a >=
|
| 48 |
-
return f"${v / 1e9:,.2f} billion"
|
| 49 |
-
if a >= 1e6:
|
| 50 |
-
return f"${v / 1e6:,.1f} million"
|
| 51 |
-
if a >= 1000:
|
| 52 |
-
return f"${v:,.0f}"
|
| 53 |
-
return f"{v:,.2f}"
|
| 54 |
|
| 55 |
|
| 56 |
def _money(v: float) -> str:
|
|
@@ -62,20 +71,34 @@ def _money(v: float) -> str:
|
|
| 62 |
return f"${v:,.0f}"
|
| 63 |
|
| 64 |
|
| 65 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
date_cols = [c for c in df.columns if re.match(r"\d{4}-\d{2}-\d{2}", str(c))]
|
| 67 |
for c in date_cols:
|
| 68 |
-
if str(c).startswith(str(
|
| 69 |
return c
|
| 70 |
-
return
|
| 71 |
|
| 72 |
|
| 73 |
-
def _statement_lines(stmt,
|
| 74 |
try:
|
| 75 |
df = stmt.to_dataframe()
|
| 76 |
except Exception:
|
| 77 |
return []
|
| 78 |
-
col = _value_column(df,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
if col is None:
|
| 80 |
return []
|
| 81 |
lines = []
|
|
@@ -92,10 +115,8 @@ def _statement_lines(stmt, fiscal_year: int) -> list[str]:
|
|
| 92 |
return lines
|
| 93 |
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def _collect_figures(tenk, fiscal_year: int) -> dict[str, list]:
|
| 98 |
-
"""Map each XBRL standard_concept -> list of (label, value) for the year."""
|
| 99 |
figs: dict[str, list] = defaultdict(list)
|
| 100 |
for attr, _name in _STATEMENTS:
|
| 101 |
stmt = getattr(tenk, attr, None)
|
|
@@ -105,7 +126,7 @@ def _collect_figures(tenk, fiscal_year: int) -> dict[str, list]:
|
|
| 105 |
df = stmt.to_dataframe()
|
| 106 |
except Exception:
|
| 107 |
continue
|
| 108 |
-
col = _value_column(df,
|
| 109 |
if col is None:
|
| 110 |
continue
|
| 111 |
for _, row in df.iterrows():
|
|
@@ -128,69 +149,103 @@ def _first(figs, concept):
|
|
| 128 |
|
| 129 |
|
| 130 |
def _operating_cash_flow(figs):
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
if "operating activ" in label.lower(): # the total line
|
| 134 |
return v
|
| 135 |
return None
|
| 136 |
|
| 137 |
|
| 138 |
-
|
| 139 |
-
"""Standard ratios, computed only where inputs are reliable for the sector."""
|
| 140 |
-
rev = _first(figs, "Revenue")
|
| 141 |
-
ni = _first(figs, "NetIncome") or _first(figs, "ProfitLoss")
|
| 142 |
-
total_assets = _first(figs, "LiabilitiesAndEquity") # == total assets
|
| 143 |
-
equity = _first(figs, "AllEquityBalance")
|
| 144 |
-
ca = _first(figs, "CurrentAssetsTotal")
|
| 145 |
-
cl = _first(figs, "CurrentLiabilitiesTotal")
|
| 146 |
-
inv = _first(figs, "Inventories") or 0
|
| 147 |
-
ltd = _first(figs, "LongTermDebt") or 0
|
| 148 |
-
std = _first(figs, "ShortTermDebt") or 0
|
| 149 |
-
capex = _first(figs, "CapitalExpenses")
|
| 150 |
-
ocf = _operating_cash_flow(figs)
|
| 151 |
-
|
| 152 |
-
# Banks / financials have no classified balance sheet (no current assets),
|
| 153 |
-
# and their revenue/COGS concepts are unreliable -> only compute ROA & ROE.
|
| 154 |
-
is_financial = ca is None
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
if not is_financial and rev:
|
| 161 |
-
gp =
|
| 162 |
-
cogs = _first(figs, "CostOfGoodsAndServicesSold")
|
| 163 |
if gp is None and cogs is not None:
|
| 164 |
gp = rev - cogs
|
| 165 |
-
oi = _first(figs, "OperatingIncomeLoss")
|
| 166 |
for name, num in (("Gross margin", gp), ("Operating margin", oi),
|
| 167 |
("Net profit margin", ni)):
|
| 168 |
-
if num is not None and abs(num / rev) <= 1.5:
|
| 169 |
-
m[name] =
|
| 170 |
if capex is not None:
|
| 171 |
-
m["Capital expenditure as % of revenue"] =
|
| 172 |
-
if
|
| 173 |
-
m["Asset turnover"] =
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
if not is_financial and ca and cl and cl > 0:
|
| 181 |
-
m["Current ratio"] =
|
| 182 |
-
m["Quick ratio"] =
|
| 183 |
-
if not is_financial and
|
| 184 |
-
m["Debt-to-equity ratio"] =
|
|
|
|
|
|
|
| 185 |
if not is_financial and ocf is not None and capex is not None:
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
| 188 |
return m
|
| 189 |
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
def financial_documents(filing, ticker: str, company: str,
|
| 192 |
fiscal_year: int) -> list[Document]:
|
| 193 |
-
"""Documents: one per financial statement + one of computed key ratios."""
|
| 194 |
try:
|
| 195 |
tenk = filing.obj()
|
| 196 |
except Exception:
|
|
@@ -200,9 +255,7 @@ def financial_documents(filing, ticker: str, company: str,
|
|
| 200 |
return Document(
|
| 201 |
page_content=content,
|
| 202 |
metadata={
|
| 203 |
-
"ticker": ticker,
|
| 204 |
-
"company": company,
|
| 205 |
-
"year": str(fiscal_year),
|
| 206 |
"accession": filing.accession_no,
|
| 207 |
"source": f"{company} 10-K (FY{fiscal_year}) - {name} (XBRL)",
|
| 208 |
"type": "financials",
|
|
@@ -215,21 +268,25 @@ def financial_documents(filing, ticker: str, company: str,
|
|
| 215 |
if stmt is None:
|
| 216 |
continue
|
| 217 |
lines = _statement_lines(stmt, fiscal_year)
|
| 218 |
-
if
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
f"(financial figures: {_KEYWORDS}; from SEC XBRL data):"
|
| 223 |
-
)
|
| 224 |
-
docs.append(_doc(name, header + "\n" + "\n".join(lines)))
|
| 225 |
|
| 226 |
-
|
|
|
|
| 227 |
if ratios:
|
| 228 |
-
header = (
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
)
|
| 232 |
-
body = "\n".join(f"{k}: {v}" for k, v in ratios.items())
|
| 233 |
docs.append(_doc("Key Ratios", header + "\n" + body))
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
return docs
|
|
|
|
| 1 |
+
"""Extract structured figures, computed ratios, and YoY trends from XBRL data.
|
| 2 |
+
|
| 3 |
+
SEC filings carry machine-readable XBRL financials. For each filing we build:
|
| 4 |
+
* one "label: value" chunk per statement (income / balance / cash flow),
|
| 5 |
+
* a "Key Ratios" chunk (margins, liquidity, returns, leverage, turnover,
|
| 6 |
+
EBITDA, free cash flow) computed deterministically in Python, and
|
| 7 |
+
* a "Financial Trends (YoY)" chunk comparing this year to the prior year.
|
| 8 |
+
|
| 9 |
+
This lets FinChat answer numeric questions -- direct figures, computed metrics,
|
| 10 |
+
and year-over-year comparisons -- that plain text RAG cannot (the filing's
|
| 11 |
+
tables collapse into unusable "number soup", and LLMs are unreliable at math).
|
|
|
|
| 12 |
"""
|
| 13 |
from __future__ import annotations
|
| 14 |
|
|
|
|
| 25 |
]
|
| 26 |
_KEYWORDS = ("revenue, sales, income, earnings, profit, margin, assets, "
|
| 27 |
"liabilities, equity, cash flow, expenses, EPS")
|
| 28 |
+
_RATIO_KEYWORDS = ("margin, ratio, return, ROE, ROA, EBITDA, liquidity, leverage, "
|
| 29 |
+
"turnover, profitability, quick ratio, current ratio, debt, "
|
| 30 |
+
"free cash flow")
|
| 31 |
+
_TREND_KEYWORDS = ("year-over-year, YoY, trend, change, improved, declined, grew, "
|
| 32 |
+
"growth, increase, decrease, historical, compared to prior year")
|
| 33 |
+
|
| 34 |
+
# Figures to show in the year-over-year trends chunk.
|
| 35 |
+
_TREND_FIGURES = [
|
| 36 |
+
("Revenue", "Revenue"),
|
| 37 |
+
("Gross profit", "GrossProfit"),
|
| 38 |
+
("Operating income", "OperatingIncomeLoss"),
|
| 39 |
+
("Net income", "NetIncome"),
|
| 40 |
+
("Total assets", "LiabilitiesAndEquity"),
|
| 41 |
+
("Property, plant & equipment (net)", "PlantPropertyEquipmentNet"),
|
| 42 |
+
("Inventory", "Inventories"),
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
|
| 46 |
+
# --- formatting -------------------------------------------------------------
|
| 47 |
|
| 48 |
def _fmt(value, label: str = "") -> str | None:
|
| 49 |
try:
|
|
|
|
| 59 |
if a >= 1e6:
|
| 60 |
return f"{v / 1e6:,.1f} million shares"
|
| 61 |
return f"{v:,.0f} shares"
|
| 62 |
+
return _money(v) if a >= 1000 else f"{v:,.2f}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
def _money(v: float) -> str:
|
|
|
|
| 71 |
return f"${v:,.0f}"
|
| 72 |
|
| 73 |
|
| 74 |
+
def _fmt_metric(value: float, kind: str) -> str:
|
| 75 |
+
if kind == "pct":
|
| 76 |
+
return f"{value * 100:.1f}%"
|
| 77 |
+
if kind == "x":
|
| 78 |
+
return f"{value:.2f}"
|
| 79 |
+
return _money(value)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# --- extraction -------------------------------------------------------------
|
| 83 |
+
|
| 84 |
+
def _value_column(df: pd.DataFrame, year: int):
|
| 85 |
date_cols = [c for c in df.columns if re.match(r"\d{4}-\d{2}-\d{2}", str(c))]
|
| 86 |
for c in date_cols:
|
| 87 |
+
if str(c).startswith(str(year)):
|
| 88 |
return c
|
| 89 |
+
return None
|
| 90 |
|
| 91 |
|
| 92 |
+
def _statement_lines(stmt, year: int) -> list[str]:
|
| 93 |
try:
|
| 94 |
df = stmt.to_dataframe()
|
| 95 |
except Exception:
|
| 96 |
return []
|
| 97 |
+
col = _value_column(df, year)
|
| 98 |
+
if col is None:
|
| 99 |
+
# primary statement may label the column with the filing year only
|
| 100 |
+
col = next((c for c in df.columns if re.match(r"\d{4}-\d{2}-\d{2}", str(c))),
|
| 101 |
+
None)
|
| 102 |
if col is None:
|
| 103 |
return []
|
| 104 |
lines = []
|
|
|
|
| 115 |
return lines
|
| 116 |
|
| 117 |
|
| 118 |
+
def _collect_figures(tenk, year: int) -> dict[str, list]:
|
| 119 |
+
"""{standard_concept: [(label, value), ...]} for a given fiscal year."""
|
|
|
|
|
|
|
| 120 |
figs: dict[str, list] = defaultdict(list)
|
| 121 |
for attr, _name in _STATEMENTS:
|
| 122 |
stmt = getattr(tenk, attr, None)
|
|
|
|
| 126 |
df = stmt.to_dataframe()
|
| 127 |
except Exception:
|
| 128 |
continue
|
| 129 |
+
col = _value_column(df, year)
|
| 130 |
if col is None:
|
| 131 |
continue
|
| 132 |
for _, row in df.iterrows():
|
|
|
|
| 149 |
|
| 150 |
|
| 151 |
def _operating_cash_flow(figs):
|
| 152 |
+
for label, v in figs.get("NetCashFromOperatingActivities", []):
|
| 153 |
+
if "operating activ" in label.lower():
|
|
|
|
| 154 |
return v
|
| 155 |
return None
|
| 156 |
|
| 157 |
|
| 158 |
+
# --- ratios -----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
def _compute_ratios(figs) -> dict[str, tuple[float, str]]:
|
| 161 |
+
"""{name: (value, kind)} -- computed only where inputs are reliable."""
|
| 162 |
+
g = lambda c: _first(figs, c)
|
| 163 |
+
rev = g("Revenue")
|
| 164 |
+
ni = g("NetIncome") or g("ProfitLoss")
|
| 165 |
+
ta = g("LiabilitiesAndEquity") # == total assets (identity)
|
| 166 |
+
eq = g("AllEquityBalance")
|
| 167 |
+
ca, cl = g("CurrentAssetsTotal"), g("CurrentLiabilitiesTotal")
|
| 168 |
+
inv = g("Inventories")
|
| 169 |
+
ltd, std = g("LongTermDebt") or 0, g("ShortTermDebt") or 0
|
| 170 |
+
capex, ocf = g("CapitalExpenses"), _operating_cash_flow(figs)
|
| 171 |
+
da, ppe, rec = g("DepreciationExpense"), g("PlantPropertyEquipmentNet"), g("TradeReceivables")
|
| 172 |
+
oi, cogs = g("OperatingIncomeLoss"), g("CostOfGoodsAndServicesSold")
|
| 173 |
+
|
| 174 |
+
# Banks / financials: no classified balance sheet, unreliable revenue/COGS.
|
| 175 |
+
is_financial = ca is None
|
| 176 |
+
m: dict[str, tuple[float, str]] = {}
|
| 177 |
|
| 178 |
if not is_financial and rev:
|
| 179 |
+
gp = g("GrossProfit")
|
|
|
|
| 180 |
if gp is None and cogs is not None:
|
| 181 |
gp = rev - cogs
|
|
|
|
| 182 |
for name, num in (("Gross margin", gp), ("Operating margin", oi),
|
| 183 |
("Net profit margin", ni)):
|
| 184 |
+
if num is not None and abs(num / rev) <= 1.5:
|
| 185 |
+
m[name] = (num / rev, "pct")
|
| 186 |
if capex is not None:
|
| 187 |
+
m["Capital expenditure as % of revenue"] = (abs(capex) / rev, "pct")
|
| 188 |
+
if ta:
|
| 189 |
+
m["Asset turnover"] = (rev / ta, "x")
|
| 190 |
+
if cogs is not None and inv:
|
| 191 |
+
m["Inventory turnover"] = (cogs / inv, "x")
|
| 192 |
+
if rec:
|
| 193 |
+
m["Receivables turnover"] = (rev / rec, "x")
|
| 194 |
+
if oi is not None and da is not None:
|
| 195 |
+
ebitda = oi + da
|
| 196 |
+
m["EBITDA"] = (ebitda, "money")
|
| 197 |
+
if capex is not None:
|
| 198 |
+
m["EBITDA less capex"] = (ebitda - abs(capex), "money")
|
| 199 |
+
|
| 200 |
+
if ni is not None and ta:
|
| 201 |
+
m["Return on assets (ROA)"] = (ni / ta, "pct")
|
| 202 |
+
if ni is not None and eq:
|
| 203 |
+
m["Return on equity (ROE)"] = (ni / eq, "pct")
|
| 204 |
if not is_financial and ca and cl and cl > 0:
|
| 205 |
+
m["Current ratio"] = (ca / cl, "x")
|
| 206 |
+
m["Quick ratio"] = ((ca - (inv or 0)) / cl, "x")
|
| 207 |
+
if not is_financial and eq and (ltd or std):
|
| 208 |
+
m["Debt-to-equity ratio"] = ((ltd + std) / eq, "x")
|
| 209 |
+
if not is_financial and ta and ppe:
|
| 210 |
+
m["Property, plant & equipment as % of assets"] = (ppe / ta, "pct")
|
| 211 |
if not is_financial and ocf is not None and capex is not None:
|
| 212 |
+
fcf = ocf - abs(capex)
|
| 213 |
+
m["Free cash flow"] = (fcf, "money")
|
| 214 |
+
if ni:
|
| 215 |
+
m["Free cash flow conversion (FCF / net income)"] = (fcf / ni, "pct")
|
| 216 |
return m
|
| 217 |
|
| 218 |
|
| 219 |
+
def _trend_line(name, cur, prev, kind, cy, py) -> str:
|
| 220 |
+
cs, ps = _fmt_metric(cur, kind), _fmt_metric(prev, kind)
|
| 221 |
+
if kind == "pct":
|
| 222 |
+
d = (cur - prev) * 100
|
| 223 |
+
w = "increased" if d > 0.05 else "decreased" if d < -0.05 else "roughly unchanged"
|
| 224 |
+
return f"{name}: FY{cy} {cs} vs FY{py} {ps} ({w} {abs(d):.1f} pp)"
|
| 225 |
+
if prev == 0:
|
| 226 |
+
return f"{name}: FY{cy} {cs} vs FY{py} {ps}"
|
| 227 |
+
d = (cur - prev) / abs(prev) * 100
|
| 228 |
+
w = "up" if d > 0.5 else "down" if d < -0.5 else "roughly flat"
|
| 229 |
+
return f"{name}: FY{cy} {cs} vs FY{py} {ps} ({w} {abs(d):.1f}%)"
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def _trend_lines(figs_cur, figs_prev, cy, py) -> list[str]:
|
| 233 |
+
lines = []
|
| 234 |
+
for name, concept in _TREND_FIGURES:
|
| 235 |
+
cur, prev = _first(figs_cur, concept), _first(figs_prev, concept)
|
| 236 |
+
if cur is not None and prev is not None:
|
| 237 |
+
lines.append(_trend_line(name, cur, prev, "money", cy, py))
|
| 238 |
+
rc, rp = _compute_ratios(figs_cur), _compute_ratios(figs_prev)
|
| 239 |
+
for name, (cur, kind) in rc.items():
|
| 240 |
+
if name in rp:
|
| 241 |
+
lines.append(_trend_line(name, cur, rp[name][0], kind, cy, py))
|
| 242 |
+
return lines
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# --- documents --------------------------------------------------------------
|
| 246 |
+
|
| 247 |
def financial_documents(filing, ticker: str, company: str,
|
| 248 |
fiscal_year: int) -> list[Document]:
|
|
|
|
| 249 |
try:
|
| 250 |
tenk = filing.obj()
|
| 251 |
except Exception:
|
|
|
|
| 255 |
return Document(
|
| 256 |
page_content=content,
|
| 257 |
metadata={
|
| 258 |
+
"ticker": ticker, "company": company, "year": str(fiscal_year),
|
|
|
|
|
|
|
| 259 |
"accession": filing.accession_no,
|
| 260 |
"source": f"{company} 10-K (FY{fiscal_year}) - {name} (XBRL)",
|
| 261 |
"type": "financials",
|
|
|
|
| 268 |
if stmt is None:
|
| 269 |
continue
|
| 270 |
lines = _statement_lines(stmt, fiscal_year)
|
| 271 |
+
if lines:
|
| 272 |
+
header = (f"{company} ({ticker}) FY{fiscal_year} {name} "
|
| 273 |
+
f"(financial figures: {_KEYWORDS}; from SEC XBRL data):")
|
| 274 |
+
docs.append(_doc(name, header + "\n" + "\n".join(lines)))
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
+
figs_cur = _collect_figures(tenk, fiscal_year)
|
| 277 |
+
ratios = _compute_ratios(figs_cur)
|
| 278 |
if ratios:
|
| 279 |
+
header = (f"{company} ({ticker}) FY{fiscal_year} Key Financial Ratios "
|
| 280 |
+
f"(computed from SEC XBRL data - {_RATIO_KEYWORDS}):")
|
| 281 |
+
body = "\n".join(f"{k}: {_fmt_metric(*v)}" for k, v in ratios.items())
|
|
|
|
|
|
|
| 282 |
docs.append(_doc("Key Ratios", header + "\n" + body))
|
| 283 |
|
| 284 |
+
figs_prev = _collect_figures(tenk, fiscal_year - 1)
|
| 285 |
+
trends = _trend_lines(figs_cur, figs_prev, fiscal_year, fiscal_year - 1)
|
| 286 |
+
if trends:
|
| 287 |
+
header = (f"{company} ({ticker}) Financial Trends "
|
| 288 |
+
f"FY{fiscal_year} vs FY{fiscal_year - 1} "
|
| 289 |
+
f"({_TREND_KEYWORDS}; from SEC XBRL data):")
|
| 290 |
+
docs.append(_doc("Financial Trends", header + "\n" + "\n".join(trends)))
|
| 291 |
+
|
| 292 |
return docs
|
src/rag.py
CHANGED
|
@@ -175,7 +175,7 @@ def retrieve(question: str, ticker: str | None):
|
|
| 175 |
if ticker and _is_financial_query(question):
|
| 176 |
fin = store.as_retriever(
|
| 177 |
search_kwargs={
|
| 178 |
-
"k":
|
| 179 |
"filter": {"$and": [{"ticker": ticker}, {"type": "financials"}]},
|
| 180 |
}
|
| 181 |
).invoke(question)
|
|
|
|
| 175 |
if ticker and _is_financial_query(question):
|
| 176 |
fin = store.as_retriever(
|
| 177 |
search_kwargs={
|
| 178 |
+
"k": 4,
|
| 179 |
"filter": {"$and": [{"ticker": ticker}, {"type": "financials"}]},
|
| 180 |
}
|
| 181 |
).invoke(question)
|
vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/data_level0.bin
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b9fd1b96c87c7aeb6ab9781a5ad48c5db0d242fcd91469621fe679bc98f671f8
|
| 3 |
+
size 35750756
|
vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/header.bin
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 100
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bafd2c0ed962055277d16427a6588bbeec9442666e126d7b26de71c3a521bbab
|
| 3 |
size 100
|
vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/index_metadata.pickle
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7bc200edd42cccc33aaf70bedd3a0aceadde01eec160b6d8467cb87d7f5f7ad0
|
| 3 |
+
size 1962664
|
vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/length.bin
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9c5a3b3cf7e71f0098ce2ffe4f11463733d3b7c5beb72ace5f369e4936446693
|
| 3 |
+
size 85324
|
vectorstore/{1b87b035-cb38-4524-82b2-6713a29f0208 → 65beb4f5-d997-4646-b2fc-3ac731fba06f}/link_lists.bin
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a79fa52068d7fd8b28f2ac047b1e0c17eeee1b0eb90b0a30616d7cfc3a195d20
|
| 3 |
+
size 183108
|
vectorstore/chroma.sqlite3
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3c5479c43914cb268b20ca1b1d97b897ac42ea7363ed8725598c4149cab3758d
|
| 3 |
+
size 124878848
|