Add indicators fetcher, rich dashboard renderer, and remove trade executor/status
This commit is contained in:
258
main_app.py
258
main_app.py
@ -8,7 +8,7 @@ import multiprocessing
|
||||
import schedule
|
||||
import sqlite3
|
||||
import pandas as pd
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
import importlib
|
||||
# --- REMOVED: import signal ---
|
||||
# --- REMOVED: from queue import Empty ---
|
||||
@ -18,6 +18,9 @@ from logging_utils import setup_logging
|
||||
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
|
||||
# --- Rich dashboard renderer ---
|
||||
from dashboard import DashboardRenderer
|
||||
from rich.live import Live
|
||||
|
||||
# --- Configuration ---
|
||||
WATCHED_COINS = ["BTC", "ETH", "SOL", "BNB", "HYPE", "SUI", "xyz:BRENTOIL", "xyz:CL"]
|
||||
@ -31,24 +34,11 @@ RESAMPLER_SCRIPT = "resampler.py"
|
||||
# --- REMOVED: Market Cap Fetcher ---
|
||||
# --- REMOVED: trade_executor.py is no longer a script ---
|
||||
DASHBOARD_DATA_FETCHER_SCRIPT = "dashboard_data_fetcher.py"
|
||||
INDICATORS_FETCHER_SCRIPT = "indicators_fetcher.py"
|
||||
STRATEGY_CONFIG_FILE = os.path.join("_data", "strategies.json")
|
||||
DB_PATH = os.path.join("_data", "market_data.db")
|
||||
# --- REMOVED: Market Cap File ---
|
||||
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():
|
||||
@ -348,17 +338,53 @@ def run_dashboard_data_fetcher():
|
||||
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:
|
||||
def __init__(self, coins_to_watch: list, processes: dict, strategy_configs: dict, shared_prices: dict):
|
||||
self.watched_coins = coins_to_watch
|
||||
self.shared_prices = shared_prices
|
||||
self.prices = {}
|
||||
# --- REMOVED: self.market_caps ---
|
||||
self.open_positions = {}
|
||||
self.background_processes = processes
|
||||
self.process_status = {}
|
||||
self.strategy_configs = strategy_configs
|
||||
self.strategy_statuses = {}
|
||||
self.indicators_status = {}
|
||||
self.renderer = DashboardRenderer(table_visibility={
|
||||
"market": True,
|
||||
"strategies": False,
|
||||
"indicators": True,
|
||||
})
|
||||
|
||||
def read_prices(self):
|
||||
"""Reads the latest prices directly from the shared memory dictionary."""
|
||||
@ -386,190 +412,47 @@ class MainApp:
|
||||
enabled_statuses[name] = {"current_signal": "Initializing..."}
|
||||
self.strategy_statuses = enabled_statuses
|
||||
|
||||
def read_executor_status(self):
|
||||
"""Reads the live status file from the trade executor."""
|
||||
if os.path.exists(TRADE_EXECUTOR_STATUS_FILE):
|
||||
def read_indicators_status(self):
|
||||
"""Reads the indicators status JSON file."""
|
||||
status_file = os.path.join(LOGS_DIR, "indicators_status.json")
|
||||
if os.path.exists(status_file):
|
||||
try:
|
||||
with open(TRADE_EXECUTOR_STATUS_FILE, 'r', encoding='utf-8') as f:
|
||||
# --- FIX: Read the 'open_positions' key from the file ---
|
||||
status_data = json.load(f)
|
||||
self.open_positions = status_data.get('open_positions', {})
|
||||
with open(status_file, 'r', encoding='utf-8') as f:
|
||||
self.indicators_status = json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
logging.debug("Could not read trade executor status file.")
|
||||
self.indicators_status = {}
|
||||
else:
|
||||
self.open_positions = {}
|
||||
self.indicators_status = {}
|
||||
|
||||
def check_process_status(self):
|
||||
"""Checks if the background processes are still running."""
|
||||
for name, process in self.background_processes.items():
|
||||
self.process_status[name] = "Running" if process.is_alive() else "STOPPED"
|
||||
|
||||
def _format_price(self, price_val, width=10):
|
||||
"""Helper function to format prices for the dashboard."""
|
||||
try:
|
||||
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 toggle_table(self, table_name, enabled=None):
|
||||
"""Toggle a dashboard table's visibility at runtime."""
|
||||
return self.renderer.toggle_table(table_name, enabled)
|
||||
|
||||
def display_dashboard(self):
|
||||
"""Displays a formatted dashboard with side-by-side tables."""
|
||||
print("\x1b[H\x1b[J", end="") # Clear screen
|
||||
|
||||
left_table_lines = ["--- Market Dashboard ---"]
|
||||
# --- MODIFIED: Adjusted width for new columns ---
|
||||
left_table_width = 65
|
||||
left_table_lines.append("-" * left_table_width)
|
||||
# --- MODIFIED: Replaced Market Cap with Gap ---
|
||||
left_table_lines.append(f"{'#':<2} | {'Coin':^6} | {'Best Bid':>10} | {'Live Price':>10} | {'Best Ask':>10} | {'Gap':>10} |")
|
||||
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()
|
||||
"""Build and return the rich dashboard layout."""
|
||||
return self.renderer.build_layout(
|
||||
self.watched_coins,
|
||||
self.prices,
|
||||
COIN_DISPLAY_NAMES,
|
||||
self.strategy_statuses,
|
||||
self.strategy_configs,
|
||||
self.indicators_status
|
||||
)
|
||||
|
||||
def run(self):
|
||||
"""Main loop to read data, display dashboard, and check processes."""
|
||||
while True:
|
||||
self.read_prices()
|
||||
# --- REMOVED: self.read_market_caps() ---
|
||||
self.read_strategy_statuses()
|
||||
self.read_executor_status()
|
||||
# --- REMOVED: self.check_process_status() ---
|
||||
self.display_dashboard()
|
||||
time.sleep(0.5)
|
||||
with Live(self.display_dashboard(), refresh_per_second=2, console=self.renderer.console) as live:
|
||||
while True:
|
||||
self.read_prices()
|
||||
self.read_strategy_statuses()
|
||||
self.read_indicators_status()
|
||||
live.update(self.display_dashboard())
|
||||
time.sleep(0.5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup_logging('normal', 'MainApp')
|
||||
@ -613,6 +496,7 @@ if __name__ == "__main__":
|
||||
processes["Resampler"] = multiprocessing.Process(target=resampler_scheduler, args=(list(required_timeframes),), daemon=True)
|
||||
# --- REMOVED: Market Cap Fetcher Process ---
|
||||
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(
|
||||
target=run_position_manager,
|
||||
|
||||
Reference in New Issue
Block a user