Skip to main content

A Python module that makes it easy to download current prices of TSP from the official website and perform basic data analysis using pandas and matplotlib.

Project description

tsp-analytics

CI PyPI

A Python module for retrieving the prices of Thrift Savings Plan (TSP) funds, calculating analytics, and visualizing historical performance.

Highlights

  • Pulls official TSP fund price history from tsp.gov and caches the CSV locally.
  • Flexible current-price access (latest row, per-fund latest prices, historical as-of anchors).
  • Optional completeness checks for as-of pricing (require all requested funds).
  • Current price alerts for stale data and large daily moves.
  • Analytics for returns, correlations, drawdowns, rolling metrics, risk stats, and rankings.
  • Dashboard-ready outputs (dataframes, long/tidy tables, JSON-friendly dictionaries).
  • Current price dashboard snapshot (prices, recency, trailing returns, risk stats).
  • Built-in Matplotlib visualization helpers for price history, rankings, recency, and risk.
  • Correlation pair summaries to spotlight the strongest fund relationships.
  • Data-quality tooling: cache status, fund coverage, missing business days, and reports.
  • Weighted portfolio analytics for value, drawdowns, and performance summaries.
  • Monthly return tables (wide, long, or dict) for seasonality dashboards.
  • Recent daily change windows with summary stats and heatmap-friendly outputs.
  • Recent price window helpers for the latest N trading days (wide, long, or JSON-ready).
  • Flexible fund name aliases (e.g., G, g-fund, L2050, Lifecycle 2050).

Installation

pip install tsp-analytics

For local development:

pip install -r requirements.txt
pip install -e .[dev]

If you prefer a single requirements file instead of extras:

pip install -r requirements-dev.txt

Cache Location

By default, the price history CSV is cached at ~/.cache/tsp/fund-price-history.csv. To customize the cache directory, pass data_dir or set TSP_DATA_DIR:

from pathlib import Path
from tsp import TspAnalytics

prices = TspAnalytics(data_dir=Path.home() / ".cache" / "tsp")
export TSP_DATA_DIR="$HOME/.cache/tsp"

Offline or Manual Refresh

Disable automatic updates when you need offline access or want to control refresh timing:

from tsp import TspAnalytics

prices = TspAnalytics(auto_update=False)
prices.refresh()

Quickstart

from tsp import TspAnalytics, TspIndividualFund

prices = TspAnalytics()
latest_price = prices.get_price(TspIndividualFund.G_FUND)
latest_prices = prices.get_current_prices()
latest_snapshot = prices.get_current_price_snapshot_dict()

Analytics & Visualization Quickstart

from datetime import date
from tsp import TspAnalytics, TspIndividualFund

prices = TspAnalytics()

# Price history (rows with all selected fund prices missing are dropped)
history = prices.get_price_history(
    funds=[TspIndividualFund.C_FUND, TspIndividualFund.S_FUND],
    start_date=date(2024, 1, 1),
)

# Long-format data for Plotly/Seaborn dashboards
history_long = prices.get_price_history_long(
    funds=[TspIndividualFund.C_FUND, TspIndividualFund.S_FUND],
)

# Recent trading-day window for dashboards
recent_prices = prices.get_recent_prices(days=10)
recent_prices_long = prices.get_recent_prices_long(days=10)

# Built-in chart helpers (Matplotlib)
prices.show_fund_price_chart(TspIndividualFund.C_FUND)
prices.show_current_prices_per_fund_chart()

Documentation

Building Documentation

To build the Sphinx documentation locally:

pip install -r requirements-dev.txt
sphinx-build -b html docs/sphinx docs/sphinx/_build/html

At a Glance

Current Prices

from datetime import date
from tsp import TspAnalytics, TspIndividualFund

prices = TspAnalytics()
latest = prices.get_current_prices(fund=TspIndividualFund.G_FUND)
latest_per_fund = prices.get_current_prices(per_fund=True)
latest_dict = prices.get_current_prices_dict()
latest_per_fund_dict = prices.get_current_prices_dict(per_fund=True)
latest_report = prices.get_current_price_report()
latest_report_long = prices.get_current_price_report_long()
latest_report_as_of = prices.get_current_price_report(as_of=date(2024, 1, 2))
latest_report_as_of_long = prices.get_current_price_report_long(as_of=date(2024, 1, 2))
latest_changes = prices.get_current_price_changes()
latest_changes_as_of = prices.get_current_price_changes(as_of=date(2024, 1, 2))
latest_changes_dict = prices.get_current_price_changes_dict(as_of=date(2024, 1, 2))
latest_changes_per_fund = prices.get_current_price_changes_per_fund(as_of=date(2024, 1, 2))
snapshot = prices.get_current_price_snapshot()
snapshot_as_of = prices.get_current_price_snapshot(as_of=date(2024, 1, 2))
snapshot_dict = prices.get_current_price_snapshot_dict(as_of=date(2024, 1, 2))
as_of_prices = prices.get_current_prices(as_of=date(2024, 1, 2))
as_of_prices_complete = prices.get_current_prices(
    as_of=date(2024, 1, 2),
    funds=[TspIndividualFund.G_FUND, TspIndividualFund.C_FUND],
    require_all_funds=True,
)
as_of_per_fund = prices.get_current_prices_per_fund(as_of=date(2024, 1, 2))
as_of_per_fund_dict = prices.get_current_prices_per_fund_dict(as_of=date(2024, 1, 2))
safe_per_fund = prices.get_current_prices_per_fund(
    funds=[TspIndividualFund.G_FUND, TspIndividualFund.C_FUND],
    allow_missing=True,
)
safe_per_fund_payload = prices.get_current_prices_per_fund_dict(
    funds=[TspIndividualFund.G_FUND, TspIndividualFund.C_FUND],
    allow_missing=True,
)
status = prices.get_current_price_status()
status_dict = prices.get_current_price_status_dict()
status_g = prices.get_current_price_status(fund=TspIndividualFund.G_FUND)
summary = prices.get_current_price_summary(stale_days=2)
summary_dict = prices.get_current_price_summary_dict(stale_days=2)
summary_as_of = prices.get_current_price_summary(as_of=date(2024, 1, 2), stale_days=2)
summary_as_of_dict = prices.get_current_price_summary_dict(
    as_of=date(2024, 1, 2),
    stale_days=2,
)
alerts = prices.get_current_price_alerts(stale_days=2, change_threshold=0.03)
alerts_dict = prices.get_current_price_alerts_dict(stale_days=2, change_threshold=0.03)
alerts_g = prices.get_current_price_alerts(fund="G", stale_days=2, change_threshold=0.03)
alert_summary = prices.get_current_price_alert_summary(stale_days=2, change_threshold=0.03)
alert_summary_dict = prices.get_current_price_alert_summary_dict(
    stale_days=2,
    change_threshold=0.03,
)

Tip: When requesting as_of prices for a single fund, the lookup skips rows where that fund has missing prices so you always get the most recent valid value.

Analytics

from datetime import date
from tsp import TspAnalytics, TspIndividualFund

prices = TspAnalytics()
returns = prices.get_daily_returns()
drawdown = prices.get_drawdown_series(fund=TspIndividualFund.C_FUND)
price_history = prices.get_price_history(start_date=date(2024, 1, 1))
risk_summary = prices.get_risk_return_summary()
performance_summary_dict = prices.get_performance_summary_dict()
risk_summary_dict = prices.get_risk_return_summary_dict()
dashboard = prices.get_current_price_dashboard(periods=[1, 5, 20])
price_summary = prices.get_price_summary()
price_recency = prices.get_price_recency()
correlation_pairs = prices.get_correlation_pairs(top_n=5)
current_overview = prices.get_current_fund_overview()
current_overview_as_of = prices.get_current_fund_overview(as_of=date(2024, 1, 2))
current_change_rank = prices.get_fund_rankings(metric="change_percent")
return_distribution = prices.get_return_distribution_summary(
    fund=TspIndividualFund.C_FUND,
    percentiles=[0.05, 0.5, 0.95],
)
price_stats = prices.get_price_statistics(start_date=date(2024, 1, 1))
return_stats = prices.get_return_statistics(end_date=date(2024, 3, 31))
price_stats_dict = prices.get_price_statistics_dict(fund=TspIndividualFund.G_FUND)
return_stats_dict = prices.get_return_statistics_dict(
    fund=TspIndividualFund.G_FUND,
    trading_days=252,
)
recent_changes = prices.get_recent_price_changes(days=5)
recent_change_summary = prices.get_recent_price_change_summary(days=5)
recent_changes_dict = prices.get_recent_price_changes_dict(days=5)
fund_report = prices.get_fund_analytics_report(
    TspIndividualFund.C_FUND,
    start_date=date(2024, 1, 1),
)
fund_report_dict = prices.get_fund_analytics_report_dict(
    TspIndividualFund.C_FUND,
    start_date=date(2024, 1, 1),
)
drawdown_summary = prices.get_drawdown_summary_dict(TspIndividualFund.C_FUND)
range_changes = prices.get_price_change_by_date_range_dict(
    start_date=date(2024, 1, 1),
    end_date=date(2024, 1, 31),
)
as_of_changes = prices.get_price_changes_as_of_per_fund(date(2024, 1, 3))
monthly_return_table = prices.get_monthly_return_table_long(TspIndividualFund.C_FUND)
rolling_volatility = prices.get_rolling_volatility(TspIndividualFund.C_FUND, window=63)
rolling_performance = prices.get_rolling_performance_summary(TspIndividualFund.C_FUND, window=63)
trailing_return = prices.get_trailing_returns(periods=20, fund=TspIndividualFund.C_FUND)
trailing_return_long = prices.get_trailing_returns_long(periods=[1, 5, 20, 63])
trailing_return_dict = prices.get_trailing_returns_dict(periods=[1, 5, 20, 63])
correlation_payload = prices.get_correlation_matrix_dict()
rolling_correlation_payload = prices.get_rolling_correlation_matrix_dict(window=63)

Visualization

from datetime import date
from tsp import TspAnalytics, TspIndividualFund

prices = TspAnalytics()
prices.show_fund_price_chart(TspIndividualFund.C_FUND)
prices.show_latest_price_changes_per_fund_chart()
prices.show_recent_price_change_heatmap(days=5)
prices.show_price_recency_chart()
prices.show_current_prices_per_fund_chart()
prices.show_price_history_chart(start_date=date(2024, 1, 1))
prices.show_return_histogram_chart(TspIndividualFund.C_FUND)
prices.show_correlation_heatmap()
prices.show_correlation_pairs_chart(top_n=5)
prices.show_rolling_performance_summary_chart(TspIndividualFund.C_FUND, window=63)
prices.show_fund_rankings_chart(metric="trailing_return", period=20, top_n=5)
prices.show_current_price_dashboard_metric_chart(metric="change_percent")
prices.show_current_price_alerts_chart(metric="change_percent", change_threshold=0.03)
prices.show_trailing_returns_chart(
    periods=[1, 5, 20, 63],
    funds=[TspIndividualFund.G_FUND, TspIndividualFund.C_FUND],
)

# Capture charts for custom dashboards or reports
fig, ax = prices.show_fund_price_chart(TspIndividualFund.C_FUND, show=False)
fig.savefig("c-fund.png", dpi=150, bbox_inches="tight")

# Current price bar chart anchored to a historical date
fig, ax = prices.show_current_prices_per_fund_chart(
    as_of=date(2024, 1, 2),
    sort_by="fund",
    show=False,
)

Data Quality & Cache Status

from tsp import TspAnalytics

prices = TspAnalytics(auto_update=False)
status = prices.get_cache_status()
status_dict = prices.get_cache_status_dict()
report = prices.get_data_quality_report()
report_dict = prices.get_data_quality_report_dict()

Fund Metadata & Aliases

from tsp import TspAnalytics

prices = TspAnalytics(auto_update=False)
aliases = prices.get_fund_aliases()
metadata = prices.get_fund_metadata()
metadata_dict = prices.get_fund_metadata_dict()

Data Sources & Caching

The library downloads the official fund-price-history.csv file from tsp.gov and caches it locally. The cache refreshes automatically after the most recent business day and the configured update time (defaults to 7:00 PM local time). You can override the cache directory with the TSP_DATA_DIR environment variable or the data_dir constructor argument:

from pathlib import Path
from tsp import TspAnalytics

prices = TspAnalytics(data_dir=Path.home() / ".cache" / "tsp")

If you need to run fully offline or inject a custom HTTP session, disable auto updates and load data explicitly:

from tsp import TspAnalytics

prices = TspAnalytics(auto_update=False)
prices.refresh()  # or prices.load_csv(...), prices.load_csv_text(...)

For custom networking (proxies, retries, shared sessions), pass a pre-configured session. The library ensures a CSV-friendly Accept header and a user-agent string are present:

import requests
from tsp import TspAnalytics

session = requests.Session()
session.headers.update({"User-Agent": "my-app/1.0"})
prices = TspAnalytics(session=session, request_timeout=15.0, max_retries=3, retry_backoff=0.5)

Downloads are validated to ensure the response looks like a CSV (including a Date header and known fund columns). If the upstream response appears to be HTML or missing expected headers, the client raises a validation error and preserves the existing cache. See docs/TROUBLESHOOTING.md for guidance.

To load a pandas dataframe directly, pass it into load_dataframe. The dataframe can include either a Date column or a date-like index (named Date or a datetime index):

import pandas as pd
from tsp import TspAnalytics, TspIndividualFund

df = pd.DataFrame(
    {
        TspIndividualFund.G_FUND.value: [100.0, 101.5],
        TspIndividualFund.C_FUND.value: [200.0, 202.0],
    },
    index=pd.to_datetime(["2024-01-02", "2024-01-03"]),
)

prices = TspAnalytics(auto_update=False)
prices.load_dataframe(df)

See docs/DATA_SOURCES.md for full details.

Analytics & Visualization Tips

  • Use the long-format helpers (get_price_history_long, get_daily_returns_long) when charting with Seaborn or Plotly.
  • Price history helpers drop rows where all selected fund prices are missing to avoid blank records in downstream charts and analytics.
  • Use the show_* helpers for quick Matplotlib visuals; they return (fig, ax) so you can save or embed charts without opening a window.
  • Use dictionary helpers (like get_current_price_report_dict) for JSON-ready dashboard payloads.

Start with docs/ANALYTICS.md and docs/VISUALIZATION.md for step-by-step examples.

Contributing & Development

Interested in contributing? Start here:

Quick setup:

python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
pip install -e .
pytest

When you add user-facing features, please update the relevant docs in docs/ and include tests under tests/.

Project details


Download files

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

Source Distribution

tsp_analytics-0.1.0.tar.gz (148.0 kB view details)

Uploaded Source

Built Distribution

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

tsp_analytics-0.1.0-py3-none-any.whl (82.8 kB view details)

Uploaded Python 3

File details

Details for the file tsp_analytics-0.1.0.tar.gz.

File metadata

  • Download URL: tsp_analytics-0.1.0.tar.gz
  • Upload date:
  • Size: 148.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for tsp_analytics-0.1.0.tar.gz
Algorithm Hash digest
SHA256 07182d29974899c5ce6220d7102bce32051ac2e70d48c5eb7d1fe2b88d0eb183
MD5 b3f61fca260e489708e34397fd4023dc
BLAKE2b-256 c337cb7110ed83966e47d9b7b6602da88166bf137c263a1660f07936d4db09b4

See more details on using hashes here.

File details

Details for the file tsp_analytics-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: tsp_analytics-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 82.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for tsp_analytics-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c461029b14ae4cb42a04e3662aec6d8b2daa8fd6eb4f8525946ed5ce6fc8f083
MD5 598a9f837b15cc0a9926e91d38765f56
BLAKE2b-256 927d402a5dcad499c38f085dbde11d4c4103ffa3a76f332632db9ba57ddb96de

See more details on using hashes here.

Supported by

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