kadeconsole 0.1.0__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.
- kadeconsole-0.1.0/PKG-INFO +164 -0
- kadeconsole-0.1.0/README.md +123 -0
- kadeconsole-0.1.0/kadeconsole/__init__.py +10 -0
- kadeconsole-0.1.0/kadeconsole/cache/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole/cache/store.py +155 -0
- kadeconsole-0.1.0/kadeconsole/cli.py +193 -0
- kadeconsole-0.1.0/kadeconsole/config/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole/config/loader.py +111 -0
- kadeconsole-0.1.0/kadeconsole/core/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole/core/parser.py +241 -0
- kadeconsole-0.1.0/kadeconsole/core/router.py +126 -0
- kadeconsole-0.1.0/kadeconsole/core/session.py +131 -0
- kadeconsole-0.1.0/kadeconsole/indicators/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole/indicators/momentum.py +166 -0
- kadeconsole-0.1.0/kadeconsole/indicators/moving_averages.py +126 -0
- kadeconsole-0.1.0/kadeconsole/indicators/volatility.py +114 -0
- kadeconsole-0.1.0/kadeconsole/indicators/volume.py +79 -0
- kadeconsole-0.1.0/kadeconsole/jev/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole/jev/client.py +140 -0
- kadeconsole-0.1.0/kadeconsole/jev/narratives.py +142 -0
- kadeconsole-0.1.0/kadeconsole/jev/ratings.py +253 -0
- kadeconsole-0.1.0/kadeconsole/jev/verdicts.py +199 -0
- kadeconsole-0.1.0/kadeconsole/pages/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole/pages/des.py +97 -0
- kadeconsole-0.1.0/kadeconsole/pages/equity.py +248 -0
- kadeconsole-0.1.0/kadeconsole/pages/fa.py +212 -0
- kadeconsole-0.1.0/kadeconsole/pages/gp.py +88 -0
- kadeconsole-0.1.0/kadeconsole/pages/help.py +144 -0
- kadeconsole-0.1.0/kadeconsole/pages/hp.py +127 -0
- kadeconsole-0.1.0/kadeconsole/pages/rate.py +157 -0
- kadeconsole-0.1.0/kadeconsole/pages/verdict.py +167 -0
- kadeconsole-0.1.0/kadeconsole/providers/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole/providers/base.py +139 -0
- kadeconsole-0.1.0/kadeconsole/providers/registry.py +102 -0
- kadeconsole-0.1.0/kadeconsole/providers/yfinance_provider.py +351 -0
- kadeconsole-0.1.0/kadeconsole/render/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole/render/charts.py +144 -0
- kadeconsole-0.1.0/kadeconsole/render/tables.py +227 -0
- kadeconsole-0.1.0/kadeconsole/render/theme.py +105 -0
- kadeconsole-0.1.0/kadeconsole/tests/__init__.py +0 -0
- kadeconsole-0.1.0/kadeconsole.egg-info/PKG-INFO +164 -0
- kadeconsole-0.1.0/kadeconsole.egg-info/SOURCES.txt +50 -0
- kadeconsole-0.1.0/kadeconsole.egg-info/dependency_links.txt +1 -0
- kadeconsole-0.1.0/kadeconsole.egg-info/entry_points.txt +2 -0
- kadeconsole-0.1.0/kadeconsole.egg-info/requires.txt +22 -0
- kadeconsole-0.1.0/kadeconsole.egg-info/top_level.txt +1 -0
- kadeconsole-0.1.0/pyproject.toml +62 -0
- kadeconsole-0.1.0/setup.cfg +4 -0
- kadeconsole-0.1.0/tests/test_cache.py +82 -0
- kadeconsole-0.1.0/tests/test_indicators.py +186 -0
- kadeconsole-0.1.0/tests/test_parser.py +150 -0
- kadeconsole-0.1.0/tests/test_tables.py +101 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kadeconsole
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bloomberg-terminal-style equity research console powered by yfinance and Jev AI
|
|
5
|
+
Author: kadeConsole Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kade-console/kade-console
|
|
8
|
+
Project-URL: Repository, https://github.com/kade-console/kade-console
|
|
9
|
+
Project-URL: Issues, https://github.com/kade-console/kade-console/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/kade-console/kade-console/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: finance,terminal,equities,bloomberg,yfinance,ai
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: yfinance>=0.2.40
|
|
24
|
+
Requires-Dist: rich>=13.7.0
|
|
25
|
+
Requires-Dist: prompt_toolkit>=3.0.43
|
|
26
|
+
Requires-Dist: plotext>=5.2.8
|
|
27
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
28
|
+
Requires-Dist: pandas>=2.1.0
|
|
29
|
+
Requires-Dist: numpy>=1.26.0
|
|
30
|
+
Requires-Dist: requests>=2.31.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
35
|
+
Provides-Extra: jev
|
|
36
|
+
Requires-Dist: typesafe-sdk>=0.1.0; extra == "jev"
|
|
37
|
+
Provides-Extra: alpaca
|
|
38
|
+
Requires-Dist: alpaca-trade-api>=3.0.0; extra == "alpaca"
|
|
39
|
+
Provides-Extra: alphavantage
|
|
40
|
+
Requires-Dist: alpha-vantage>=2.3.1; extra == "alphavantage"
|
|
41
|
+
|
|
42
|
+
# kadeConsole
|
|
43
|
+
|
|
44
|
+
> A Bloomberg-terminal-style equity research console — free, open-source, and AI-powered.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
██╗ ██╗ █████╗ ██████╗ ███████╗
|
|
48
|
+
██║ ██╔╝██╔══██╗██╔══██╗██╔════╝
|
|
49
|
+
█████╔╝ ███████║██║ ██║█████╗
|
|
50
|
+
██╔═██╗ ██╔══██║██║ ██║██╔══╝
|
|
51
|
+
██║ ██╗██║ ██║██████╔╝███████╗
|
|
52
|
+
╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝ CONSOLE v0.1.0
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Bloomberg terminals cost ~$24k/year. kadeConsole is free. It gives you the muscle-memory command grammar of a real terminal, dense richly-formatted output, and **Jev** (TypeSafe AI) embedded in every screen — delivering the one non-obvious insight that raw numbers alone can't.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install kadeconsole
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then launch the REPL:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
kadeconsole
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quickstart
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
kadeConsole > AAPL EQUITY # resolve Apple, set session
|
|
77
|
+
kadeConsole > DES # company description
|
|
78
|
+
kadeConsole > FA # full fundamentals
|
|
79
|
+
kadeConsole > FA Q # quarterly fundamentals
|
|
80
|
+
kadeConsole > GP # 1Y price chart
|
|
81
|
+
kadeConsole > GP 6M # 6-month chart
|
|
82
|
+
kadeConsole > HP # historical OHLCV
|
|
83
|
+
kadeConsole > VERDICT # Jev buy/sell/hold distribution
|
|
84
|
+
kadeConsole > RATE # Jev multi-axis fundamental rating
|
|
85
|
+
kadeConsole > HELP # full command reference
|
|
86
|
+
kadeConsole > EXIT # quit
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Indian markets:**
|
|
90
|
+
```
|
|
91
|
+
kadeConsole > RELIANCE IN NS EQUITY # Reliance Industries on NSE
|
|
92
|
+
kadeConsole > INFY BO EQUITY # Infosys on BSE
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Command Reference
|
|
98
|
+
|
|
99
|
+
| Command | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `<TICKER> [EXCHANGE] EQUITY` | Resolve security, set session, show snapshot |
|
|
102
|
+
| `DES` | Company description, sector, key people |
|
|
103
|
+
| `FA` / `FA Q` | Full fundamentals (annual / quarterly) |
|
|
104
|
+
| `GP [timeframe]` | In-terminal price chart (`1D 5D 1M 3M 6M 1Y 2Y 5Y`) |
|
|
105
|
+
| `HP` | Historical OHLCV table |
|
|
106
|
+
| `VERDICT` | Jev buy/sell/hold probability distribution |
|
|
107
|
+
| `RATE` | Jev multi-axis rating (valuation/growth/quality/momentum) |
|
|
108
|
+
| `HELP` / `?` | Command reference |
|
|
109
|
+
| `EXIT` / `QUIT` | Exit the REPL |
|
|
110
|
+
|
|
111
|
+
*(Phase 2: TA, NI, COMPARE, SOURCE — Phase 3: SCREEN, MACRO, WATCH, EXPORT)*
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
On first run, kadeConsole creates `~/.kadeconsole/config.yaml`:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
provider_priority:
|
|
121
|
+
- yfinance
|
|
122
|
+
|
|
123
|
+
typesafe_api_key: "" # Get yours at https://typesafe.ai
|
|
124
|
+
alpaca_api_key: ""
|
|
125
|
+
alphavantage_api_key: ""
|
|
126
|
+
|
|
127
|
+
default_timeframe: 1Y
|
|
128
|
+
theme: default
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Set `typesafe_api_key` to enable **Jev** AI analysis (VERDICT, RATE, anomaly flags). The terminal is fully functional without it — Jev sections show a graceful "unavailable" notice.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Provider Plugins
|
|
136
|
+
|
|
137
|
+
The `providers/base.py` `DataProvider` interface is public. To add a new source:
|
|
138
|
+
|
|
139
|
+
1. Subclass `DataProvider`
|
|
140
|
+
2. Implement `get_info()`, `get_price_history()`, `get_financials()`, `get_news()`, `get_peers()`
|
|
141
|
+
3. Register in `~/.kadeconsole/config.yaml` under `provider_priority`
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Requirements
|
|
146
|
+
|
|
147
|
+
- Python 3.10+
|
|
148
|
+
- A modern terminal: **iTerm2** (macOS), **Windows Terminal** (Windows), **GNOME Terminal** / **Kitty** (Linux)
|
|
149
|
+
- *Optional:* TypeSafe AI key for Jev features
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Roadmap
|
|
154
|
+
|
|
155
|
+
- **v0.1 (MVP):** EQUITY, DES, FA, GP, HP, VERDICT, RATE
|
|
156
|
+
- **v0.2:** TA (technical panel), NI (news + sentiment), COMPARE, multi-provider
|
|
157
|
+
- **v0.3:** SCREEN (NL filtering), MACRO, WATCH, EXPORT
|
|
158
|
+
- **v0.4:** Full Textual TUI, tabbed panels, community plugins
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT — use freely, contribute openly.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# kadeConsole
|
|
2
|
+
|
|
3
|
+
> A Bloomberg-terminal-style equity research console — free, open-source, and AI-powered.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
██╗ ██╗ █████╗ ██████╗ ███████╗
|
|
7
|
+
██║ ██╔╝██╔══██╗██╔══██╗██╔════╝
|
|
8
|
+
█████╔╝ ███████║██║ ██║█████╗
|
|
9
|
+
██╔═██╗ ██╔══██║██║ ██║██╔══╝
|
|
10
|
+
██║ ██╗██║ ██║██████╔╝███████╗
|
|
11
|
+
╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝ CONSOLE v0.1.0
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Bloomberg terminals cost ~$24k/year. kadeConsole is free. It gives you the muscle-memory command grammar of a real terminal, dense richly-formatted output, and **Jev** (TypeSafe AI) embedded in every screen — delivering the one non-obvious insight that raw numbers alone can't.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install kadeconsole
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then launch the REPL:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
kadeconsole
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
kadeConsole > AAPL EQUITY # resolve Apple, set session
|
|
36
|
+
kadeConsole > DES # company description
|
|
37
|
+
kadeConsole > FA # full fundamentals
|
|
38
|
+
kadeConsole > FA Q # quarterly fundamentals
|
|
39
|
+
kadeConsole > GP # 1Y price chart
|
|
40
|
+
kadeConsole > GP 6M # 6-month chart
|
|
41
|
+
kadeConsole > HP # historical OHLCV
|
|
42
|
+
kadeConsole > VERDICT # Jev buy/sell/hold distribution
|
|
43
|
+
kadeConsole > RATE # Jev multi-axis fundamental rating
|
|
44
|
+
kadeConsole > HELP # full command reference
|
|
45
|
+
kadeConsole > EXIT # quit
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Indian markets:**
|
|
49
|
+
```
|
|
50
|
+
kadeConsole > RELIANCE IN NS EQUITY # Reliance Industries on NSE
|
|
51
|
+
kadeConsole > INFY BO EQUITY # Infosys on BSE
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Command Reference
|
|
57
|
+
|
|
58
|
+
| Command | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `<TICKER> [EXCHANGE] EQUITY` | Resolve security, set session, show snapshot |
|
|
61
|
+
| `DES` | Company description, sector, key people |
|
|
62
|
+
| `FA` / `FA Q` | Full fundamentals (annual / quarterly) |
|
|
63
|
+
| `GP [timeframe]` | In-terminal price chart (`1D 5D 1M 3M 6M 1Y 2Y 5Y`) |
|
|
64
|
+
| `HP` | Historical OHLCV table |
|
|
65
|
+
| `VERDICT` | Jev buy/sell/hold probability distribution |
|
|
66
|
+
| `RATE` | Jev multi-axis rating (valuation/growth/quality/momentum) |
|
|
67
|
+
| `HELP` / `?` | Command reference |
|
|
68
|
+
| `EXIT` / `QUIT` | Exit the REPL |
|
|
69
|
+
|
|
70
|
+
*(Phase 2: TA, NI, COMPARE, SOURCE — Phase 3: SCREEN, MACRO, WATCH, EXPORT)*
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
On first run, kadeConsole creates `~/.kadeconsole/config.yaml`:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
provider_priority:
|
|
80
|
+
- yfinance
|
|
81
|
+
|
|
82
|
+
typesafe_api_key: "" # Get yours at https://typesafe.ai
|
|
83
|
+
alpaca_api_key: ""
|
|
84
|
+
alphavantage_api_key: ""
|
|
85
|
+
|
|
86
|
+
default_timeframe: 1Y
|
|
87
|
+
theme: default
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Set `typesafe_api_key` to enable **Jev** AI analysis (VERDICT, RATE, anomaly flags). The terminal is fully functional without it — Jev sections show a graceful "unavailable" notice.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Provider Plugins
|
|
95
|
+
|
|
96
|
+
The `providers/base.py` `DataProvider` interface is public. To add a new source:
|
|
97
|
+
|
|
98
|
+
1. Subclass `DataProvider`
|
|
99
|
+
2. Implement `get_info()`, `get_price_history()`, `get_financials()`, `get_news()`, `get_peers()`
|
|
100
|
+
3. Register in `~/.kadeconsole/config.yaml` under `provider_priority`
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Requirements
|
|
105
|
+
|
|
106
|
+
- Python 3.10+
|
|
107
|
+
- A modern terminal: **iTerm2** (macOS), **Windows Terminal** (Windows), **GNOME Terminal** / **Kitty** (Linux)
|
|
108
|
+
- *Optional:* TypeSafe AI key for Jev features
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Roadmap
|
|
113
|
+
|
|
114
|
+
- **v0.1 (MVP):** EQUITY, DES, FA, GP, HP, VERDICT, RATE
|
|
115
|
+
- **v0.2:** TA (technical panel), NI (news + sentiment), COMPARE, multi-provider
|
|
116
|
+
- **v0.3:** SCREEN (NL filtering), MACRO, WATCH, EXPORT
|
|
117
|
+
- **v0.4:** Full Textual TUI, tabbed panels, community plugins
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT — use freely, contribute openly.
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cache/store.py — SQLite-backed response and Jev-verdict cache.
|
|
3
|
+
|
|
4
|
+
Two tables:
|
|
5
|
+
provider_cache — raw market data, keyed by (symbol, endpoint), TTL 15 min
|
|
6
|
+
jev_cache — Jev responses, keyed by sha256 of (ticker+timeframe+data_hash), TTL 24h
|
|
7
|
+
|
|
8
|
+
The cache lives at ~/.kadeconsole/cache.db and is created automatically.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import sqlite3
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
CACHE_DB = Path.home() / ".kadeconsole" / "cache.db"
|
|
21
|
+
|
|
22
|
+
_CREATE_PROVIDER = """
|
|
23
|
+
CREATE TABLE IF NOT EXISTS provider_cache (
|
|
24
|
+
key TEXT PRIMARY KEY,
|
|
25
|
+
data_json TEXT NOT NULL,
|
|
26
|
+
expires_at REAL NOT NULL
|
|
27
|
+
);
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_CREATE_JEV = """
|
|
31
|
+
CREATE TABLE IF NOT EXISTS jev_cache (
|
|
32
|
+
key TEXT PRIMARY KEY,
|
|
33
|
+
response_json TEXT NOT NULL,
|
|
34
|
+
created_at REAL NOT NULL,
|
|
35
|
+
expires_at REAL NOT NULL
|
|
36
|
+
);
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CacheStore:
|
|
41
|
+
"""Thread-safe SQLite cache store."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
db_path: Path = CACHE_DB,
|
|
46
|
+
provider_ttl_minutes: int = 15,
|
|
47
|
+
jev_ttl_hours: int = 24,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.db_path = db_path
|
|
50
|
+
self.provider_ttl = provider_ttl_minutes * 60
|
|
51
|
+
self.jev_ttl = jev_ttl_hours * 3600
|
|
52
|
+
self._ensure_db()
|
|
53
|
+
|
|
54
|
+
# ── Public API ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
def get_provider(self, symbol: str, endpoint: str) -> Optional[Any]:
|
|
57
|
+
"""Return cached provider data or None if missing/expired."""
|
|
58
|
+
key = self._provider_key(symbol, endpoint)
|
|
59
|
+
row = self._fetch("provider_cache", key)
|
|
60
|
+
return json.loads(row) if row else None
|
|
61
|
+
|
|
62
|
+
def set_provider(self, symbol: str, endpoint: str, data: Any) -> None:
|
|
63
|
+
"""Cache provider data with TTL."""
|
|
64
|
+
key = self._provider_key(symbol, endpoint)
|
|
65
|
+
self._upsert("provider_cache", key, json.dumps(data, default=str), self.provider_ttl)
|
|
66
|
+
|
|
67
|
+
def get_jev(self, ticker: str, timeframe: str, data_fingerprint: str) -> Optional[Any]:
|
|
68
|
+
"""Return cached Jev response or None if missing/expired."""
|
|
69
|
+
key = self._jev_key(ticker, timeframe, data_fingerprint)
|
|
70
|
+
row = self._fetch_jev(key)
|
|
71
|
+
return json.loads(row) if row else None
|
|
72
|
+
|
|
73
|
+
def set_jev(self, ticker: str, timeframe: str, data_fingerprint: str, response: Any) -> None:
|
|
74
|
+
"""Cache a Jev response with TTL."""
|
|
75
|
+
key = self._jev_key(ticker, timeframe, data_fingerprint)
|
|
76
|
+
self._upsert("jev_cache", key, json.dumps(response, default=str), self.jev_ttl)
|
|
77
|
+
|
|
78
|
+
def clear_expired(self) -> None:
|
|
79
|
+
"""Purge all expired rows from both tables."""
|
|
80
|
+
now = time.time()
|
|
81
|
+
with self._connect() as conn:
|
|
82
|
+
conn.execute("DELETE FROM provider_cache WHERE expires_at < ?", (now,))
|
|
83
|
+
conn.execute("DELETE FROM jev_cache WHERE expires_at < ?", (now,))
|
|
84
|
+
|
|
85
|
+
def clear_all(self) -> None:
|
|
86
|
+
"""Nuke entire cache (useful for debugging)."""
|
|
87
|
+
with self._connect() as conn:
|
|
88
|
+
conn.execute("DELETE FROM provider_cache")
|
|
89
|
+
conn.execute("DELETE FROM jev_cache")
|
|
90
|
+
|
|
91
|
+
# ── Helpers ────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def make_data_fingerprint(data: Any) -> str:
|
|
95
|
+
"""Return a short sha256 fingerprint of any JSON-serialisable data."""
|
|
96
|
+
raw = json.dumps(data, sort_keys=True, default=str).encode()
|
|
97
|
+
return hashlib.sha256(raw).hexdigest()[:16]
|
|
98
|
+
|
|
99
|
+
# ── Internal ───────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def _ensure_db(self) -> None:
|
|
102
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
with self._connect() as conn:
|
|
104
|
+
conn.execute(_CREATE_PROVIDER)
|
|
105
|
+
conn.execute(_CREATE_JEV)
|
|
106
|
+
|
|
107
|
+
def _connect(self) -> sqlite3.Connection:
|
|
108
|
+
return sqlite3.connect(str(self.db_path))
|
|
109
|
+
|
|
110
|
+
def _fetch(self, table: str, key: str) -> Optional[str]:
|
|
111
|
+
"""Fetch from provider_cache (data_json column)."""
|
|
112
|
+
now = time.time()
|
|
113
|
+
with self._connect() as conn:
|
|
114
|
+
row = conn.execute(
|
|
115
|
+
f"SELECT data_json FROM {table} WHERE key=? AND expires_at>?",
|
|
116
|
+
(key, now),
|
|
117
|
+
).fetchone()
|
|
118
|
+
return row[0] if row else None
|
|
119
|
+
|
|
120
|
+
def _fetch_jev(self, key: str) -> Optional[str]:
|
|
121
|
+
"""Fetch from jev_cache (response_json column)."""
|
|
122
|
+
now = time.time()
|
|
123
|
+
with self._connect() as conn:
|
|
124
|
+
row = conn.execute(
|
|
125
|
+
"SELECT response_json FROM jev_cache WHERE key=? AND expires_at>?",
|
|
126
|
+
(key, now),
|
|
127
|
+
).fetchone()
|
|
128
|
+
return row[0] if row else None
|
|
129
|
+
|
|
130
|
+
def _upsert(self, table: str, key: str, data_json: str, ttl: float) -> None:
|
|
131
|
+
now = time.time()
|
|
132
|
+
expires = now + ttl
|
|
133
|
+
if table == "provider_cache":
|
|
134
|
+
sql = (
|
|
135
|
+
"INSERT OR REPLACE INTO provider_cache (key, data_json, expires_at) "
|
|
136
|
+
"VALUES (?, ?, ?)"
|
|
137
|
+
)
|
|
138
|
+
with self._connect() as conn:
|
|
139
|
+
conn.execute(sql, (key, data_json, expires))
|
|
140
|
+
else:
|
|
141
|
+
sql = (
|
|
142
|
+
"INSERT OR REPLACE INTO jev_cache (key, response_json, created_at, expires_at) "
|
|
143
|
+
"VALUES (?, ?, ?, ?)"
|
|
144
|
+
)
|
|
145
|
+
with self._connect() as conn:
|
|
146
|
+
conn.execute(sql, (key, data_json, now, expires))
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _provider_key(symbol: str, endpoint: str) -> str:
|
|
150
|
+
return f"{symbol.upper()}:{endpoint}"
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _jev_key(ticker: str, timeframe: str, fingerprint: str) -> str:
|
|
154
|
+
raw = f"{ticker.upper()}:{timeframe}:{fingerprint}"
|
|
155
|
+
return hashlib.sha256(raw.encode()).hexdigest()
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli.py — kadeConsole REPL entrypoint.
|
|
3
|
+
|
|
4
|
+
Entry point: `kadeconsole` (via pyproject.toml console_scripts)
|
|
5
|
+
|
|
6
|
+
Startup sequence:
|
|
7
|
+
1. Load config from ~/.kadeconsole/config.yaml
|
|
8
|
+
2. Initialise provider registry + Jev client + SQLite cache
|
|
9
|
+
3. Print startup banner
|
|
10
|
+
4. Drop into prompt_toolkit REPL loop
|
|
11
|
+
5. Parse each input line → Router.dispatch() → continue or exit
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
from kadeconsole.render.theme import RICH_THEME, t
|
|
22
|
+
from kadeconsole import __version__
|
|
23
|
+
|
|
24
|
+
console = Console(theme=RICH_THEME)
|
|
25
|
+
|
|
26
|
+
# ── Banner ─────────────────────────────────────────────────────────────────────
|
|
27
|
+
_BANNER = r"""
|
|
28
|
+
██╗ ██╗ █████╗ ██████╗ ███████╗
|
|
29
|
+
██║ ██╔╝██╔══██╗██╔══██╗██╔════╝
|
|
30
|
+
█████╔╝ ███████║██║ ██║█████╗
|
|
31
|
+
██╔═██╗ ██╔══██║██║ ██║██╔══╝
|
|
32
|
+
██║ ██╗██║ ██║██████╔╝███████╗
|
|
33
|
+
╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print_banner() -> None:
|
|
38
|
+
console.print(f"[accent]{_BANNER}[/accent]", highlight=False)
|
|
39
|
+
console.print(
|
|
40
|
+
f" [bold {t.accent}]CONSOLE[/bold {t.accent}] "
|
|
41
|
+
f"[{t.muted}]v{__version__}[/] "
|
|
42
|
+
f"[{t.dim_text}]· Bloomberg-style equity research terminal[/]\n"
|
|
43
|
+
)
|
|
44
|
+
console.print(
|
|
45
|
+
f" [{t.muted}]Type [accent]HELP[/accent] or [accent]?[/accent] for the command reference. "
|
|
46
|
+
f"[accent]EXIT[/accent] or [accent]Q[/accent] to quit.[/]\n"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Autocomplete word list ─────────────────────────────────────────────────────
|
|
51
|
+
_AUTOCOMPLETE_WORDS = [
|
|
52
|
+
"EQUITY", "DES", "FA", "FA Q", "GP", "HP", "VERDICT", "RATE",
|
|
53
|
+
"TA", "NI", "RV", "COMPARE", "MACRO", "SCREEN", "WATCH", "EXPORT",
|
|
54
|
+
"SOURCE", "HELP", "EXIT", "QUIT",
|
|
55
|
+
"1D", "5D", "1M", "3M", "6M", "1Y", "2Y", "5Y",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_completer():
|
|
60
|
+
"""Build a prompt_toolkit WordCompleter for the REPL."""
|
|
61
|
+
try:
|
|
62
|
+
from prompt_toolkit.completion import WordCompleter
|
|
63
|
+
return WordCompleter(_AUTOCOMPLETE_WORDS, ignore_case=True, sentence=True)
|
|
64
|
+
except ImportError:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _build_prompt_session():
|
|
69
|
+
"""Build a prompt_toolkit PromptSession (with history)."""
|
|
70
|
+
try:
|
|
71
|
+
from prompt_toolkit import PromptSession
|
|
72
|
+
from prompt_toolkit.history import FileHistory
|
|
73
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
|
|
76
|
+
hist_file = Path.home() / ".kadeconsole" / "history"
|
|
77
|
+
hist_file.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
return PromptSession(
|
|
80
|
+
history=AutoSuggestFromHistory() and FileHistory(str(hist_file)),
|
|
81
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
82
|
+
completer=_build_completer(),
|
|
83
|
+
complete_while_typing=False,
|
|
84
|
+
)
|
|
85
|
+
except ImportError:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _simple_input(prompt: str) -> str:
|
|
90
|
+
"""Fallback to built-in input() when prompt_toolkit is unavailable."""
|
|
91
|
+
try:
|
|
92
|
+
return input(prompt)
|
|
93
|
+
except EOFError:
|
|
94
|
+
return "EXIT"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main() -> None:
|
|
98
|
+
"""Main entrypoint — called by the `kadeconsole` console script."""
|
|
99
|
+
|
|
100
|
+
# ── 1. Load config ─────────────────────────────────────────────────────────
|
|
101
|
+
from kadeconsole.config.loader import load_config
|
|
102
|
+
config = load_config()
|
|
103
|
+
|
|
104
|
+
# ── 2. Initialise cache ────────────────────────────────────────────────────
|
|
105
|
+
from kadeconsole.cache.store import CacheStore
|
|
106
|
+
cache = CacheStore(
|
|
107
|
+
provider_ttl_minutes = config.get("cache_ttl_provider_minutes", 15),
|
|
108
|
+
jev_ttl_hours = config.get("cache_ttl_jev_hours", 24),
|
|
109
|
+
)
|
|
110
|
+
cache.clear_expired() # housekeeping on startup
|
|
111
|
+
|
|
112
|
+
# ── 3. Initialise providers ────────────────────────────────────────────────
|
|
113
|
+
from kadeconsole.providers.registry import ProviderRegistry
|
|
114
|
+
registry = ProviderRegistry(config)
|
|
115
|
+
provider = registry.get_primary()
|
|
116
|
+
|
|
117
|
+
# ── 4. Initialise Jev client ───────────────────────────────────────────────
|
|
118
|
+
from kadeconsole.jev.client import JevClient
|
|
119
|
+
jev = JevClient(
|
|
120
|
+
api_key = config.get("typesafe_api_key", ""),
|
|
121
|
+
cache = cache,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# ── 5. Session + Router ───────────────────────────────────────────────────
|
|
125
|
+
from kadeconsole.core.session import Session
|
|
126
|
+
from kadeconsole.core.router import Router
|
|
127
|
+
session = Session(timeframe=config.get("default_timeframe", "1Y"))
|
|
128
|
+
router = Router(session=session, provider=provider, jev=jev)
|
|
129
|
+
|
|
130
|
+
# ── 6. Banner ──────────────────────────────────────────────────────────────
|
|
131
|
+
_print_banner()
|
|
132
|
+
|
|
133
|
+
# Provider status line
|
|
134
|
+
provider_line = f"[{t.muted}] Data: [accent]{provider.name}[/accent]"
|
|
135
|
+
if registry.provider_names:
|
|
136
|
+
provider_line += f" [{t.muted}]({', '.join(registry.provider_names)})[/]"
|
|
137
|
+
jev_status = (
|
|
138
|
+
f"[positive]◆ Jev active[/positive]"
|
|
139
|
+
if jev.available
|
|
140
|
+
else f"[{t.muted}]◆ Jev inactive — set TYPESAFE_API_KEY to enable AI analysis[/]"
|
|
141
|
+
)
|
|
142
|
+
console.print(f"{provider_line} {jev_status}\n")
|
|
143
|
+
|
|
144
|
+
# ── 7. REPL loop ───────────────────────────────────────────────────────────
|
|
145
|
+
prompt_session = _build_prompt_session()
|
|
146
|
+
|
|
147
|
+
# Prompt style — amber "kadeConsole >" prefix
|
|
148
|
+
prompt_str = f"kadeConsole > "
|
|
149
|
+
|
|
150
|
+
while True:
|
|
151
|
+
try:
|
|
152
|
+
if prompt_session is not None:
|
|
153
|
+
try:
|
|
154
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
155
|
+
pt_style = PTStyle.from_dict({
|
|
156
|
+
"": f"#{t.text[1:]}",
|
|
157
|
+
"prompt": f"bold #{t.accent[1:]}",
|
|
158
|
+
})
|
|
159
|
+
raw = prompt_session.prompt(
|
|
160
|
+
[("class:prompt", prompt_str)],
|
|
161
|
+
style=pt_style,
|
|
162
|
+
)
|
|
163
|
+
except Exception:
|
|
164
|
+
raw = prompt_session.prompt(prompt_str)
|
|
165
|
+
else:
|
|
166
|
+
raw = _simple_input(prompt_str)
|
|
167
|
+
|
|
168
|
+
except KeyboardInterrupt:
|
|
169
|
+
console.print(f"\n[{t.muted}] Use EXIT or Q to quit.[/]\n")
|
|
170
|
+
continue
|
|
171
|
+
except EOFError:
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
raw = raw.strip()
|
|
175
|
+
if not raw:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Parse and dispatch
|
|
179
|
+
from kadeconsole.core.parser import parse, ParseError
|
|
180
|
+
try:
|
|
181
|
+
parsed = parse(raw)
|
|
182
|
+
except ParseError as exc:
|
|
183
|
+
console.print(f"[negative] Parse error: {exc}[/negative]")
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
keep_going = router.dispatch(parsed)
|
|
187
|
+
if not keep_going:
|
|
188
|
+
console.print(f"\n[{t.muted}] Goodbye.[/]\n")
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|
|
File without changes
|