TradeChart 2.2.0__tar.gz → 2.3.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.3.1}/PKG-INFO +23 -11
  2. {tradechart-2.2.0 → tradechart-2.3.1}/README.md +22 -10
  3. {tradechart-2.2.0 → tradechart-2.3.1}/TradeChart.egg-info/PKG-INFO +23 -11
  4. {tradechart-2.2.0 → tradechart-2.3.1}/pyproject.toml +1 -1
  5. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/__init__.py +1 -1
  6. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/charts/heatmap.py +9 -2
  7. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/charts/renderer.py +20 -12
  8. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/config/settings.py +3 -3
  9. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/data/fetcher.py +65 -24
  10. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/data/store.py +36 -1
  11. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart_examples/discord_demo_bot/discord_bot.py +12 -5
  12. {tradechart-2.2.0 → tradechart-2.3.1}/LICENSE +0 -0
  13. {tradechart-2.2.0 → tradechart-2.3.1}/TradeChart.egg-info/SOURCES.txt +0 -0
  14. {tradechart-2.2.0 → tradechart-2.3.1}/TradeChart.egg-info/dependency_links.txt +0 -0
  15. {tradechart-2.2.0 → tradechart-2.3.1}/TradeChart.egg-info/requires.txt +0 -0
  16. {tradechart-2.2.0 → tradechart-2.3.1}/TradeChart.egg-info/top_level.txt +0 -0
  17. {tradechart-2.2.0 → tradechart-2.3.1}/setup.cfg +0 -0
  18. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/charts/__init__.py +0 -0
  19. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/charts/indicators.py +0 -0
  20. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/charts/themes.py +0 -0
  21. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/charts/watermark.py +0 -0
  22. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/config/__init__.py +0 -0
  23. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/config/logger.py +0 -0
  24. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/core/__init__.py +0 -0
  25. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/core/engine.py +0 -0
  26. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/data/__init__.py +0 -0
  27. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/data/groups.py +0 -0
  28. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/data/models.py +0 -0
  29. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/data/provider_base.py +0 -0
  30. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/providers/__init__.py +0 -0
  31. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/providers/stooq_provider.py +0 -0
  32. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/providers/tradingview_provider.py +0 -0
  33. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/providers/yfinance_provider.py +0 -0
  34. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/utils/__init__.py +0 -0
  35. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/utils/exceptions.py +0 -0
  36. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/utils/formatting.py +0 -0
  37. {tradechart-2.2.0 → tradechart-2.3.1}/tradechart/utils/install.py +0 -0
  38. {tradechart-2.2.0 → tradechart-2.3.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.3.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.3.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
@@ -438,8 +450,8 @@ tc.config(
438
450
  | `theme` | `str` | `"dark"` | `"dark"`, `"light"`, `"classic"` | Chart colour theme. See [Themes](#themes). |
439
451
  | `watermark` | `bool` | `True` | `True`, `False` | Show or hide the TRADELY logo in the bottom-left corner of charts. |
440
452
  | `overwrite` | `bool` | `False` | `True`, `False` | `False` — append `_1`, `_2`, … to the filename if it already exists. `True` — overwrite the existing file silently. |
441
- | `dpi` | `int` | `150` | `50`–`600` | Output resolution in dots per inch. Higher values produce larger, sharper files. |
442
- | `fig_size` | `tuple[int, int]` | `(14, 7)` | Any valid `(width, height)` in inches | Matplotlib figure size. Increase for wide monitors or presentations. |
453
+ | `dpi` | `int` | `100` | `50`–`600` | Output resolution in dots per inch. Higher values produce larger, sharper files. |
454
+ | `fig_size` | `tuple[int, int]` | `(12, 6)` | Any valid `(width, height)` in inches | Matplotlib figure size. Increase for wide monitors or presentations. |
443
455
  | `cache_ttl` | `int` | `300` | Any positive integer (seconds) | How long fetched data is kept in memory. `0` effectively disables caching. |
444
456
 
445
457
  ---
@@ -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.
@@ -590,7 +602,7 @@ The `"heikin_ashi"` chart type converts standard OHLC data to Heikin-Ashi candle
590
602
  When `mplfinance` is installed (`pip install TradeChart[mplfinance]`), candlestick and Heikin-Ashi charts use it for higher-quality rendering. Otherwise a pure-matplotlib fallback is used automatically — no configuration required.
591
603
 
592
604
  **Output resolution**
593
- Default DPI is 150, producing a ~2100 × 1050 px image at the default figure size. Increase with `tc.config(dpi=300)` for print-quality output.
605
+ Default DPI is 100, producing a ~1200 × 600 px image at the default figure size — well under 300 KB for all supported formats. Increase with `tc.config(dpi=200)` for sharper output, or `tc.config(dpi=300)` for print quality.
594
606
 
595
607
  ---
596
608
 
@@ -1,4 +1,4 @@
1
- # TradeChart — Library Edition v2.2.0
1
+ # TradeChart — Library Edition v2.3.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
@@ -391,8 +403,8 @@ tc.config(
391
403
  | `theme` | `str` | `"dark"` | `"dark"`, `"light"`, `"classic"` | Chart colour theme. See [Themes](#themes). |
392
404
  | `watermark` | `bool` | `True` | `True`, `False` | Show or hide the TRADELY logo in the bottom-left corner of charts. |
393
405
  | `overwrite` | `bool` | `False` | `True`, `False` | `False` — append `_1`, `_2`, … to the filename if it already exists. `True` — overwrite the existing file silently. |
394
- | `dpi` | `int` | `150` | `50`–`600` | Output resolution in dots per inch. Higher values produce larger, sharper files. |
395
- | `fig_size` | `tuple[int, int]` | `(14, 7)` | Any valid `(width, height)` in inches | Matplotlib figure size. Increase for wide monitors or presentations. |
406
+ | `dpi` | `int` | `100` | `50`–`600` | Output resolution in dots per inch. Higher values produce larger, sharper files. |
407
+ | `fig_size` | `tuple[int, int]` | `(12, 6)` | Any valid `(width, height)` in inches | Matplotlib figure size. Increase for wide monitors or presentations. |
396
408
  | `cache_ttl` | `int` | `300` | Any positive integer (seconds) | How long fetched data is kept in memory. `0` effectively disables caching. |
397
409
 
398
410
  ---
@@ -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.
@@ -543,7 +555,7 @@ The `"heikin_ashi"` chart type converts standard OHLC data to Heikin-Ashi candle
543
555
  When `mplfinance` is installed (`pip install TradeChart[mplfinance]`), candlestick and Heikin-Ashi charts use it for higher-quality rendering. Otherwise a pure-matplotlib fallback is used automatically — no configuration required.
544
556
 
545
557
  **Output resolution**
546
- Default DPI is 150, producing a ~2100 × 1050 px image at the default figure size. Increase with `tc.config(dpi=300)` for print-quality output.
558
+ Default DPI is 100, producing a ~1200 × 600 px image at the default figure size — well under 300 KB for all supported formats. Increase with `tc.config(dpi=200)` for sharper output, or `tc.config(dpi=300)` for print quality.
547
559
 
548
560
  ---
549
561
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TradeChart
3
- Version: 2.2.0
3
+ Version: 2.3.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.3.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
@@ -438,8 +450,8 @@ tc.config(
438
450
  | `theme` | `str` | `"dark"` | `"dark"`, `"light"`, `"classic"` | Chart colour theme. See [Themes](#themes). |
439
451
  | `watermark` | `bool` | `True` | `True`, `False` | Show or hide the TRADELY logo in the bottom-left corner of charts. |
440
452
  | `overwrite` | `bool` | `False` | `True`, `False` | `False` — append `_1`, `_2`, … to the filename if it already exists. `True` — overwrite the existing file silently. |
441
- | `dpi` | `int` | `150` | `50`–`600` | Output resolution in dots per inch. Higher values produce larger, sharper files. |
442
- | `fig_size` | `tuple[int, int]` | `(14, 7)` | Any valid `(width, height)` in inches | Matplotlib figure size. Increase for wide monitors or presentations. |
453
+ | `dpi` | `int` | `100` | `50`–`600` | Output resolution in dots per inch. Higher values produce larger, sharper files. |
454
+ | `fig_size` | `tuple[int, int]` | `(12, 6)` | Any valid `(width, height)` in inches | Matplotlib figure size. Increase for wide monitors or presentations. |
443
455
  | `cache_ttl` | `int` | `300` | Any positive integer (seconds) | How long fetched data is kept in memory. `0` effectively disables caching. |
444
456
 
445
457
  ---
@@ -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.
@@ -590,7 +602,7 @@ The `"heikin_ashi"` chart type converts standard OHLC data to Heikin-Ashi candle
590
602
  When `mplfinance` is installed (`pip install TradeChart[mplfinance]`), candlestick and Heikin-Ashi charts use it for higher-quality rendering. Otherwise a pure-matplotlib fallback is used automatically — no configuration required.
591
603
 
592
604
  **Output resolution**
593
- Default DPI is 150, producing a ~2100 × 1050 px image at the default figure size. Increase with `tc.config(dpi=300)` for print-quality output.
605
+ Default DPI is 100, producing a ~1200 × 600 px image at the default figure size — well under 300 KB for all supported formats. Increase with `tc.config(dpi=200)` for sharper output, or `tc.config(dpi=300)` for print quality.
594
606
 
595
607
  ---
596
608
 
@@ -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.3.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.3.1"
91
91
  __all__ = [
92
92
  "terminal", "theme", "watermark", "config",
93
93
  "chart", "compare", "heatmap", "data", "export", "clear_cache",
@@ -345,8 +345,15 @@ class HeatmapRenderer:
345
345
  stamp_logo(fig, provider=provider)
346
346
 
347
347
  out_file = output_path.with_suffix(f".{fmt}")
348
- fig.savefig(out_file, dpi=settings.dpi,
349
- facecolor=fig.get_facecolor(), bbox_inches="tight")
348
+ pil_kwargs: dict = {}
349
+ if fmt == "png":
350
+ pil_kwargs = {"compress_level": 6, "optimize": True}
351
+ elif fmt in ("jpg", "jpeg", "webp"):
352
+ pil_kwargs = {"quality": 82, "optimize": True}
353
+ save_kwargs: dict = {"dpi": settings.dpi, "facecolor": fig.get_facecolor(), "bbox_inches": "tight"}
354
+ if pil_kwargs:
355
+ save_kwargs["pil_kwargs"] = pil_kwargs
356
+ fig.savefig(out_file, **save_kwargs)
350
357
  plt.close(fig)
351
358
  log.detail("Saved heatmap → %s", out_file)
352
359
  return out_file
@@ -131,9 +131,9 @@ class ChartRenderer:
131
131
 
132
132
  # Wicks — two batch vlines calls instead of N individual plot() calls
133
133
  ax_price.vlines(x_arr[up_mask], lows[up_mask], highs[up_mask],
134
- colors=theme.up_color, linewidth=0.6)
134
+ colors=theme.up_color, linewidth=0.5)
135
135
  ax_price.vlines(x_arr[~up_mask], lows[~up_mask], highs[~up_mask],
136
- colors=theme.down_color, linewidth=0.6)
136
+ colors=theme.down_color, linewidth=0.5)
137
137
 
138
138
  # Bodies — two batch bar calls instead of N individual bar() calls
139
139
  body_w = max(0.4, min(0.8, 60 / max(len(df), 1)))
@@ -186,8 +186,7 @@ class ChartRenderer:
186
186
  stamp_logo(fig, provider=meta.provider)
187
187
 
188
188
  out_file = out.with_suffix(f".{fmt}")
189
- fig.savefig(out_file, dpi=get_settings().dpi,
190
- facecolor=fig.get_facecolor(), bbox_inches="tight")
189
+ self._savefig(fig, out_file, fmt, get_settings().dpi)
191
190
  plt.close(fig)
192
191
  log.detail("Saved candlestick chart → %s", out_file)
193
192
  return out_file
@@ -255,8 +254,7 @@ class ChartRenderer:
255
254
  stamp_logo(fig, provider=meta.provider)
256
255
 
257
256
  out_file = out.with_suffix(f".{fmt}")
258
- fig.savefig(str(out_file), dpi=settings.dpi,
259
- bbox_inches="tight", facecolor=fig.get_facecolor())
257
+ self._savefig(fig, out_file, fmt, settings.dpi)
260
258
  plt.close(fig)
261
259
  get_logger().detail("Saved mplfinance chart → %s", out_file)
262
260
  return out_file
@@ -321,8 +319,7 @@ class ChartRenderer:
321
319
  stamp_logo(fig, provider=meta.provider)
322
320
 
323
321
  out_file = out.with_suffix(f".{fmt}")
324
- fig.savefig(out_file, dpi=settings.dpi,
325
- facecolor=fig.get_facecolor(), bbox_inches="tight")
322
+ self._savefig(fig, out_file, fmt, settings.dpi)
326
323
  plt.close(fig)
327
324
  get_logger().detail("Saved OHLC chart → %s", out_file)
328
325
  return out_file
@@ -352,8 +349,7 @@ class ChartRenderer:
352
349
  stamp_logo(fig, provider=meta.provider)
353
350
 
354
351
  out_file = out.with_suffix(f".{fmt}")
355
- fig.savefig(out_file, dpi=settings.dpi,
356
- facecolor=fig.get_facecolor(), bbox_inches="tight")
352
+ self._savefig(fig, out_file, fmt, settings.dpi)
357
353
  plt.close(fig)
358
354
  get_logger().detail("Saved line chart → %s", out_file)
359
355
  return out_file
@@ -384,8 +380,7 @@ class ChartRenderer:
384
380
  stamp_logo(fig, provider=meta.provider)
385
381
 
386
382
  out_file = out.with_suffix(f".{fmt}")
387
- fig.savefig(out_file, dpi=settings.dpi,
388
- facecolor=fig.get_facecolor(), bbox_inches="tight")
383
+ self._savefig(fig, out_file, fmt, settings.dpi)
389
384
  plt.close(fig)
390
385
  get_logger().detail("Saved area chart → %s", out_file)
391
386
  return out_file
@@ -439,6 +434,19 @@ class ChartRenderer:
439
434
  for spine in ax.spines.values():
440
435
  spine.set_visible(False)
441
436
 
437
+ @staticmethod
438
+ def _savefig(fig: plt.Figure, out_file: Path, fmt: str, dpi: int) -> None:
439
+ """Save *fig* with format-specific compression to keep file sizes small."""
440
+ pil_kwargs: dict = {}
441
+ if fmt == "png":
442
+ pil_kwargs = {"compress_level": 6, "optimize": True}
443
+ elif fmt in ("jpg", "jpeg", "webp"):
444
+ pil_kwargs = {"quality": 82, "optimize": True}
445
+ kwargs: dict = {"dpi": dpi, "facecolor": fig.get_facecolor(), "bbox_inches": "tight"}
446
+ if pil_kwargs:
447
+ kwargs["pil_kwargs"] = pil_kwargs
448
+ fig.savefig(out_file, **kwargs)
449
+
442
450
  @staticmethod
443
451
  def _set_date_labels(ax: plt.Axes, df: pd.DataFrame, theme: Theme) -> None:
444
452
  n = len(df)
@@ -32,9 +32,9 @@ class Settings:
32
32
  inst._theme: ThemeName = "dark"
33
33
  inst._watermark_enabled: bool = True
34
34
  inst._overwrite: bool = False
35
- inst._dpi: int = 150
36
- inst._fig_width: int = 14
37
- inst._fig_height: int = 7
35
+ inst._dpi: int = 100
36
+ inst._fig_width: int = 12
37
+ inst._fig_height: int = 6
38
38
  inst._cache_ttl: int = 300
39
39
  inst._disk_store: DiskStore | None = None
40
40
  cls._instance = inst
@@ -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