Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ All notable changes to Investo are documented here. The format follows
`investo.export` (`save_html`, `save_pdf`, `find_browser`, `html_to_pdf`).

### Changed
- **Quality-aware scoring — cash-rich, fairly-priced compounders are no longer under-rated.** The
valuation bucket was a pure cheapness screen (P/E · P/B · EV/EBITDA scored strictly
lower-is-better with hard ceilings), so a high-quality company at a fair premium lost almost the
whole bucket and a large net-cash balance sheet earned nothing. Now the multiple ceilings widen
quadratically with a four-signal **quality factor** (ROE, ROCE, operating margin, revenue+EPS-CAGR
growth); **net cash** is surfaced on `Ratios` (FX-normalized against market cap) and rewarded both
by lowering the effective equity multiple and by a signed term in the renamed **Balance Sheet**
bucket (which now also penalizes net debt); DCF is a 30% cross-check recentred so fair value scores
neutral; and the weights lean toward durable quality (Valuation 15→10, Profitability 15→17,
Competitive Moat 10→12, Balance Sheet 10→11). Rank order is preserved (Spearman ≈ 0.99 on a
synthetic basket) — the shift is targeted, not blanket inflation.
- **`investo analyze` output flags now compose.** `--json`, `--html` and `--pdf` each do one thing
and can be combined; previously `--html` silently suppressed `--json`. Bare `--html`/`--pdf` write
`investo-<SYMBOL>-<YYYY-MM-DD>.<ext>`; parent directories are created; a PDF-engine failure exits
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,18 @@ document rather than a dashboard — are available too.

### Rating buckets (out of 100)

| Growth | Profitability | Cash Flow | Debt | Valuation | Moat | Management | Industry | Innovation | Risk | ESG* |
| Growth | Profitability | Cash Flow | Balance Sheet | Valuation | Moat | Management | Industry | Innovation | Risk | ESG* |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| 15 | 15 | 10 | 10 | 15 | 10 | 10 | 5 | 5 | 5 | 5* |
| 15 | 17 | 10 | 11 | 10 | 12 | 10 | 5 | 5 | 5 | 5* |

\*ESG is optional; when unavailable the remaining buckets renormalize to 100.

**Valuation is quality-aware.** A premium multiple isn't punished when the company's economics
justify it — the acceptable P/E · P/B · EV/EBITDA ceilings widen with ROE, margins and growth, so a
proven compounder isn't floored just for not being cheap (while a low-quality expensive name still
is). **Net cash** strengthens the score — it lifts the Balance Sheet bucket and lowers the effective
equity multiple — and **net debt** weakens both.

---

## Install
Expand All @@ -88,16 +94,23 @@ copy `.env.example` to `.env` and fill in any you have.
## Try it from the command line

```bash
investo analyze "Infosys"
investo analyze "Infosys" # terminal report + auto HTML note
investo analyze "Reliance Industries"
investo analyze "Tata Motors"
investo analyze AAPL
investo analyze "Reliance Industries" --html reliance.html # self-contained research note
investo analyze "Infosys" --pdf infosys.pdf # PDF via headless Chrome/Edge
investo analyze "Infosys" --json --html infy.html # flags compose; nothing is discarded
investo analyze "Infosys" --no-html # skip the automatic HTML note
investo search "tata motors"
```

Every `investo analyze` **writes a self-contained HTML research note automatically** (named
`investo-<SYMBOL>-<date>.html` in the working directory) alongside its terminal output — the path is
announced on stderr, so `--json` stays pipeable. Use `--no-html` to skip it, or `--html FILE` to
choose the location. The MCP `analyze_company` tool does the same, returning the file in
`html_report_path`; pass `emit_html=false` to suppress it.

`--pdf` needs a Chromium-family browser: it uses a system **Chrome, Edge, Chromium or Brave** if one
is installed (no setup), falls back to a managed Chromium via `pip install 'investo[pdf]' &&
playwright install chromium`, and otherwise prints exactly how to fix it while still leaving the
Expand Down Expand Up @@ -180,7 +193,7 @@ of Reliance?"*
| `compare_companies` | Head-to-head across 2–6 named tickers (not a curated group) |
| `peer_group_directory` | List the curated peer groups and their members |
| `export_report` | Render a full analysis to an HTML/PDF file (writes a file; path sandboxed) |
| `analyze_company` | Everything above bundled into one report (with a confidence/provenance evidence layer) |
| `analyze_company` | Everything above bundled into one report (with a confidence/provenance evidence layer); also auto-writes an HTML note and returns its `html_report_path` unless `emit_html=false` |
| `get_sec_facts` | SEC EDGAR cross-check (US/ADR only) |

---
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
}
},
"tools": [
{ "name": "analyze_company", "description": "Full analysis bundle + 0-100 rating" },
{ "name": "analyze_company", "description": "Full analysis bundle + 0-100 rating; also writes an HTML report (emit_html)" },
{ "name": "search_company", "description": "Resolve a name to an NSE/BSE/global ticker" },
{ "name": "get_company_profile", "description": "Sector, business summary, market cap" },
{ "name": "get_financials", "description": "Income statement / balance sheet / cash flow" },
Expand Down
2 changes: 1 addition & 1 deletion src/investo/analysis/finutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import Any

from ..models import Financials, FinancialPeriod
from ..models import FinancialPeriod, Financials


def safe_div(numerator: float | None, denominator: float | None) -> float | None:
Expand Down
10 changes: 10 additions & 0 deletions src/investo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,14 @@ def _cmd_analyze(args: argparse.Namespace) -> int:
print(f"error: PDF export failed.\n{exc}", file=sys.stderr)
exit_code = 2

# Automatic HTML report: write one by default unless the user asked for a specific artifact
# (--html / --pdf already produce a document) or opted out with --no-html. The announcement
# goes to stderr so `investo analyze X --json` stays pipeable on stdout.
if not (want_html or want_pdf or getattr(args, "no_html", False)):
from .export import save_html
out = save_html(report, None)
print(f"Wrote HTML report to {out}", file=sys.stderr)

if not (args.json or want_html or want_pdf):
print(render_report(report))
return exit_code
Expand Down Expand Up @@ -385,6 +393,8 @@ def build_parser() -> argparse.ArgumentParser:
help="Write a self-contained HTML research note (default name if FILE omitted)")
pa.add_argument("--pdf", nargs="?", const=None, default=_UNSET, metavar="FILE",
help="Write a PDF via headless Chrome/Edge (default name if FILE omitted)")
pa.add_argument("--no-html", action="store_true",
help="Suppress the HTML report that is otherwise written automatically")
_add_market(pa)
pa.set_defaults(func=_cmd_analyze)

Expand Down
5 changes: 3 additions & 2 deletions src/investo/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,9 @@ def _playwright_pdf(html: str, out_path: Path, timeout: float) -> str:
try:
page = browser.new_page()
page.set_content(html, wait_until="load")
page.pdf(path=str(out_path), format="A4", print_background=True,
# margins live in the document's @page rule; keep Playwright's off.
# Let the document's @page rule own the sheet size and margins (A4 + running
# header/footer room) rather than Playwright's defaults, so the PDF matches print.
page.pdf(path=str(out_path), print_background=True, prefer_css_page_size=True,
margin={"top": "0", "bottom": "0", "left": "0", "right": "0"})
finally:
browser.close()
Expand Down
6 changes: 6 additions & 0 deletions src/investo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,12 @@ class AnalysisReport(_Base):
growth_driver_hints: list[str] = Field(default_factory=list)
llm_guidance: str | None = None
warnings: list[str] = Field(default_factory=list)
# Auto-export metadata: set when analyze_company writes an HTML report so a client can open
# it without a second tool call. Optional — the report is complete without them.
html_report_path: str | None = None
generated_at: str | None = None # ISO-8601 UTC timestamp of when the report was written
investo_version: str | None = None
html_bytes: int | None = None


# --------------------------------------------------------------------------------------
Expand Down
Loading
Loading