kadeconsole 0.1.0__py3-none-any.whl

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 (43) hide show
  1. kadeconsole/__init__.py +10 -0
  2. kadeconsole/cache/__init__.py +0 -0
  3. kadeconsole/cache/store.py +155 -0
  4. kadeconsole/cli.py +193 -0
  5. kadeconsole/config/__init__.py +0 -0
  6. kadeconsole/config/loader.py +111 -0
  7. kadeconsole/core/__init__.py +0 -0
  8. kadeconsole/core/parser.py +241 -0
  9. kadeconsole/core/router.py +126 -0
  10. kadeconsole/core/session.py +131 -0
  11. kadeconsole/indicators/__init__.py +0 -0
  12. kadeconsole/indicators/momentum.py +166 -0
  13. kadeconsole/indicators/moving_averages.py +126 -0
  14. kadeconsole/indicators/volatility.py +114 -0
  15. kadeconsole/indicators/volume.py +79 -0
  16. kadeconsole/jev/__init__.py +0 -0
  17. kadeconsole/jev/client.py +140 -0
  18. kadeconsole/jev/narratives.py +142 -0
  19. kadeconsole/jev/ratings.py +253 -0
  20. kadeconsole/jev/verdicts.py +199 -0
  21. kadeconsole/pages/__init__.py +0 -0
  22. kadeconsole/pages/des.py +97 -0
  23. kadeconsole/pages/equity.py +248 -0
  24. kadeconsole/pages/fa.py +212 -0
  25. kadeconsole/pages/gp.py +88 -0
  26. kadeconsole/pages/help.py +144 -0
  27. kadeconsole/pages/hp.py +127 -0
  28. kadeconsole/pages/rate.py +157 -0
  29. kadeconsole/pages/verdict.py +167 -0
  30. kadeconsole/providers/__init__.py +0 -0
  31. kadeconsole/providers/base.py +139 -0
  32. kadeconsole/providers/registry.py +102 -0
  33. kadeconsole/providers/yfinance_provider.py +351 -0
  34. kadeconsole/render/__init__.py +0 -0
  35. kadeconsole/render/charts.py +144 -0
  36. kadeconsole/render/tables.py +227 -0
  37. kadeconsole/render/theme.py +105 -0
  38. kadeconsole/tests/__init__.py +0 -0
  39. kadeconsole-0.1.0.dist-info/METADATA +164 -0
  40. kadeconsole-0.1.0.dist-info/RECORD +43 -0
  41. kadeconsole-0.1.0.dist-info/WHEEL +5 -0
  42. kadeconsole-0.1.0.dist-info/entry_points.txt +2 -0
  43. kadeconsole-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,10 @@
1
+ """
2
+ kadeConsole — Bloomberg-terminal-style equity research console.
3
+
4
+ Usage:
5
+ pip install kadeconsole
6
+ kadeconsole
7
+ """
8
+
9
+ __version__ = "0.1.0"
10
+ __author__ = "kadeConsole Contributors"
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()
kadeconsole/cli.py ADDED
@@ -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
@@ -0,0 +1,111 @@
1
+ """
2
+ config/loader.py — ~/.kadeconsole/config.yaml reader.
3
+
4
+ Creates a default config file on first run. Returns a validated config dict.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ try:
14
+ import yaml
15
+ _YAML_AVAILABLE = True
16
+ except ImportError:
17
+ _YAML_AVAILABLE = False
18
+
19
+ CONFIG_DIR = Path.home() / ".kadeconsole"
20
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
21
+
22
+ DEFAULT_CONFIG: dict[str, Any] = {
23
+ "provider_priority": ["yfinance"],
24
+ "typesafe_api_key": "",
25
+ "alpaca_api_key": "",
26
+ "alphavantage_api_key": "",
27
+ "polygon_api_key": "",
28
+ "default_timeframe": "1Y",
29
+ "theme": "default",
30
+ "cache_ttl_provider_minutes": 15,
31
+ "cache_ttl_jev_hours": 24,
32
+ }
33
+
34
+
35
+ def load_config() -> dict[str, Any]:
36
+ """
37
+ Load configuration from ~/.kadeconsole/config.yaml.
38
+
39
+ Creates the directory and a default config file if they don't exist.
40
+ Merges loaded values over the defaults (new keys in defaults are preserved
41
+ across upgrades without blowing away user customisations).
42
+ """
43
+ _ensure_dir()
44
+
45
+ if not CONFIG_FILE.exists():
46
+ _write_default()
47
+
48
+ if not _YAML_AVAILABLE:
49
+ # Fallback: return defaults, don't crash if PyYAML not installed yet
50
+ return DEFAULT_CONFIG.copy()
51
+
52
+ try:
53
+ with open(CONFIG_FILE, "r") as f:
54
+ user_config: dict[str, Any] = yaml.safe_load(f) or {}
55
+ except Exception: # noqa: BLE001
56
+ user_config = {}
57
+
58
+ # Merge: defaults first, user values override
59
+ merged = {**DEFAULT_CONFIG, **user_config}
60
+
61
+ # Also honour environment variable overrides (CI-friendly)
62
+ _apply_env_overrides(merged)
63
+
64
+ return merged
65
+
66
+
67
+ def _ensure_dir() -> None:
68
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
69
+
70
+
71
+ def _write_default() -> None:
72
+ if not _YAML_AVAILABLE:
73
+ return
74
+ comment_block = """\
75
+ # kadeConsole configuration
76
+ # ─────────────────────────────────────────────────────────────
77
+ # Set your API keys below to unlock all data providers.
78
+ # kadeConsole works without any keys using yfinance (free).
79
+ #
80
+ # Get a TypeSafe AI key at: https://typesafe.ai
81
+ # ─────────────────────────────────────────────────────────────
82
+
83
+ """
84
+ with open(CONFIG_FILE, "w") as f:
85
+ f.write(comment_block)
86
+ yaml.dump(DEFAULT_CONFIG, f, default_flow_style=False, allow_unicode=True)
87
+
88
+
89
+ def _apply_env_overrides(config: dict[str, Any]) -> None:
90
+ """Override config values from environment variables."""
91
+ env_map = {
92
+ "TYPESAFE_API_KEY": "typesafe_api_key",
93
+ "ALPACA_API_KEY": "alpaca_api_key",
94
+ "ALPHAVANTAGE_API_KEY": "alphavantage_api_key",
95
+ "POLYGON_API_KEY": "polygon_api_key",
96
+ }
97
+ for env_key, config_key in env_map.items():
98
+ val = os.environ.get(env_key)
99
+ if val:
100
+ config[config_key] = val
101
+
102
+
103
+ def get_api_key(config: dict[str, Any], provider: str) -> str:
104
+ """Convenience helper to retrieve a provider's API key from config."""
105
+ key_map = {
106
+ "typesafe": "typesafe_api_key",
107
+ "alpaca": "alpaca_api_key",
108
+ "alphavantage": "alphavantage_api_key",
109
+ "polygon": "polygon_api_key",
110
+ }
111
+ return config.get(key_map.get(provider, ""), "") or ""
File without changes