terminex 0.4.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.
Files changed (41) hide show
  1. terminex-0.4.0/LICENSE +21 -0
  2. terminex-0.4.0/PKG-INFO +209 -0
  3. terminex-0.4.0/README.md +161 -0
  4. terminex-0.4.0/pyproject.toml +41 -0
  5. terminex-0.4.0/setup.cfg +4 -0
  6. terminex-0.4.0/terminex/__init__.py +1 -0
  7. terminex-0.4.0/terminex/__main__.py +10 -0
  8. terminex-0.4.0/terminex/alert_engine.py +69 -0
  9. terminex-0.4.0/terminex/alerts.py +117 -0
  10. terminex-0.4.0/terminex/alerts_ui.py +129 -0
  11. terminex-0.4.0/terminex/app.py +842 -0
  12. terminex-0.4.0/terminex/config.py +67 -0
  13. terminex-0.4.0/terminex/converter.py +197 -0
  14. terminex-0.4.0/terminex/currencies.py +35 -0
  15. terminex-0.4.0/terminex/display.py +192 -0
  16. terminex-0.4.0/terminex/help.py +89 -0
  17. terminex-0.4.0/terminex/keyboard.py +134 -0
  18. terminex-0.4.0/terminex/providers/__init__.py +1 -0
  19. terminex-0.4.0/terminex/providers/base.py +35 -0
  20. terminex-0.4.0/terminex/providers/commodities_stooq.py +133 -0
  21. terminex-0.4.0/terminex/providers/crypto_coincap.py +101 -0
  22. terminex-0.4.0/terminex/providers/crypto_coingecko.py +103 -0
  23. terminex-0.4.0/terminex/providers/fx_erapi.py +74 -0
  24. terminex-0.4.0/terminex/providers/watchlist_agg.py +81 -0
  25. terminex-0.4.0/terminex/quote.py +33 -0
  26. terminex-0.4.0/terminex/series.py +75 -0
  27. terminex-0.4.0/terminex/sparkline.py +50 -0
  28. terminex-0.4.0/terminex/statusbar.py +75 -0
  29. terminex-0.4.0/terminex/store.py +87 -0
  30. terminex-0.4.0/terminex/theme.py +48 -0
  31. terminex-0.4.0/terminex/watchlist.py +85 -0
  32. terminex-0.4.0/terminex.egg-info/PKG-INFO +209 -0
  33. terminex-0.4.0/terminex.egg-info/SOURCES.txt +39 -0
  34. terminex-0.4.0/terminex.egg-info/dependency_links.txt +1 -0
  35. terminex-0.4.0/terminex.egg-info/entry_points.txt +2 -0
  36. terminex-0.4.0/terminex.egg-info/requires.txt +2 -0
  37. terminex-0.4.0/terminex.egg-info/top_level.txt +1 -0
  38. terminex-0.4.0/tests/test_alerts.py +169 -0
  39. terminex-0.4.0/tests/test_converter.py +136 -0
  40. terminex-0.4.0/tests/test_filters_sorts.py +107 -0
  41. terminex-0.4.0/tests/test_watchlist.py +83 -0
terminex-0.4.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 johnd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.4
2
+ Name: terminex
3
+ Version: 0.4.0
4
+ Summary: Terminal dashboard for live FX, crypto, and commodity prices
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 johnd
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://github.com/johnd/terminex
28
+ Project-URL: Issues, https://github.com/johnd/terminex/issues
29
+ Keywords: terminal,tui,forex,crypto,commodities,dashboard
30
+ Classifier: Development Status :: 4 - Beta
31
+ Classifier: Environment :: Console
32
+ Classifier: Intended Audience :: End Users/Desktop
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: POSIX
35
+ Classifier: Operating System :: POSIX :: Linux
36
+ Classifier: Operating System :: MacOS :: MacOS X
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: Office/Business :: Financial
41
+ Classifier: Topic :: Terminals
42
+ Requires-Python: >=3.11
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Requires-Dist: requests>=2.31
46
+ Requires-Dist: rich>=13.0
47
+ Dynamic: license-file
48
+
49
+ # terminex
50
+
51
+ A terminal dashboard for live prices across three asset classes:
52
+
53
+ - **FX** — top 25 most-traded currencies (BIS triennial survey)
54
+ - **Crypto** — top 25 coins by market cap
55
+ - **Commodities** — metals, energy, and agriculturals futures
56
+
57
+ ## Install
58
+
59
+ Requires Python 3.11+.
60
+
61
+ ```bash
62
+ pipx install terminex
63
+ # or, from a local checkout:
64
+ pipx install .
65
+ # or editable for development:
66
+ pip install -e .
67
+ ```
68
+
69
+ ## Run
70
+
71
+ ```bash
72
+ terminex
73
+ # or without installing:
74
+ python3 -m terminex
75
+ ```
76
+
77
+ ### Keys
78
+
79
+ | Key | Action |
80
+ |-----------|-----------------------------------------------------|
81
+ | `1/2/3/4` | Switch to FX / Crypto / Commodities / Watchlist tab |
82
+ | `j` / `k` | Move highlighted row down / up |
83
+ | `g` / `G` | Jump to first / last row |
84
+ | `s` | Cycle sort key: default → 24h → price |
85
+ | `S` | Toggle sort direction |
86
+ | `/` | Filter by symbol or name substring (live) |
87
+ | `Esc` | Clear filter / close help / close converter |
88
+ | `w` | Toggle watchlist pin for the highlighted row |
89
+ | `~` | Toggle sparkline column |
90
+ | `c` | Open cross-rate calculator |
91
+ | `a` | Create alert on highlighted row |
92
+ | `A` | List / delete alerts |
93
+ | `?` | Toggle help overlay |
94
+ | `r` | Force refresh the current tab |
95
+ | `q` | Quit |
96
+
97
+ ### CLI options
98
+
99
+ - `--base USD` — FX base currency (default: `USD`)
100
+ - `--tab fx|crypto|commodity|watchlist` — tab to start on
101
+ - `--interval 10` — refresh interval in seconds
102
+
103
+ ### Filter
104
+
105
+ Press `/` to enter live-filter mode and start typing. The table filters
106
+ as you type against both symbol and name. Enter exits filter mode
107
+ (keeping the filter active); Esc cancels and clears.
108
+
109
+ ### Sparklines
110
+
111
+ Press `~` to toggle a 20-point sparkline column showing recent price
112
+ movement. Series are built up from live polls — newly-started series
113
+ start empty and fill in over time. Green means net up, red means net
114
+ down. The column hides the natural units but makes trend-shape obvious
115
+ at a glance.
116
+
117
+ ### Cross-rate calculator
118
+
119
+ Press `c` to open an interactive conversion prompt that resolves across
120
+ all three asset classes via USD pivot. Type expressions like:
121
+
122
+ ```
123
+ 1 BTC in EUR
124
+ 500 EUR in JPY
125
+ 1 GC.F in EUR
126
+ 1,000 USD in GBP
127
+ ```
128
+
129
+ Press `Enter` to compute, `Esc` to close. The last 5 results stay
130
+ visible above the prompt. Symbols are whatever is loaded in the
131
+ fx / crypto / commodity tabs (commodity symbols use Stooq notation like
132
+ `GC.F`, `CL.F`).
133
+
134
+ ### Price alerts
135
+
136
+ Highlight any row and press `a` to open the alert-creation modal. The
137
+ threshold field pre-fills with the current price. While the modal is
138
+ open:
139
+
140
+ - `0-9` / `.` — edit threshold
141
+ - `<` / `>` — toggle the comparison operator
142
+ - `r` — toggle recurring (one-shot by default)
143
+ - `Enter` — create alert
144
+ - `Esc` — cancel
145
+
146
+ Alerts fire when the price *crosses* the threshold, not every tick
147
+ while above/below it. On fire: terminal bell, toast in the status
148
+ line, and a desktop notification via `notify-send` (Linux; silently
149
+ skipped if unavailable). Non-recurring alerts auto-deactivate on first
150
+ fire.
151
+
152
+ Press `A` for the list view — browse all alerts, type an ID and press
153
+ `Enter` to delete. The status bar shows the active-alert count.
154
+
155
+ Alerts are stored in SQLite at
156
+ `~/.local/share/terminex/terminex.db` (or `$XDG_DATA_HOME/terminex/`)
157
+ and survive across runs.
158
+
159
+ ### Watchlist
160
+
161
+ Highlight any row on the FX / Crypto / Commodities tabs and press `w` to
162
+ pin it. Press `4` to see all pins across asset classes in a single
163
+ unified view, sortable the same way as any other tab. Pins persist to
164
+ `~/.config/terminex/watchlist.toml` and survive across runs.
165
+
166
+ ## Tests
167
+
168
+ ```bash
169
+ python3 -m unittest discover -s tests -t .
170
+ ```
171
+
172
+ Stdlib `unittest` only, no extra dependencies. Covers the pure-logic
173
+ modules (converter, alerts DAO & engine, watchlist, filter/sort
174
+ helpers, age formatter).
175
+
176
+ ## Data sources
177
+
178
+ | Asset | Provider | Key required? | Notes |
179
+ |-------------|-----------------------------------|-----------------|---------------------------------|
180
+ | FX | `open.er-api.com` | no | updates ~hourly |
181
+ | Crypto | `coingecko.com` (default) | no | top 25 by mcap, ~1 min cache |
182
+ | Crypto | `rest.coincap.io` (if key set) | yes (free) | top 25 by mcap, sub-minute data |
183
+ | Commodities | `stooq.com` (CSV) | no | 14 futures, ~15-min delayed |
184
+
185
+ **Crypto tab works out of the box** via CoinGecko's public API. For
186
+ faster / higher-rate data, [grab a free CoinCap API key](https://coincap.io/)
187
+ and export it — terminex will automatically prefer it:
188
+
189
+ ```bash
190
+ export TERMINEX_COINCAP_KEY=your-key-here
191
+ ```
192
+
193
+ Or put it in the config file (see below).
194
+
195
+ ## Config file
196
+
197
+ terminex looks for `~/.config/terminex/config.toml` (or
198
+ `$XDG_CONFIG_HOME/terminex/config.toml`). All keys optional:
199
+
200
+ ```toml
201
+ base_currency = "USD"
202
+ refresh_interval = 10
203
+ active_tab = "fx"
204
+
205
+ [providers.crypto]
206
+ api_key = "your-coincap-key"
207
+ ```
208
+
209
+ Environment variables win over config file values.
@@ -0,0 +1,161 @@
1
+ # terminex
2
+
3
+ A terminal dashboard for live prices across three asset classes:
4
+
5
+ - **FX** — top 25 most-traded currencies (BIS triennial survey)
6
+ - **Crypto** — top 25 coins by market cap
7
+ - **Commodities** — metals, energy, and agriculturals futures
8
+
9
+ ## Install
10
+
11
+ Requires Python 3.11+.
12
+
13
+ ```bash
14
+ pipx install terminex
15
+ # or, from a local checkout:
16
+ pipx install .
17
+ # or editable for development:
18
+ pip install -e .
19
+ ```
20
+
21
+ ## Run
22
+
23
+ ```bash
24
+ terminex
25
+ # or without installing:
26
+ python3 -m terminex
27
+ ```
28
+
29
+ ### Keys
30
+
31
+ | Key | Action |
32
+ |-----------|-----------------------------------------------------|
33
+ | `1/2/3/4` | Switch to FX / Crypto / Commodities / Watchlist tab |
34
+ | `j` / `k` | Move highlighted row down / up |
35
+ | `g` / `G` | Jump to first / last row |
36
+ | `s` | Cycle sort key: default → 24h → price |
37
+ | `S` | Toggle sort direction |
38
+ | `/` | Filter by symbol or name substring (live) |
39
+ | `Esc` | Clear filter / close help / close converter |
40
+ | `w` | Toggle watchlist pin for the highlighted row |
41
+ | `~` | Toggle sparkline column |
42
+ | `c` | Open cross-rate calculator |
43
+ | `a` | Create alert on highlighted row |
44
+ | `A` | List / delete alerts |
45
+ | `?` | Toggle help overlay |
46
+ | `r` | Force refresh the current tab |
47
+ | `q` | Quit |
48
+
49
+ ### CLI options
50
+
51
+ - `--base USD` — FX base currency (default: `USD`)
52
+ - `--tab fx|crypto|commodity|watchlist` — tab to start on
53
+ - `--interval 10` — refresh interval in seconds
54
+
55
+ ### Filter
56
+
57
+ Press `/` to enter live-filter mode and start typing. The table filters
58
+ as you type against both symbol and name. Enter exits filter mode
59
+ (keeping the filter active); Esc cancels and clears.
60
+
61
+ ### Sparklines
62
+
63
+ Press `~` to toggle a 20-point sparkline column showing recent price
64
+ movement. Series are built up from live polls — newly-started series
65
+ start empty and fill in over time. Green means net up, red means net
66
+ down. The column hides the natural units but makes trend-shape obvious
67
+ at a glance.
68
+
69
+ ### Cross-rate calculator
70
+
71
+ Press `c` to open an interactive conversion prompt that resolves across
72
+ all three asset classes via USD pivot. Type expressions like:
73
+
74
+ ```
75
+ 1 BTC in EUR
76
+ 500 EUR in JPY
77
+ 1 GC.F in EUR
78
+ 1,000 USD in GBP
79
+ ```
80
+
81
+ Press `Enter` to compute, `Esc` to close. The last 5 results stay
82
+ visible above the prompt. Symbols are whatever is loaded in the
83
+ fx / crypto / commodity tabs (commodity symbols use Stooq notation like
84
+ `GC.F`, `CL.F`).
85
+
86
+ ### Price alerts
87
+
88
+ Highlight any row and press `a` to open the alert-creation modal. The
89
+ threshold field pre-fills with the current price. While the modal is
90
+ open:
91
+
92
+ - `0-9` / `.` — edit threshold
93
+ - `<` / `>` — toggle the comparison operator
94
+ - `r` — toggle recurring (one-shot by default)
95
+ - `Enter` — create alert
96
+ - `Esc` — cancel
97
+
98
+ Alerts fire when the price *crosses* the threshold, not every tick
99
+ while above/below it. On fire: terminal bell, toast in the status
100
+ line, and a desktop notification via `notify-send` (Linux; silently
101
+ skipped if unavailable). Non-recurring alerts auto-deactivate on first
102
+ fire.
103
+
104
+ Press `A` for the list view — browse all alerts, type an ID and press
105
+ `Enter` to delete. The status bar shows the active-alert count.
106
+
107
+ Alerts are stored in SQLite at
108
+ `~/.local/share/terminex/terminex.db` (or `$XDG_DATA_HOME/terminex/`)
109
+ and survive across runs.
110
+
111
+ ### Watchlist
112
+
113
+ Highlight any row on the FX / Crypto / Commodities tabs and press `w` to
114
+ pin it. Press `4` to see all pins across asset classes in a single
115
+ unified view, sortable the same way as any other tab. Pins persist to
116
+ `~/.config/terminex/watchlist.toml` and survive across runs.
117
+
118
+ ## Tests
119
+
120
+ ```bash
121
+ python3 -m unittest discover -s tests -t .
122
+ ```
123
+
124
+ Stdlib `unittest` only, no extra dependencies. Covers the pure-logic
125
+ modules (converter, alerts DAO & engine, watchlist, filter/sort
126
+ helpers, age formatter).
127
+
128
+ ## Data sources
129
+
130
+ | Asset | Provider | Key required? | Notes |
131
+ |-------------|-----------------------------------|-----------------|---------------------------------|
132
+ | FX | `open.er-api.com` | no | updates ~hourly |
133
+ | Crypto | `coingecko.com` (default) | no | top 25 by mcap, ~1 min cache |
134
+ | Crypto | `rest.coincap.io` (if key set) | yes (free) | top 25 by mcap, sub-minute data |
135
+ | Commodities | `stooq.com` (CSV) | no | 14 futures, ~15-min delayed |
136
+
137
+ **Crypto tab works out of the box** via CoinGecko's public API. For
138
+ faster / higher-rate data, [grab a free CoinCap API key](https://coincap.io/)
139
+ and export it — terminex will automatically prefer it:
140
+
141
+ ```bash
142
+ export TERMINEX_COINCAP_KEY=your-key-here
143
+ ```
144
+
145
+ Or put it in the config file (see below).
146
+
147
+ ## Config file
148
+
149
+ terminex looks for `~/.config/terminex/config.toml` (or
150
+ `$XDG_CONFIG_HOME/terminex/config.toml`). All keys optional:
151
+
152
+ ```toml
153
+ base_currency = "USD"
154
+ refresh_interval = 10
155
+ active_tab = "fx"
156
+
157
+ [providers.crypto]
158
+ api_key = "your-coincap-key"
159
+ ```
160
+
161
+ Environment variables win over config file values.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "terminex"
7
+ version = "0.4.0"
8
+ description = "Terminal dashboard for live FX, crypto, and commodity prices"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { file = "LICENSE" }
12
+ keywords = ["terminal", "tui", "forex", "crypto", "commodities", "dashboard"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: End Users/Desktop",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: POSIX",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Operating System :: MacOS :: MacOS X",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Office/Business :: Financial",
25
+ "Topic :: Terminals",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.31",
29
+ "rich>=13.0",
30
+ ]
31
+
32
+ [project.scripts]
33
+ terminex = "terminex.app:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/johnd/terminex"
37
+ Issues = "https://github.com/johnd/terminex/issues"
38
+
39
+ [tool.setuptools.packages.find]
40
+ include = ["terminex*"]
41
+ exclude = ["tests*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """terminex — live FX rates in the terminal."""
@@ -0,0 +1,10 @@
1
+ """Entry point for ``python -m terminex``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .app import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -0,0 +1,69 @@
1
+ """Alert evaluation against live snapshots.
2
+
3
+ Holds in-memory ``last_price`` per (asset_class, symbol) to detect
4
+ threshold *crossings* rather than firing every tick while the
5
+ condition stays true.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sqlite3
11
+ from dataclasses import dataclass
12
+
13
+ from . import alerts as alerts_dao
14
+ from .alerts import Alert, _row_to_alert
15
+ from .quote import Snapshot
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class FireEvent:
20
+ alert: Alert
21
+ price: float
22
+
23
+
24
+ class AlertEngine:
25
+ def __init__(self, conn: sqlite3.Connection) -> None:
26
+ self.conn = conn
27
+ self._last_price: dict[tuple[str, str], float] = {}
28
+
29
+ def evaluate(self, snapshot: Snapshot, asset_class: str) -> list[FireEvent]:
30
+ quote_by_sym = {q.symbol: q.price for q in snapshot.quotes}
31
+ # Only load alerts for this asset_class
32
+ rows = self.conn.execute(
33
+ """
34
+ SELECT * FROM alerts
35
+ WHERE active = 1 AND asset_class = ?
36
+ """,
37
+ (asset_class,),
38
+ ).fetchall()
39
+ fires: list[FireEvent] = []
40
+ for row in rows:
41
+ alert = _row_to_alert(row)
42
+ price = quote_by_sym.get(alert.symbol)
43
+ if price is None:
44
+ continue
45
+ key = (asset_class, alert.symbol)
46
+ prev = self._last_price.get(key)
47
+ triggered = _is_triggered(alert, price, prev)
48
+ if triggered:
49
+ alerts_dao.record_fire(self.conn, alert.id, price)
50
+ if not alert.recurring:
51
+ alerts_dao.deactivate(self.conn, alert.id)
52
+ fires.append(FireEvent(alert=alert, price=price))
53
+ # update last-seen prices for every symbol in this snapshot
54
+ for sym, price in quote_by_sym.items():
55
+ self._last_price[(asset_class, sym)] = price
56
+ return fires
57
+
58
+
59
+ def _is_triggered(alert: Alert, price: float, prev: float | None) -> bool:
60
+ if alert.op == ">":
61
+ if price <= alert.threshold:
62
+ return False
63
+ # first observation OR prev did not satisfy the condition
64
+ return prev is None or prev <= alert.threshold
65
+ if alert.op == "<":
66
+ if price >= alert.threshold:
67
+ return False
68
+ return prev is None or prev >= alert.threshold
69
+ return False
@@ -0,0 +1,117 @@
1
+ """Alert dataclass and DAO.
2
+
3
+ An alert fires when ``price`` crosses ``threshold`` in the ``op``
4
+ direction. Non-recurring alerts auto-deactivate on fire; recurring
5
+ ones stay active (but only fire again after the price leaves the
6
+ triggered region, so we don't spam).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sqlite3
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timezone
14
+ from typing import Literal
15
+
16
+ Op = Literal[">", "<"]
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Alert:
21
+ id: int
22
+ asset_class: str
23
+ symbol: str
24
+ op: Op
25
+ threshold: float
26
+ recurring: bool
27
+ active: bool
28
+ created_at: str
29
+ last_fired_at: str | None
30
+
31
+
32
+ def _now_utc_iso() -> str:
33
+ return datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
34
+
35
+
36
+ def create(
37
+ conn: sqlite3.Connection,
38
+ *,
39
+ asset_class: str,
40
+ symbol: str,
41
+ op: Op,
42
+ threshold: float,
43
+ recurring: bool = False,
44
+ ) -> Alert:
45
+ cur = conn.execute(
46
+ """
47
+ INSERT INTO alerts
48
+ (asset_class, symbol, op, threshold, recurring, active, created_at)
49
+ VALUES (?, ?, ?, ?, ?, 1, ?)
50
+ """,
51
+ (asset_class, symbol, op, threshold, int(recurring), _now_utc_iso()),
52
+ )
53
+ return get(conn, cur.lastrowid)
54
+
55
+
56
+ def get(conn: sqlite3.Connection, alert_id: int) -> Alert:
57
+ row = conn.execute(
58
+ "SELECT * FROM alerts WHERE id = ?", (alert_id,)
59
+ ).fetchone()
60
+ if row is None:
61
+ raise KeyError(alert_id)
62
+ return _row_to_alert(row)
63
+
64
+
65
+ def list_active(conn: sqlite3.Connection) -> list[Alert]:
66
+ rows = conn.execute(
67
+ "SELECT * FROM alerts WHERE active = 1 ORDER BY id"
68
+ ).fetchall()
69
+ return [_row_to_alert(r) for r in rows]
70
+
71
+
72
+ def list_all(conn: sqlite3.Connection) -> list[Alert]:
73
+ rows = conn.execute("SELECT * FROM alerts ORDER BY active DESC, id").fetchall()
74
+ return [_row_to_alert(r) for r in rows]
75
+
76
+
77
+ def delete(conn: sqlite3.Connection, alert_id: int) -> bool:
78
+ cur = conn.execute("DELETE FROM alerts WHERE id = ?", (alert_id,))
79
+ return cur.rowcount > 0
80
+
81
+
82
+ def deactivate(conn: sqlite3.Connection, alert_id: int) -> None:
83
+ conn.execute("UPDATE alerts SET active = 0 WHERE id = ?", (alert_id,))
84
+
85
+
86
+ def record_fire(
87
+ conn: sqlite3.Connection, alert_id: int, price: float
88
+ ) -> None:
89
+ ts = _now_utc_iso()
90
+ conn.execute(
91
+ "INSERT INTO fires (alert_id, fired_at, price) VALUES (?, ?, ?)",
92
+ (alert_id, ts, price),
93
+ )
94
+ conn.execute(
95
+ "UPDATE alerts SET last_fired_at = ? WHERE id = ?",
96
+ (ts, alert_id),
97
+ )
98
+
99
+
100
+ def count_active(conn: sqlite3.Connection) -> int:
101
+ return conn.execute(
102
+ "SELECT COUNT(*) FROM alerts WHERE active = 1"
103
+ ).fetchone()[0]
104
+
105
+
106
+ def _row_to_alert(row: sqlite3.Row) -> Alert:
107
+ return Alert(
108
+ id=row["id"],
109
+ asset_class=row["asset_class"],
110
+ symbol=row["symbol"],
111
+ op=row["op"],
112
+ threshold=row["threshold"],
113
+ recurring=bool(row["recurring"]),
114
+ active=bool(row["active"]),
115
+ created_at=row["created_at"],
116
+ last_fired_at=row["last_fired_at"],
117
+ )