{"id":44752682,"url":"https://github.com/srichs/tsp-analytics","last_synced_at":"2026-02-15T23:16:33.146Z","repository":{"id":337235697,"uuid":"1152787759","full_name":"srichs/tsp-analytics","owner":"srichs","description":"A python module for retrieving the prices of TSP funds, calculating total fund values, performing analytics, and visualizing the data using charts.","archived":false,"fork":false,"pushed_at":"2026-02-08T13:29:26.000Z","size":178,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-08T20:08:22.597Z","etag":null,"topics":["finance","pandas","plan","python","savings","thrift","tsp"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/tsp-analytics/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/srichs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-08T12:42:46.000Z","updated_at":"2026-02-08T14:10:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/srichs/tsp-analytics","commit_stats":null,"previous_names":["srichs/tsp-analytics"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/srichs/tsp-analytics","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Ftsp-analytics","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Ftsp-analytics/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Ftsp-analytics/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Ftsp-analytics/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/srichs","download_url":"https://codeload.github.com/srichs/tsp-analytics/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Ftsp-analytics/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29492030,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-15T19:29:10.908Z","status":"ssl_error","status_checked_at":"2026-02-15T19:29:10.419Z","response_time":118,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["finance","pandas","plan","python","savings","thrift","tsp"],"created_at":"2026-02-15T23:16:32.462Z","updated_at":"2026-02-15T23:16:33.131Z","avatar_url":"https://github.com/srichs.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tsp-analytics\n\n[![CI](https://github.com/srichs/tsp-analytics/actions/workflows/ci.yml/badge.svg)](https://github.com/srichs/tsp-analytics/actions/workflows/ci.yml)\n[![Docs](https://img.shields.io/github/actions/workflow/status/srichs/tsp-analytics/ci.yml?branch=main\u0026label=docs)](https://github.com/srichs/tsp-analytics/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/tsp-analytics)](https://pypi.org/project/tsp-analytics/)\n\nA Python module for retrieving the prices of Thrift Savings Plan (TSP) funds, calculating analytics, and visualizing historical performance.\n\n## Highlights\n\n- Pulls official TSP fund price history from `tsp.gov` and caches the CSV locally.\n- Flexible current-price access (latest row, per-fund latest prices, historical as-of anchors).\n- Optional completeness checks for as-of pricing (require all requested funds).\n- Current price alerts for stale data and large daily moves.\n- Analytics for returns, correlations, drawdowns, rolling metrics, risk stats, and rankings.\n- Dashboard-ready outputs (dataframes, long/tidy tables, JSON-friendly dictionaries).\n- Current price dashboard snapshot (prices, recency, trailing returns, risk stats).\n- Built-in Matplotlib visualization helpers for price history, rankings, recency, and risk.\n- Correlation pair summaries to spotlight the strongest fund relationships.\n- Data-quality tooling: cache status, fund coverage, missing business days, and reports.\n- Weighted portfolio analytics for value, drawdowns, and performance summaries.\n- Monthly return tables (wide, long, or dict) for seasonality dashboards.\n- Recent daily change windows with summary stats and heatmap-friendly outputs.\n- Recent price window helpers for the latest N trading days (wide, long, or JSON-ready).\n- Flexible fund name aliases (e.g., `G`, `g-fund`, `L2050`, `Lifecycle 2050`).\n\n## Installation\n\n```bash\npip install tsp-analytics\n```\n\nFor local development:\n\n```bash\npip install -r requirements.txt\npip install -e .[dev]\n```\n\nIf you prefer a single requirements file instead of extras:\n\n```bash\npip install -r requirements-dev.txt\n```\n\n## Cache Location\n\nBy default, the price history CSV is cached at `~/.cache/tsp/fund-price-history.csv`.\nTo customize the cache directory, pass `data_dir` or set `TSP_DATA_DIR`:\n\n```python\nfrom pathlib import Path\nfrom tsp import TspAnalytics\n\nprices = TspAnalytics(data_dir=Path.home() / \".cache\" / \"tsp\")\n```\n\n```bash\nexport TSP_DATA_DIR=\"$HOME/.cache/tsp\"\n```\n\n## Offline or Manual Refresh\n\nDisable automatic updates when you need offline access or want to control refresh timing:\n\n```python\nfrom tsp import TspAnalytics\n\nprices = TspAnalytics(auto_update=False)\nprices.refresh()\n```\n\n## Quickstart\n\n```python\nfrom tsp import TspAnalytics, TspIndividualFund\n\nprices = TspAnalytics()\nlatest_price = prices.get_price(TspIndividualFund.G_FUND)\nlatest_prices = prices.get_current_prices()\nlatest_snapshot = prices.get_current_price_snapshot_dict()\n```\n\n## Analytics \u0026 Visualization Quickstart\n\n```python\nfrom datetime import date\nfrom tsp import TspAnalytics, TspIndividualFund\n\nprices = TspAnalytics()\n\n# Price history (rows with all selected fund prices missing are dropped)\nhistory = prices.get_price_history(\n    funds=[TspIndividualFund.C_FUND, TspIndividualFund.S_FUND],\n    start_date=date(2024, 1, 1),\n)\n\n# Long-format data for Plotly/Seaborn dashboards\nhistory_long = prices.get_price_history_long(\n    funds=[TspIndividualFund.C_FUND, TspIndividualFund.S_FUND],\n)\n\n# Recent trading-day window for dashboards\nrecent_prices = prices.get_recent_prices(days=10)\nrecent_prices_long = prices.get_recent_prices_long(days=10)\n\n# Built-in chart helpers (Matplotlib)\nprices.show_fund_price_chart(TspIndividualFund.C_FUND)\nprices.show_current_prices_per_fund_chart()\n```\n\n## Documentation\n\n- [Documentation index](docs/README.md)\n- [Getting started](docs/GETTING_STARTED.md)\n- [Usage guide](docs/USAGE.md)\n- [Current prices \u0026 snapshots](docs/CURRENT_PRICES.md)\n- [Analytics \u0026 visualization](docs/ANALYTICS.md)\n- [Visualization guide](docs/VISUALIZATION.md)\n- [Portfolio analytics](docs/PORTFOLIO.md)\n- [Examples \u0026 recipes](docs/EXAMPLES.md)\n- [Dashboards \u0026 reporting](docs/DASHBOARDS.md)\n- [Configuration](docs/CONFIGURATION.md)\n- [Data sources \u0026 caching](docs/DATA_SOURCES.md)\n- [API reference](docs/REFERENCE.md)\n- [Troubleshooting](docs/TROUBLESHOOTING.md)\n- [Contributing guide](docs/CONTRIBUTING.md)\n- [Development guide](docs/DEVELOPMENT.md)\n- [Testing guide](docs/TESTING.md)\n- [Architecture overview](docs/ARCHITECTURE.md)\n\n## Building Documentation\n\nTo build the Sphinx documentation locally:\n\n```bash\npip install -r requirements-dev.txt\nsphinx-build -b html docs/sphinx docs/sphinx/_build/html\n```\n\n## At a Glance\n\n### Current Prices\n\n```python\nfrom datetime import date\nfrom tsp import TspAnalytics, TspIndividualFund\n\nprices = TspAnalytics()\nlatest = prices.get_current_prices(fund=TspIndividualFund.G_FUND)\nlatest_per_fund = prices.get_current_prices(per_fund=True)\nlatest_dict = prices.get_current_prices_dict()\nlatest_per_fund_dict = prices.get_current_prices_dict(per_fund=True)\nlatest_report = prices.get_current_price_report()\nlatest_report_long = prices.get_current_price_report_long()\nlatest_report_as_of = prices.get_current_price_report(as_of=date(2024, 1, 2))\nlatest_report_as_of_long = prices.get_current_price_report_long(as_of=date(2024, 1, 2))\nlatest_changes = prices.get_current_price_changes()\nlatest_changes_as_of = prices.get_current_price_changes(as_of=date(2024, 1, 2))\nlatest_changes_dict = prices.get_current_price_changes_dict(as_of=date(2024, 1, 2))\nlatest_changes_per_fund = prices.get_current_price_changes_per_fund(as_of=date(2024, 1, 2))\nsnapshot = prices.get_current_price_snapshot()\nsnapshot_as_of = prices.get_current_price_snapshot(as_of=date(2024, 1, 2))\nsnapshot_dict = prices.get_current_price_snapshot_dict(as_of=date(2024, 1, 2))\nas_of_prices = prices.get_current_prices(as_of=date(2024, 1, 2))\nas_of_prices_complete = prices.get_current_prices(\n    as_of=date(2024, 1, 2),\n    funds=[TspIndividualFund.G_FUND, TspIndividualFund.C_FUND],\n    require_all_funds=True,\n)\nas_of_per_fund = prices.get_current_prices_per_fund(as_of=date(2024, 1, 2))\nas_of_per_fund_dict = prices.get_current_prices_per_fund_dict(as_of=date(2024, 1, 2))\nsafe_per_fund = prices.get_current_prices_per_fund(\n    funds=[TspIndividualFund.G_FUND, TspIndividualFund.C_FUND],\n    allow_missing=True,\n)\nsafe_per_fund_payload = prices.get_current_prices_per_fund_dict(\n    funds=[TspIndividualFund.G_FUND, TspIndividualFund.C_FUND],\n    allow_missing=True,\n)\nstatus = prices.get_current_price_status()\nstatus_dict = prices.get_current_price_status_dict()\nstatus_g = prices.get_current_price_status(fund=TspIndividualFund.G_FUND)\nsummary = prices.get_current_price_summary(stale_days=2)\nsummary_dict = prices.get_current_price_summary_dict(stale_days=2)\nsummary_as_of = prices.get_current_price_summary(as_of=date(2024, 1, 2), stale_days=2)\nsummary_as_of_dict = prices.get_current_price_summary_dict(\n    as_of=date(2024, 1, 2),\n    stale_days=2,\n)\nalerts = prices.get_current_price_alerts(stale_days=2, change_threshold=0.03)\nalerts_dict = prices.get_current_price_alerts_dict(stale_days=2, change_threshold=0.03)\nalerts_g = prices.get_current_price_alerts(fund=\"G\", stale_days=2, change_threshold=0.03)\nalert_summary = prices.get_current_price_alert_summary(stale_days=2, change_threshold=0.03)\nalert_summary_dict = prices.get_current_price_alert_summary_dict(\n    stale_days=2,\n    change_threshold=0.03,\n)\n```\n\n\u003e Tip: When requesting `as_of` prices for a single fund, the lookup skips rows where that fund\n\u003e has missing prices so you always get the most recent valid value.\n\n### Analytics\n\n```python\nfrom datetime import date\nfrom tsp import TspAnalytics, TspIndividualFund\n\nprices = TspAnalytics()\nreturns = prices.get_daily_returns()\ndrawdown = prices.get_drawdown_series(fund=TspIndividualFund.C_FUND)\nprice_history = prices.get_price_history(start_date=date(2024, 1, 1))\nrisk_summary = prices.get_risk_return_summary()\nperformance_summary_dict = prices.get_performance_summary_dict()\nrisk_summary_dict = prices.get_risk_return_summary_dict()\ndashboard = prices.get_current_price_dashboard(periods=[1, 5, 20])\nprice_summary = prices.get_price_summary()\nprice_recency = prices.get_price_recency()\ncorrelation_pairs = prices.get_correlation_pairs(top_n=5)\ncurrent_overview = prices.get_current_fund_overview()\ncurrent_overview_as_of = prices.get_current_fund_overview(as_of=date(2024, 1, 2))\ncurrent_change_rank = prices.get_fund_rankings(metric=\"change_percent\")\nreturn_distribution = prices.get_return_distribution_summary(\n    fund=TspIndividualFund.C_FUND,\n    percentiles=[0.05, 0.5, 0.95],\n)\nprice_stats = prices.get_price_statistics(start_date=date(2024, 1, 1))\nreturn_stats = prices.get_return_statistics(end_date=date(2024, 3, 31))\nprice_stats_dict = prices.get_price_statistics_dict(fund=TspIndividualFund.G_FUND)\nreturn_stats_dict = prices.get_return_statistics_dict(\n    fund=TspIndividualFund.G_FUND,\n    trading_days=252,\n)\nrecent_changes = prices.get_recent_price_changes(days=5)\nrecent_change_summary = prices.get_recent_price_change_summary(days=5)\nrecent_changes_dict = prices.get_recent_price_changes_dict(days=5)\nfund_report = prices.get_fund_analytics_report(\n    TspIndividualFund.C_FUND,\n    start_date=date(2024, 1, 1),\n)\nfund_report_dict = prices.get_fund_analytics_report_dict(\n    TspIndividualFund.C_FUND,\n    start_date=date(2024, 1, 1),\n)\ndrawdown_summary = prices.get_drawdown_summary_dict(TspIndividualFund.C_FUND)\nrange_changes = prices.get_price_change_by_date_range_dict(\n    start_date=date(2024, 1, 1),\n    end_date=date(2024, 1, 31),\n)\nas_of_changes = prices.get_price_changes_as_of_per_fund(date(2024, 1, 3))\nmonthly_return_table = prices.get_monthly_return_table_long(TspIndividualFund.C_FUND)\nrolling_volatility = prices.get_rolling_volatility(TspIndividualFund.C_FUND, window=63)\nrolling_performance = prices.get_rolling_performance_summary(TspIndividualFund.C_FUND, window=63)\ntrailing_return = prices.get_trailing_returns(periods=20, fund=TspIndividualFund.C_FUND)\ntrailing_return_long = prices.get_trailing_returns_long(periods=[1, 5, 20, 63])\ntrailing_return_dict = prices.get_trailing_returns_dict(periods=[1, 5, 20, 63])\ncorrelation_payload = prices.get_correlation_matrix_dict()\nrolling_correlation_payload = prices.get_rolling_correlation_matrix_dict(window=63)\n```\n\n### Visualization\n\n```python\nfrom datetime import date\nfrom tsp import TspAnalytics, TspIndividualFund\n\nprices = TspAnalytics()\nprices.show_fund_price_chart(TspIndividualFund.C_FUND)\nprices.show_latest_price_changes_per_fund_chart()\nprices.show_recent_price_change_heatmap(days=5)\nprices.show_price_recency_chart()\nprices.show_current_prices_per_fund_chart()\nprices.show_price_history_chart(start_date=date(2024, 1, 1))\nprices.show_return_histogram_chart(TspIndividualFund.C_FUND)\nprices.show_correlation_heatmap()\nprices.show_correlation_pairs_chart(top_n=5)\nprices.show_rolling_performance_summary_chart(TspIndividualFund.C_FUND, window=63)\nprices.show_fund_rankings_chart(metric=\"trailing_return\", period=20, top_n=5)\nprices.show_current_price_dashboard_metric_chart(metric=\"change_percent\")\nprices.show_current_price_alerts_chart(metric=\"change_percent\", change_threshold=0.03)\nprices.show_trailing_returns_chart(\n    periods=[1, 5, 20, 63],\n    funds=[TspIndividualFund.G_FUND, TspIndividualFund.C_FUND],\n)\n\n# Capture charts for custom dashboards or reports\nfig, ax = prices.show_fund_price_chart(TspIndividualFund.C_FUND, show=False)\nfig.savefig(\"c-fund.png\", dpi=150, bbox_inches=\"tight\")\n\n# Current price bar chart anchored to a historical date\nfig, ax = prices.show_current_prices_per_fund_chart(\n    as_of=date(2024, 1, 2),\n    sort_by=\"fund\",\n    show=False,\n)\n```\n\n### Data Quality \u0026 Cache Status\n\n```python\nfrom tsp import TspAnalytics\n\nprices = TspAnalytics(auto_update=False)\nstatus = prices.get_cache_status()\nstatus_dict = prices.get_cache_status_dict()\nreport = prices.get_data_quality_report()\nreport_dict = prices.get_data_quality_report_dict()\n```\n\n### Fund Metadata \u0026 Aliases\n\n```python\nfrom tsp import TspAnalytics\n\nprices = TspAnalytics(auto_update=False)\naliases = prices.get_fund_aliases()\nmetadata = prices.get_fund_metadata()\nmetadata_dict = prices.get_fund_metadata_dict()\n```\n\n## Data Sources \u0026 Caching\n\nThe library downloads the official `fund-price-history.csv` file from `tsp.gov` and caches it\nlocally. The cache refreshes automatically after the most recent business day and the configured\nupdate time (defaults to 7:00 PM local time). You can override the cache directory with the\n`TSP_DATA_DIR` environment variable or the `data_dir` constructor argument:\n\n```python\nfrom pathlib import Path\nfrom tsp import TspAnalytics\n\nprices = TspAnalytics(data_dir=Path.home() / \".cache\" / \"tsp\")\n```\n\nIf you need to run fully offline or inject a custom HTTP session, disable auto updates and\nload data explicitly:\n\n```python\nfrom tsp import TspAnalytics\n\nprices = TspAnalytics(auto_update=False)\nprices.refresh()  # or prices.load_csv(...), prices.load_csv_text(...)\n```\n\nFor custom networking (proxies, retries, shared sessions), pass a pre-configured session.\nThe library ensures a CSV-friendly `Accept` header and a user-agent string are present:\n\n```python\nimport requests\nfrom tsp import TspAnalytics\n\nsession = requests.Session()\nsession.headers.update({\"User-Agent\": \"my-app/1.0\"})\nprices = TspAnalytics(session=session, request_timeout=15.0, max_retries=3, retry_backoff=0.5)\n```\n\nDownloads are validated to ensure the response looks like a CSV (including a `Date` header\nand known fund columns). If the upstream response appears to be HTML or missing expected headers,\nthe client raises\na validation error and preserves the existing cache. See\n[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for guidance.\n\nTo load a pandas dataframe directly, pass it into `load_dataframe`. The dataframe can include\neither a `Date` column or a date-like index (named `Date` or a datetime index):\n\n```python\nimport pandas as pd\nfrom tsp import TspAnalytics, TspIndividualFund\n\ndf = pd.DataFrame(\n    {\n        TspIndividualFund.G_FUND.value: [100.0, 101.5],\n        TspIndividualFund.C_FUND.value: [200.0, 202.0],\n    },\n    index=pd.to_datetime([\"2024-01-02\", \"2024-01-03\"]),\n)\n\nprices = TspAnalytics(auto_update=False)\nprices.load_dataframe(df)\n```\n\nSee [docs/DATA_SOURCES.md](docs/DATA_SOURCES.md) for full details.\n\n## Analytics \u0026 Visualization Tips\n\n- Use the long-format helpers (`get_price_history_long`, `get_daily_returns_long`) when charting\n  with Seaborn or Plotly.\n- Price history helpers drop rows where all selected fund prices are missing to avoid blank\n  records in downstream charts and analytics.\n- Use the `show_*` helpers for quick Matplotlib visuals; they return `(fig, ax)` so you can save\n  or embed charts without opening a window.\n- Use dictionary helpers (like `get_current_price_report_dict`) for JSON-ready dashboard payloads.\n\nStart with [docs/ANALYTICS.md](docs/ANALYTICS.md) and [docs/VISUALIZATION.md](docs/VISUALIZATION.md)\nfor step-by-step examples.\n\n## Contributing \u0026 Development\n\nInterested in contributing? Start here:\n\n- [Contributing guide](CONTRIBUTING.md)\n- [Contributor guide](docs/CONTRIBUTING.md)\n- [Development guide](docs/DEVELOPMENT.md) (architecture \u0026 tooling notes)\n- [Testing guide](docs/TESTING.md)\n- [Troubleshooting](docs/TROUBLESHOOTING.md)\n\nQuick setup:\n\n```bash\npython -m venv .venv\nsource .venv/bin/activate\npip install -r requirements-dev.txt\npip install -e .\npytest\n```\n\nWhen you add user-facing features, please update the relevant docs in `docs/` and include\ntests under `tests/`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsrichs%2Ftsp-analytics","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsrichs%2Ftsp-analytics","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsrichs%2Ftsp-analytics/lists"}