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.
- terminex-0.4.0/LICENSE +21 -0
- terminex-0.4.0/PKG-INFO +209 -0
- terminex-0.4.0/README.md +161 -0
- terminex-0.4.0/pyproject.toml +41 -0
- terminex-0.4.0/setup.cfg +4 -0
- terminex-0.4.0/terminex/__init__.py +1 -0
- terminex-0.4.0/terminex/__main__.py +10 -0
- terminex-0.4.0/terminex/alert_engine.py +69 -0
- terminex-0.4.0/terminex/alerts.py +117 -0
- terminex-0.4.0/terminex/alerts_ui.py +129 -0
- terminex-0.4.0/terminex/app.py +842 -0
- terminex-0.4.0/terminex/config.py +67 -0
- terminex-0.4.0/terminex/converter.py +197 -0
- terminex-0.4.0/terminex/currencies.py +35 -0
- terminex-0.4.0/terminex/display.py +192 -0
- terminex-0.4.0/terminex/help.py +89 -0
- terminex-0.4.0/terminex/keyboard.py +134 -0
- terminex-0.4.0/terminex/providers/__init__.py +1 -0
- terminex-0.4.0/terminex/providers/base.py +35 -0
- terminex-0.4.0/terminex/providers/commodities_stooq.py +133 -0
- terminex-0.4.0/terminex/providers/crypto_coincap.py +101 -0
- terminex-0.4.0/terminex/providers/crypto_coingecko.py +103 -0
- terminex-0.4.0/terminex/providers/fx_erapi.py +74 -0
- terminex-0.4.0/terminex/providers/watchlist_agg.py +81 -0
- terminex-0.4.0/terminex/quote.py +33 -0
- terminex-0.4.0/terminex/series.py +75 -0
- terminex-0.4.0/terminex/sparkline.py +50 -0
- terminex-0.4.0/terminex/statusbar.py +75 -0
- terminex-0.4.0/terminex/store.py +87 -0
- terminex-0.4.0/terminex/theme.py +48 -0
- terminex-0.4.0/terminex/watchlist.py +85 -0
- terminex-0.4.0/terminex.egg-info/PKG-INFO +209 -0
- terminex-0.4.0/terminex.egg-info/SOURCES.txt +39 -0
- terminex-0.4.0/terminex.egg-info/dependency_links.txt +1 -0
- terminex-0.4.0/terminex.egg-info/entry_points.txt +2 -0
- terminex-0.4.0/terminex.egg-info/requires.txt +2 -0
- terminex-0.4.0/terminex.egg-info/top_level.txt +1 -0
- terminex-0.4.0/tests/test_alerts.py +169 -0
- terminex-0.4.0/tests/test_converter.py +136 -0
- terminex-0.4.0/tests/test_filters_sorts.py +107 -0
- 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.
|
terminex-0.4.0/PKG-INFO
ADDED
|
@@ -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.
|
terminex-0.4.0/README.md
ADDED
|
@@ -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*"]
|
terminex-0.4.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""terminex — live FX rates in the terminal."""
|
|
@@ -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
|
+
)
|