390 lines
15 KiB
Python
390 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Main Trading Application
|
|
Entry point for the automated trading system.
|
|
Supports Docker deployment with health monitoring.
|
|
"""
|
|
|
|
import sys
|
|
import time
|
|
import signal
|
|
import argparse
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
|
|
# Add src directory to path for imports
|
|
sys.path.append('src')
|
|
sys.path.append('config')
|
|
|
|
from src.logger_setup import setup_logging, log_trading_action, log_performance_metric
|
|
from src.trading_engine import TradingEngine
|
|
from config.trading_config import trading_config
|
|
|
|
# Global variables for graceful shutdown
|
|
running = True
|
|
trading_engine = None
|
|
last_health_check = datetime.now()
|
|
|
|
def signal_handler(signum, frame):
|
|
"""Handle shutdown signals gracefully"""
|
|
global running
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f"Received signal {signum}, shutting down gracefully...")
|
|
running = False
|
|
|
|
def health_check():
|
|
"""Perform health check for Docker monitoring"""
|
|
global last_health_check
|
|
try:
|
|
from src.data_handler import DataHandler
|
|
data_handler = DataHandler()
|
|
# Quick health check - just verify we can get market data
|
|
price = data_handler.get_latest_price(trading_config.symbol)
|
|
last_health_check = datetime.now()
|
|
|
|
# Write health status to file for Docker monitoring
|
|
with open('/tmp/health', 'w') as f:
|
|
f.write(f"OK:{datetime.now().isoformat()}:{price}")
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f"Health check failed: {e}")
|
|
return False
|
|
|
|
def run_backtesting_mode(strategy_type=None):
|
|
"""Run backtesting using historical data"""
|
|
logger = logging.getLogger(__name__)
|
|
|
|
strategy_name = strategy_type or trading_config.strategy_type
|
|
logger.info(f"Starting backtesting mode with {strategy_name} strategy")
|
|
|
|
try:
|
|
# Import backtesting logic from existing files
|
|
from src.data_handler import DataHandler
|
|
from src.strategy_factory import get_strategy
|
|
|
|
data_handler = DataHandler()
|
|
strategy = get_strategy(strategy_type)
|
|
|
|
# Get historical data (180 days for comprehensive backtest)
|
|
bars = data_handler.get_historical_data(days=180)
|
|
|
|
if bars.empty:
|
|
logger.error("No historical data available for backtesting")
|
|
return
|
|
|
|
# Calculate technical indicators
|
|
bars = data_handler.calculate_technical_indicators(bars)
|
|
|
|
# Generate signals
|
|
bars = strategy.generate_signals(bars)
|
|
|
|
# Run backtest simulation
|
|
results = run_backtest_simulation(bars, strategy)
|
|
|
|
# Display results
|
|
display_backtest_results(results, strategy.name)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in backtesting mode: {e}")
|
|
|
|
def run_backtest_simulation(bars, strategy):
|
|
"""Run backtest simulation with position management"""
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f"Running backtest simulation with {strategy.name}")
|
|
|
|
# Initialize tracking variables
|
|
position = 0
|
|
entry_price = None
|
|
highest_price_since_entry = None
|
|
bars_since_entry = 0
|
|
|
|
# Initialize position tracking
|
|
bars['position'] = 0
|
|
|
|
for i in range(1, len(bars)):
|
|
current_price = bars['close'].iloc[i]
|
|
|
|
if bars['buy_signal'].iloc[i] and position == 0:
|
|
# Use strategy-specific entry conditions
|
|
if strategy.get_entry_conditions(bars, i):
|
|
position = 1
|
|
entry_price = current_price
|
|
highest_price_since_entry = current_price
|
|
bars_since_entry = 0
|
|
|
|
elif position == 1:
|
|
bars_since_entry += 1
|
|
|
|
# Update highest price
|
|
if current_price > highest_price_since_entry:
|
|
highest_price_since_entry = current_price
|
|
|
|
# Use strategy-specific exit conditions
|
|
should_exit, exit_reason = strategy.should_exit_position(
|
|
current_price=current_price,
|
|
entry_price=entry_price,
|
|
highest_price=highest_price_since_entry,
|
|
bars_since_entry=bars_since_entry,
|
|
sell_signal=bars['sell_signal'].iloc[i],
|
|
ema_trend=bars['ema_trend'].iloc[i]
|
|
)
|
|
|
|
if should_exit:
|
|
position = 0
|
|
entry_price = None
|
|
highest_price_since_entry = None
|
|
bars_since_entry = 0
|
|
|
|
bars.iloc[i, bars.columns.get_loc('position')] = position
|
|
|
|
# Calculate returns
|
|
bars['market_return'] = bars['close'].pct_change()
|
|
bars['strategy_return'] = bars['position'].shift(1) * bars['market_return']
|
|
|
|
# Apply trading costs
|
|
trade_cost = trading_config.trade_cost
|
|
bars['position_change'] = bars['position'].diff().abs()
|
|
bars['costs'] = bars['position_change'] * trade_cost
|
|
bars['strategy_return'] = bars['strategy_return'] - bars['costs']
|
|
|
|
# Cumulative returns
|
|
bars['cum_market'] = (1 + bars['market_return']).cumprod() - 1
|
|
bars['cum_strategy'] = (1 + bars['strategy_return']).cumprod() - 1
|
|
|
|
return calculate_performance_metrics(bars)
|
|
|
|
def calculate_performance_metrics(bars):
|
|
"""Calculate comprehensive performance metrics"""
|
|
|
|
# Basic returns
|
|
total_return = bars['cum_strategy'].iloc[-1] * 100
|
|
market_return_pct = bars['cum_market'].iloc[-1] * 100
|
|
|
|
# Trade analysis
|
|
position_changes = bars['position'].diff()
|
|
buys = (position_changes == 1).sum()
|
|
sells = (position_changes == -1).sum()
|
|
|
|
# Calculate individual trade returns
|
|
trade_returns = []
|
|
entry_price = None
|
|
|
|
for i in range(len(bars)):
|
|
if position_changes.iloc[i] == 1:
|
|
entry_price = bars['close'].iloc[i]
|
|
elif position_changes.iloc[i] == -1 and entry_price is not None:
|
|
exit_price = bars['close'].iloc[i]
|
|
trade_return = (exit_price / entry_price - 1) - (2 * trading_config.trade_cost)
|
|
trade_returns.append(trade_return)
|
|
entry_price = None
|
|
|
|
# Trade statistics
|
|
if len(trade_returns) > 0:
|
|
import numpy as np
|
|
trade_returns = np.array(trade_returns)
|
|
win_rate = (trade_returns > 0).mean() * 100
|
|
avg_win = trade_returns[trade_returns > 0].mean() * 100 if (trade_returns > 0).any() else 0
|
|
avg_loss = trade_returns[trade_returns < 0].mean() * 100 if (trade_returns < 0).any() else 0
|
|
max_win = trade_returns.max() * 100
|
|
max_loss = trade_returns.min() * 100
|
|
|
|
total_wins = trade_returns[trade_returns > 0].sum()
|
|
total_losses = abs(trade_returns[trade_returns < 0].sum())
|
|
profit_factor = total_wins / total_losses if total_losses > 0 else float('inf')
|
|
else:
|
|
win_rate = avg_win = avg_loss = max_win = max_loss = profit_factor = 0
|
|
|
|
# Risk metrics
|
|
if bars['strategy_return'].std() > 0:
|
|
import numpy as np
|
|
sharpe = bars['strategy_return'].mean() / bars['strategy_return'].std() * np.sqrt(365 * 24)
|
|
else:
|
|
sharpe = 0
|
|
|
|
# Max drawdown
|
|
running_max = bars['cum_strategy'].cummax()
|
|
drawdown = (bars['cum_strategy'] - running_max)
|
|
max_drawdown = drawdown.min() * 100
|
|
|
|
# Time in market
|
|
time_in_market = (bars['position'] > 0).mean() * 100
|
|
|
|
return {
|
|
'total_return': total_return,
|
|
'market_return': market_return_pct,
|
|
'outperformance': total_return - market_return_pct,
|
|
'sharpe_ratio': sharpe,
|
|
'max_drawdown': max_drawdown,
|
|
'total_trades': len(trade_returns),
|
|
'win_rate': win_rate,
|
|
'profit_factor': profit_factor,
|
|
'avg_win': avg_win,
|
|
'avg_loss': avg_loss,
|
|
'max_win': max_win,
|
|
'max_loss': max_loss,
|
|
'time_in_market': time_in_market,
|
|
'buy_signals': buys,
|
|
'sell_signals': sells
|
|
}
|
|
|
|
def display_backtest_results(results, strategy_name):
|
|
"""Display backtest results in a formatted way"""
|
|
print("\n" + "="*60)
|
|
print(f"BACKTEST RESULTS - {strategy_name.upper()}")
|
|
print("="*60)
|
|
print(f"Strategy Return: {results['total_return']:>8.2f}%")
|
|
print(f"Market Return: {results['market_return']:>8.2f}%")
|
|
print(f"Outperformance: {results['outperformance']:>8.2f}%")
|
|
print(f"Sharpe Ratio: {results['sharpe_ratio']:>8.2f}")
|
|
print(f"Max Drawdown: {results['max_drawdown']:>8.2f}%")
|
|
print(f"Total Trades: {results['total_trades']:>8}")
|
|
print(f"Win Rate: {results['win_rate']:>8.2f}%")
|
|
print(f"Profit Factor: {results['profit_factor']:>8.2f}")
|
|
print(f"Avg Win: {results['avg_win']:>8.2f}%")
|
|
print(f"Avg Loss: {results['avg_loss']:>8.2f}%")
|
|
print(f"Max Win: {results['max_win']:>8.2f}%")
|
|
print(f"Max Loss: {results['max_loss']:>8.2f}%")
|
|
print(f"Time in Market: {results['time_in_market']:>8.1f}%")
|
|
print("="*60)
|
|
|
|
def run_live_trading_mode():
|
|
"""Run live trading mode with Docker health monitoring"""
|
|
global trading_engine, running
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.info("🚀 Starting live trading mode for 24/7 operation")
|
|
|
|
try:
|
|
# Initialize trading engine
|
|
trading_engine = TradingEngine(paper_trading=True)
|
|
|
|
# Track start time for uptime monitoring
|
|
start_time = datetime.now()
|
|
|
|
# Main trading loop
|
|
cycle_count = 0
|
|
last_summary_time = datetime.now()
|
|
|
|
# Initial health check
|
|
health_check()
|
|
|
|
logger.info("📊 Entering main trading loop...")
|
|
|
|
while running:
|
|
try:
|
|
# Run trading cycle
|
|
cycle_results = trading_engine.run_trading_cycle()
|
|
cycle_count += 1
|
|
|
|
# Log cycle results
|
|
if cycle_results['success']:
|
|
logger.info(f"✅ Cycle {cycle_count} completed successfully")
|
|
|
|
if cycle_results['actions_taken']:
|
|
log_trading_action("trading_cycle", {
|
|
'cycle': cycle_count,
|
|
'actions': cycle_results['actions_taken'],
|
|
'price': cycle_results['current_price'],
|
|
'signals': cycle_results['signals']
|
|
})
|
|
|
|
# Log performance metrics periodically and health check
|
|
if datetime.now() - last_summary_time > timedelta(hours=1):
|
|
# Performance logging
|
|
summary = trading_engine.get_trading_summary()
|
|
if summary['current_position'] > 0 and summary['entry_price']:
|
|
current_profit = (cycle_results['current_price'] / summary['entry_price'] - 1) * 100
|
|
log_performance_metric("unrealized_pnl_pct", current_profit, {
|
|
'position': summary['current_position'],
|
|
'entry_price': summary['entry_price'],
|
|
'current_price': cycle_results['current_price']
|
|
})
|
|
|
|
# Health check for Docker monitoring
|
|
health_check()
|
|
last_summary_time = datetime.now()
|
|
|
|
# Docker deployment status
|
|
logger.info(f"🐳 Docker Status - Cycle: {cycle_count}, Uptime: {datetime.now() - start_time}")
|
|
else:
|
|
logger.error(f"Cycle {cycle_count} failed")
|
|
|
|
# Wait before next cycle (3600 seconds = 1 hour for hourly timeframe)
|
|
sleep_duration = 3600 # 1 hour
|
|
|
|
# Health check every 5 minutes during sleep
|
|
for i in range(sleep_duration):
|
|
if not running:
|
|
break
|
|
if i % 300 == 0: # Every 5 minutes
|
|
health_check()
|
|
time.sleep(1)
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("Received keyboard interrupt")
|
|
running = False
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"Error in trading loop: {e}")
|
|
time.sleep(60) # Wait 1 minute before retrying
|
|
|
|
logger.info("Live trading mode stopped")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in live trading mode: {e}")
|
|
|
|
def main():
|
|
"""Main application entry point"""
|
|
|
|
# Setup command line arguments
|
|
parser = argparse.ArgumentParser(description='Automated Trading System')
|
|
parser.add_argument('--mode', choices=['live', 'paper', 'backtest'], default='backtest',
|
|
help='Trading mode: live, paper, or backtest (default: backtest)')
|
|
parser.add_argument('--strategy', choices=['conservative', 'enhanced'],
|
|
default=None, help='Strategy type (default: from config)')
|
|
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
|
default='INFO', help='Logging level (default: INFO)')
|
|
parser.add_argument('--days', type=int, default=180,
|
|
help='Days of historical data for backtesting (default: 180)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Setup logging
|
|
setup_logging()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Set log level from command line
|
|
logging.getLogger().setLevel(getattr(logging, args.log_level))
|
|
|
|
# Setup signal handlers for graceful shutdown
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
logger.info(f"Starting trading system in {args.mode} mode")
|
|
|
|
# Override strategy type if specified
|
|
if args.strategy:
|
|
logger.info(f"Strategy override: {args.strategy}")
|
|
# We'll pass this to the functions that need it
|
|
|
|
try:
|
|
if args.mode == 'backtest':
|
|
run_backtesting_mode(args.strategy)
|
|
elif args.mode in ['live', 'paper']:
|
|
# Both live and paper use the same function - paper is safer
|
|
logger.info(f"🛡️ Running in {'PAPER' if args.mode == 'paper' else 'LIVE'} trading mode")
|
|
run_live_trading_mode()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Critical error in main: {e}")
|
|
return 1
|
|
|
|
logger.info("Trading system shutdown complete")
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
exit_code = main()
|
|
sys.exit(exit_code)
|