Skip to content

feat: export, technical, DCF-sensitivity and multi-compare tools - #19

Merged
YashvantHange merged 1 commit into
mainfrom
feat/analysis-tools
Jul 18, 2026
Merged

feat: export, technical, DCF-sensitivity and multi-compare tools#19
YashvantHange merged 1 commit into
mainfrom
feat/analysis-tools

Conversation

@YashvantHange

Copy link
Copy Markdown
Owner

Adds five MCP tools (23 → 28). Fourth and final RFC; builds on #13 (peers/DCF) and #15 (export.py), both merged.

The tools

Tool What Notable
technical_snapshot 50/200-DMA + golden/death cross, RSI(14), volatility, max drawdown, beta, 52-wk position Context, not a signal — no buy/sell language
dcf_sensitivity Intrinsic value across a discount-rate × terminal-growth grid + implied break-even growth Fetches statements once across 25 cells
compare_companies Head-to-head across 2–6 arbitrary tickers The "compare KPIT with Tata Elxsi and Tata Tech" ask, directly
peer_group_directory Lists the curated peer groups + members Offline, makes the curated data inspectable
export_report Renders a full analysis to HTML/PDF The only non-read-only tool

The three things worth reviewing

export_report is the sharp edge. It's the first tool that writes a file (so server.py's "every tool is read-only" is now scoped), and its path is LLM-controlled. _safe_export_path confines it to the export dir and rejects any path that resolves outside — a .. traversal or an absolute path — rather than silently clamping. I hit and fixed a real cross-platform bug here mid-build: Path("/etc/shadow").is_absolute() is False on Windows, so my first dual-policy version behaved differently per-OS. Reject-on-escape is uniform and there's a parametrized test across both traversal styles.

RSI is Wilder's smoothing, not a rolling mean — pinned by a hand-computed value against the canonical worked example (≈70.53), so a future "simplification" to a rolling mean would fail. RSI of a pure rally is 100 (the divide-by-zero guard), of a pure selloff ~0.

The N+1 guard in dcf_sensitivity. A 5×5 grid calls compute_dcf 25 times and get_financials is uncached, so info/financials/ratios are fetched once and threaded through every call. A test asserts get_financials is called exactly once — the deterministic form of a performance test, since wall-clock targets can't run in an offline suite.

Verification

ruff + mypy clean; 263 tests pass (was 223). Every new test is offline: technical against synthetic price frames (including engineered golden/death-cross series), sensitivity and multi_compare against a stubbed data facade, server_tools for registration + the sandbox + manifest parity (the manifest tool list must equal the registered tools — this repo has drifted there before).

End-to-end on live data:

  • export_report("KPIT", "pdf") through the MCP tool → a real 414 KB %PDF- via headless Chrome, written inside the sandboxed export dir.
  • dcf_sensitivity("KPITTECH.NS") → 5×5 grid, monotonic (₹1149→₹517 as the discount rate rises), market implying 5.56% growth at ₹554.
  • compare_companies(["KPITTECH.NS","TATAELXSI.NS","TATATECH.NS"]) → Tata Tech largest & fastest-growing, Tata Elxsi highest-margin, KPIT cheapest P/E and best ROE.

No breaking changes

All additive: five new tools, new models with defaults, get_history added to the Yahoo source. Every prior tool stays read-only.

Add five MCP tools (23 -> 28), each with a typed model, a manifest entry and offline tests.

technical_snapshot gives the price/momentum backdrop a fundamentals tool usually lacks: 50/200-day
moving averages and any recent golden/death cross, RSI(14) with Wilder's smoothing (not a simple
mean), annualized volatility, one-year max drawdown, beta vs the market index over date-aligned
returns, and where the price sits in its 52-week range. It is framed as context, not a signal --
the model docstring and tool description say so, and it emits no buy/sell language -- because a
technical readout inside a fundamentals tool is easy for a host LLM to over-read. It is backed by a
new sources/yahoo.get_history that follows the file's existing cache/ratelimit conventions and hands
out a copy so a caller adding indicator columns can't mutate the cached frame. The analysis is a
pure function of a price frame, so it unit-tests offline against synthetic series.

dcf_sensitivity shows how much a DCF rests on its two key assumptions: intrinsic value across a
discount-rate x terminal-growth grid, plus the break-even growth the market is implying at today's
price (found by bisection, since there is no closed form for a two-stage-plus-Gordon model). The
trap here is an N+1 storm -- 25 grid cells each calling compute_dcf while get_financials is uncached
-- so info/financials/ratios are fetched once and threaded through every call; a test asserts the
single statement fetch.

compare_companies runs a head-to-head across 2-6 arbitrary tickers rather than a curated group,
answering "compare KPIT with Tata Elxsi and Tata Tech" directly. It reuses peers._peer_row for the
same currency normalisation, dedupes preserving order, drops unresolvable rows, and names its
revenue share as set-relative rather than calling it market share. peer_group_directory lists the
curated groups so a client can see how companies are grouped and why.

export_report renders a full analysis to an HTML/PDF file via the RFC 3 engine and is the one
non-read-only tool (annotated readOnlyHint=False; the server docstring notes the exception). Its
path is LLM-controlled, so _safe_export_path confines it to the export directory: anything that
resolves outside -- a .. traversal or an absolute path on either OS -- is rejected outright rather
than clamped, so the behaviour is identical on Windows and POSIX. Verified end to end by producing
a real PDF through the tool, and the manifest-parity test pins the tool list against the server so
the two can't drift.
@YashvantHange
YashvantHange merged commit c8dcf92 into main Jul 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant