Compare commits
9 Commits
68e528c1f6
...
ade9b708a2
| Author | SHA1 | Date | |
|---|---|---|---|
| ade9b708a2 | |||
| 76f58386dc | |||
| a5660bf479 | |||
| a620025365 | |||
| 5d13280f7d | |||
| f6d95de49f | |||
| 8b88aee61f | |||
| 63bab43557 | |||
| 2a8ee9c8c5 |
90
WIKI/dashboard_configuration.md
Normal file
90
WIKI/dashboard_configuration.md
Normal 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
263
WIKI/indicators.md
Normal 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
221
WIKI/symbol_management.md
Normal 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.
|
||||||
Binary file not shown.
Binary file not shown.
@ -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
32
_data/indicators.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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
347
dashboard.py
Normal 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
|
||||||
@ -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
101
fetch_history.py
Normal 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
445
indicators.py
Normal 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
86
indicators_fetcher.py
Normal 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.")
|
||||||
301
main_app.py
301
main_app.py
@ -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,189 +424,77 @@ 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."""
|
||||||
|
with Live(self.display_dashboard(), refresh_per_second=2, console=self.renderer.console) as live:
|
||||||
while True:
|
while True:
|
||||||
self.read_prices()
|
self.read_prices()
|
||||||
# --- REMOVED: self.read_market_caps() ---
|
|
||||||
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__":
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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} |"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user