TradeChart 2.2.0__tar.gz → 2.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {tradechart-2.2.0 → tradechart-2.2.1}/PKG-INFO +20 -8
  2. {tradechart-2.2.0 → tradechart-2.2.1}/README.md +19 -7
  3. {tradechart-2.2.0 → tradechart-2.2.1}/TradeChart.egg-info/PKG-INFO +20 -8
  4. {tradechart-2.2.0 → tradechart-2.2.1}/pyproject.toml +1 -1
  5. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/__init__.py +1 -1
  6. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/data/fetcher.py +65 -24
  7. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/data/store.py +36 -1
  8. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart_examples/discord_demo_bot/discord_bot.py +12 -5
  9. {tradechart-2.2.0 → tradechart-2.2.1}/LICENSE +0 -0
  10. {tradechart-2.2.0 → tradechart-2.2.1}/TradeChart.egg-info/SOURCES.txt +0 -0
  11. {tradechart-2.2.0 → tradechart-2.2.1}/TradeChart.egg-info/dependency_links.txt +0 -0
  12. {tradechart-2.2.0 → tradechart-2.2.1}/TradeChart.egg-info/requires.txt +0 -0
  13. {tradechart-2.2.0 → tradechart-2.2.1}/TradeChart.egg-info/top_level.txt +0 -0
  14. {tradechart-2.2.0 → tradechart-2.2.1}/setup.cfg +0 -0
  15. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/charts/__init__.py +0 -0
  16. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/charts/heatmap.py +0 -0
  17. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/charts/indicators.py +0 -0
  18. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/charts/renderer.py +0 -0
  19. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/charts/themes.py +0 -0
  20. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/charts/watermark.py +0 -0
  21. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/config/__init__.py +0 -0
  22. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/config/logger.py +0 -0
  23. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/config/settings.py +0 -0
  24. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/core/__init__.py +0 -0
  25. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/core/engine.py +0 -0
  26. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/data/__init__.py +0 -0
  27. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/data/groups.py +0 -0
  28. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/data/models.py +0 -0
  29. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/data/provider_base.py +0 -0
  30. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/providers/__init__.py +0 -0
  31. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/providers/stooq_provider.py +0 -0
  32. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/providers/tradingview_provider.py +0 -0
  33. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/providers/yfinance_provider.py +0 -0
  34. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/utils/__init__.py +0 -0
  35. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/utils/exceptions.py +0 -0
  36. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/utils/formatting.py +0 -0
  37. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/utils/install.py +0 -0
  38. {tradechart-2.2.0 → tradechart-2.2.1}/tradechart/utils/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TradeChart
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Production-quality financial chart generator — candlestick, line, area, OHLC, Heikin-Ashi, performance heatmaps, and sector group charts.
5
5
  Author-email: "TRADELY.DEV" <dev@tradely.dev>
6
6
  License: Apache-2.0
@@ -45,7 +45,7 @@ Requires-Dist: build>=1.0; extra == "dev"
45
45
  Requires-Dist: twine>=5.0; extra == "dev"
46
46
  Dynamic: license-file
47
47
 
48
- # TradeChart — Library Edition v2.2.0
48
+ # TradeChart — Library Edition v2.2.1
49
49
 
50
50
  **Python → Financial Charts**
51
51
  Generate production-quality candlestick, line, area, OHLC, Heikin-Ashi, and performance heatmap charts from code.
@@ -116,11 +116,20 @@ Any valid `SECTOR_GROUPS` key (`"mag7"`, `"tech"`, `"finance"`, `"energy"`, …)
116
116
 
117
117
  ### How it works
118
118
 
119
- Once a store path is configured, every call to `chart()`, `data()`, `export()`, `compare()`, or `heatmap()` checks the disk store before touching the network:
119
+ Once a store path is configured, every call to `chart()`, `data()`, `export()`, `compare()`, or `heatmap()` uses a three-level lookup before touching the network:
120
120
 
121
121
  1. **In-memory cache** — sub-millisecond, TTL-based (existing behaviour).
122
- 2. **Disk store** — CSV file per `(ticker, duration)` pair, persists across sessions.
123
- 3. **Live provider chain** — yfinance → TradingView → Stooq. On success the result is written to disk automatically.
122
+ 2. **Disk store (fresh)** — if the stored file is younger than the bar-resolution threshold, it is served as-is. No network call.
123
+ 3. **Disk store (stale) + live fetch** — if the stored file is older than the threshold, the library fetches the current window from a live provider to get the missing bars, then **merges** those new rows on top of the stored history. Historical bars are never discarded or re-requested. The merged result is written back to disk and served.
124
+
125
+ The staleness threshold matches the resolution of the data so charts are never more than one bar behind:
126
+
127
+ | Duration | Bar resolution | Refreshes after |
128
+ |---|---|---|
129
+ | `"1d"`, `"5d"` | 5 / 15-min bars | 4 hours |
130
+ | `"1mo"`, `"3mo"`, `"6mo"` | Daily bars | 24 hours |
131
+ | `"1y"`, `"2y"`, `"5y"` | Weekly bars | 7 days |
132
+ | `"10y"`, `"max"` | Monthly bars | 30 days |
124
133
 
125
134
  ```python
126
135
  tc.store("/data/myproject")
@@ -129,7 +138,10 @@ tc.store("AAPL", "MSFT", "mag7", "3mo") # fetch once, write to disk
129
138
  # Later — same session or a completely new one:
130
139
  tc.chart("AAPL", "3mo") # served from disk, no network call
131
140
  tc.data("MSFT", "3mo") # same
132
- tc.heatmap(tc.SECTOR_GROUPS["mag7"], "3mo") # same
141
+
142
+ # After 24 hours (daily bars): only the new day's bar is fetched and merged
143
+ # in — the months of stored history are kept and reused as-is
144
+ tc.chart("AAPL", "3mo") # delta fetch + merge, then cached
133
145
  ```
134
146
 
135
147
  ### Clearing stored data
@@ -557,7 +569,7 @@ TradeChart tries providers in priority order and falls back automatically on fai
557
569
 
558
570
  | Priority | Provider | Requires | Notes |
559
571
  |---|---|---|---|
560
- | 0 | **Disk store** | `tc.store(path)` called once | Fastest — served from local CSV, no network. Persists across sessions. |
572
+ | 0 | **Disk store** | `tc.store(path)` called once | Served from local CSV when fresh. When stale, only the missing bars are fetched from a live provider and merged on top — historical rows are never re-requested. |
561
573
  | 1 | **yfinance** | Included by default | Primary live source. Covers stocks, ETFs, indices, crypto, forex. |
562
574
  | 2 | **TradingView** | `pip install TradeChart[tradingview]` | Tried if yfinance returns empty or fails. Attempts multiple exchanges automatically (`NASDAQ`, `NYSE`, `AMEX`, `CRYPTO`, `FX`). |
563
575
  | 3 | **Stooq** | Nothing — free CSV endpoint | Final fallback. No API key required. Duration mapped to a rolling date range. |
@@ -569,7 +581,7 @@ If all live providers fail and no disk data exists, a `DataFetchError` is raised
569
581
  ## Notes
570
582
 
571
583
  **Persistent disk store**
572
- When `tc.store()` is configured, every successful live fetch is written to `tradechart_FetchData/` as a CSV file. Subsequent fetches — even in a new Python session — read from disk rather than the network. Use `tc.clear_cache(disk=True)` to wipe stored files and force a fresh fetch from providers.
584
+ When `tc.store()` is configured, every successful live fetch is written to `tradechart_FetchData/` as a CSV file. On subsequent requests the library checks whether the stored data is fresh enough (based on bar resolution — 24 hours for daily bars, 4 hours for intraday, etc.). If fresh, it is served directly with no network call. If stale, only the most recent bars are fetched from a live provider and merged on top of the stored history — historical rows are never discarded or re-fetched. Use `tc.clear_cache(disk=True)` to wipe stored files and force a full re-fetch.
573
585
 
574
586
  **In-memory caching**
575
587
  Fetched data is also cached in-memory for `cache_ttl` seconds (default 300 s / 5 minutes). All subsequent calls with the same ticker and duration within that window are served from the memory cache. Call `tc.clear_cache()` to force re-evaluation (disk store is still checked before going to the network). Set `tc.config(cache_ttl=0)` to disable the memory cache entirely.
@@ -1,4 +1,4 @@
1
- # TradeChart — Library Edition v2.2.0
1
+ # TradeChart — Library Edition v2.2.1
2
2
 
3
3
  **Python → Financial Charts**
4
4
  Generate production-quality candlestick, line, area, OHLC, Heikin-Ashi, and performance heatmap charts from code.
@@ -69,11 +69,20 @@ Any valid `SECTOR_GROUPS` key (`"mag7"`, `"tech"`, `"finance"`, `"energy"`, …)
69
69
 
70
70
  ### How it works
71
71
 
72
- Once a store path is configured, every call to `chart()`, `data()`, `export()`, `compare()`, or `heatmap()` checks the disk store before touching the network:
72
+ Once a store path is configured, every call to `chart()`, `data()`, `export()`, `compare()`, or `heatmap()` uses a three-level lookup before touching the network:
73
73
 
74
74
  1. **In-memory cache** — sub-millisecond, TTL-based (existing behaviour).
75
- 2. **Disk store** — CSV file per `(ticker, duration)` pair, persists across sessions.
76
- 3. **Live provider chain** — yfinance → TradingView → Stooq. On success the result is written to disk automatically.
75
+ 2. **Disk store (fresh)** — if the stored file is younger than the bar-resolution threshold, it is served as-is. No network call.
76
+ 3. **Disk store (stale) + live fetch** — if the stored file is older than the threshold, the library fetches the current window from a live provider to get the missing bars, then **merges** those new rows on top of the stored history. Historical bars are never discarded or re-requested. The merged result is written back to disk and served.
77
+
78
+ The staleness threshold matches the resolution of the data so charts are never more than one bar behind:
79
+
80
+ | Duration | Bar resolution | Refreshes after |
81
+ |---|---|---|
82
+ | `"1d"`, `"5d"` | 5 / 15-min bars | 4 hours |
83
+ | `"1mo"`, `"3mo"`, `"6mo"` | Daily bars | 24 hours |
84
+ | `"1y"`, `"2y"`, `"5y"` | Weekly bars | 7 days |
85
+ | `"10y"`, `"max"` | Monthly bars | 30 days |
77
86
 
78
87
  ```python
79
88
  tc.store("/data/myproject")
@@ -82,7 +91,10 @@ tc.store("AAPL", "MSFT", "mag7", "3mo") # fetch once, write to disk
82
91
  # Later — same session or a completely new one:
83
92
  tc.chart("AAPL", "3mo") # served from disk, no network call
84
93
  tc.data("MSFT", "3mo") # same
85
- tc.heatmap(tc.SECTOR_GROUPS["mag7"], "3mo") # same
94
+
95
+ # After 24 hours (daily bars): only the new day's bar is fetched and merged
96
+ # in — the months of stored history are kept and reused as-is
97
+ tc.chart("AAPL", "3mo") # delta fetch + merge, then cached
86
98
  ```
87
99
 
88
100
  ### Clearing stored data
@@ -510,7 +522,7 @@ TradeChart tries providers in priority order and falls back automatically on fai
510
522
 
511
523
  | Priority | Provider | Requires | Notes |
512
524
  |---|---|---|---|
513
- | 0 | **Disk store** | `tc.store(path)` called once | Fastest — served from local CSV, no network. Persists across sessions. |
525
+ | 0 | **Disk store** | `tc.store(path)` called once | Served from local CSV when fresh. When stale, only the missing bars are fetched from a live provider and merged on top — historical rows are never re-requested. |
514
526
  | 1 | **yfinance** | Included by default | Primary live source. Covers stocks, ETFs, indices, crypto, forex. |
515
527
  | 2 | **TradingView** | `pip install TradeChart[tradingview]` | Tried if yfinance returns empty or fails. Attempts multiple exchanges automatically (`NASDAQ`, `NYSE`, `AMEX`, `CRYPTO`, `FX`). |
516
528
  | 3 | **Stooq** | Nothing — free CSV endpoint | Final fallback. No API key required. Duration mapped to a rolling date range. |
@@ -522,7 +534,7 @@ If all live providers fail and no disk data exists, a `DataFetchError` is raised
522
534
  ## Notes
523
535
 
524
536
  **Persistent disk store**
525
- When `tc.store()` is configured, every successful live fetch is written to `tradechart_FetchData/` as a CSV file. Subsequent fetches — even in a new Python session — read from disk rather than the network. Use `tc.clear_cache(disk=True)` to wipe stored files and force a fresh fetch from providers.
537
+ When `tc.store()` is configured, every successful live fetch is written to `tradechart_FetchData/` as a CSV file. On subsequent requests the library checks whether the stored data is fresh enough (based on bar resolution — 24 hours for daily bars, 4 hours for intraday, etc.). If fresh, it is served directly with no network call. If stale, only the most recent bars are fetched from a live provider and merged on top of the stored history — historical rows are never discarded or re-fetched. Use `tc.clear_cache(disk=True)` to wipe stored files and force a full re-fetch.
526
538
 
527
539
  **In-memory caching**
528
540
  Fetched data is also cached in-memory for `cache_ttl` seconds (default 300 s / 5 minutes). All subsequent calls with the same ticker and duration within that window are served from the memory cache. Call `tc.clear_cache()` to force re-evaluation (disk store is still checked before going to the network). Set `tc.config(cache_ttl=0)` to disable the memory cache entirely.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TradeChart
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Production-quality financial chart generator — candlestick, line, area, OHLC, Heikin-Ashi, performance heatmaps, and sector group charts.
5
5
  Author-email: "TRADELY.DEV" <dev@tradely.dev>
6
6
  License: Apache-2.0
@@ -45,7 +45,7 @@ Requires-Dist: build>=1.0; extra == "dev"
45
45
  Requires-Dist: twine>=5.0; extra == "dev"
46
46
  Dynamic: license-file
47
47
 
48
- # TradeChart — Library Edition v2.2.0
48
+ # TradeChart — Library Edition v2.2.1
49
49
 
50
50
  **Python → Financial Charts**
51
51
  Generate production-quality candlestick, line, area, OHLC, Heikin-Ashi, and performance heatmap charts from code.
@@ -116,11 +116,20 @@ Any valid `SECTOR_GROUPS` key (`"mag7"`, `"tech"`, `"finance"`, `"energy"`, …)
116
116
 
117
117
  ### How it works
118
118
 
119
- Once a store path is configured, every call to `chart()`, `data()`, `export()`, `compare()`, or `heatmap()` checks the disk store before touching the network:
119
+ Once a store path is configured, every call to `chart()`, `data()`, `export()`, `compare()`, or `heatmap()` uses a three-level lookup before touching the network:
120
120
 
121
121
  1. **In-memory cache** — sub-millisecond, TTL-based (existing behaviour).
122
- 2. **Disk store** — CSV file per `(ticker, duration)` pair, persists across sessions.
123
- 3. **Live provider chain** — yfinance → TradingView → Stooq. On success the result is written to disk automatically.
122
+ 2. **Disk store (fresh)** — if the stored file is younger than the bar-resolution threshold, it is served as-is. No network call.
123
+ 3. **Disk store (stale) + live fetch** — if the stored file is older than the threshold, the library fetches the current window from a live provider to get the missing bars, then **merges** those new rows on top of the stored history. Historical bars are never discarded or re-requested. The merged result is written back to disk and served.
124
+
125
+ The staleness threshold matches the resolution of the data so charts are never more than one bar behind:
126
+
127
+ | Duration | Bar resolution | Refreshes after |
128
+ |---|---|---|
129
+ | `"1d"`, `"5d"` | 5 / 15-min bars | 4 hours |
130
+ | `"1mo"`, `"3mo"`, `"6mo"` | Daily bars | 24 hours |
131
+ | `"1y"`, `"2y"`, `"5y"` | Weekly bars | 7 days |
132
+ | `"10y"`, `"max"` | Monthly bars | 30 days |
124
133
 
125
134
  ```python
126
135
  tc.store("/data/myproject")
@@ -129,7 +138,10 @@ tc.store("AAPL", "MSFT", "mag7", "3mo") # fetch once, write to disk
129
138
  # Later — same session or a completely new one:
130
139
  tc.chart("AAPL", "3mo") # served from disk, no network call
131
140
  tc.data("MSFT", "3mo") # same
132
- tc.heatmap(tc.SECTOR_GROUPS["mag7"], "3mo") # same
141
+
142
+ # After 24 hours (daily bars): only the new day's bar is fetched and merged
143
+ # in — the months of stored history are kept and reused as-is
144
+ tc.chart("AAPL", "3mo") # delta fetch + merge, then cached
133
145
  ```
134
146
 
135
147
  ### Clearing stored data
@@ -557,7 +569,7 @@ TradeChart tries providers in priority order and falls back automatically on fai
557
569
 
558
570
  | Priority | Provider | Requires | Notes |
559
571
  |---|---|---|---|
560
- | 0 | **Disk store** | `tc.store(path)` called once | Fastest — served from local CSV, no network. Persists across sessions. |
572
+ | 0 | **Disk store** | `tc.store(path)` called once | Served from local CSV when fresh. When stale, only the missing bars are fetched from a live provider and merged on top — historical rows are never re-requested. |
561
573
  | 1 | **yfinance** | Included by default | Primary live source. Covers stocks, ETFs, indices, crypto, forex. |
562
574
  | 2 | **TradingView** | `pip install TradeChart[tradingview]` | Tried if yfinance returns empty or fails. Attempts multiple exchanges automatically (`NASDAQ`, `NYSE`, `AMEX`, `CRYPTO`, `FX`). |
563
575
  | 3 | **Stooq** | Nothing — free CSV endpoint | Final fallback. No API key required. Duration mapped to a rolling date range. |
@@ -569,7 +581,7 @@ If all live providers fail and no disk data exists, a `DataFetchError` is raised
569
581
  ## Notes
570
582
 
571
583
  **Persistent disk store**
572
- When `tc.store()` is configured, every successful live fetch is written to `tradechart_FetchData/` as a CSV file. Subsequent fetches — even in a new Python session — read from disk rather than the network. Use `tc.clear_cache(disk=True)` to wipe stored files and force a fresh fetch from providers.
584
+ When `tc.store()` is configured, every successful live fetch is written to `tradechart_FetchData/` as a CSV file. On subsequent requests the library checks whether the stored data is fresh enough (based on bar resolution — 24 hours for daily bars, 4 hours for intraday, etc.). If fresh, it is served directly with no network call. If stale, only the most recent bars are fetched from a live provider and merged on top of the stored history — historical rows are never discarded or re-fetched. Use `tc.clear_cache(disk=True)` to wipe stored files and force a full re-fetch.
573
585
 
574
586
  **In-memory caching**
575
587
  Fetched data is also cached in-memory for `cache_ttl` seconds (default 300 s / 5 minutes). All subsequent calls with the same ticker and duration within that window are served from the memory cache. Call `tc.clear_cache()` to force re-evaluation (disk store is still checked before going to the network). Set `tc.config(cache_ttl=0)` to disable the memory cache entirely.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "TradeChart"
7
- version = "2.2.0"
7
+ version = "2.2.1"
8
8
  description = "Production-quality financial chart generator — candlestick, line, area, OHLC, Heikin-Ashi, performance heatmaps, and sector group charts."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -87,7 +87,7 @@ from tradechart.utils.exceptions import (
87
87
  )
88
88
  from tradechart.utils.validation import VALID_DURATIONS
89
89
 
90
- __version__ = "2.2.0"
90
+ __version__ = "2.2.1"
91
91
  __all__ = [
92
92
  "terminal", "theme", "watermark", "config",
93
93
  "chart", "compare", "heatmap", "data", "export", "clear_cache",
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import time
6
6
  from typing import Sequence
7
7
 
8
+ import pandas as pd
9
+
8
10
  from tradechart.config.logger import get_logger
9
11
  from tradechart.config.settings import get_settings
10
12
  from tradechart.data.models import MarketData
@@ -12,6 +14,19 @@ from tradechart.data.provider_base import BaseProvider
12
14
  from tradechart.utils.exceptions import DataFetchError
13
15
 
14
16
 
17
+ def _merge(stored: pd.DataFrame, fresh: pd.DataFrame) -> pd.DataFrame:
18
+ """Merge stored history with freshly-fetched rows.
19
+
20
+ Historical rows from *stored* are preserved as-is. Any row whose
21
+ timestamp also appears in *fresh* is overwritten by the fresh value
22
+ (handles end-of-day corrections / partial bars). Rows that exist only
23
+ in *fresh* (new bars) are appended. The result is sorted by date.
24
+ """
25
+ combined = pd.concat([stored, fresh])
26
+ combined = combined[~combined.index.duplicated(keep="last")]
27
+ return combined.sort_index()
28
+
29
+
15
30
  class _Cache:
16
31
  """Simple TTL-based in-memory cache."""
17
32
 
@@ -49,25 +64,8 @@ class DataFetcher:
49
64
  self._cache = _Cache()
50
65
  self._log = get_logger()
51
66
 
52
- def fetch(self, ticker: str, duration: str) -> MarketData:
53
- # 1 — in-memory cache (fastest)
54
- cached = self._cache.get(ticker, duration)
55
- if cached is not None:
56
- self._log.detail("Cache hit for %s/%s", ticker, duration)
57
- self._log.summary(f"✓ Data loaded from cache ({cached.provider})")
58
- return cached
59
-
60
- # 2 — persistent disk store (avoids network for already-fetched data)
61
- disk_store = get_settings().disk_store
62
- if disk_store is not None:
63
- disk_data = disk_store.load(ticker, duration)
64
- if disk_data is not None:
65
- self._log.detail("Disk store hit for %s/%s", ticker, duration)
66
- self._log.summary(f"✓ Data loaded from disk store ({len(disk_data.df)} rows)")
67
- self._cache.put(disk_data) # promote to memory cache
68
- return disk_data
69
-
70
- # 3 — live provider chain
67
+ def _fetch_live(self, ticker: str, duration: str) -> MarketData:
68
+ """Try each provider in order and return the first successful result."""
71
69
  errors: list[str] = []
72
70
  for provider in self._providers:
73
71
  self._log.section(f"Trying provider: {provider.name}")
@@ -79,10 +77,6 @@ class DataFetcher:
79
77
  errors.append(msg)
80
78
  continue
81
79
  data.clean().downsample()
82
- self._cache.put(data)
83
- # 4 — persist to disk store for future sessions
84
- if disk_store is not None:
85
- disk_store.save(data)
86
80
  self._log.detail("Fetched %d rows from %s", len(data.df), provider.name)
87
81
  self._log.summary(f"✓ Data fetched via {provider.name} ({len(data.df)} rows)")
88
82
  return data
@@ -90,12 +84,59 @@ class DataFetcher:
90
84
  msg = f"{provider.name} failed: {exc}"
91
85
  self._log.detail(msg)
92
86
  errors.append(msg)
93
-
94
87
  raise DataFetchError(
95
88
  f"All data providers failed for {ticker}/{duration}.\n"
96
89
  + "\n".join(f" • {e}" for e in errors)
97
90
  )
98
91
 
92
+ def fetch(self, ticker: str, duration: str) -> MarketData:
93
+ # 1 — in-memory cache (fastest)
94
+ cached = self._cache.get(ticker, duration)
95
+ if cached is not None:
96
+ self._log.detail("Cache hit for %s/%s", ticker, duration)
97
+ self._log.summary(f"✓ Data loaded from cache ({cached.provider})")
98
+ return cached
99
+
100
+ disk_store = get_settings().disk_store
101
+
102
+ # 2 — persistent disk store
103
+ disk_data: MarketData | None = None
104
+ if disk_store is not None:
105
+ disk_data = disk_store.load(ticker, duration)
106
+ if disk_data is not None and not disk_store.is_stale(ticker, duration):
107
+ # Data is fresh — serve directly, no network call needed
108
+ self._log.detail("Disk store hit for %s/%s", ticker, duration)
109
+ self._log.summary(f"✓ Data loaded from disk store ({len(disk_data.df)} rows)")
110
+ self._cache.put(disk_data)
111
+ return disk_data
112
+ # disk_data may be stale (or absent) — fall through to live fetch,
113
+ # then merge the new rows on top of the stored history
114
+
115
+ # 3 — live provider chain (only fetches what the provider returns for
116
+ # this duration, typically the most recent N bars)
117
+ fresh = self._fetch_live(ticker, duration)
118
+
119
+ # 4 — merge fresh rows onto stored history, then persist
120
+ if disk_store is not None:
121
+ if disk_data is not None:
122
+ merged_df = _merge(disk_data.df, fresh.df)
123
+ result = MarketData(
124
+ ticker=ticker, duration=duration,
125
+ provider=fresh.provider, df=merged_df,
126
+ )
127
+ self._log.detail(
128
+ "Merged disk (%d rows) + live (%d rows) → %d rows",
129
+ len(disk_data.df), len(fresh.df), len(merged_df),
130
+ )
131
+ else:
132
+ result = fresh
133
+ disk_store.save(result)
134
+ self._cache.put(result)
135
+ return result
136
+
137
+ self._cache.put(fresh)
138
+ return fresh
139
+
99
140
  def clear_cache(self) -> None:
100
141
  """Flush the in-memory data cache."""
101
142
  self._cache.clear()
@@ -2,12 +2,30 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import time
5
6
  from pathlib import Path
6
7
 
7
8
  import pandas as pd
8
9
 
9
10
  from tradechart.data.models import MarketData
10
11
 
12
+ # Maximum age (seconds) for a stored file before it is considered stale and
13
+ # re-fetched from a live provider. Keyed by the user-facing duration string;
14
+ # the value matches the bar resolution so a daily-bar file never goes more
15
+ # than one trading day without refreshing.
16
+ _MAX_AGE: dict[str, int] = {
17
+ "1d": 4 * 3_600, # intraday (5-min bars) → 4 hours
18
+ "5d": 4 * 3_600, # intraday (15-min bars) → 4 hours
19
+ "1mo": 24 * 3_600, # daily bars → 1 day
20
+ "3mo": 24 * 3_600, # daily bars → 1 day
21
+ "6mo": 24 * 3_600, # daily bars → 1 day
22
+ "1y": 7 * 24 * 3_600, # weekly bars → 1 week
23
+ "2y": 7 * 24 * 3_600,
24
+ "5y": 7 * 24 * 3_600,
25
+ "10y": 30 * 24 * 3_600, # monthly bars → 30 days
26
+ "max": 30 * 24 * 3_600,
27
+ }
28
+
11
29
 
12
30
  class DiskStore:
13
31
  """Saves and loads MarketData as CSV files under a dedicated folder.
@@ -52,8 +70,25 @@ class DiskStore:
52
70
  """Return True if data for this (ticker, duration) pair is on disk."""
53
71
  return self._file_path(ticker, duration).exists()
54
72
 
73
+ def is_stale(self, ticker: str, duration: str) -> bool:
74
+ """Return True if the stored file is older than the bar-resolution threshold.
75
+
76
+ A fresh file is served as-is. A stale file still holds valid
77
+ historical rows — the caller should merge fresh provider data on top
78
+ rather than discarding it.
79
+ """
80
+ path = self._file_path(ticker, duration)
81
+ if not path.exists():
82
+ return True
83
+ max_age = _MAX_AGE.get(duration, 24 * 3_600)
84
+ return time.time() - path.stat().st_mtime > max_age
85
+
55
86
  def load(self, ticker: str, duration: str) -> MarketData | None:
56
- """Load a previously stored dataset. Returns *None* on any error."""
87
+ """Load a previously stored dataset regardless of age.
88
+
89
+ Returns *None* only if the file is missing or unreadable. Staleness
90
+ is a separate concern handled by :meth:`is_stale`.
91
+ """
57
92
  path = self._file_path(ticker, duration)
58
93
  if not path.exists():
59
94
  return None
@@ -100,6 +100,7 @@ MAX_QUEUE_SIZE = max(1, int(os.getenv("MAX_QUEUE_SIZE", "20")))
100
100
  tc.theme(os.getenv("THEME", "dark"))
101
101
  tc.terminal(os.getenv("TERMINAL", "none"))
102
102
  tc.watermark(os.getenv("WATERMARK", "True").lower() == "true")
103
+ tc.store(DATA_DIR) # persist fetched data to TradeChartBot_Data/tradechart_FetchData/
103
104
 
104
105
  # ============================================================
105
106
  # BUILT-IN SECTOR GROUPS (mirroring tc.SECTOR_GROUPS)
@@ -565,7 +566,8 @@ async def cmd_heatmap(
565
566
  # ============================================================
566
567
 
567
568
  @client.tree.command(name="clearcache", description="MOD ONLY: Clear the chart data cache")
568
- async def cmd_clearcache(interaction: discord.Interaction) -> None:
569
+ @app_commands.describe(disk="Also wipe the persistent disk store and force a full re-fetch (default: False)")
570
+ async def cmd_clearcache(interaction: discord.Interaction, disk: bool = False) -> None:
569
571
  if interaction.guild is None:
570
572
  await interaction.response.send_message("This bot only works in servers.", ephemeral=True)
571
573
  return
@@ -574,8 +576,9 @@ async def cmd_clearcache(interaction: discord.Interaction) -> None:
574
576
  await interaction.response.send_message("Only mods can clear the cache.", ephemeral=True)
575
577
  return
576
578
 
577
- tc.clear_cache()
578
- await interaction.response.send_message(EMOJI_GREEN + " Cache cleared.", ephemeral=True)
579
+ tc.clear_cache(disk=disk)
580
+ label = EMOJI_GREEN + " Cache cleared (memory + disk store)." if disk else EMOJI_GREEN + " Memory cache cleared."
581
+ await interaction.response.send_message(label, ephemeral=True)
579
582
 
580
583
 
581
584
  # ============================================================
@@ -929,8 +932,12 @@ async def cmd_help(interaction: discord.Interaction) -> None:
929
932
  inline=False,
930
933
  )
931
934
  embed.add_field(
932
- name="/clearcache (mod only)",
933
- value="Clear cached chart data to force a fresh fetch.",
935
+ name="/clearcache [disk] (mod only)",
936
+ value=(
937
+ "Clear cached chart data. Without `disk:True` only the in-memory cache is cleared "
938
+ "and stored data on disk is still reused. Use `disk:True` to wipe the disk store "
939
+ "and force a full re-fetch from the network."
940
+ ),
934
941
  inline=False,
935
942
  )
936
943
  embed.add_field(
File without changes
File without changes