Metadata-Version: 2.4
Name: fin-eda
Version: 1.2.0
Summary: Comprehensive financial Exploratory Data Analysis for price series and tickers
License-Expression: MIT
Keywords: finance,eda,stocks,risk,portfolio,yfinance
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=1.3.0
Requires-Dist: numpy>=1.21.0
Requires-Dist: scipy>=1.7.0
Requires-Dist: yfinance>=0.2.0
Requires-Dist: rich>=12.0.0
Provides-Extra: plot
Requires-Dist: matplotlib>=3.5.0; extra == "plot"
Dynamic: license-file

# fin-eda

Comprehensive financial Exploratory Data Analysis for any stock ticker or price series.
Produces a numerical tearsheet (`eda`) and a visual tearsheet (`eda_plot`) covering returns, risk, drawdowns, benchmark comparison, volatility, liquidity, and more — all in one call.

## Installation

```bash
# Numerical tearsheet only
pip install fin-eda

# Numerical + visual tearsheet
pip install fin-eda[plot]
```

## Quick Start — Numerical (`eda`)

```python
from fin_eda import eda

# Fetch data automatically via yfinance
eda("AAPL")

# Custom date range
eda("MSFT", start="2020-01-01", end="2024-01-01")

# Custom period
eda("TSLA", period="5y")

# Different benchmark
eda("QQQ", benchmark_ticker="SPY")

# Include a risk-free rate
eda("AAPL", risk_free_rate=0.05)

# Pass your own price series
import pandas as pd
prices = pd.Series(...)
eda(prices, benchmark_ticker="SPY")

# Return results as a dict (no print)
results = eda("AAPL", return_results=True, quiet=True)
```

## Quick Start — Visual (`eda_plot`)

Requires `pip install fin-eda[plot]`.

```python
from fin_eda import eda_plot

# Full tearsheet
eda_plot("AAPL")

# Select specific panels
eda_plot("AAPL", panels=["price", "drawdown", "distribution", "heatmap"])

# Save to file (PNG, PDF, SVG — format inferred from extension)
eda_plot("MSFT", save_path="msft_tearsheet.png")

# Return the Figure for notebook embedding or further customisation
fig = eda_plot("TSLA", period="5y", return_fig=True)

# Custom benchmark and risk-free rate
eda_plot("QQQ", benchmark_ticker="^GSPC", risk_free_rate=0.05)
```

### Available panels

| Name | Description |
|---|---|
| `price` | Price history with 50D/200D moving averages and volume |
| `cumulative` | Cumulative return vs benchmark with shaded outperformance gap |
| `drawdown` | Underwater drawdown chart with top-3 trough annotations |
| `heatmap` | Monthly returns heatmap (year x month grid) |
| `annual_returns` | Year-by-year return bars (green/red) with benchmark overlay |
| `distribution` | Daily return histogram with normal overlay and VaR lines |
| `beta_scatter` | Return scatter vs benchmark with OLS regression line and beta/R-squared annotation |
| `rolling_beta` | Rolling 1Y (252D) beta vs benchmark, shaded above/below beta = 1 |
| `rolling_sharpe` | Rolling 252D and 126D annualized Sharpe ratio |
| `rolling_vol` | Rolling 21D vs 252D volatility regime with elevated-vol shading |

Benchmark-dependent panels (`cumulative`, `beta_scatter`, `rolling_beta`) are automatically
skipped if no benchmark data is available.

## Output

`eda()` renders in the terminal using [Rich](https://github.com/Textualize/rich) with
colour-coded values (green = positive/good, yellow = neutral, red = negative/risk).

The header panel shows:

- Ticker or series label, current price, and the benchmark symbol actually used
- Data coverage: number of trading days and date range
- 50D/100D/200D moving average levels and the 50D-200D spread
- Price distance from the 200D MA and trend persistence (% days above 200D MA)
- 52W price range (low - high), days since the 52W high, and drawdown from the 52W high

Below the header, one table is printed per section. Period columns (e.g. 1M, 1Y, 5Y, 10Y)
are shown only when the available data is sufficient to support them — longer columns appear
automatically as data coverage grows, up to 30Y.

`eda_plot()` produces a single dark-themed `matplotlib` figure with up to 10 panels.
Use `save_path` to export, or `return_fig=True` to get the `Figure` object.

## Metrics Covered

| Section | Key Metrics |
|---|---|
| **Core Return & Risk** | Cumulative return, CAGR, arithmetic and geometric mean daily return, median daily return, excess return over risk-free rate, standard deviation, annualized realized volatility, annualized variance — across 1M, 3M, 6M, 1Y, 3Y, 5Y, 10Y, 15Y, 20Y, 25Y, 30Y, YTD |
| **Risk-Adjusted Performance** | Sharpe ratio, Sortino ratio, Omega ratio, Calmar ratio, Treynor ratio, Jensen's alpha, downside deviation, semi-variance, profit factor, win rate, average return on up days, average return on down days, gain-loss ratio — from 6M+ for period ratios |
| **Drawdown & Capital Destruction** | Max drawdown, average drawdown, time to recovery, max consecutive loss days — from 3M+ |
| **Trend Structure & Price Health** | 50D/100D/200D moving average levels, price vs 200D MA, golden/death cross spread, trend persistence, 52W high price, 52W low price, time since 52W high, drawdown from 52W high |
| **Relative Performance vs Benchmark** | Geometric excess return vs benchmark, tracking error, information ratio — from 6M+ |
| **Beta, Correlation & Market Dependence** | Beta vs market, correlation vs market, R-squared vs market — from 3M+ |
| **Capture Ratios** | Up-market and down-market capture (annualized geometric) — from 6M+ |
| **Return Distribution & Non-Normality** | Skewness, excess kurtosis — from 3M+ |
| **Tail Risk & Stress** | Historical VaR (95% and 99%), Expected Shortfall/CVaR (95%) for 1Y+, best and worst daily/weekly/monthly return, extreme loss frequency beyond -2 sigma and -3 sigma |
| **Volatility Metrics** | Parkinson (high-low) volatility (21D/63D/126D), rolling volatility percentile, volatility of volatility, current 21D vs 1Y volatility ratio |
| **Liquidity Metrics** | Average daily volume (30D/90D), volume trend, Amihud illiquidity ratio (30D/90D/full history) |
| **Annual Returns** | Calendar-year return for each year in the data, partial current-year return, best and worst calendar year |

Period columns scale automatically with available data. With 1Y of data the output caps at 1Y
columns; with 30Y of data all columns through 30Y render.

## Parameters

### `eda()` — shared with `eda_plot()`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `ticker_or_prices` | `str` or `pd.Series` | — | Yahoo Finance ticker or a Series of close prices indexed by date |
| `benchmark_ticker` | `str` or `None` | `'SPY'` | Benchmark symbol. Falls back to `^GSPC` only if the requested ticker fails to fetch or returns no data. Pass `None` to disable benchmark metrics entirely |
| `risk_free_rate` | `float` | `0.0` | Annual risk-free rate used in Sharpe, Sortino, Omega, Treynor, and Jensen's alpha (e.g. `0.05` for 5%) |
| `period` | `str` or `None` | `'10y'` | yfinance history period (`'1y'`, `'5y'`, `'10y'`, `'max'`, etc.). Ignored when `start`/`end` are provided |
| `start` | `str` or `None` | `None` | Start date in `YYYY-MM-DD` format |
| `end` | `str` or `None` | `None` | End date in `YYYY-MM-DD` format |
| `quiet` | `bool` | `False` | Suppress benchmark fetch status messages |

### `eda()` only

| Parameter | Type | Default | Description |
|---|---|---|---|
| `return_results` | `bool` | `False` | When `True`, return the full metrics dict in addition to (or instead of, if `quiet=True`) printing the report |

### `eda_plot()` only

| Parameter | Type | Default | Description |
|---|---|---|---|
| `panels` | `list[str]` or `None` | `None` | Panels to render. `None` renders all available panels. See panel name table above |
| `save_path` | `str` or `None` | `None` | Save the figure to this path before displaying (e.g. `'out.png'`, `'report.pdf'`) |
| `return_fig` | `bool` | `False` | Return the `matplotlib.Figure` object instead of calling `plt.show()` |

## Calculation Notes

**Returns and CAGR.** Cumulative returns use log-sum form (`expm1(sum(log1p(r)))`) for numerical stability on long histories. CAGR is derived from the same log-sum: `expm1(log_sum x 252 / n)`.

**Excess return vs benchmark.** Reported as the geometric excess: `(1 + asset_return) / (1 + benchmark_return) - 1`. This avoids the distortion of arithmetic differences over long compounding periods.

**Sharpe ratio.** Daily excess returns divided by their standard deviation, annualized by `sqrt(252)`, per Sharpe (1994).

**Sortino ratio.** Uses `sqrt(E[min(r - rf, 0)^2])` as the downside deviation denominator — all periods enter the average, with positive days contributing zero. This is the formulation from Sortino and Price (1994) and differs from implementations that take the standard deviation only of the negative tail.

**Downside deviation and semi-variance.** Same RMS-over-all-periods formula as the Sortino denominator, annualized.

**Omega ratio.** `sum(max(r - rf, 0)) / sum(max(rf - r, 0))` — a full-distribution gain/loss ratio that does not assume normality.

**Calmar ratio.** CAGR divided by the absolute value of the maximum drawdown for the same period.

**Treynor ratio.** `(CAGR - annual_rf) / beta`.

**Jensen's alpha.** `asset_CAGR - (rf + beta x (bench_CAGR - rf))` — CAPM-expected return removed from realized CAGR.

**CVaR / Expected Shortfall.** Mean of all daily returns at or below the historical 5th percentile (95% confidence). Reported for periods of 1Y and above.

**Parkinson volatility.** `sqrt((1 / (4T ln 2)) x sum(ln(H/L)^2) x 252)` per Parkinson (1980).

**Amihud illiquidity.** Mean of `|return| / dollar_volume` scaled by 10^6. Dollar volume is used (price x shares) rather than share volume alone, providing a size-neutral measure of price impact.

## Dependencies

**Core** (installed automatically):

- [pandas](https://pandas.pydata.org/)
- [numpy](https://numpy.org/)
- [scipy](https://scipy.org/)
- [yfinance](https://github.com/ranaroussi/yfinance)
- [rich](https://github.com/Textualize/rich)

**Optional** (for `eda_plot`, installed via `pip install fin-eda[plot]`):

- [matplotlib](https://matplotlib.org/) >= 3.5

## License

MIT

---

## Changelog

### 1.2.0

**New metrics**

- **CAGR** — annualized geometric return (`expm1(log_sum x 252 / n)`) added to Core Return & Risk for every period.
- **Calmar ratio** — CAGR divided by absolute max drawdown, per period.
- **Treynor ratio** — annualized excess return per unit of beta, per period (requires benchmark).
- **Jensen's alpha** — CAPM-adjusted outperformance (asset CAGR minus the CAPM-predicted return), per period (requires benchmark).
- **Omega ratio** — full-distribution gain/loss ratio above the risk-free threshold, per period.
- **Win rate** — percentage of trading days with a positive return (full history scalar).
- **Average return on up days / down days** — mean daily return on positive and negative days separately (full history scalars).
- **Gain-loss ratio** — average daily gain divided by the absolute average daily loss (full history scalar).
- **Tracking error** — annualized standard deviation of active (asset minus benchmark) daily returns, surfaced explicitly alongside the information ratio.
- **R-squared vs market** — square of the correlation with the benchmark, per period.
- **Best daily / weekly / monthly return** — counterpart to the existing worst-period metrics.
- **52W high price and 52W low price** — absolute price levels added to Trend Structure & Price Health.
- **Annual returns** — new section with a calendar-year-by-year breakdown, partial current-year return, and best/worst calendar year summary.

**Calculation corrections**

- **Sortino ratio and downside deviation** — corrected to use `sqrt(E[min(r - rf, 0)^2])` averaged over all periods (Sortino and Price, 1994). The previous implementation used `std(negative_tail_returns)`, which divides by the count of negative days only and measures dispersion around the negative-tail mean rather than around the threshold — both incorrect.
- **Excess return vs benchmark** — changed from arithmetic (`asset_cum - bench_cum`) to geometric (`(1 + asset_cum) / (1 + bench_cum) - 1`). Arithmetic excess is misleading for periods beyond 2-3 years.
- **52W high date comparison** — replaced `is not pd.NaT` with `not pd.isna()` for correct behaviour with timezone-aware timestamps returned by recent yfinance versions.

**Removed metrics**

- **Jarque-Bera statistic** — removed from Return Distribution & Non-Normality. For most financial return series the statistic is near-certain to reject normality regardless of sample size or economic significance; skewness and kurtosis convey the distributional shape more directly.
- **Return autocorrelation** — the Regime & Time-Series Behavior section (1D, 5D, 21D autocorrelation lags) has been removed from the numerical tearsheet.

**Display improvements**

- Header now shows: current price, the benchmark symbol actually used (including when the fallback to ^GSPC is triggered), and the full data coverage line (trading day count and date range).
- 52W section in the header now shows actual high and low prices alongside the existing days-since-high and drawdown figures.
- Period columns are suppressed automatically when all values for that column are NaN, so the output scales from the shortest available window up to 30Y without manual configuration. Long-period columns (15Y, 20Y, 25Y, 30Y) appear as data coverage grows.
- Each section table uses a descriptive row label (e.g. "Risk-Adjusted Metric", "Benchmark Comparison", "Calendar Year") instead of the generic "Metric".
- Best-period metrics (best daily, weekly, monthly, annual) are always coloured green; worst-period metrics remain always red.

### 1.1.1

**`eda_plot()` — panel changes**

- **Added `annual_returns`:** Year-by-year geometric return bars (green/red) with optional benchmark overlay. Annotates each bar when 15 or fewer years of data are shown.
- **Added `rolling_beta`:** Rolling 1Y (252D) beta vs benchmark computed as `cov(asset, bench) / var(bench)`. Shaded above beta = 1, shaded below. Current beta annotated.
- Rolling Sharpe primary window clarified: 252D (1Y) is primary, 126D (6M) is the secondary overlay.

**Bug fixes**

- **Benchmark fetch threshold** — the previous check (`len > 25 x 252 trading days`) silently fell back to ^GSPC for any benchmark with less than roughly 25 years of history (e.g. GLD, sector ETFs, international funds). Now accepts any non-empty result; `period='max'` is used to retrieve full available history. Fallback to ^GSPC only triggers on a failed fetch.
- **Distribution normal overlay** — `std` now uses `ddof=1` (sample standard deviation) to match the convention used by `stats.skew` and `stats.kurtosis` in the same panel.

### 1.1.0

**New: `eda_plot()` — visual tearsheet**

- 10-panel dark-themed matplotlib tearsheet mirroring all `eda()` inputs.
- Panels: price history, cumulative return vs benchmark, underwater drawdown, monthly returns heatmap, annual returns, return distribution, beta scatter, rolling beta, rolling Sharpe, and rolling volatility regime.
- `panels=[...]` parameter for rendering any subset of panels in one call.
- `save_path` parameter to export to PNG, PDF, SVG, or any matplotlib-supported format.
- `return_fig=True` for notebook embedding or programmatic figure composition.
- Benchmark-dependent panels degrade gracefully when data is unavailable.
- Style applied via `mpl.rc_context` — does not pollute the caller's global matplotlib state.
- Added as an optional dependency: `pip install fin-eda[plot]`.

### 1.0.1

**Bug fixes**

- **YTD period** — corrected to use the current calendar year at runtime rather than the last year present in the data, which produced wrong results when analyzing historical series ending before the current year.
- **Average drawdown** — now returns no data when a period has no negative drawdown observations, instead of incorrectly showing `0.0`.
- **Time to recovery** — now returns no data when a period has no drawdown to recover from, instead of showing `0` (which implied an instantaneous recovery had occurred).
- **Volatility of volatility key name** — internal key mismatch between the success and failure paths caused the metric to appear twice in the output under different names. Now consistently labeled.
- **`quiet=True` not fully respected** — benchmark data-fetch status messages were always printed to stdout regardless of the `quiet` flag. They now correctly respect `quiet=True` and route through the Rich console for consistent formatting.
- **Monthly return resampling** — added compatibility fallback for pandas < 2.2, where the `'ME'` month-end alias was not yet available.

**Numerical improvements**

- **Cumulative and geometric mean returns** — switched from chained `.prod()` to log-sum form (`np.expm1(np.log1p(r).sum())`). Mathematically equivalent, but avoids floating-point overflow on very long return histories (20Y+) and resolves a pandas type-stub incompatibility with newer versions.
- **Capture ratios** — same log-sum refactor applied to up- and down-market annualization, eliminating potential overflow for assets with extreme up-market streaks.

**Code quality**

- `period`, `start`, `end`, and `benchmark_ticker` parameters now carry correct `Optional[str]` type annotations (previously typed as `str` despite accepting `None`).
- Period-pattern regex precompiled at module load time instead of on every report render.
- Removed stale internal comment referencing previously deleted metrics.
- Docstrings added to all internal helper functions.
