Add daily model training scripts and terminal UI for live trading
- Introduced `train_daily.sh` for automating daily model retraining, including data download and model training steps. - Added `install_cron.sh` for setting up a cron job to run the daily training script. - Created `setup_schedule.sh` for configuring Systemd timers for daily training tasks. - Implemented a terminal UI using Rich for real-time monitoring of trading performance, including metrics display and log handling. - Updated `pyproject.toml` to include the `rich` dependency for UI functionality. - Enhanced `.gitignore` to exclude model and log files. - Added database support for trade persistence and metrics calculation. - Updated README with installation and usage instructions for the new features.
This commit is contained in:
@@ -11,14 +11,19 @@ Usage:
|
||||
|
||||
# Run with specific settings
|
||||
uv run python -m live_trading.main --max-position 500 --leverage 2
|
||||
|
||||
# Run without UI (headless mode)
|
||||
uv run python -m live_trading.main --no-ui
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import queue
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
@@ -28,22 +33,47 @@ from live_trading.okx_client import OKXClient
|
||||
from live_trading.data_feed import DataFeed
|
||||
from live_trading.position_manager import PositionManager
|
||||
from live_trading.live_regime_strategy import LiveRegimeStrategy
|
||||
from live_trading.db.database import init_db, TradingDatabase
|
||||
from live_trading.db.migrations import run_migrations
|
||||
from live_trading.ui.state import SharedState, PositionState
|
||||
from live_trading.ui.dashboard import TradingDashboard, setup_ui_logging
|
||||
|
||||
|
||||
def setup_logging(log_dir: Path) -> logging.Logger:
|
||||
"""Configure logging for the trading bot."""
|
||||
def setup_logging(
|
||||
log_dir: Path,
|
||||
log_queue: Optional[queue.Queue] = None,
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
Configure logging for the trading bot.
|
||||
|
||||
Args:
|
||||
log_dir: Directory for log files
|
||||
log_queue: Optional queue for UI log handler
|
||||
|
||||
Returns:
|
||||
Logger instance
|
||||
"""
|
||||
log_file = log_dir / "live_trading.log"
|
||||
|
||||
handlers = [
|
||||
logging.FileHandler(log_file),
|
||||
]
|
||||
|
||||
# Only add StreamHandler if no UI (log_queue is None)
|
||||
if log_queue is None:
|
||||
handlers.append(logging.StreamHandler(sys.stdout))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file),
|
||||
logging.StreamHandler(sys.stdout),
|
||||
],
|
||||
force=True
|
||||
handlers=handlers,
|
||||
force=True,
|
||||
)
|
||||
|
||||
# Add UI log handler if queue provided
|
||||
if log_queue is not None:
|
||||
setup_ui_logging(log_queue)
|
||||
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -59,11 +89,15 @@ class LiveTradingBot:
|
||||
self,
|
||||
okx_config: OKXConfig,
|
||||
trading_config: TradingConfig,
|
||||
path_config: PathConfig
|
||||
path_config: PathConfig,
|
||||
database: Optional[TradingDatabase] = None,
|
||||
shared_state: Optional[SharedState] = None,
|
||||
):
|
||||
self.okx_config = okx_config
|
||||
self.trading_config = trading_config
|
||||
self.path_config = path_config
|
||||
self.db = database
|
||||
self.state = shared_state
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.running = True
|
||||
@@ -74,7 +108,7 @@ class LiveTradingBot:
|
||||
self.okx_client = OKXClient(okx_config, trading_config)
|
||||
self.data_feed = DataFeed(self.okx_client, trading_config, path_config)
|
||||
self.position_manager = PositionManager(
|
||||
self.okx_client, trading_config, path_config
|
||||
self.okx_client, trading_config, path_config, database
|
||||
)
|
||||
self.strategy = LiveRegimeStrategy(trading_config, path_config)
|
||||
|
||||
@@ -82,6 +116,16 @@ class LiveTradingBot:
|
||||
signal.signal(signal.SIGINT, self._handle_shutdown)
|
||||
signal.signal(signal.SIGTERM, self._handle_shutdown)
|
||||
|
||||
# Initialize shared state if provided
|
||||
if self.state:
|
||||
mode = "DEMO" if okx_config.demo_mode else "LIVE"
|
||||
self.state.set_mode(mode)
|
||||
self.state.set_symbols(
|
||||
trading_config.eth_symbol,
|
||||
trading_config.btc_symbol,
|
||||
)
|
||||
self.state.update_account(0.0, 0.0, trading_config.leverage)
|
||||
|
||||
self._print_startup_banner()
|
||||
|
||||
def _print_startup_banner(self) -> None:
|
||||
@@ -109,6 +153,8 @@ class LiveTradingBot:
|
||||
"""Handle shutdown signals gracefully."""
|
||||
self.logger.info("Shutdown signal received, stopping...")
|
||||
self.running = False
|
||||
if self.state:
|
||||
self.state.stop()
|
||||
|
||||
def run_trading_cycle(self) -> None:
|
||||
"""
|
||||
@@ -118,10 +164,20 @@ class LiveTradingBot:
|
||||
2. Update open positions
|
||||
3. Generate trading signal
|
||||
4. Execute trades if signal triggers
|
||||
5. Update shared state for UI
|
||||
"""
|
||||
# Reload model if it has changed (e.g. daily training)
|
||||
try:
|
||||
self.strategy.reload_model_if_changed()
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to reload model: {e}")
|
||||
|
||||
cycle_start = datetime.now(timezone.utc)
|
||||
self.logger.info(f"--- Trading Cycle Start: {cycle_start.isoformat()} ---")
|
||||
|
||||
if self.state:
|
||||
self.state.set_last_cycle_time(cycle_start.isoformat())
|
||||
|
||||
try:
|
||||
# 1. Fetch market data
|
||||
features = self.data_feed.get_latest_data()
|
||||
@@ -154,15 +210,22 @@ class LiveTradingBot:
|
||||
funding = self.data_feed.get_current_funding_rates()
|
||||
|
||||
# 5. Generate trading signal
|
||||
signal = self.strategy.generate_signal(features, funding)
|
||||
sig = self.strategy.generate_signal(features, funding)
|
||||
|
||||
# 6. Execute trades based on signal
|
||||
if signal['action'] == 'entry':
|
||||
self._execute_entry(signal, eth_price)
|
||||
elif signal['action'] == 'check_exit':
|
||||
self._execute_exit(signal)
|
||||
# 6. Update shared state with strategy info
|
||||
self._update_strategy_state(sig, funding)
|
||||
|
||||
# 7. Log portfolio summary
|
||||
# 7. Execute trades based on signal
|
||||
if sig['action'] == 'entry':
|
||||
self._execute_entry(sig, eth_price)
|
||||
elif sig['action'] == 'check_exit':
|
||||
self._execute_exit(sig)
|
||||
|
||||
# 8. Update shared state with position and account
|
||||
self._update_position_state(eth_price)
|
||||
self._update_account_state()
|
||||
|
||||
# 9. Log portfolio summary
|
||||
summary = self.position_manager.get_portfolio_summary()
|
||||
self.logger.info(
|
||||
f"Portfolio: {summary['open_positions']} positions, "
|
||||
@@ -178,6 +241,61 @@ class LiveTradingBot:
|
||||
cycle_duration = (datetime.now(timezone.utc) - cycle_start).total_seconds()
|
||||
self.logger.info(f"--- Cycle completed in {cycle_duration:.1f}s ---")
|
||||
|
||||
def _update_strategy_state(self, sig: dict, funding: dict) -> None:
|
||||
"""Update shared state with strategy information."""
|
||||
if not self.state:
|
||||
return
|
||||
|
||||
self.state.update_strategy(
|
||||
z_score=sig.get('z_score', 0.0),
|
||||
probability=sig.get('probability', 0.0),
|
||||
funding_rate=funding.get('btc_funding', 0.0),
|
||||
action=sig.get('action', 'hold'),
|
||||
reason=sig.get('reason', ''),
|
||||
)
|
||||
|
||||
def _update_position_state(self, current_price: float) -> None:
|
||||
"""Update shared state with current position."""
|
||||
if not self.state:
|
||||
return
|
||||
|
||||
symbol = self.trading_config.eth_symbol
|
||||
position = self.position_manager.get_position_for_symbol(symbol)
|
||||
|
||||
if position is None:
|
||||
self.state.clear_position()
|
||||
return
|
||||
|
||||
pos_state = PositionState(
|
||||
trade_id=position.trade_id,
|
||||
symbol=position.symbol,
|
||||
side=position.side,
|
||||
entry_price=position.entry_price,
|
||||
current_price=position.current_price,
|
||||
size=position.size,
|
||||
size_usdt=position.size_usdt,
|
||||
unrealized_pnl=position.unrealized_pnl,
|
||||
unrealized_pnl_pct=position.unrealized_pnl_pct,
|
||||
stop_loss_price=position.stop_loss_price,
|
||||
take_profit_price=position.take_profit_price,
|
||||
)
|
||||
self.state.set_position(pos_state)
|
||||
|
||||
def _update_account_state(self) -> None:
|
||||
"""Update shared state with account information."""
|
||||
if not self.state:
|
||||
return
|
||||
|
||||
try:
|
||||
balance = self.okx_client.get_balance()
|
||||
self.state.update_account(
|
||||
balance=balance.get('total', 0.0),
|
||||
available=balance.get('free', 0.0),
|
||||
leverage=self.trading_config.leverage,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to update account state: {e}")
|
||||
|
||||
def _execute_entry(self, signal: dict, current_price: float) -> None:
|
||||
"""Execute entry trade."""
|
||||
symbol = self.trading_config.eth_symbol
|
||||
@@ -191,11 +309,15 @@ class LiveTradingBot:
|
||||
# Get account balance
|
||||
balance = self.okx_client.get_balance()
|
||||
available_usdt = balance['free']
|
||||
|
||||
self.logger.info(f"Account balance: ${available_usdt:.2f} USDT available")
|
||||
|
||||
# Calculate position size
|
||||
size_usdt = self.strategy.calculate_position_size(signal, available_usdt)
|
||||
if size_usdt <= 0:
|
||||
self.logger.info("Position size too small, skipping entry")
|
||||
self.logger.info(
|
||||
f"Position size too small (${size_usdt:.2f}), skipping entry. "
|
||||
f"Min required: ${self.strategy.config.min_position_usdt:.2f}"
|
||||
)
|
||||
return
|
||||
|
||||
size_eth = size_usdt / current_price
|
||||
@@ -290,22 +412,30 @@ class LiveTradingBot:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Exit execution failed: {e}", exc_info=True)
|
||||
|
||||
def _is_running(self) -> bool:
|
||||
"""Check if bot should continue running."""
|
||||
if not self.running:
|
||||
return False
|
||||
if self.state and not self.state.is_running():
|
||||
return False
|
||||
return True
|
||||
|
||||
def run(self) -> None:
|
||||
"""Main trading loop."""
|
||||
self.logger.info("Starting trading loop...")
|
||||
|
||||
while self.running:
|
||||
while self._is_running():
|
||||
try:
|
||||
self.run_trading_cycle()
|
||||
|
||||
if self.running:
|
||||
if self._is_running():
|
||||
sleep_seconds = self.trading_config.sleep_seconds
|
||||
minutes = sleep_seconds // 60
|
||||
self.logger.info(f"Sleeping for {minutes} minutes...")
|
||||
|
||||
# Sleep in smaller chunks to allow faster shutdown
|
||||
for _ in range(sleep_seconds):
|
||||
if not self.running:
|
||||
if not self._is_running():
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
@@ -319,6 +449,8 @@ class LiveTradingBot:
|
||||
# Cleanup
|
||||
self.logger.info("Shutting down...")
|
||||
self.position_manager.save_positions()
|
||||
if self.db:
|
||||
self.db.close()
|
||||
self.logger.info("Shutdown complete")
|
||||
|
||||
|
||||
@@ -350,6 +482,11 @@ def parse_args():
|
||||
action="store_true",
|
||||
help="Use live trading mode (requires OKX_DEMO_MODE=false)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-ui",
|
||||
action="store_true",
|
||||
help="Run in headless mode without terminal UI"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -370,19 +507,64 @@ def main():
|
||||
if args.live:
|
||||
okx_config.demo_mode = False
|
||||
|
||||
# Determine if UI should be enabled
|
||||
use_ui = not args.no_ui and sys.stdin.isatty()
|
||||
|
||||
# Initialize database
|
||||
db_path = path_config.base_dir / "live_trading" / "trading.db"
|
||||
db = init_db(db_path)
|
||||
|
||||
# Run migrations (imports CSV if exists)
|
||||
run_migrations(db, path_config.trade_log_file)
|
||||
|
||||
# Initialize UI components if enabled
|
||||
log_queue: Optional[queue.Queue] = None
|
||||
shared_state: Optional[SharedState] = None
|
||||
dashboard: Optional[TradingDashboard] = None
|
||||
|
||||
if use_ui:
|
||||
log_queue = queue.Queue(maxsize=1000)
|
||||
shared_state = SharedState()
|
||||
|
||||
# Setup logging
|
||||
logger = setup_logging(path_config.logs_dir)
|
||||
logger = setup_logging(path_config.logs_dir, log_queue)
|
||||
|
||||
try:
|
||||
# Create and run bot
|
||||
bot = LiveTradingBot(okx_config, trading_config, path_config)
|
||||
# Create bot
|
||||
bot = LiveTradingBot(
|
||||
okx_config,
|
||||
trading_config,
|
||||
path_config,
|
||||
database=db,
|
||||
shared_state=shared_state,
|
||||
)
|
||||
|
||||
# Start dashboard if UI enabled
|
||||
if use_ui and shared_state and log_queue:
|
||||
dashboard = TradingDashboard(
|
||||
state=shared_state,
|
||||
db=db,
|
||||
log_queue=log_queue,
|
||||
on_quit=lambda: setattr(bot, 'running', False),
|
||||
)
|
||||
dashboard.start()
|
||||
logger.info("Dashboard started")
|
||||
|
||||
# Run bot
|
||||
bot.run()
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Configuration error: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Fatal error: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
# Cleanup
|
||||
if dashboard:
|
||||
dashboard.stop()
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user