Compare commits

...

9 Commits

Author SHA1 Message Date
ade9b708a2 Add account data fetching and display balances in dashboard 2026-07-30 21:15:48 +02:00
76f58386dc Fix spot balance Value column to show USD value instead of raw token amount 2026-07-30 11:53:28 +02:00
a5660bf479 Change XYZ100/USTECH fallback reference to 41.10 2026-07-29 20:28:17 +02:00
a620025365 Add XYZ100/USTECH ratio indicator with 41.18 fallback reference 2026-07-29 18:16:22 +02:00
5d13280f7d Add mkts:USTECH and xyz:XYZ100 to dashboard market table 2026-07-29 18:07:17 +02:00
f6d95de49f Add GOLD/SILVER ratio indicator to dashboard
Add gold_silver_ratio indicator (xyz:GOLD / xyz:SILVER) with fallback
reference of 61.59, mirroring the existing WTI/BRENT ratio setup.
Also register xyz:GOLD and xyz:SILVER in WATCHED_COINS and data_fetcher
defaults so the candle data is fetched for the new indicator.
2026-07-29 17:02:15 +02:00
8b88aee61f Add fallback_reference for deviation when insufficient data points
Add optional min_data_points (default 100) and fallback_reference config
fields to indicator definitions. When available daily data points are
below min_data_points, the fallback_reference value is used as the
deviation reference instead of the computed mean.

Applied to ratio, price, spread, and diff_pct indicator types.
Configured WTI/BRENT ratio with fallback_reference=0.96065.
2026-07-29 09:36:35 +02:00
63bab43557 Add indicators fetcher, rich dashboard renderer, and remove trade executor/status 2026-07-29 09:11:13 +02:00
2a8ee9c8c5 Remove ASTER, PUMP, ZEC from dashboard and stop tracking their history
- Remove ASTER, PUMP, ZEC from WATCHED_COINS in main_app.py
- Remove ASTER, PUMP, ZEC from coin_id_map.json (stops market cap collection)
- Remove ASTER, PUMP, ZEC from market_cap_data.json summary
- Remove ASTER, PUMP, ZEC manual overrides from coin_id_map.py
- Remove ASTER, PUMP, ZEC from resampling_status.json (gitignored)
- Add WIKI/symbol_management.md documentation for adding/removing symbols

Existing data in market_data.db is preserved; only new data collection stops.
2026-07-28 09:43:39 +02:00
17 changed files with 1724 additions and 216 deletions

View File

@ -0,0 +1,90 @@
# Dashboard Configuration Guide
This guide explains how to configure which tables are displayed on the live terminal dashboard.
## Overview
The dashboard is rendered by the `DashboardRenderer` class in `dashboard.py`. It currently supports two tables:
| Table Key | Title | Description |
|-----------|-------|-------------|
| `market` | Market Dashboard | Live prices, best bid/ask, gap, and direction for watched coins |
| `strategies` | Strategies | Signal, signal price, last change, timeframe, and size for each enabled strategy |
Each table can be independently enabled or disabled. When only one table is visible, it takes the full terminal width. When both are visible, they split side-by-side.
## Configuration
### Default Visibility
The default table visibility is set when `DashboardRenderer` is instantiated in `main_app.py` (`MainApp.__init__`):
```python
self.renderer = DashboardRenderer(table_visibility={
"market": True,
"strategies": False,
})
```
By default, the **market table is enabled** and the **strategies table is disabled**.
### Changing Default Visibility
To change which tables are shown by default, edit the `table_visibility` dict in `main_app.py` (`MainApp.__init__`, line 349):
```python
self.renderer = DashboardRenderer(table_visibility={
"market": True,
"strategies": True, # enable strategies table
})
```
### Runtime Toggling
Tables can be toggled at runtime through the `MainApp.toggle_table()` method, which delegates to `DashboardRenderer.toggle_table()`:
```python
# Flip the strategies table on/off
app.toggle_table("strategies")
# Explicitly enable
app.toggle_table("strategies", enabled=True)
# Explicitly disable
app.toggle_table("strategies", enabled=False)
```
The same methods are available directly on the renderer:
```python
renderer = DashboardRenderer()
renderer.toggle_table("market") # flip
renderer.toggle_table("strategies", False) # disable
```
## How It Works
### DashboardRenderer (`dashboard.py`)
- `__init__(console=None, table_visibility=None)` — accepts an optional `table_visibility` dict. If not provided, defaults to `{"market": True, "strategies": False}`.
- `toggle_table(table_name, enabled=None)` — flips the visibility state when `enabled` is `None`, or sets it to the given boolean. Raises `ValueError` for unknown table names.
- `build_layout(...)` — conditionally builds only the tables that are enabled, then arranges them:
- **One table:** `Layout(table)` — full width
- **Two tables:** `Layout.split_row(Layout(t1), Layout(t2))` — side-by-side
- **Zero tables:** empty `Layout`
### MainApp (`main_app.py`)
- `MainApp.__init__` creates the `DashboardRenderer` with the `table_visibility` config.
- `MainApp.toggle_table(table_name, enabled=None)` delegates to the renderer for runtime toggling.
- `MainApp.display_dashboard()` calls `renderer.build_layout()` which respects the current visibility settings.
## File Reference
| File | Line | Description |
|------|------|-------------|
| `dashboard.py` | 22 | `DashboardRenderer.__init__` — accepts `table_visibility` parameter |
| `dashboard.py` | 32 | `toggle_table()` method — flips or sets table visibility |
| `dashboard.py` | 172 | `build_layout()` — conditionally includes tables based on visibility |
| `main_app.py` | 349 | `MainApp.__init__` — sets default `table_visibility` |
| `main_app.py` | 385 | `MainApp.toggle_table()` — runtime toggle method |

263
WIKI/indicators.md Normal file
View File

@ -0,0 +1,263 @@
# Indicators Guide
This guide explains how to configure and use the Indicators table on the live terminal dashboard.
## Overview
The Indicators table displays computed financial indicators (e.g., WTI/BRENT ratio, live prices, moving averages, RSI) with their current value, 1-hour and 1-day percentage changes, and deviation from a long-term average.
The system is **config-driven** — new indicators are added by editing `_data/indicators.json`. No code changes are required for standard indicator types.
## Dashboard Table
The Indicators table is displayed below the Market table in the dashboard. It shows:
| Column | Description |
|--------|-------------|
| `#` | Indicator number |
| `Indicator` | Display name from config |
| `Value` | Current indicator value |
| `1h Change` | Percentage change over the last 1 hour |
| `1D Change` | Percentage change over the last 1 day |
| `Deviation` | Deviation from the long-term average |
Changes are color-coded: **green** for positive, **red** for negative, **yellow** for neutral.
## Configuration
### Default Visibility
The Indicators table is enabled by default. The visibility is set in `main_app.py` (`MainApp.__init__`):
```python
self.renderer = DashboardRenderer(table_visibility={
"market": True,
"strategies": False,
"indicators": True,
})
```
### Runtime Toggling
Toggle the Indicators table at runtime:
```python
app.toggle_table("indicators") # flip on/off
app.toggle_table("indicators", enabled=True) # explicitly enable
app.toggle_table("indicators", enabled=False) # explicitly disable
```
## Indicator Types
The following indicator types are supported in `_data/indicators.json`:
### `ratio` — A/B Ratio
Computes `numerator / denominator`.
```json
"wti_brent_ratio": {
"display_name": "WTI/BRENT",
"type": "ratio",
"numerator": "xyz:CL",
"denominator": "xyz:BRENTOIL",
"changes": ["1h", "1d"],
"show_deviation": true,
"min_data_points": 100,
"fallback_reference": 0.96065
}
```
- **Value**: `live(numerator) / live(denominator)` from latest 1m candle closes
- **1h Change**: compares to ratio from 1h candle close prices
- **1D Change**: compares to ratio from 1d candle close prices
- **Deviation**: `(current - long_avg) / long_avg * 100`, where `long_avg` is the mean of daily ratios over all available history. If fewer than `min_data_points` (default 100) daily data points exist and `fallback_reference` is set, the fallback value is used instead.
```json
"gold_silver_ratio": {
"display_name": "GOLD/SILVER",
"type": "ratio",
"numerator": "xyz:GOLD",
"denominator": "xyz:SILVER",
"changes": ["1h", "1d"],
"show_deviation": true,
"min_data_points": 100,
"fallback_reference": 61.59
}
```
### `price` — Single Price
```json
"wti_price": {
"display_name": "WTI",
"type": "price",
"coin": "xyz:CL",
"changes": ["1h", "1d"],
"show_deviation": true
}
```
- **Value**: latest close price from `{coin}_1m` table
- **1h/1D Change**: compares to close from 1h/1d candle tables
- **Deviation**: `(current - long_avg) / long_avg * 100`, where `long_avg` is the mean of daily closes
### `spread` — Price Difference
Computes `numerator - denominator`.
```json
"wti_brent_spread": {
"display_name": "WTI-BRENT Spread",
"type": "spread",
"numerator": "xyz:CL",
"denominator": "xyz:BRENTOIL",
"changes": ["1h", "1d"],
"show_deviation": true
}
```
### `diff_pct` — Percentage Difference
Computes `(numerator - denominator) / denominator * 100`.
```json
"wti_brent_diff": {
"display_name": "WTI-BRENT Diff%",
"type": "diff_pct",
"numerator": "xyz:CL",
"denominator": "xyz:BRENTOIL",
"changes": ["1h", "1d"],
"show_deviation": true
}
```
### `ma` — Moving Average
```json
"wti_ma_20": {
"display_name": "WTI MA(20)",
"type": "ma",
"coin": "xyz:CL",
"timeframe": "1h",
"period": 20,
"changes": ["1h", "1d"],
"show_deviation": true
}
```
- **Value**: latest SMA value on the specified timeframe
- **1h Change**: compares to MA value from 1h candle table
- **1D Change**: compares to MA value from 1d candle table
- **Deviation**: `(current_price - MA) / MA * 100` (how far the live price is from the MA)
### `rsi` — Relative Strength Index
```json
"wti_rsi_14": {
"display_name": "WTI RSI(14)",
"type": "rsi",
"coin": "xyz:CL",
"timeframe": "1h",
"period": 14,
"changes": ["1h", "1d"],
"show_deviation": true
}
```
- **Value**: latest RSI value (0-100) on the specified timeframe
- **1h/1D Change**: absolute change in RSI points
- **Deviation**: `RSI - 50` (deviation from neutral)
### `custom` — Custom Function
Calls a user-defined Python function.
```json
"custom_indicator": {
"display_name": "My Custom Indicator",
"type": "custom",
"module": "indicators.custom_indicators",
"function": "my_custom_calc",
"args": {"param1": "value1"},
"changes": ["1h", "1d"],
"show_deviation": true
}
```
The custom function must accept `db_path` as the first argument, plus any `args` from the config, and return a dict:
```python
def my_custom_calc(db_path, **kwargs):
return {
"value": 0.96611,
"reference": 0.96044,
"changes": {"1h": 0.12, "1d": -0.45},
"deviation": 0.59
}
}
```
## Data Sources
All indicator calculations read from the SQLite database `_data/market_data.db`:
- **Live value**: latest close price from `{coin}_1m` candle table (updated in real-time by `live_candle_fetcher.py`)
- **1h change**: close price from `{coin}_1h` candle table (second-to-last completed 1h candle)
- **1D change**: close price from `{coin}_1d` candle table (second-to-last completed 1d candle)
- **Reference value**: mean of daily values over all available historical data. If fewer than `min_data_points` daily data points exist and `fallback_reference` is set, the fallback value is used instead.
## Process Architecture
```
indicators_fetcher.py (subprocess, runs every 30s)
|
+---> indicators.py (IndicatorCalculator)
| |
| +---> _data/market_data.db (SQLite candle data)
| +---> _data/indicators.json (config)
|
+---> _logs/indicators_status.json (output)
|
+---> main_app.py (MainApp.read_indicators_status)
|
+---> dashboard.py (DashboardRenderer.build_indicators_table)
```
## File Reference
| File | Description |
|------|-------------|
| `_data/indicators.json` | Indicator definitions (config) |
| `indicators.py` | `IndicatorCalculator` class — computation logic |
| `indicators_fetcher.py` | Standalone script — runs in a loop, computes indicators, writes JSON |
| `dashboard.py` | `DashboardRenderer.build_indicators_table()` — renders the table |
| `main_app.py` | `run_indicators_fetcher()` — process target; `MainApp.read_indicators_status()` — reads JSON |
| `_logs/indicators_status.json` | Output file with computed indicator values |
## Adding a New Indicator
1. Edit `_data/indicators.json` and add a new entry:
```json
"my_new_indicator": {
"display_name": "My Indicator",
"type": "price",
"coin": "BTC",
"changes": ["1h", "1d"],
"show_deviation": true
}
```
2. Restart the application (`python main_app.py`). The Indicators Fetcher will automatically pick up the new config on its next run.
No code changes are needed for standard indicator types (`ratio`, `price`, `spread`, `diff_pct`, `ma`, `rsi`). For custom calculations, use the `custom` type.
### Optional Deviation Config Fields
The following optional fields control the deviation reference value:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `min_data_points` | int | 100 | Minimum number of historical daily data points required before using the computed mean as the reference |
| `fallback_reference` | float | null | If set and available data points are below `min_data_points`, this value is used as the reference instead of the computed mean |

221
WIKI/symbol_management.md Normal file
View File

@ -0,0 +1,221 @@
# Symbol Management Guide
This guide explains how to add or remove Hyperliquid trading symbols (coins) from the trading bot's dashboard, data pipeline, and market cap tracking.
## Overview
The system tracks coins through multiple interconnected components. Each component reads its coin list from a specific source:
| Component | Source | Purpose |
|-----------|--------|---------|
| Dashboard display | `WATCHED_COINS` in `main_app.py` | Shows live prices in terminal |
| Live candle fetcher | `--coins` CLI arg (from `WATCHED_COINS`) | Collects 1-minute candle data |
| Resampler | `--coins` CLI arg (from `WATCHED_COINS`) | Resamples 1m data to 15+ timeframes |
| Live price feed | `coins_to_watch` arg (from `WATCHED_COINS`) | WebSocket BBO/trade subscriptions |
| Market cap fetcher | `coin_id_map.json` | CoinGecko market cap data |
| Resampling status | `resampling_status.json` | Tracks progress per coin/timeframe |
| Market cap summary | `market_cap_data.json` | Aggregated market cap snapshots |
## Data Pipeline
```
Hyperliquid WebSocket
|
+---> Live Candle Fetcher (1m candles) --> SQLite: {coin}_1m
| |
| +---> Resampler --> SQLite: {coin}_{3m,5m,15m,...,1M}
|
+---> Live Price Feed (BBO/trades) --> shared_prices dict --> Dashboard
CoinGecko API
|
+---> Market Cap Fetcher --> SQLite: {coin}_market_cap
--> market_cap_data.json (summary)
```
All historical data is stored in `_data/market_data.db` (SQLite). Existing data is **preserved** when removing coins; only new data collection stops.
---
## Adding a Symbol
### Step 1: Add to the Watched Coins List
Edit `main_app.py` (line 23):
```python
WATCHED_COINS = ["BTC", "ETH", "SOL", "BNB", "HYPE", "NEW_COIN", "xyz:BRENTOIL", "xyz:CL"]
```
### Step 2: Add Display Name (Optional)
If the symbol contains special characters or you want a custom display name, add it to `COIN_DISPLAY_NAMES` in `main_app.py` (lines 25-28):
```python
COIN_DISPLAY_NAMES = {
"xyz:BRENTOIL": "BRENT",
"xyz:CL": "WTI",
"NEW_COIN": "NewCoin"
}
```
### Step 3: Add to Coin ID Map (for Market Cap)
Edit `_data/coin_id_map.json` and add an entry mapping the Hyperliquid symbol to the CoinGecko ID:
```json
"NEW_COIN": "new-coin-id-on-coingecko"
```
If the coin is already in the map (e.g., it was previously fetched), skip this step.
### Step 4: Add to Manual Overrides (Optional)
If the CoinGecko ID is ambiguous, add it to the `manual_overrides` dictionary in `coin_id_map.py` (lines 49-61):
```python
manual_overrides = {
"BTC": "bitcoin",
"ETH": "ethereum",
"NEW_COIN": "new-coin-id-on-coingecko",
...
}
```
### Step 5: Restart the Application
Stop all running processes, then start `main_app.py`:
```bash
python main_app.py
```
The system will automatically:
- Create new candle tables in `market_data.db`
- Begin collecting 1-minute candle data
- Begin resampling to all timeframes
- Begin collecting market cap data
- Display the coin on the dashboard
---
## Removing a Symbol
### Step 1: Stop All Running Processes
Before making changes, stop all Python processes related to the project:
```powershell
# Find running processes
Get-WmiObject Win32_Process | Where-Object { $_.ExecutablePath -like "*python*" -and $_.CommandLine -like "*hyper*" }
# Stop them (replace PIDs with actual values)
Stop-Process -Id <PID1>, <PID2>, ... -Force
```
### Step 2: Remove from Watched Coins List
Edit `main_app.py` (line 23) and remove the coin from `WATCHED_COINS`:
```python
WATCHED_COINS = ["BTC", "ETH", "SOL", "BNB", "HYPE", "SUI", "xyz:BRENTOIL", "xyz:CL"]
```
### Step 3: Remove from Resampling Status
Edit `_data/resampling_status.json` and delete the entire block for the coin, e.g.:
```json
"REMOVED_COIN": {
"12h": { ... },
"148m": { ... },
...
}
```
### Step 4: Remove from Coin ID Map
Edit `_data/coin_id_map.json` and delete the entry:
```json
"REMOVED_COIN": "coingecko-id"
```
### Step 5: Remove from Market Cap Summary
Edit `_data/market_cap_data.json` and delete the entry:
```json
"REMOVED_COIN_market_cap": { ... }
```
### Step 6: Remove from Manual Overrides (if present)
Edit `coin_id_map.py` and remove the entry from `manual_overrides`:
```python
manual_overrides = {
"BTC": "bitcoin",
"ETH": "ethereum",
...
}
```
### Step 7: Restart the Application
```bash
python main_app.py
```
**Note:** Existing data in `_data/market_data.db` (candle tables, market cap tables) is **not deleted**. The coin's data remains available for historical analysis; only new data collection stops.
---
## File Reference
### Core Configuration
| File | Line | Description |
|------|------|-------------|
| `main_app.py` | 23 | `WATCHED_COINS` list - master coin list for dashboard, candle fetcher, resampler, and live feed |
| `main_app.py` | 25-28 | `COIN_DISPLAY_NAMES` dict - maps internal symbols to display names |
| `main_app.py` | 591-594 | `required_timeframes` list - timeframes for resampling |
### Data Files
| File | Description |
|------|-------------|
| `_data/market_data.db` | SQLite database with all candle and market cap data. Tables: `{coin}_1m`, `{coin}_{timeframe}`, `{coin}_market_cap` |
| `_data/resampling_status.json` | Tracks `last_candle_utc` and `total_candles` per coin/timeframe |
| `_data/coin_id_map.json` | Maps Hyperliquid symbols to CoinGecko IDs for market cap fetching |
| `_data/market_cap_data.json` | Summary of latest market cap data per coin |
| `_data/coin_precision.json` | All Hyperliquid coins with trade precision (reference only) |
| `_data/strategies.json` | Trading strategy configurations (separate from watched coins) |
### Scripts
| File | Description |
|------|-------------|
| `main_app.py` | Main orchestrator - starts all processes, renders dashboard |
| `live_candle_fetcher.py` | Collects 1-minute candles via WebSocket + historical catch-up |
| `resampler.py` | Resamples 1m candles to multiple timeframes using pandas |
| `live_market_utils.py` | WebSocket feed for live BBO (best bid/offer) and trade data |
| `market_cap_fetcher.py` | Fetches daily market cap data from CoinGecko API |
| `coin_id_map.py` | Generates `coin_id_map.json` from Hyperliquid + CoinGecko APIs |
| `dashboard_data_fetcher.py` | Fetches account balances and positions for dashboard |
---
## Important Notes
1. **Always stop processes before editing config files.** Running processes will overwrite changes to `resampling_status.json` and `market_data.db`.
2. **Existing data is preserved.** Removing a coin from the lists stops new data collection but does not delete existing data from the SQLite database.
3. **The `coin_id_map.json` is auto-generated.** Running `python coin_id_map.py` regenerates it from the Hyperliquid API. Manual overrides in `coin_id_map.py` ensure correct CoinGecko mappings.
4. **Market cap fetcher is not auto-started.** The market cap fetcher process is currently disabled in `main_app.py` (line 614). It can be run manually: `python market_cap_fetcher.py`.
5. **Strategy coins are separate.** Trading strategies in `_data/strategies.json` define their own coins independently of `WATCHED_COINS`. A coin can be traded by a strategy even if it's not in the watched list.
6. **Special symbols.** Coins with the `xyz:` prefix (e.g., `xyz:BRENTOIL`, `xyz:CL`) are synthetic/derivative symbols on Hyperliquid. They follow the same management process as regular coins.

View File

@ -16,7 +16,6 @@
"AR": "arweave", "AR": "arweave",
"ARB": "osmosis-allarb", "ARB": "osmosis-allarb",
"ARK": "ark-3", "ARK": "ark-3",
"ASTER": "astar",
"ATOM": "lost-bitcoin-layer", "ATOM": "lost-bitcoin-layer",
"AVAX": "binance-peg-avalanche", "AVAX": "binance-peg-avalanche",
"AVNT": "avantis", "AVNT": "avantis",
@ -139,7 +138,6 @@
"POPCAT": "popcat", "POPCAT": "popcat",
"PROMPT": "wayfinder", "PROMPT": "wayfinder",
"PROVE": "succinct", "PROVE": "succinct",
"PUMP": "pump-fun",
"PURR": "purr-2", "PURR": "purr-2",
"PYTH": "pyth-network", "PYTH": "pyth-network",
"RDNT": "radiant-capital", "RDNT": "radiant-capital",
@ -198,7 +196,6 @@
"XRP": "ripple", "XRP": "ripple",
"YGG": "yield-guild-games", "YGG": "yield-guild-games",
"YZY": "yzy", "YZY": "yzy",
"ZEC": "zcash",
"ZEN": "zenith-3", "ZEN": "zenith-3",
"ZEREBRO": "zerebro", "ZEREBRO": "zerebro",
"ZETA": "zeta", "ZETA": "zeta",

32
_data/indicators.json Normal file
View File

@ -0,0 +1,32 @@
{
"wti_brent_ratio": {
"display_name": "WTI/BRENT",
"type": "ratio",
"numerator": "xyz:CL",
"denominator": "xyz:BRENTOIL",
"changes": ["1h", "1d"],
"show_deviation": true,
"min_data_points": 100,
"fallback_reference": 0.96065
},
"gold_silver_ratio": {
"display_name": "GOLD/SILVER",
"type": "ratio",
"numerator": "xyz:GOLD",
"denominator": "xyz:SILVER",
"changes": ["1h", "1d"],
"show_deviation": true,
"min_data_points": 100,
"fallback_reference": 61.59
},
"xyz100_ustech_ratio": {
"display_name": "XYZ100/USTECH",
"type": "ratio",
"numerator": "xyz:XYZ100",
"denominator": "mkts:USTECH",
"changes": ["1h", "1d"],
"show_deviation": true,
"min_data_points": 100,
"fallback_reference": 41.10
}
}

View File

@ -84,11 +84,6 @@
"timestamp_ms": 1762214400000, "timestamp_ms": 1762214400000,
"market_cap": 411547691.74511635 "market_cap": 411547691.74511635
}, },
"ASTER_market_cap": {
"datetime_utc": "2025-11-04 00:00:00",
"timestamp_ms": 1762214400000,
"market_cap": 122331099.54500043
},
"ATOM_market_cap": { "ATOM_market_cap": {
"datetime_utc": "2025-11-04 00:00:00", "datetime_utc": "2025-11-04 00:00:00",
"timestamp_ms": 1762214400000, "timestamp_ms": 1762214400000,
@ -699,11 +694,6 @@
"timestamp_ms": 1762214400000, "timestamp_ms": 1762214400000,
"market_cap": 116187315.47981949 "market_cap": 116187315.47981949
}, },
"PUMP_market_cap": {
"datetime_utc": "2025-11-04 00:00:00",
"timestamp_ms": 1762214400000,
"market_cap": 1369591728.1563232
},
"PURR_market_cap": { "PURR_market_cap": {
"datetime_utc": "2025-11-04 00:00:00", "datetime_utc": "2025-11-04 00:00:00",
"timestamp_ms": 1762214400000, "timestamp_ms": 1762214400000,
@ -994,11 +984,6 @@
"timestamp_ms": 1762214400000, "timestamp_ms": 1762214400000,
"market_cap": 49793986.29032182 "market_cap": 49793986.29032182
}, },
"ZEC_market_cap": {
"datetime_utc": "2025-11-04 00:00:00",
"timestamp_ms": 1762214400000,
"market_cap": 6917445577.244665
},
"ZEN_market_cap": { "ZEN_market_cap": {
"datetime_utc": "2025-11-04 00:00:00", "datetime_utc": "2025-11-04 00:00:00",
"timestamp_ms": 1762214400000, "timestamp_ms": 1762214400000,

View File

@ -52,9 +52,6 @@ def update_coin_mapping():
"SOL": "solana", "SOL": "solana",
"BNB": "binancecoin", "BNB": "binancecoin",
"HYPE": "hyperliquid", "HYPE": "hyperliquid",
"PUMP": "pump-fun",
"ASTER": "astar",
"ZEC": "zcash",
"SUI": "sui", "SUI": "sui",
"ACE": "endurance", "ACE": "endurance",
# Add other important ones you watch here # Add other important ones you watch here

347
dashboard.py Normal file
View File

@ -0,0 +1,347 @@
"""
Dashboard rendering module using rich.
Provides DashboardRenderer for building rich terminal tables and layouts.
"""
from datetime import datetime, timezone
try:
from rich.console import Console
from rich.table import Table
from rich.live import Live
from rich.layout import Layout
from rich.text import Text
from rich.padding import Padding
RICH_AVAILABLE = True
except ImportError:
RICH_AVAILABLE = False
class DashboardRenderer:
"""Encapsulates all rich-based dashboard rendering logic."""
def __init__(self, console=None, table_visibility=None):
if not RICH_AVAILABLE:
raise ImportError("rich is not available. Install with: pip install rich")
self.console = console or Console()
self.previous_prices = {}
self.table_visibility = table_visibility or {
"market": True,
"strategies": False,
"indicators": True,
"balances": True,
}
def toggle_table(self, table_name, enabled=None):
"""Toggle a table's visibility on the dashboard.
Args:
table_name: The key of the table to toggle (e.g. "market", "strategies").
enabled: If None, flips the current state. Otherwise sets to the given value.
Returns:
The new visibility state for the table.
"""
if table_name not in self.table_visibility:
raise ValueError(f"Unknown table: {table_name}")
if enabled is None:
self.table_visibility[table_name] = not self.table_visibility[table_name]
else:
self.table_visibility[table_name] = enabled
return self.table_visibility[table_name]
def _format_price(self, price_val, width=10):
"""Format a price value with appropriate precision."""
try:
price_float = float(price_val)
if price_float < 1:
return f"{price_float:>{width}.6f}"
elif price_float < 100:
return f"{price_float:>{width}.4f}"
else:
return f"{price_float:>{width}.2f}"
except (ValueError, TypeError):
return f"{'Loading...':>{width}}"
def build_market_table(self, watched_coins, prices, display_names):
"""Build the market dashboard table."""
table = Table(title="Market Dashboard", show_header=True, header_style="bold cyan", title_style="bold white")
table.add_column("#", justify="right", style="dim", width=3)
table.add_column("Coin", justify="center", width=8)
table.add_column("Best Bid", justify="right")
table.add_column("Live Price", justify="right")
table.add_column("Best Ask", justify="right")
table.add_column("Gap", justify="right")
table.add_column("Dir", justify="center", width=3)
for i, coin in enumerate(watched_coins, 1):
display_name = display_names.get(coin, coin)
mid = prices.get(coin)
bid = prices.get(f"{coin}_bid")
ask = prices.get(f"{coin}_ask")
formatted_mid = self._format_price(mid)
formatted_bid = self._format_price(bid)
formatted_ask = self._format_price(ask)
gap_str = "Loading..."
gap_style = "dim"
try:
gap_val = float(ask) - float(bid)
if gap_val < 1:
gap_str = f"{gap_val:.6f}"
else:
gap_str = f"{gap_val:.4f}"
gap_style = "green" if gap_val > 0 else "red"
except (ValueError, TypeError):
pass
direction = " "
direction_style = "dim"
prev_mid = self.previous_prices.get(coin)
if prev_mid is not None and mid is not None:
try:
if float(mid) > float(prev_mid):
direction = ""
direction_style = "green"
elif float(mid) < float(prev_mid):
direction = ""
direction_style = "red"
except (ValueError, TypeError):
pass
table.add_row(
str(i), display_name, formatted_bid, formatted_mid, formatted_ask,
Text(gap_str, style=gap_style),
Text(direction, style=direction_style)
)
if coin == "SUI":
table.add_section()
if mid is not None:
self.previous_prices[coin] = mid
return table
def build_strategy_table(self, strategy_statuses, strategy_configs):
"""Build the strategies table."""
table = Table(title="Strategies", show_header=True, header_style="bold cyan", title_style="bold white")
table.add_column("#", justify="center", width=3)
table.add_column("Strategy Name", width=25)
table.add_column("Coin", justify="center", width=8)
table.add_column("Signal", justify="center", width=10)
table.add_column("Signal Price", justify="right", width=14)
table.add_column("Last Change", justify="right", width=19)
table.add_column("TF", justify="center", width=7)
table.add_column("Size", justify="center", width=10)
for i, (name, status) in enumerate(strategy_statuses.items(), 1):
signal = status.get('current_signal', 'N/A')
price = status.get('signal_price')
price_display = f"{price:.4f}" if isinstance(price, (int, float)) else "-"
last_change = status.get('last_signal_change_utc')
last_change_display = 'Never'
if last_change:
dt_utc = datetime.fromisoformat(last_change.replace('Z', '+00:00')).replace(tzinfo=timezone.utc)
dt_local = dt_utc.astimezone(None)
last_change_display = dt_local.strftime('%Y-%m-%d %H:%M')
config_params = strategy_configs.get(name, {}).get('parameters', {})
coin = status.get('coin', config_params.get('coin', 'N/A'))
size = status.get('size')
if not size:
if 'coins_to_copy' in config_params:
size = 'Multi'
else:
size = config_params.get('size', 'N/A')
timeframe = config_params.get('timeframe', 'N/A')
signal_style = ""
if signal == "BUY":
signal_style = "green"
elif signal == "SELL":
signal_style = "red"
elif signal == "NEUTRAL":
signal_style = "yellow"
table.add_row(
str(i), name, coin,
Text(signal, style=signal_style) if signal_style else signal,
price_display, last_change_display, timeframe, str(size)
)
return table
def _format_change_value(self, value):
"""Format a percentage change value with color styling."""
if value is None:
return Text("N/A", style="dim")
if value > 0:
return Text(f"+{value:.2f}%", style="green")
elif value < 0:
return Text(f"{value:.2f}%", style="red")
else:
return Text(f"{value:.2f}%", style="yellow")
def _format_value(self, value, width=12):
"""Format a numeric value for display."""
if value is None:
return Text("N/A", style="dim")
try:
val = float(value)
if abs(val) < 1:
return Text(f"{val:>{width}.6f}")
elif abs(val) < 100:
return Text(f"{val:>{width}.4f}")
else:
return Text(f"{val:>{width}.2f}")
except (ValueError, TypeError):
return Text("N/A", style="dim")
def build_indicators_table(self, indicators_status):
"""Build the indicators dashboard table."""
table = Table(title="Indicators", show_header=True, header_style="bold cyan", title_style="bold white")
table.add_column("#", justify="right", style="dim", width=3)
table.add_column("Indicator", width=20)
table.add_column("Value", justify="right")
table.add_column("1h Change", justify="right", width=12)
table.add_column("1D Change", justify="right", width=12)
table.add_column("Deviation", justify="right", width=12)
if not indicators_status:
table.add_row("1", "Loading...", "N/A", "N/A", "N/A", "N/A")
return table
indicators = indicators_status.get("indicators", {})
for i, (name, data) in enumerate(indicators.items(), 1):
display_name = data.get("display_name", name)
value = data.get("value")
changes = data.get("changes", {})
deviation = data.get("deviation")
formatted_value = self._format_value(value)
change_1h = self._format_change_value(changes.get("1h"))
change_1d = self._format_change_value(changes.get("1d"))
if deviation is not None:
if deviation > 0:
deviation_str = Text(f"+{deviation:.2f}%", style="green")
elif deviation < 0:
deviation_str = Text(f"{deviation:.2f}%", style="red")
else:
deviation_str = Text(f"{deviation:.2f}%", style="yellow")
else:
deviation_str = Text("N/A", style="dim")
table.add_row(
str(i), display_name, formatted_value,
change_1h, change_1d, deviation_str
)
return table
def build_balances_table(self, account_data, prices=None):
"""Build a combined balances and open positions table.
Args:
account_data: dict with keys:
- spot_balances: list of {coin, total}
- positions: list of position dicts with position data
- account_value: float
- margin_used: float
- utilization: float
prices: dict mapping coin names to current mark prices
"""
if prices is None:
prices = {}
table = Table(show_header=True, header_style="bold cyan", title="Account Summary")
table.add_column("Type", justify="center", width=8)
table.add_column("Coin", justify="center", width=8)
table.add_column("Size", justify="right", width=12)
table.add_column("Value", justify="right", width=12)
spot_balances = account_data.get('spot_balances', [])
for bal in spot_balances:
total = float(bal.get('total', 0))
if total > 0:
coin = bal.get('coin', 'Unknown')
mark_price = float(prices.get(coin, 0))
usd_value = total * mark_price
table.add_row(
Text("Spot", style="blue"),
coin,
f"{total:,.4f}",
f"${usd_value:,.2f}"
)
positions = account_data.get('positions', [])
for pos in positions:
position = pos.get('position', {})
coin = position.get('coin', 'Unknown')
size = float(position.get('szi', 0))
if size != 0:
position_value = float(position.get('positionValue', 0))
side = "LONG" if size > 0 else "SHORT"
side_style = "green" if size > 0 else "red"
table.add_row(
Text(f"P({side})", style=side_style),
coin,
f"{size:,.4f}",
f"${position_value:,.2f}"
)
if not spot_balances and not positions:
table.add_row("None", "-", "-", "-")
account_value = account_data.get('account_value', 0)
margin_used = account_data.get('margin_used', 0)
utilization = account_data.get('utilization', 0)
# table.add_section()
table.add_row(
Text("Acct", style="bold"),
"-", "-",
f"${account_value:,.2f}"
)
table.add_row(
Text("Util", style="bold"),
"-", "-",
f"{utilization:.2f}%"
)
return table
def build_layout(self, watched_coins, prices, display_names, strategy_statuses, strategy_configs, indicators_status=None, account_data=None):
"""Build the complete dashboard layout in a 2x2 grid."""
from rich.layout import Layout as RichLayout
tables = []
if self.table_visibility.get("market", True):
tables.append(self.build_market_table(watched_coins, prices, display_names))
if self.table_visibility.get("indicators", True):
tables.append(Padding(self.build_indicators_table(indicators_status), (0, 0, 0, 2)))
if account_data is not None and self.table_visibility.get("balances", True):
tables.append(Padding(self.build_balances_table(account_data, prices), (0, 0, 0, 2)))
if self.table_visibility.get("strategies", True):
tables.append(self.build_strategy_table(strategy_statuses, strategy_configs))
if not tables:
return RichLayout()
if len(tables) <= 2:
layout = RichLayout()
layout.split_row(*tables)
return layout
top = RichLayout(ratio=1)
bottom = RichLayout(ratio=2)
top.split_row(*tables[:2])
bottom.split_row(*tables[2:])
layout = RichLayout()
layout.split_column(top, bottom)
return layout

View File

@ -175,7 +175,7 @@ if __name__ == "__main__":
parser.add_argument( parser.add_argument(
"--coins", "--coins",
nargs='+', nargs='+',
default=["BTC", "ETH", "xyz:BRENTOIL", "xyz:CL"], default=["BTC", "ETH", "xyz:BRENTOIL", "xyz:CL", "xyz:GOLD", "xyz:SILVER"],
help="List of coins to fetch (e.g., BTC ETH), or 'all' to fetch all coins." help="List of coins to fetch (e.g., BTC ETH), or 'all' to fetch all coins."
) )
parser.add_argument("--interval", default="1m", help="Candle interval (e.g., 1m, 5m, 1h).") parser.add_argument("--interval", default="1m", help="Candle interval (e.g., 1m, 5m, 1h).")

101
fetch_history.py Normal file
View File

@ -0,0 +1,101 @@
import requests
import json
import sqlite3
import time
from datetime import datetime, timezone
DB_PATH = "_data/market_data.db"
URL = "https://api.hyperliquid.xyz/info"
def fetch_historical_candles(coin, start_ms, end_ms, interval="1m"):
"""Fetch historical candles using the raw HTTP API."""
candles = []
current_start = start_ms
while current_start < end_ms:
payload = {
"type": "candleSnapshot",
"req": {
"coin": coin,
"interval": interval,
"startTime": current_start,
"endTime": end_ms
}
}
resp = requests.post(URL, json=payload)
batch = resp.json()
if not batch:
break
for candle in batch:
candle['coin'] = coin
candles.append(candle)
last_ts = batch[-1]['t']
if last_ts < current_start:
break
current_start = last_ts + 1
time.sleep(0.5)
return candles
def write_candles_to_db(coin, candles, interval="1m"):
"""Write candles to the database."""
table_name = coin + "_" + interval
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Ensure table exists
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS "{table_name}" (
datetime_utc TEXT,
timestamp_ms INTEGER PRIMARY KEY,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
number_of_trades INTEGER
)
''')
for candle in candles:
record = (
datetime.fromtimestamp(candle['t'] / 1000, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S'),
candle['t'],
candle.get('o'), candle.get('h'), candle.get('l'), candle.get('c'),
candle.get('v'), candle.get('n')
)
cursor.execute(f'''
INSERT OR REPLACE INTO "{table_name}" (datetime_utc, timestamp_ms, open, high, low, close, volume, number_of_trades)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', record)
conn.commit()
conn.close()
def get_last_timestamp(coin):
"""Get the most recent timestamp from the database."""
table_name = coin + "_1m"
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
try:
cursor.execute(f'SELECT MAX(timestamp_ms) FROM "{table_name}"')
result = cursor.fetchone()
return int(result[0]) if result and result[0] is not None else None
except:
return None
finally:
conn.close()
coins = ["mkts:USTECH", "xyz:XYZ100"]
now_ms = int(time.time() * 1000)
seven_days_ms = 7 * 24 * 60 * 60 * 1000
for coin in coins:
for tf in ["1m", "1d"]:
start_ts = now_ms - seven_days_ms
if start_ts >= now_ms:
print(f"{coin} ({tf}): Already up to date")
continue
print(f"{coin} ({tf}): Fetching historical candles from {datetime.fromtimestamp(start_ts/1000, tz=timezone.utc)} to {datetime.fromtimestamp(now_ms/1000, tz=timezone.utc)}...")
candles = fetch_historical_candles(coin, start_ts, now_ms, interval=tf)
print(f"{coin} ({tf}): Fetched {len(candles)} candles")
write_candles_to_db(coin, candles, interval=tf)
print(f"{coin} ({tf}): Written to database")
print("Done!")

445
indicators.py Normal file
View File

@ -0,0 +1,445 @@
"""
Indicator calculation module.
Provides IndicatorCalculator for computing various financial indicators
from SQLite candle data, including ratios, prices, moving averages, RSI,
and custom functions.
"""
import json
import os
import sqlite3
import importlib
import logging
import pandas as pd
import numpy as np
class IndicatorCalculator:
"""
Computes indicator values from SQLite candle data.
Supports ratio, price, spread, diff_pct, ma, rsi, and custom types.
"""
def __init__(self, config_path, db_path):
self.config_path = config_path
self.db_path = db_path
self.config = self._load_config()
def _load_config(self):
"""Load indicator definitions from JSON config file."""
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
logging.error(f"Failed to load indicators config from '{self.config_path}': {e}")
return {}
def _get_latest_close(self, coin, timeframe="1m"):
"""Get the latest close price from a candle table."""
table = f"{coin}_{timeframe}"
try:
with sqlite3.connect(self.db_path) as conn:
result = conn.execute(
f'SELECT close FROM "{table}" ORDER BY timestamp_ms DESC LIMIT 1'
).fetchone()
return float(result[0]) if result and result[0] is not None else None
except Exception as e:
logging.debug(f"Could not get latest close for {coin} ({timeframe}): {e}")
return None
def _get_close_n_candles_ago(self, coin, timeframe, n=1):
"""Get the close price from n candles ago (n=1 = most recent completed candle)."""
table = f"{coin}_{timeframe}"
try:
with sqlite3.connect(self.db_path) as conn:
result = conn.execute(
f'SELECT close FROM "{table}" ORDER BY timestamp_ms DESC LIMIT 1 OFFSET {n}'
).fetchone()
return float(result[0]) if result and result[0] is not None else None
except Exception as e:
logging.debug(f"Could not get close {n} candles ago for {coin} ({timeframe}): {e}")
return None
def _get_all_closes(self, coin, timeframe="1d"):
"""Get all close prices from a candle table, ordered by time."""
table = f"{coin}_{timeframe}"
try:
with sqlite3.connect(self.db_path) as conn:
result = conn.execute(
f'SELECT close FROM "{table}" ORDER BY timestamp_ms'
).fetchall()
return [float(r[0]) for r in result if r[0] is not None]
except Exception as e:
logging.debug(f"Could not get all closes for {coin} ({timeframe}): {e}")
return []
def _get_all_ratio(self, num_coin, den_coin, timeframe="1d"):
"""Get all ratio values (num/den) from candle tables, ordered by time."""
num_table = f"{num_coin}_{timeframe}"
den_table = f"{den_coin}_{timeframe}"
try:
with sqlite3.connect(self.db_path) as conn:
result = conn.execute(
f'SELECT n.close / d.close as ratio '
f'FROM "{num_table}" n '
f'JOIN "{den_table}" d ON n.timestamp_ms = d.timestamp_ms '
f'ORDER BY n.timestamp_ms'
).fetchall()
return [float(r[0]) for r in result if r[0] is not None]
except Exception as e:
logging.debug(f"Could not get ratio series for {num_coin}/{den_coin} ({timeframe}): {e}")
return []
def _get_all_spread(self, num_coin, den_coin, timeframe="1d"):
"""Get all spread values (num - den) from candle tables, ordered by time."""
num_table = f"{num_coin}_{timeframe}"
den_table = f"{den_coin}_{timeframe}"
try:
with sqlite3.connect(self.db_path) as conn:
result = conn.execute(
f'SELECT n.close - d.close as spread '
f'FROM "{num_table}" n '
f'JOIN "{den_table}" d ON n.timestamp_ms = d.timestamp_ms '
f'ORDER BY n.timestamp_ms'
).fetchall()
return [float(r[0]) for r in result if r[0] is not None]
except Exception as e:
logging.debug(f"Could not get spread series for {num_coin}/{den_coin} ({timeframe}): {e}")
return []
def _get_all_diff_pct(self, num_coin, den_coin, timeframe="1d"):
"""Get all percentage difference values ((num-den)/den*100) from candle tables."""
num_table = f"{num_coin}_{timeframe}"
den_table = f"{den_coin}_{timeframe}"
try:
with sqlite3.connect(self.db_path) as conn:
result = conn.execute(
f'SELECT (n.close - d.close) / d.close * 100 as diff_pct '
f'FROM "{num_table}" n '
f'JOIN "{den_table}" d ON n.timestamp_ms = d.timestamp_ms '
f'ORDER BY n.timestamp_ms'
).fetchall()
return [float(r[0]) for r in result if r[0] is not None]
except Exception as e:
logging.debug(f"Could not get diff_pct series for {num_coin}/{den_coin} ({timeframe}): {e}")
return []
def _compute_ma(self, closes, period):
"""Compute Simple Moving Average using pandas."""
if len(closes) < period:
return []
series = pd.Series(closes)
ma = series.rolling(window=period).mean()
return ma.dropna().tolist()
def _compute_rsi(self, closes, period):
"""Compute RSI using Wilder's smoothing method."""
if len(closes) < period + 1:
return []
series = pd.Series(closes)
delta = series.diff()
gain = delta.where(delta > 0, 0)
loss = (-delta).where(delta < 0, 0)
avg_gain = gain.rolling(window=period, min_periods=period).mean()
avg_loss = loss.rolling(window=period, min_periods=period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi = 100 - (100 / (1 + rs))
return rsi.dropna().tolist()
def _get_ma_value(self, coin, timeframe, period, n_candles_ago=0):
"""Get MA value from n candles ago (0 = latest, 1 = second-to-last)."""
closes = self._get_all_closes(coin, timeframe)
if not closes:
return None
ma_values = self._compute_ma(closes, period)
if not ma_values:
return None
if n_candles_ago < len(ma_values):
return ma_values[-(1 + n_candles_ago)]
return None
def _get_rsi_value(self, coin, timeframe, period, n_candles_ago=0):
"""Get RSI value from n candles ago (0 = latest, 1 = second-to-last)."""
closes = self._get_all_closes(coin, timeframe)
if not closes:
return None
rsi_values = self._compute_rsi(closes, period)
if not rsi_values:
return None
if n_candles_ago < len(rsi_values):
return rsi_values[-(1 + n_candles_ago)]
return None
def _format_change(self, current, past):
"""Compute percentage change between two values."""
if past is None or past == 0 or current is None:
return None
return (current - past) / past * 100
def calculate_indicator(self, ind_def):
"""
Calculate a single indicator based on its definition.
Returns a dict with value, changes, reference, and deviation.
"""
ind_type = ind_def.get("type", "price")
if ind_type == "ratio":
return self._calc_ratio(ind_def)
elif ind_type == "price":
return self._calc_price(ind_def)
elif ind_type == "spread":
return self._calc_spread(ind_def)
elif ind_type == "diff_pct":
return self._calc_diff_pct(ind_def)
elif ind_type == "ma":
return self._calc_ma(ind_def)
elif ind_type == "rsi":
return self._calc_rsi(ind_def)
elif ind_type == "custom":
return self._calc_custom(ind_def)
else:
logging.warning(f"Unknown indicator type: {ind_type}")
return None
def _calc_ratio(self, ind_def):
"""Calculate a ratio indicator (numerator / denominator)."""
num = ind_def["numerator"]
den = ind_def["denominator"]
num_now = self._get_latest_close(num)
den_now = self._get_latest_close(den)
if num_now is None or den_now is None or den_now == 0:
return None
current = num_now / den_now
changes = {}
for period in ind_def.get("changes", []):
num_past = self._get_close_n_candles_ago(num, period, n=1)
den_past = self._get_close_n_candles_ago(den, period, n=1)
if num_past is not None and den_past is not None and den_past != 0:
past = num_past / den_past
changes[period] = self._format_change(current, past)
else:
changes[period] = None
reference = None
deviation = None
if ind_def.get("show_deviation", False):
ratios = self._get_all_ratio(num, den, "1d")
if ratios:
min_points = ind_def.get("min_data_points", 100)
fallback_ref = ind_def.get("fallback_reference")
if len(ratios) < min_points and fallback_ref is not None:
reference = fallback_ref
else:
reference = sum(ratios) / len(ratios)
deviation = self._format_change(current, reference)
return {"value": current, "reference": reference, "changes": changes, "deviation": deviation}
def _calc_price(self, ind_def):
"""Calculate a single price indicator."""
coin = ind_def["coin"]
current = self._get_latest_close(coin)
if current is None:
return None
changes = {}
for period in ind_def.get("changes", []):
past = self._get_close_n_candles_ago(coin, period, n=1)
changes[period] = self._format_change(current, past)
reference = None
deviation = None
if ind_def.get("show_deviation", False):
closes = self._get_all_closes(coin, "1d")
if closes:
min_points = ind_def.get("min_data_points", 100)
fallback_ref = ind_def.get("fallback_reference")
if len(closes) < min_points and fallback_ref is not None:
reference = fallback_ref
else:
reference = sum(closes) / len(closes)
deviation = self._format_change(current, reference)
return {"value": current, "reference": reference, "changes": changes, "deviation": deviation}
def _calc_spread(self, ind_def):
"""Calculate a spread indicator (numerator - denominator)."""
num = ind_def["numerator"]
den = ind_def["denominator"]
num_now = self._get_latest_close(num)
den_now = self._get_latest_close(den)
if num_now is None or den_now is None:
return None
current = num_now - den_now
changes = {}
for period in ind_def.get("changes", []):
num_past = self._get_close_n_candles_ago(num, period, n=1)
den_past = self._get_close_n_candles_ago(den, period, n=1)
if num_past is not None and den_past is not None:
past = num_past - den_past
changes[period] = self._format_change(current, past)
else:
changes[period] = None
reference = None
deviation = None
if ind_def.get("show_deviation", False):
spreads = self._get_all_spread(num, den, "1d")
if spreads:
min_points = ind_def.get("min_data_points", 100)
fallback_ref = ind_def.get("fallback_reference")
if len(spreads) < min_points and fallback_ref is not None:
reference = fallback_ref
else:
reference = sum(spreads) / len(spreads)
deviation = self._format_change(current, reference)
return {"value": current, "reference": reference, "changes": changes, "deviation": deviation}
def _calc_diff_pct(self, ind_def):
"""Calculate a percentage difference indicator ((num-den)/den*100)."""
num = ind_def["numerator"]
den = ind_def["denominator"]
num_now = self._get_latest_close(num)
den_now = self._get_latest_close(den)
if num_now is None or den_now is None or den_now == 0:
return None
current = (num_now - den_now) / den_now * 100
changes = {}
for period in ind_def.get("changes", []):
num_past = self._get_close_n_candles_ago(num, period, n=1)
den_past = self._get_close_n_candles_ago(den, period, n=1)
if num_past is not None and den_past is not None and den_past != 0:
past = (num_past - den_past) / den_past * 100
changes[period] = self._format_change(current, past)
else:
changes[period] = None
reference = None
deviation = None
if ind_def.get("show_deviation", False):
diffs = self._get_all_diff_pct(num, den, "1d")
if diffs:
min_points = ind_def.get("min_data_points", 100)
fallback_ref = ind_def.get("fallback_reference")
if len(diffs) < min_points and fallback_ref is not None:
reference = fallback_ref
else:
reference = sum(diffs) / len(diffs)
deviation = self._format_change(current, reference)
return {"value": current, "reference": reference, "changes": changes, "deviation": deviation}
def _calc_ma(self, ind_def):
"""Calculate a moving average indicator."""
coin = ind_def["coin"]
timeframe = ind_def.get("timeframe", "1h")
period = ind_def.get("period", 20)
current = self._get_ma_value(coin, timeframe, period, n_candles_ago=0)
if current is None:
return None
changes = {}
for period_label in ind_def.get("changes", []):
if period_label == "1h":
past = self._get_ma_value(coin, "1h", period, n_candles_ago=1)
elif period_label == "1d":
past = self._get_ma_value(coin, "1d", period, n_candles_ago=1)
else:
past = self._get_ma_value(coin, period_label, period, n_candles_ago=1)
changes[period_label] = self._format_change(current, past)
reference = None
deviation = None
if ind_def.get("show_deviation", False):
live_price = self._get_latest_close(coin)
if live_price is not None and current != 0:
reference = current
deviation = (live_price - current) / current * 100
return {"value": current, "reference": reference, "changes": changes, "deviation": deviation}
def _calc_rsi(self, ind_def):
"""Calculate an RSI indicator."""
coin = ind_def["coin"]
timeframe = ind_def.get("timeframe", "1h")
period = ind_def.get("period", 14)
current = self._get_rsi_value(coin, timeframe, period, n_candles_ago=0)
if current is None:
return None
changes = {}
for period_label in ind_def.get("changes", []):
if period_label == "1h":
past = self._get_rsi_value(coin, "1h", period, n_candles_ago=1)
elif period_label == "1d":
past = self._get_rsi_value(coin, "1d", period, n_candles_ago=1)
else:
past = self._get_rsi_value(coin, period_label, period, n_candles_ago=1)
if past is not None:
changes[period_label] = current - past
else:
changes[period_label] = None
reference = 50.0
deviation = None
if ind_def.get("show_deviation", False):
deviation = current - 50.0
return {"value": current, "reference": reference, "changes": changes, "deviation": deviation}
def _calc_custom(self, ind_def):
"""Calculate a custom indicator by calling a user-defined function."""
module_path = ind_def.get("module")
function_name = ind_def.get("function")
args = ind_def.get("args", {})
if not module_path or not function_name:
logging.error(f"Custom indicator missing 'module' or 'function': {ind_def}")
return None
try:
module = importlib.import_module(module_path)
func = getattr(module, function_name)
except (ImportError, AttributeError) as e:
logging.error(f"Failed to load custom indicator {module_path}.{function_name}: {e}")
return None
try:
result = func(self.db_path, **args)
if not isinstance(result, dict):
logging.error(f"Custom indicator {function_name} must return a dict, got {type(result)}")
return None
return result
except Exception as e:
logging.error(f"Custom indicator {function_name} raised an error: {e}", exc_info=True)
return None
def calculate_all(self):
"""Calculate all indicators defined in the config file."""
results = {}
for name, ind_def in self.config.items():
result = self.calculate_indicator(ind_def)
if result:
results[name] = {
"display_name": ind_def.get("display_name", name),
**result
}
else:
results[name] = {
"display_name": ind_def.get("display_name", name),
"value": None,
"reference": None,
"changes": {},
"deviation": None
}
return results

86
indicators_fetcher.py Normal file
View File

@ -0,0 +1,86 @@
"""
Indicators Data Fetcher
A standalone process that runs in a loop to compute financial indicators
(ratios, prices, MAs, RSI, custom) from SQLite candle data and save
the results to a JSON status file for the main dashboard to display.
Follows the same pattern as dashboard_data_fetcher.py.
"""
import logging
import os
import sys
import json
import time
import argparse
from datetime import datetime, timezone
from logging_utils import setup_logging
from indicators import IndicatorCalculator
class IndicatorsFetcher:
"""
Periodically computes all configured indicators and saves them to a JSON file.
"""
def __init__(self, log_level: str):
setup_logging(log_level, 'IndicatorsFetcher')
project_root = os.path.dirname(os.path.abspath(__file__))
self.db_path = os.path.join(project_root, "_data", "market_data.db")
self.config_path = os.path.join(project_root, "_data", "indicators.json")
self.status_file_path = os.path.join(project_root, "_logs", "indicators_status.json")
self.calculator = IndicatorCalculator(
config_path=self.config_path,
db_path=self.db_path
)
logging.info(f"Indicators Fetcher initialized. DB: {self.db_path}, Config: {self.config_path}")
def fetch_and_save_indicators(self):
"""Compute all indicators and save to JSON status file."""
try:
results = self.calculator.calculate_all()
status = {
"last_updated_utc": datetime.now(timezone.utc).isoformat(),
"indicators": results
}
logs_dir = os.path.dirname(self.status_file_path)
os.makedirs(logs_dir, exist_ok=True)
temp_file_path = self.status_file_path + ".tmp"
with open(temp_file_path, 'w', encoding='utf-8') as f:
json.dump(status, f, indent=4, default=str)
os.replace(temp_file_path, self.status_file_path)
logging.debug(f"Successfully updated indicators status file with {len(results)} indicators.")
except Exception as e:
logging.error(f"Failed to fetch or save indicators: {e}", exc_info=True)
def run(self):
"""Main loop to periodically compute and save indicators."""
logging.info("Starting Indicators Fetcher loop (update interval: 30s)")
while True:
try:
self.fetch_and_save_indicators()
except Exception as e:
logging.error(f"Indicators Fetcher loop error: {e}", exc_info=True)
time.sleep(30)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the Indicators Data Fetcher.")
parser.add_argument("--log-level", default="normal", choices=['off', 'normal', 'debug'])
args = parser.parse_args()
fetcher = IndicatorsFetcher(log_level=args.log_level)
try:
fetcher.run()
except KeyboardInterrupt:
logging.info("Indicators Data Fetcher stopped.")

View File

@ -8,47 +8,42 @@ import multiprocessing
import schedule import schedule
import sqlite3 import sqlite3
import pandas as pd import pandas as pd
from datetime import datetime, timezone from datetime import datetime
import importlib import importlib
from dotenv import load_dotenv
load_dotenv()
# --- REMOVED: import signal --- # --- REMOVED: import signal ---
# --- REMOVED: from queue import Empty --- # --- REMOVED: from queue import Empty ---
from logging_utils import setup_logging from logging_utils import setup_logging
# --- Using the new high-performance WebSocket utility for live prices ---
from live_market_utils import start_live_feed from live_market_utils import start_live_feed
# --- Import the base class for type hinting (optional but good practice) ---
from strategies.base_strategy import BaseStrategy from strategies.base_strategy import BaseStrategy
from dashboard import DashboardRenderer
from rich.live import Live
from hyperliquid.info import Info
from hyperliquid.utils import constants
# --- Configuration --- # --- Configuration ---
WATCHED_COINS = ["BTC", "ETH", "SOL", "BNB", "HYPE", "ASTER", "ZEC", "PUMP", "SUI", "xyz:BRENTOIL", "xyz:CL"] WATCHED_COINS = ["BTC", "ETH", "SOL", "BNB", "HYPE", "SUI", "xyz:BRENTOIL", "xyz:CL", "xyz:GOLD", "xyz:SILVER", "mkts:USTECH", "xyz:XYZ100"]
# Display name mapping for dashboard (internal symbol -> display name) # Display name mapping for dashboard (internal symbol -> display name)
COIN_DISPLAY_NAMES = { COIN_DISPLAY_NAMES = {
"xyz:BRENTOIL": "BRENT", "xyz:BRENTOIL": "BRENT",
"xyz:CL": "WTI" "xyz:CL": "WTI",
"xyz:GOLD": "GOLD",
"xyz:SILVER": "SILVER",
"mkts:USTECH": "USTECH",
"xyz:XYZ100": "XYZ100"
} }
LIVE_CANDLE_FETCHER_SCRIPT = "live_candle_fetcher.py" LIVE_CANDLE_FETCHER_SCRIPT = "live_candle_fetcher.py"
RESAMPLER_SCRIPT = "resampler.py" RESAMPLER_SCRIPT = "resampler.py"
# --- REMOVED: Market Cap Fetcher --- # --- REMOVED: Market Cap Fetcher ---
# --- REMOVED: trade_executor.py is no longer a script --- # --- REMOVED: trade_executor.py is no longer a script ---
DASHBOARD_DATA_FETCHER_SCRIPT = "dashboard_data_fetcher.py" DASHBOARD_DATA_FETCHER_SCRIPT = "dashboard_data_fetcher.py"
INDICATORS_FETCHER_SCRIPT = "indicators_fetcher.py"
STRATEGY_CONFIG_FILE = os.path.join("_data", "strategies.json") STRATEGY_CONFIG_FILE = os.path.join("_data", "strategies.json")
DB_PATH = os.path.join("_data", "market_data.db") DB_PATH = os.path.join("_data", "market_data.db")
# --- REMOVED: Market Cap File --- # --- REMOVED: Market Cap File ---
LOGS_DIR = "_logs" LOGS_DIR = "_logs"
TRADE_EXECUTOR_STATUS_FILE = os.path.join(LOGS_DIR, "trade_executor_status.json")
def format_market_cap(mc_value):
"""Formats a large number into a human-readable market cap string."""
if not isinstance(mc_value, (int, float)) or mc_value == 0:
return "N/A"
if mc_value >= 1_000_000_000_000:
return f"${mc_value / 1_000_000_000_000:.2f}T"
if mc_value >= 1_000_000_000:
return f"${mc_value / 1_000_000_000:.2f}B"
if mc_value >= 1_000_000:
return f"${mc_value / 1_000_000:.2f}M"
return f"${mc_value:,.2f}"
def run_live_candle_fetcher(): def run_live_candle_fetcher():
@ -348,17 +343,60 @@ def run_dashboard_data_fetcher():
time.sleep(10) time.sleep(10)
def run_indicators_fetcher():
"""Target function to run the indicators_fetcher.py script."""
# --- GRACEFUL SHUTDOWN HANDLER ---
import signal
def handle_shutdown_signal(signum, frame):
try:
logging.info(f"Shutdown signal ({signum}) received. Initiating graceful exit...")
except NameError:
print(f"[IndicatorsFetcher] Shutdown signal ({signum}) received. Initiating graceful exit...")
raise KeyboardInterrupt
signal.signal(signal.SIGTERM, handle_shutdown_signal)
# --- END GRACEFUL SHUTDOWN HANDLER ---
log_file = os.path.join(LOGS_DIR, "indicators_fetcher.log")
while True:
try:
with open(log_file, 'a') as f:
f.write(f"\n--- Starting Indicators Fetcher at {datetime.now()} ---\n")
subprocess.run([sys.executable, INDICATORS_FETCHER_SCRIPT, "--log-level", "normal"], check=True, stdout=f, stderr=subprocess.STDOUT)
except KeyboardInterrupt:
logging.info("Indicators Fetcher stopping.")
break
except (subprocess.CalledProcessError, Exception) as e:
with open(log_file, 'a') as f:
f.write(f"\n--- PROCESS ERROR at {datetime.now()} ---\n")
f.write(f"Indicators Fetcher failed: {e}. Restarting...\n")
time.sleep(10)
class MainApp: class MainApp:
def __init__(self, coins_to_watch: list, processes: dict, strategy_configs: dict, shared_prices: dict): def __init__(self, coins_to_watch: list, processes: dict, strategy_configs: dict, shared_prices: dict):
self.watched_coins = coins_to_watch self.watched_coins = coins_to_watch
self.shared_prices = shared_prices self.shared_prices = shared_prices
self.prices = {} self.prices = {}
# --- REMOVED: self.market_caps ---
self.open_positions = {}
self.background_processes = processes self.background_processes = processes
self.process_status = {} self.process_status = {}
self.strategy_configs = strategy_configs self.strategy_configs = strategy_configs
self.strategy_statuses = {} self.strategy_statuses = {}
self.indicators_status = {}
self.account_data = None
self.wallet_address = os.environ.get("MAIN_WALLET_ADDRESS")
if self.wallet_address:
self.info_client = Info(constants.MAINNET_API_URL, skip_ws=True)
else:
self.info_client = None
self.renderer = DashboardRenderer(table_visibility={
"market": True,
"strategies": False,
"indicators": True,
"balances": True,
})
def read_prices(self): def read_prices(self):
"""Reads the latest prices directly from the shared memory dictionary.""" """Reads the latest prices directly from the shared memory dictionary."""
@ -386,190 +424,78 @@ class MainApp:
enabled_statuses[name] = {"current_signal": "Initializing..."} enabled_statuses[name] = {"current_signal": "Initializing..."}
self.strategy_statuses = enabled_statuses self.strategy_statuses = enabled_statuses
def read_executor_status(self): def read_indicators_status(self):
"""Reads the live status file from the trade executor.""" """Reads the indicators status JSON file."""
if os.path.exists(TRADE_EXECUTOR_STATUS_FILE): status_file = os.path.join(LOGS_DIR, "indicators_status.json")
if os.path.exists(status_file):
try: try:
with open(TRADE_EXECUTOR_STATUS_FILE, 'r', encoding='utf-8') as f: with open(status_file, 'r', encoding='utf-8') as f:
# --- FIX: Read the 'open_positions' key from the file --- self.indicators_status = json.load(f)
status_data = json.load(f)
self.open_positions = status_data.get('open_positions', {})
except (IOError, json.JSONDecodeError): except (IOError, json.JSONDecodeError):
logging.debug("Could not read trade executor status file.") self.indicators_status = {}
else: else:
self.open_positions = {} self.indicators_status = {}
def read_account_data(self):
"""Fetches account balances and positions from Hyperliquid API."""
if not self.wallet_address or not self.info_client:
self.account_data = None
return
try:
perp_state = self.info_client.user_state(self.wallet_address)
spot_state = self.info_client.spot_user_state(self.wallet_address)
margin_summary = perp_state.get('marginSummary', {})
account_value = float(margin_summary.get('accountValue', 0))
margin_used = float(margin_summary.get('totalMarginUsed', 0))
utilization = (margin_used / account_value) * 100 if account_value > 0 else 0
spot_balances = spot_state.get('balances', [])
positions = perp_state.get('assetPositions', [])
self.account_data = {
'account_value': account_value,
'margin_used': margin_used,
'utilization': utilization,
'spot_balances': spot_balances,
'positions': positions,
}
except Exception as e:
logging.error(f"Could not fetch account data: {e}")
self.account_data = None
def check_process_status(self): def check_process_status(self):
"""Checks if the background processes are still running.""" """Checks if the background processes are still running."""
for name, process in self.background_processes.items(): for name, process in self.background_processes.items():
self.process_status[name] = "Running" if process.is_alive() else "STOPPED" self.process_status[name] = "Running" if process.is_alive() else "STOPPED"
def _format_price(self, price_val, width=10): def toggle_table(self, table_name, enabled=None):
"""Helper function to format prices for the dashboard.""" """Toggle a dashboard table's visibility at runtime."""
try: return self.renderer.toggle_table(table_name, enabled)
price_float = float(price_val)
if price_float < 1:
price_str = f"{price_float:>{width}.6f}"
elif price_float < 100:
price_str = f"{price_float:>{width}.4f}"
else:
price_str = f"{price_float:>{width}.2f}"
except (ValueError, TypeError):
price_str = f"{'Loading...':>{width}}"
return price_str
def display_dashboard(self): def display_dashboard(self):
"""Displays a formatted dashboard with side-by-side tables.""" """Build and return the rich dashboard layout."""
print("\x1b[H\x1b[J", end="") # Clear screen return self.renderer.build_layout(
self.watched_coins,
left_table_lines = ["--- Market Dashboard ---"] self.prices,
# --- MODIFIED: Adjusted width for new columns --- COIN_DISPLAY_NAMES,
left_table_width = 65 self.strategy_statuses,
left_table_lines.append("-" * left_table_width) self.strategy_configs,
# --- MODIFIED: Replaced Market Cap with Gap --- self.indicators_status,
left_table_lines.append(f"{'#':<2} | {'Coin':^6} | {'Best Bid':>10} | {'Live Price':>10} | {'Best Ask':>10} | {'Gap':>10} |") self.account_data
left_table_lines.append("-" * left_table_width) )
for i, coin in enumerate(self.watched_coins, 1):
# Use display name for dashboard, but keep internal symbol for price lookup
display_name = COIN_DISPLAY_NAMES.get(coin, coin)
# --- MODIFIED: Fetch all three price types ---
mid_price = self.prices.get(coin, "Loading...")
bid_price = self.prices.get(f"{coin}_bid", "Loading...")
ask_price = self.prices.get(f"{coin}_ask", "Loading...")
# --- MODIFIED: Use the new formatting helper ---
formatted_mid = self._format_price(mid_price)
formatted_bid = self._format_price(bid_price)
formatted_ask = self._format_price(ask_price)
# --- MODIFIED: Calculate gap ---
gap_str = f"{'Loading...':>10}"
try:
# Calculate the spread
gap_val = float(ask_price) - float(bid_price)
# Format gap with high precision, similar to price
if gap_val < 1:
gap_str = f"{gap_val:>{10}.6f}"
else:
gap_str = f"{gap_val:>{10}.4f}"
except (ValueError, TypeError):
pass # Keep 'Loading...'
# --- REMOVED: Market Cap logic ---
# --- MODIFIED: Print all price columns including gap ---
left_table_lines.append(f"{i:<2} | {display_name:^6} | {formatted_bid} | {formatted_mid} | {formatted_ask} | {gap_str} |")
left_table_lines.append("-" * left_table_width)
right_table_lines = ["--- Strategy Status ---"]
# --- FIX: Adjusted table width after removing parameters ---
right_table_width = 105
right_table_lines.append("-" * right_table_width)
# --- FIX: Removed 'Parameters' from header ---
right_table_lines.append(f"{'#':^2} | {'Strategy Name':<25} | {'Coin':^6} | {'Signal':^8} | {'Signal Price':>12} | {'Last Change':>17} | {'TF':^5} | {'Size':^8} |")
right_table_lines.append("-" * right_table_width)
for i, (name, status) in enumerate(self.strategy_statuses.items(), 1):
signal = status.get('current_signal', 'N/A')
price = status.get('signal_price')
price_display = f"{price:.4f}" if isinstance(price, (int, float)) else "-"
last_change = status.get('last_signal_change_utc')
last_change_display = 'Never'
if last_change:
dt_utc = datetime.fromisoformat(last_change.replace('Z', '+00:00')).replace(tzinfo=timezone.utc)
dt_local = dt_utc.astimezone(None)
last_change_display = dt_local.strftime('%Y-%m-%d %H:%M')
config_params = self.strategy_configs.get(name, {}).get('parameters', {})
# --- FIX: Read coin/size from status file first, fallback to config ---
coin = status.get('coin', config_params.get('coin', 'N/A'))
# --- FIX: Handle nested 'coins_to_copy' logic for size ---
# --- MODIFIED: Read 'size' from status first, then config, then 'Multi' ---
size = status.get('size')
if not size:
if 'coins_to_copy' in config_params:
size = 'Multi'
else:
size = config_params.get('size', 'N/A')
timeframe = config_params.get('timeframe', 'N/A')
# --- FIX: Removed parameter string logic ---
# --- FIX: Removed 'params_str' from the formatted line ---
size_display = f"{size:>8}"
if isinstance(size, (int, float)):
# --- MODIFIED: More flexible size formatting ---
if size < 0.0001:
size_display = f"{size:>8.6f}"
elif size < 1:
size_display = f"{size:>8.4f}"
else:
size_display = f"{size:>8.2f}"
# --- END NEW LOGIC ---
right_table_lines.append(f"{i:^2} | {name:<25} | {coin:^6} | {signal:^8} | {price_display:>12} | {last_change_display:>17} | {timeframe:^5} | {size_display} |")
right_table_lines.append("-" * right_table_width)
output_lines = []
max_rows = max(len(left_table_lines), len(right_table_lines))
separator = " "
indent = " " * 10
for i in range(max_rows):
left_part = left_table_lines[i] if i < len(left_table_lines) else " " * left_table_width
right_part = indent + right_table_lines[i] if i < len(right_table_lines) else ""
output_lines.append(f"{left_part}{separator}{right_part}")
output_lines.append("\n--- Open Positions ---")
pos_table_width = 100
output_lines.append("-" * pos_table_width)
output_lines.append(f"{'Account':<10} | {'Coin':<6} | {'Size':>15} | {'Entry Price':>12} | {'Mark Price':>12} | {'PNL':>15} | {'Leverage':>10} |")
output_lines.append("-" * pos_table_width)
# --- FIX: Correctly read and display open positions ---
if not self.open_positions:
output_lines.append(f"{'No open positions.':^{pos_table_width}}")
else:
for account, positions in self.open_positions.items():
if not positions:
continue
for coin, pos in positions.items():
try:
size_f = float(pos.get('size', 0))
entry_f = float(pos.get('entry_price', 0))
mark_f = float(self.prices.get(coin, 0))
pnl_f = (mark_f - entry_f) * size_f if size_f > 0 else (entry_f - mark_f) * abs(size_f)
lev = pos.get('leverage', 1)
size_str = f"{size_f:>{15}.5f}"
entry_str = f"{entry_f:>{12}.2f}"
mark_str = f"{mark_f:>{12}.2f}"
pnl_str = f"{pnl_f:>{15}.2f}"
lev_str = f"{lev}x"
output_lines.append(f"{account:<10} | {coin:<6} | {size_str} | {entry_str} | {mark_str} | {pnl_str} | {lev_str:>10} |")
except (ValueError, TypeError):
output_lines.append(f"{account:<10} | {coin:<6} | {'Error parsing data...':^{pos_table_width-20}} |")
output_lines.append("-" * pos_table_width)
final_output = "\n".join(output_lines)
print(final_output)
sys.stdout.flush()
def run(self): def run(self):
"""Main loop to read data, display dashboard, and check processes.""" """Main loop to read data, display dashboard, and check processes."""
while True: with Live(self.display_dashboard(), refresh_per_second=2, console=self.renderer.console) as live:
self.read_prices() while True:
# --- REMOVED: self.read_market_caps() --- self.read_prices()
self.read_strategy_statuses() self.read_strategy_statuses()
self.read_executor_status() self.read_indicators_status()
# --- REMOVED: self.check_process_status() --- self.read_account_data()
self.display_dashboard() live.update(self.display_dashboard())
time.sleep(0.5) time.sleep(0.5)
if __name__ == "__main__": if __name__ == "__main__":
setup_logging('normal', 'MainApp') setup_logging('normal', 'MainApp')
@ -613,6 +539,7 @@ if __name__ == "__main__":
processes["Resampler"] = multiprocessing.Process(target=resampler_scheduler, args=(list(required_timeframes),), daemon=True) processes["Resampler"] = multiprocessing.Process(target=resampler_scheduler, args=(list(required_timeframes),), daemon=True)
# --- REMOVED: Market Cap Fetcher Process --- # --- REMOVED: Market Cap Fetcher Process ---
processes["Dashboard Data"] = multiprocessing.Process(target=run_dashboard_data_fetcher, daemon=True) processes["Dashboard Data"] = multiprocessing.Process(target=run_dashboard_data_fetcher, daemon=True)
processes["Indicators"] = multiprocessing.Process(target=run_indicators_fetcher, daemon=True)
processes["Position Manager"] = multiprocessing.Process( processes["Position Manager"] = multiprocessing.Process(
target=run_position_manager, target=run_position_manager,

View File

@ -77,14 +77,13 @@ class PositionMonitor:
output_lines.append("\n--- Perpetuals Account Summary ---") output_lines.append("\n--- Perpetuals Account Summary ---")
output_lines.append(f" Account Value: ${account_value:,.2f} | Margin Used: ${margin_used:,.2f} | Utilization: {utilization:.2f}%") output_lines.append(f" Account Value: ${account_value:,.2f} | Margin Used: ${margin_used:,.2f} | Utilization: {utilization:.2f}%")
# --- 2. Spot Balances Summary --- # --- 2. Spot Balances Table ---
output_lines.append("\n--- Spot Balances ---") output_lines.append("\n--- Spot Balances ---")
spot_balances = spot_state.get('balances', []) spot_balances = spot_state.get('balances', [])
if not spot_balances: if not spot_balances:
output_lines.append(" No spot balances found.") output_lines.append(" No spot balances found.")
else: else:
balances_str = ", ".join([f"{b.get('coin')}: {float(b.get('total', 0)):,.4f}" for b in spot_balances if float(b.get('total', 0)) > 0]) self.build_spot_balances_table(spot_balances, output_lines)
output_lines.append(f" {balances_str}")
# --- 3. Open Positions Table --- # --- 3. Open Positions Table ---
output_lines.append("\n--- Open Perpetual Positions ---") output_lines.append("\n--- Open Perpetual Positions ---")
@ -106,6 +105,23 @@ class PositionMonitor:
self._lines_printed = len(output_lines) self._lines_printed = len(output_lines)
sys.stdout.flush() sys.stdout.flush()
def build_spot_balances_table(self, spot_balances: list, output_lines: list):
"""Builds the text for the spot balances table."""
header = f"| {'Coin':<10} | {'Total':>18} |"
output_lines.append(header)
output_lines.append("-" * len(header))
for balance in spot_balances:
coin = balance.get('coin', 'Unknown')
total = float(balance.get('total', 0))
coin_str = f"{coin:<10}"
total_str = f"{total:>18,.4f}"
output_lines.append(f"| {coin_str} | {total_str} |")
output_lines.append("-" * len(header))
def build_positions_table(self, positions: list, coin_to_strategy_map: dict, output_lines: list): def build_positions_table(self, positions: list, coin_to_strategy_map: dict, output_lines: list):
"""Builds the text for the positions summary table.""" """Builds the text for the positions summary table."""
header = f"| {'Strategy':<25} | {'Coin':<6} | {'Side':<5} | {'Size':>15} | {'Entry Price':>12} | {'Mark Price':>12} | {'PNL':>15} | {'Leverage':>10} |" header = f"| {'Strategy':<25} | {'Coin':<6} | {'Side':<5} | {'Size':>15} | {'Entry Price':>12} | {'Mark Price':>12} | {'PNL':>15} | {'Leverage':>10} |"

View File

@ -39,6 +39,7 @@ pydantic_core==2.41.5
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-dotenv==1.2.1 python-dotenv==1.2.1
pytz==2025.2 pytz==2025.2
rich==13.9.4
regex==2025.11.3 regex==2025.11.3
requests==2.32.5 requests==2.32.5
rlp==4.1.0 rlp==4.1.0