Skip to main content

marketkit

CI PyPI Python versions License: MIT

Reliable, clean market data and analytics — pure Python, no C dependencies.

marketkit fetches OHLCV price history from public sources, normalizes it into one predictable DataFrame shape, and gives you the analytics and indicators you need on top of it — returns, risk metrics, drawdown, and a standard set of technical indicators — all behind a tiny, beginner-friendly API.

import marketkit as mk

df = mk.get("RELIANCE.NS", period="2y")   # clean, adjusted OHLCV
mk.sharpe(df)                             # -0.12
df["rsi"] = mk.rsi(df)                    # add an indicator column
mk.summary("RELIANCE.NS")                 # one-line risk/return report

Table of contents


Why marketkit

  • Pure Python — installs with a plain pip install. No compiler, no system libraries, no wheels that fail on Apple Silicon or locked-down CI runners.
  • Doesn't break — automatic source fallback (Yahoo → Stooq) plus local caching, so one bad day from a data provider doesn't crash your script.
  • Clean, predictable output — flat columns, stable dtypes, adjusted prices by default. The same shape every time, whether you ask for one ticker or fifty.
  • Beginner-friendly — sensible defaults, clear exceptions, a small surface area you can learn in five minutes.
  • Typed — ships a py.typed marker and passes mypy --strict.

Install

pip install marketkit

Requires Python 3.9+. The only runtime dependencies are pandas, requests, pyarrow, and platformdirs — all pure-Python or pre-built wheels.

Plotting and a couple of statistical helpers are optional extras so the base install never pulls in matplotlib/plotly/scipy:

pip install marketkit[plot]      # matplotlib charts
pip install marketkit[plotly]    # interactive plotly charts
pip install marketkit[stats]     # scipy, for select statistical helpers
pip install marketkit[all]       # everything above

Calling a plotting function without the extra installed raises marketkit.errors.PlottingUnavailable with the exact pip install command to run — it never fails with a bare ImportError.


Quick start

import marketkit as mk

# 1. Fetch clean, adjusted OHLCV data
df = mk.get("RELIANCE.NS", period="2y")

# 2. Risk / return analytics
mk.cagr(df)        # -0.046  (compound annual growth rate)
mk.sharpe(df)      # -0.12   (annualized Sharpe ratio)
mk.drawdown(df)    # (drawdown series, max drawdown)

# 3. Indicators — return Series/DataFrames you can assign back
df["sma50"] = mk.sma(df, window=50)
df["rsi"] = mk.rsi(df)

# 4. One-shot summary, no manual fetch needed
mk.summary("RELIANCE.NS")

Indian listings use Yahoo's exchange suffixes — .NS for NSE (RELIANCE.NS, TCS.NS, INFY.NS) and .BO for BSE (RELIANCE.BO).

A runnable, end-to-end walkthrough (with plots) lives in examples/quickstart.ipynb.


What you can do

1. Fetching data

mk.get() is the single entry point for price history. It handles single or multiple tickers, date ranges or named periods, intervals, source selection, and offline reads.

mk.get("RELIANCE.NS")                                 # 1 year of daily bars
mk.get("RELIANCE.NS", period="5y")                     # "1y" | "2y" | "5y" | "10y" | "max"
mk.get("RELIANCE.NS", start="2020-01-01", end="2023-01-01")
mk.get("RELIANCE.NS", interval="1wk")                  # "1d" | "1wk" | "1mo"
mk.get("RELIANCE.NS", interval="5m")                   # intraday: "1m"|"5m"|"15m"|"30m"|"1h" too

# Multiple tickers — long (tidy) frame by default, with a `ticker` column
mk.get(["RELIANCE.NS", "TCS.NS", "INFY.NS"])

# Multiple tickers — wide frame, one column level per ticker
mk.get(["RELIANCE.NS", "TCS.NS", "INFY.NS"], wide=True)

# Choose / reorder sources, or pin a single source
mk.get("RELIANCE.NS", sources=["stooq"])

# Cache-only: never touch the network
mk.get("RELIANCE.NS", offline=True)

Tickers are case-insensitive ("reliance.ns""RELIANCE.NS"). In a multi-ticker request, tickers are fetched concurrently and tickers that fail on every source are silently skipped rather than killing the whole call — you always get back whatever succeeded.

Intraday bars (1m/5m/15m/30m/1h) use a shorter 5-minute cache TTL instead of the daily 12h TTL, since they go stale much faster. Use resample() to roll daily bars up to weekly/monthly:

weekly = mk.resample(df, "1wk")
monthly = mk.resample(df, "1mo")

2. The data contract

Every frame marketkit returns has the same canonical shape, so downstream code never has to special-case a provider's quirks.

Property Guarantee
Index DatetimeIndex named date, timezone-naive, sorted ascending, no duplicates
Columns open, high, low, close, adj_close, volume (exactly, in this order)
OHLC / adj_close dtype float64
volume dtype Int64 (pandas nullable integer)
Adjusted prices adj_close is always present; equals close if the source has no adjusted series
Multi-ticker adds a ticker column (long format) or one column level per ticker (wide=True)

This means df["adj_close"], df["volume"], and a clean date index are always there — no renaming, no tz wrangling, no surprise object dtypes.

3. Returns & growth

mk.returns(df)                     # daily simple returns (Series)
mk.returns(df, kind="log")         # log returns
mk.returns(df, column="close")     # use raw close instead of adj_close

mk.cagr(df)                        # compound annual growth rate (float)
mk.cagr(df, periods_per_year=52)   # for weekly data

By default, returns are computed on adj_close so dividends and splits are accounted for.

4. Risk metrics

All risk functions accept the same keyword args as returns() (kind, column) plus an annualization factor, and most accept a risk-free rate.

mk.volatility(df)                  # annualized standard deviation of returns
mk.sharpe(df)                      # annualized Sharpe ratio
mk.sharpe(df, rf=0.04)             # with a 4% annual risk-free rate
mk.sortino(df)                     # annualized Sortino ratio (downside risk)

dd_series, max_dd = mk.drawdown(df)  # full drawdown curve + worst drawdown

drawdown() returns a tuple: the running drawdown Series (peak-to-trough at every point) and the single worst (most negative) drawdown as a float.

5. Technical indicators

Indicators take a price frame (or a bare Series) and return a Series or DataFrame you can assign straight back onto your data. They operate on close by default; pass column="adj_close" to use adjusted prices.

mk.sma(df, window=20)              # simple moving average
mk.ema(df, window=20)              # exponential moving average
mk.rsi(df, period=14)              # Wilder's relative strength index

macd = mk.macd(df)                 # DataFrame: macd, signal, hist
macd = mk.macd(df, fast=12, slow=26, signal=9)

bands = mk.bollinger(df)           # DataFrame: mid, upper, lower
bands = mk.bollinger(df, window=20, k=2)

6. Statistics

Beyond risk metrics, marketkit ships a set of statistical tools for comparing a ticker against a benchmark or studying its own return behavior.

mk.beta(df, benchmark_df)                  # market beta
mk.alpha(df, benchmark_df, rf=0.04)        # Jensen's alpha
mk.correlation(df, benchmark_df)           # return correlation
mk.tracking_error(df, benchmark_df)        # annualized std of return spread
mk.information_ratio(df, benchmark_df)     # active return / tracking error
mk.rolling_beta(df, benchmark_df, window=63)

mk.zscore(df, window=20)                   # rolling or full-sample z-score
mk.normalize(df, base=100)                 # rebase a price series to 100
mk.autocorr(df, lags=10)                   # return autocorrelation by lag
mk.linreg_channel(df, window=20, k=2)      # rolling OLS trend channel
mk.hurst(df)                               # Hurst exponent (trend vs. mean-reversion)
mk.rolling_corr({"a": df_a, "b": df_b}, window=30)  # pairwise rolling correlation

Calmar, Omega, VaR/CVaR, downside deviation, the Ulcer index, and rolling Sharpe/volatility live alongside the original risk metrics:

mk.calmar(df)               # CAGR / |max drawdown|
mk.omega(df, threshold=0.0)
mk.var(df, level=0.05)                       # historical VaR (default)
mk.var(df, level=0.05, method="gaussian")    # parametric VaR
mk.cvar(df, level=0.05)                      # expected shortfall beyond VaR
mk.downside_deviation(df)
mk.ulcer_index(df)
mk.rolling_sharpe(df, window=63)
mk.rolling_volatility(df, window=63)

7. Plotting

marketkit[plot] (or [plotly]) adds chart functions that take the same canonical DataFrame you already have — no separate plotting data prep.

mk.plot(df, column="close")                          # line chart
mk.plot(df, kind="candle")                            # candlesticks
mk.plot(df, indicators=["sma:50", "ema:20"])           # overlay indicators on price
mk.plot_indicators(df, ["sma:50", "rsi", "macd"])      # overlays + stacked sub-panels
mk.candlestick(df, volume=True)                        # candles with a volume sub-axis
mk.plot_drawdown(df)                                   # drawdown curve
mk.plot_returns(df, kind="hist")                        # return distribution
mk.plot_returns(df, kind="cumulative")                  # cumulative growth curve
mk.plot_correlation({"RELIANCE": df_a, "TCS": df_b})   # correlation heatmap

# Multi-ticker (long-format) frames can be rebased to a common start
mk.plot(long_df, column="close", rebase=True)

# Interactive charts via Plotly instead of matplotlib
mk.plot(df, kind="candle", backend="plotly")

Indicator overlays are specified as strings ("sma:50", "macd:12,26,9"); plot_indicators() decides whether each one draws on the price axis (SMA/EMA/Bollinger/...) or its own stacked panel (RSI/MACD/ADX/...).

8. Signals

Boolean helper functions for spotting common crossover patterns — these are building blocks, not a backtester: no position sizing, no P&L, no execution modeling.

mk.crossover(fast_series, slow_series)     # True where fast crosses above slow
mk.crossunder(fast_series, slow_series)    # True where fast crosses below slow
mk.golden_cross(df, fast=50, slow=200)     # SMA50 crossing above SMA200
mk.death_cross(df, fast=50, slow=200)      # SMA50 crossing below SMA200
mk.rsi_oversold(df, threshold=30)
mk.rsi_overbought(df, threshold=70)

9. One-shot summary & reports

When you just want the headline numbers, summary() fetches the data and computes the key metrics in a single call, returning a labeled Series.

mk.summary("RELIANCE.NS")
mk.summary("RELIANCE.NS", period="5y")
ticker          RELIANCE.NS
start            2021-06-21
end              2026-06-19
last_close           1309.5
cagr                 0.0536
annual_vol           0.2234
sharpe                 0.35
max_drawdown        -0.2718
dtype: object

Output above is the real period="5y" summary for RELIANCE.NS. Numbers shift as new data arrives.

For a fuller picture, report() (requires marketkit[plot]) builds a 4-panel tear sheet — price with SMA20/SMA50, drawdown, return histogram, rolling 63-day Sharpe — and returns the same metrics alongside the figure:

fig, metrics = mk.report("RELIANCE.NS", period="2y")
fig.savefig("reliance_report.png")

10. Ergonomics

A pandas accessor, a stateful Ticker object, and multi-ticker screening helpers cut down on repeated get() calls when you're working with the same frame or symbol set repeatedly.

# .mk accessor — works on any canonical marketkit DataFrame
df = mk.get("RELIANCE.NS")
df.mk.sharpe()
df.mk.rsi()
df.mk.plot(indicators=["sma:50"])

# Ticker — fetches once, exposes everything as a method
t = mk.Ticker("RELIANCE.NS", period="2y")
t.sharpe()
t.rsi()
t.plot()

# compare() — a metrics table across several tickers
mk.compare(["RELIANCE.NS", "TCS.NS", "INFY.NS"], metrics=["cagr", "sharpe", "max_drawdown"])

# screen() — filter tickers by a predicate over their metrics
mk.screen(["RELIANCE.NS", "TCS.NS", "INFY.NS"], filters={"sharpe": lambda s: s > 0.5})

11. Command-line interface

Installing marketkit also installs a marketkit console script for quick terminal use:

marketkit get RELIANCE.NS --period 5y --out reliance.csv
marketkit summary RELIANCE.NS
marketkit plot RELIANCE.NS --indicators sma:50,rsi --save chart.png   # needs marketkit[plot]

Caching & offline mode

Fetched data is cached locally as Parquet in a platform-appropriate cache directory (resolved via platformdirs) and reused while still fresh — a 12h TTL by default for daily bars. This makes repeated runs fast and keeps you off the network when you don't need fresh data.

mk.get("RELIANCE.NS")                 # first call hits the network, caches the result
mk.get("RELIANCE.NS")                 # subsequent calls within 12h read from cache
mk.get("RELIANCE.NS", offline=True)   # force a cache-only read; raises if nothing fresh

You can also flip offline mode globally:

import marketkit.config as config
config.OFFLINE = True          # every get() now reads cache only

Reliability & error handling

marketkit tries each configured source in order and falls back on failure, so a single provider outage or rate-limit doesn't break your script. When data genuinely can't be retrieved, it raises a typed exception you can catch.

from marketkit.errors import (
    MarketkitError,             # base class for everything below
    InvalidRequest,             # bad arguments (e.g. start >= end, bad interval)
    SourceError,                # a source failed in a non-recoverable way
    RateLimited,                # a source refused (HTTP 429 / auth throttling) — subclass of SourceError
    DataUnavailable,            # no source could satisfy the request
    PlottingUnavailable,        # matplotlib isn't installed — run `pip install marketkit[plot]`
    OptionalDependencyMissing,  # some other optional extra is missing
)

try:
    df = mk.get("RELIANCE.NS", period="2y")
except DataUnavailable:
    ...  # offline with no fresh cache, or every source failed
except InvalidRequest:
    ...  # you passed something invalid

Catching MarketkitError handles all of the above at once.


Configuration

Defaults live in marketkit.config and can be overridden per-call (preferred) or globally.

Setting Default Meaning
DEFAULT_SOURCE_ORDER ["yahoo", "stooq"] Source priority; first to succeed wins
CACHE_TTL_SECONDS 43200 (12h) How long cached bars are considered fresh
CACHE_DIR platform cache dir Where Parquet cache files are stored
PERIODS_PER_YEAR {"1d": 252, "1wk": 52, "1mo": 12} Annualization factors by interval
REQUEST_TIMEOUT 15 HTTP timeout in seconds
OFFLINE False If True, only read cache, never hit the network

An optional Alpha Vantage API key is read from the ALPHAVANTAGE_API_KEY environment variable (never hardcode keys).


Full API reference

Everything below is importable directly from the top-level marketkit package.

Data

Function Returns Description
get(tickers, *, period="1y", start=None, end=None, interval="1d", sources=None, offline=None, wide=False) DataFrame Fetch clean OHLCV for one ticker or a list. Multi-ticker requests fetch concurrently
resample(data, interval) DataFrame Roll daily OHLCV up to "1wk" or "1mo" bars

Returns & growth

Function Returns Description
returns(data, *, kind="simple", column="adj_close") Series Simple or log returns
cagr(data, *, periods_per_year=252, column="adj_close") float Compound annual growth rate

Risk

Function Returns Description
volatility(data, *, periods_per_year=252, **kw) float Annualized volatility
sharpe(data, *, rf=0.0, periods_per_year=252, **kw) float Annualized Sharpe ratio
sortino(data, *, rf=0.0, periods_per_year=252, **kw) float Annualized Sortino ratio
drawdown(data, **kw) (Series, float) Drawdown curve and max drawdown
calmar(data, *, periods_per_year=252, **kw) float CAGR / |max drawdown| (±inf/nan if no drawdown)
omega(data, *, threshold=0.0, **kw) float Omega ratio around a return threshold
var(data, *, level=0.05, method="historical", **kw) float Value at Risk ("historical" or "gaussian")
cvar(data, *, level=0.05, **kw) float Conditional VaR / expected shortfall
downside_deviation(data, *, threshold=0.0, **kw) float Std. dev. of below-threshold returns
ulcer_index(data, **kw) float RMS of drawdown depth
rolling_sharpe(data, *, window=63, **kw) Series Rolling annualized Sharpe
rolling_volatility(data, *, window=63, **kw) Series Rolling annualized volatility

Indicators

Function Returns Description
sma(data, window=20, column="close") Series Simple moving average
ema(data, window=20, column="close") Series Exponential moving average
wma(data, window=20, column="close") Series Weighted moving average
hma(data, window=20, column="close") Series Hull moving average
dema(data, window=20, column="close") Series Double exponential moving average
tema(data, window=20, column="close") Series Triple exponential moving average
rsi(data, period=14, column="close") Series Relative strength index (Wilder)
macd(data, fast=12, slow=26, signal=9, column="close") DataFrame MACD line, signal, histogram
bollinger(data, window=20, k=2, column="close") DataFrame Bollinger bands (mid, upper, lower)
true_range(data) Series True range (needs OHLC)
atr(data, period=14) Series Average true range, Wilder-smoothed
stochastic(data, k=14, d=3) DataFrame Stochastic oscillator (%K, %D)
williams_r(data, period=14) Series Williams %R
cci(data, period=20) Series Commodity channel index
adx(data, period=14) DataFrame Average directional index (adx, +di, -di), Wilder-smoothed
roc(data, period=12, column="close") Series Rate of change (%)
momentum(data, period=12, column="close") Series Raw price momentum
obv(data) Series On-balance volume
vwap(data) Series Volume-weighted average price (cumulative)
mfi(data, period=14) Series Money flow index
keltner(data, window=20, k=2, column="close") DataFrame Keltner channel (mid, upper, lower)
donchian(data, window=20) DataFrame Donchian channel (upper, mid, lower)
ichimoku(data, *, tenkan=9, kijun=26, senkou_b=52) DataFrame Ichimoku cloud components
psar(data, step=0.02, max_step=0.2) Series Parabolic SAR

Statistics

Function Returns Description
beta(data, benchmark, **kw) float Market beta vs. a benchmark
alpha(data, benchmark, *, rf=0.0, **kw) float Jensen's alpha vs. a benchmark
correlation(data, benchmark, **kw) float Return correlation
tracking_error(data, benchmark, **kw) float Annualized std. dev. of return spread
information_ratio(data, benchmark, **kw) float Active return / tracking error
rolling_beta(data, benchmark, *, window=63, **kw) Series Rolling beta
zscore(data, *, window=None, column="close") Series Rolling or full-sample z-score
normalize(data, *, base=100, column="close") Series Rebase a price series to a common start
autocorr(data, *, lags=20, **kw) Series Return autocorrelation by lag
linreg_channel(data, *, window=100, k=2.0, column="close") DataFrame Rolling OLS trend channel (mid, upper, lower)
hurst(data, *, max_lag=100, column="close") float Hurst exponent
rolling_corr(series_map, *, window=30) DataFrame Pairwise rolling correlation, tuple-keyed columns

Plotting (marketkit[plot] / [plotly])

Function Returns Description
plot(data, *, kind="line", column="adj_close", indicators=None, ax=None, rebase=False, backend="matplotlib") Axes (or Plotly Figure) Line or candlestick chart with optional overlays
candlestick(data, *, volume=False, ax=None) Axes Candlestick chart, optional volume sub-axis
plot_indicators(data, indicators, *, ax=None) Axes or list[Axes] Overlays + stacked panel indicators
plot_drawdown(data, **kw) Axes Drawdown curve
plot_returns(data, *, kind="hist", **kw) Axes Return histogram ("hist") or cumulative growth ("cumulative")
plot_correlation(series_map, **kw) Axes Correlation heatmap across tickers

Signals

Function Returns Description
crossover(fast, slow) Series[bool] True where fast crosses above slow
crossunder(fast, slow) Series[bool] True where fast crosses below slow
golden_cross(data, *, fast=50, slow=200) Series[bool] SMA(fast) crossing above SMA(slow)
death_cross(data, *, fast=50, slow=200) Series[bool] SMA(fast) crossing below SMA(slow)
rsi_oversold(data, *, period=14, threshold=30, column="close") Series[bool] RSI below threshold
rsi_overbought(data, *, period=14, threshold=70, column="close") Series[bool] RSI above threshold

Convenience

Function Returns Description
summary(ticker, *, period="1y", **kw) Series Fetch + report ticker, dates, last close, CAGR, vol, Sharpe, max drawdown
report(ticker, *, period="1y", **kw) (Figure, Series) 4-panel tear sheet + the same headline metrics (needs marketkit[plot])
compare(tickers, *, metrics=None, period="1y", **kw) DataFrame Metrics table across several tickers
screen(tickers, *, filters, period="1y", **kw) DataFrame Filter tickers by predicates over their metrics
Ticker(symbol, **kw) Ticker Fetches once, exposes analytics/plotting as methods
df.mk.* Pandas accessor exposing the same analytics/plotting as df.mk.sharpe(), df.mk.plot(), etc.

All analytics and indicator functions accept either a canonical DataFrame or a bare price Series, so you can compose them however you like.


Development

git clone https://github.com/aditya33agrawal/marketkit.git
cd marketkit
pip install -e ".[dev]"
pytest

Linting and type-checking mirror CI:

ruff check .
mypy --strict src

See docs/tech-plan.md for the internal architecture and the full data contract.


Disclaimer

Not affiliated with any data provider. Data is for personal/research use only. Users must comply with each source's terms of service. This is not financial advice.

License

MIT © Aditya Agrawal

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

marketkit-1.0.0.tar.gz (60.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

marketkit-1.0.0-py3-none-any.whl (39.3 kB view details)

Uploaded Python 3

File details

Details for the file marketkit-1.0.0.tar.gz.

File metadata

  • Download URL: marketkit-1.0.0.tar.gz
  • Upload date:
  • Size: 60.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for marketkit-1.0.0.tar.gz
Algorithm Hash digest
SHA256 ae06b3853985144dee10c3ecbf2b17c41a0b15550d8bed352a4c43df67c6e998
MD5 cf532f975b67fbe8b2a68c87c887ef44
BLAKE2b-256 1f6de098dfb374320be0461c9328f7d12bc9fdb0cbf04f8805fa77535dd0b7cd

See more details on using hashes here.

Provenance

The following attestation bundles were made for marketkit-1.0.0.tar.gz:

Publisher: publish.yml on aditya33agrawal/marketkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file marketkit-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: marketkit-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 39.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for marketkit-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 94d9e02b58bba012e06bfb6e05ee4f2005458b5e760e9a2e2a3bf555f7a23d1d
MD5 bf7cef21bebb5d8b2281a0be372a0750
BLAKE2b-256 7fb9db6b6a516f73deb83a5d26793952d475564e7976750922400763a2a1c5c4

See more details on using hashes here.

Provenance

The following attestation bundles were made for marketkit-1.0.0-py3-none-any.whl:

Publisher: publish.yml on aditya33agrawal/marketkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

0.1.3

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page