Implement FastAPI backend and Vue 3 frontend for Lowkey Backtest UI
- Added FastAPI backend with core API endpoints for strategies, backtests, and data management. - Introduced Vue 3 frontend with a dark theme, enabling users to run backtests, adjust parameters, and compare results. - Implemented Pydantic schemas for request/response validation and SQLAlchemy models for database interactions. - Enhanced project structure with dedicated modules for services, routers, and components. - Updated dependencies in `pyproject.toml` and `frontend/package.json` to include FastAPI, SQLAlchemy, and Vue-related packages. - Improved `.gitignore` to exclude unnecessary files and directories.
This commit is contained in:
390
live_trading/main.py
Normal file
390
live_trading/main.py
Normal file
@@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Live Trading Bot for Regime Reversion Strategy on OKX.
|
||||
|
||||
This script runs the regime-based mean reversion strategy
|
||||
on ETH perpetual futures using OKX exchange.
|
||||
|
||||
Usage:
|
||||
# Run with demo account (default)
|
||||
uv run python -m live_trading.main
|
||||
|
||||
# Run with specific settings
|
||||
uv run python -m live_trading.main --max-position 500 --leverage 2
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from live_trading.config import get_config, OKXConfig, TradingConfig, PathConfig
|
||||
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
|
||||
|
||||
|
||||
def setup_logging(log_dir: Path) -> logging.Logger:
|
||||
"""Configure logging for the trading bot."""
|
||||
log_file = log_dir / "live_trading.log"
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LiveTradingBot:
|
||||
"""
|
||||
Main trading bot orchestrator.
|
||||
|
||||
Coordinates data fetching, signal generation, and order execution
|
||||
in a continuous loop.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
okx_config: OKXConfig,
|
||||
trading_config: TradingConfig,
|
||||
path_config: PathConfig
|
||||
):
|
||||
self.okx_config = okx_config
|
||||
self.trading_config = trading_config
|
||||
self.path_config = path_config
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.running = True
|
||||
|
||||
# Initialize components
|
||||
self.logger.info("Initializing trading bot components...")
|
||||
|
||||
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.strategy = LiveRegimeStrategy(trading_config, path_config)
|
||||
|
||||
# Register signal handlers for graceful shutdown
|
||||
signal.signal(signal.SIGINT, self._handle_shutdown)
|
||||
signal.signal(signal.SIGTERM, self._handle_shutdown)
|
||||
|
||||
self._print_startup_banner()
|
||||
|
||||
def _print_startup_banner(self) -> None:
|
||||
"""Print startup information."""
|
||||
mode = "DEMO/SANDBOX" if self.okx_config.demo_mode else "LIVE"
|
||||
|
||||
print("=" * 60)
|
||||
print(f" Regime Reversion Strategy - Live Trading Bot")
|
||||
print("=" * 60)
|
||||
print(f" Mode: {mode}")
|
||||
print(f" Trading Pair: {self.trading_config.eth_symbol}")
|
||||
print(f" Context Pair: {self.trading_config.btc_symbol}")
|
||||
print(f" Timeframe: {self.trading_config.timeframe}")
|
||||
print(f" Max Position: ${self.trading_config.max_position_usdt}")
|
||||
print(f" Leverage: {self.trading_config.leverage}x")
|
||||
print(f" Stop Loss: {self.trading_config.stop_loss_pct * 100:.1f}%")
|
||||
print(f" Take Profit: {self.trading_config.take_profit_pct * 100:.1f}%")
|
||||
print(f" Cycle Interval: {self.trading_config.sleep_seconds // 60} minutes")
|
||||
print("=" * 60)
|
||||
|
||||
if not self.okx_config.demo_mode:
|
||||
print("\n *** WARNING: LIVE TRADING MODE - REAL FUNDS AT RISK ***\n")
|
||||
|
||||
def _handle_shutdown(self, signum, frame) -> None:
|
||||
"""Handle shutdown signals gracefully."""
|
||||
self.logger.info("Shutdown signal received, stopping...")
|
||||
self.running = False
|
||||
|
||||
def run_trading_cycle(self) -> None:
|
||||
"""
|
||||
Execute one trading cycle.
|
||||
|
||||
1. Fetch latest market data
|
||||
2. Update open positions
|
||||
3. Generate trading signal
|
||||
4. Execute trades if signal triggers
|
||||
"""
|
||||
cycle_start = datetime.now(timezone.utc)
|
||||
self.logger.info(f"--- Trading Cycle Start: {cycle_start.isoformat()} ---")
|
||||
|
||||
try:
|
||||
# 1. Fetch market data
|
||||
features = self.data_feed.get_latest_data()
|
||||
if features is None or features.empty:
|
||||
self.logger.warning("No market data available, skipping cycle")
|
||||
return
|
||||
|
||||
# Get current prices
|
||||
eth_price = features['eth_close'].iloc[-1]
|
||||
btc_price = features['btc_close'].iloc[-1]
|
||||
|
||||
current_prices = {
|
||||
self.trading_config.eth_symbol: eth_price,
|
||||
self.trading_config.btc_symbol: btc_price,
|
||||
}
|
||||
|
||||
# 2. Update existing positions (check SL/TP)
|
||||
closed_trades = self.position_manager.update_positions(current_prices)
|
||||
if closed_trades:
|
||||
for trade in closed_trades:
|
||||
self.logger.info(
|
||||
f"Trade closed: {trade['trade_id']} "
|
||||
f"PnL=${trade['pnl_usd']:.2f} ({trade['reason']})"
|
||||
)
|
||||
|
||||
# 3. Sync with exchange positions
|
||||
self.position_manager.sync_with_exchange()
|
||||
|
||||
# 4. Get current funding rates
|
||||
funding = self.data_feed.get_current_funding_rates()
|
||||
|
||||
# 5. Generate trading signal
|
||||
signal = 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)
|
||||
|
||||
# 7. Log portfolio summary
|
||||
summary = self.position_manager.get_portfolio_summary()
|
||||
self.logger.info(
|
||||
f"Portfolio: {summary['open_positions']} positions, "
|
||||
f"exposure=${summary['total_exposure_usdt']:.2f}, "
|
||||
f"unrealized_pnl=${summary['total_unrealized_pnl']:.2f}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Trading cycle error: {e}", exc_info=True)
|
||||
# Save positions on error
|
||||
self.position_manager.save_positions()
|
||||
|
||||
cycle_duration = (datetime.now(timezone.utc) - cycle_start).total_seconds()
|
||||
self.logger.info(f"--- Cycle completed in {cycle_duration:.1f}s ---")
|
||||
|
||||
def _execute_entry(self, signal: dict, current_price: float) -> None:
|
||||
"""Execute entry trade."""
|
||||
symbol = self.trading_config.eth_symbol
|
||||
side = signal['side']
|
||||
|
||||
# Check if we can open a position
|
||||
if not self.position_manager.can_open_position():
|
||||
self.logger.info("Cannot open position: max positions reached")
|
||||
return
|
||||
|
||||
# Get account balance
|
||||
balance = self.okx_client.get_balance()
|
||||
available_usdt = balance['free']
|
||||
|
||||
# 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")
|
||||
return
|
||||
|
||||
size_eth = size_usdt / current_price
|
||||
|
||||
# Calculate SL/TP
|
||||
stop_loss, take_profit = self.strategy.calculate_sl_tp(current_price, side)
|
||||
|
||||
self.logger.info(
|
||||
f"Executing {side.upper()} entry: {size_eth:.4f} ETH @ {current_price:.2f} "
|
||||
f"(${size_usdt:.2f}), SL={stop_loss:.2f}, TP={take_profit:.2f}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Place market order
|
||||
order_side = "buy" if side == "long" else "sell"
|
||||
order = self.okx_client.place_market_order(symbol, order_side, size_eth)
|
||||
|
||||
# Get filled price (handle None values from OKX response)
|
||||
filled_price = order.get('average') or order.get('price') or current_price
|
||||
filled_amount = order.get('filled') or order.get('amount') or size_eth
|
||||
|
||||
# Ensure we have valid numeric values
|
||||
if filled_price is None or filled_price == 0:
|
||||
self.logger.warning(f"No fill price in order response, using current price: {current_price}")
|
||||
filled_price = current_price
|
||||
if filled_amount is None or filled_amount == 0:
|
||||
self.logger.warning(f"No fill amount in order response, using requested: {size_eth}")
|
||||
filled_amount = size_eth
|
||||
|
||||
# Recalculate SL/TP with filled price
|
||||
stop_loss, take_profit = self.strategy.calculate_sl_tp(filled_price, side)
|
||||
|
||||
# Get order ID from response
|
||||
order_id = order.get('id', '')
|
||||
|
||||
# Record position locally
|
||||
position = self.position_manager.open_position(
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
entry_price=filled_price,
|
||||
size=filled_amount,
|
||||
stop_loss_price=stop_loss,
|
||||
take_profit_price=take_profit,
|
||||
order_id=order_id,
|
||||
)
|
||||
|
||||
if position:
|
||||
self.logger.info(
|
||||
f"Position opened: {position.trade_id}, "
|
||||
f"{filled_amount:.4f} ETH @ {filled_price:.2f}"
|
||||
)
|
||||
|
||||
# Try to set SL/TP on exchange
|
||||
try:
|
||||
self.okx_client.set_stop_loss_take_profit(
|
||||
symbol, side, filled_amount, stop_loss, take_profit
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not set SL/TP on exchange: {e}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Order execution failed: {e}", exc_info=True)
|
||||
|
||||
def _execute_exit(self, signal: dict) -> None:
|
||||
"""Execute exit based on mean reversion signal."""
|
||||
symbol = self.trading_config.eth_symbol
|
||||
|
||||
# Get position for ETH
|
||||
position = self.position_manager.get_position_for_symbol(symbol)
|
||||
if not position:
|
||||
return
|
||||
|
||||
current_price = signal.get('eth_price', position.current_price)
|
||||
|
||||
self.logger.info(
|
||||
f"Mean reversion exit signal: closing {position.trade_id} "
|
||||
f"@ {current_price:.2f}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Close position on exchange
|
||||
exit_order = self.okx_client.close_position(symbol)
|
||||
exit_order_id = exit_order.get('id', '') if exit_order else ''
|
||||
|
||||
# Record closure locally
|
||||
self.position_manager.close_position(
|
||||
position.trade_id,
|
||||
current_price,
|
||||
reason="mean_reversion_complete",
|
||||
exit_order_id=exit_order_id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Exit execution failed: {e}", exc_info=True)
|
||||
|
||||
def run(self) -> None:
|
||||
"""Main trading loop."""
|
||||
self.logger.info("Starting trading loop...")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
self.run_trading_cycle()
|
||||
|
||||
if self.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:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.logger.info("Keyboard interrupt received")
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error in main loop: {e}", exc_info=True)
|
||||
time.sleep(60) # Wait before retry
|
||||
|
||||
# Cleanup
|
||||
self.logger.info("Shutting down...")
|
||||
self.position_manager.save_positions()
|
||||
self.logger.info("Shutdown complete")
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Live Trading Bot for Regime Reversion Strategy"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-position",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Maximum position size in USDT"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--leverage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Trading leverage (1-125)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interval",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Trading cycle interval in seconds"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live",
|
||||
action="store_true",
|
||||
help="Use live trading mode (requires OKX_DEMO_MODE=false)"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
args = parse_args()
|
||||
|
||||
# Load configuration
|
||||
okx_config, trading_config, path_config = get_config()
|
||||
|
||||
# Apply command line overrides
|
||||
if args.max_position is not None:
|
||||
trading_config.max_position_usdt = args.max_position
|
||||
if args.leverage is not None:
|
||||
trading_config.leverage = args.leverage
|
||||
if args.interval is not None:
|
||||
trading_config.sleep_seconds = args.interval
|
||||
if args.live:
|
||||
okx_config.demo_mode = False
|
||||
|
||||
# Setup logging
|
||||
logger = setup_logging(path_config.logs_dir)
|
||||
|
||||
try:
|
||||
# Create and run bot
|
||||
bot = LiveTradingBot(okx_config, trading_config, path_config)
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user