diff --git a/README.md b/README.md index adcc086..86f7e7d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,133 @@ -# QuantumEdge -QuantumEdge AI-powered trading system for forex and futures starting with $50. Features self-correcting algorithms, technical analysis, and machine learning with strict risk management. Adapts to market changes while preserving capital and gradually scaling positions through an intuitive monitoring dashboard.RetryClaude +# AI-Powered Trading Bot + +A sophisticated trading bot that combines technical analysis, machine learning, sentiment analysis, and risk management to make informed trading decisions. + +## Features + +- **Technical Analysis** + - Multiple timeframe analysis + - Advanced indicators (SMA, EMA, RSI, MACD, Bollinger Bands) + - Pattern recognition + - Support and resistance levels + +- **Machine Learning** + - LSTM-based price prediction + - Reinforcement learning for strategy optimization + - Feature engineering and selection + - Model persistence and retraining + +- **Sentiment Analysis** + - News sentiment analysis + - Social media sentiment (Twitter) + - Market sentiment indicators + - Weighted sentiment scoring + +- **Risk Management** + - Position sizing based on risk percentage + - Stop-loss and take-profit management + - Maximum drawdown protection + - Performance monitoring + - Risk metrics calculation + +- **Web Dashboard** + - Real-time performance monitoring + - Trade history visualization + - Risk metrics display + - Configuration management + +## Requirements + +- Python 3.8+ +- CUDA-capable GPU (recommended for machine learning) +- API keys for: + - Cryptocurrency exchange (e.g., Binance) + - News API + - Twitter API + +## Installation + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/trading-bot.git +cd trading-bot +``` + +2. Create a virtual environment: +```bash +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +3. Install dependencies: +```bash +pip install -r requirements.txt +``` + +4. Configure the bot: + - Copy `config/config.json.example` to `config/config.json` + - Update the configuration with your API keys and preferences + +## Usage + +1. Start the trading bot: +```bash +python -m src.main +``` + +2. Access the web dashboard: + - Open your browser and navigate to `http://localhost:5000` + - Monitor performance and manage settings + +3. Monitor logs: + - Check `trading_bot.log` for detailed information + - Monitor system performance and error messages + +## Configuration + +The bot can be configured through `config/config.json`. Key settings include: + +- Trading pairs and timeframes +- Risk management parameters +- Technical analysis settings +- Machine learning model parameters +- Sentiment analysis weights +- API credentials + +## Risk Warning + +Trading cryptocurrencies involves significant risk of loss. This bot is for educational purposes only. Always: + +- Start with small amounts +- Use testnet for initial testing +- Monitor performance closely +- Implement proper risk management +- Never trade with money you cannot afford to lose + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- Thanks to the open-source community for various libraries used in this project +- Special thanks to contributors and maintainers of key dependencies +- Inspired by various trading strategies and research papers + +## Support + +For support, please: +1. Check the documentation +2. Search existing issues +3. Create a new issue if needed + +## Disclaimer + +This trading bot is provided as-is, without any warranties. Use at your own risk. The developers are not responsible for any financial losses incurred through the use of this software. \ No newline at end of file diff --git a/backtesting.py b/backtesting.py new file mode 100644 index 0000000..cf2c18a --- /dev/null +++ b/backtesting.py @@ -0,0 +1,266 @@ +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +import logging +from typing import Dict, List, Optional +import ccxt +from concurrent.futures import ThreadPoolExecutor +import json +import os + +from .trading_strategy import TradingStrategy +from .config import load_config + +logger = logging.getLogger(__name__) + +class Backtester: + def __init__(self, config_path: str): + # Load configuration + self.config = load_config(config_path) + + # Initialize exchange + self.exchange = self._initialize_exchange() + + # Initialize trading strategy + self.strategy = TradingStrategy(self.config) + + # Initialize results storage + self.results = { + 'trades': [], + 'equity_curve': [], + 'performance_metrics': {} + } + + def _initialize_exchange(self) -> ccxt.Exchange: + """Initialize the cryptocurrency exchange""" + try: + exchange_class = getattr(ccxt, self.config['exchange']['name']) + exchange = exchange_class({ + 'apiKey': self.config['exchange']['api_key'], + 'secret': self.config['exchange']['api_secret'], + 'enableRateLimit': True + }) + + # Test connection + exchange.load_markets() + logger.info(f"Successfully connected to {self.config['exchange']['name']}") + + return exchange + + except Exception as e: + logger.error(f"Error initializing exchange: {str(e)}") + raise + + def fetch_historical_data(self, + symbol: str, + timeframe: str, + start_date: datetime, + end_date: datetime) -> pd.DataFrame: + """Fetch historical market data""" + try: + # Convert dates to timestamps + start_timestamp = int(start_date.timestamp() * 1000) + end_timestamp = int(end_date.timestamp() * 1000) + + # Fetch OHLCV data + ohlcv = self.exchange.fetch_ohlcv( + symbol, + timeframe=timeframe, + since=start_timestamp, + limit=1000 # Maximum limit per request + ) + + # Convert to DataFrame + df = pd.DataFrame( + ohlcv, + columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'] + ) + + # Convert timestamp to datetime + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + df.set_index('timestamp', inplace=True) + + return df + + except Exception as e: + logger.error(f"Error fetching historical data for {symbol}: {str(e)}") + return None + + def run_backtest(self, + symbol: str, + timeframe: str, + start_date: datetime, + end_date: datetime, + initial_capital: float = 50.0) -> Dict: + """Run backtest for a given period""" + try: + logger.info(f"Starting backtest for {symbol} from {start_date} to {end_date}") + + # Fetch historical data + df = self.fetch_historical_data(symbol, timeframe, start_date, end_date) + if df is None: + raise ValueError("Failed to fetch historical data") + + # Initialize variables + current_capital = initial_capital + position = 0 + entry_price = 0 + trades = [] + equity_curve = [] + + # Iterate through data + for i in range(len(df)): + current_data = df.iloc[:i+1] + current_price = current_data['close'].iloc[-1] + + # Generate trading signal + signal = self.strategy.analyze_market(symbol, current_data) + + if signal: + # Execute trade if conditions are met + if signal.action == 'buy' and position <= 0: + position = 1 + entry_price = current_price + trades.append({ + 'timestamp': current_data.index[-1], + 'action': 'buy', + 'price': current_price, + 'position_size': signal.position_size + }) + + elif signal.action == 'sell' and position >= 0: + position = -1 + entry_price = current_price + trades.append({ + 'timestamp': current_data.index[-1], + 'action': 'sell', + 'price': current_price, + 'position_size': signal.position_size + }) + + # Update position PnL + if position != 0: + pnl = position * (current_price - entry_price) * signal.position_size + current_capital += pnl + + # Record equity + equity_curve.append({ + 'timestamp': current_data.index[-1], + 'equity': current_capital + }) + + # Calculate performance metrics + self.results['trades'] = trades + self.results['equity_curve'] = equity_curve + self.results['performance_metrics'] = self._calculate_performance_metrics(trades) + + return self.results + + except Exception as e: + logger.error(f"Error running backtest: {str(e)}") + return None + + def _calculate_performance_metrics(self, trades: List[Dict]) -> Dict: + """Calculate performance metrics from trades""" + if not trades: + return {} + + # Convert trades to DataFrame + df = pd.DataFrame(trades) + + # Calculate basic metrics + total_trades = len(df) + winning_trades = len(df[df['action'] == 'buy']) + losing_trades = len(df[df['action'] == 'sell']) + win_rate = winning_trades / total_trades if total_trades > 0 else 0 + + # Calculate returns + returns = df['price'].pct_change() + total_return = (1 + returns).prod() - 1 + annual_return = (1 + total_return) ** (252 / len(df)) - 1 + + # Calculate risk metrics + volatility = returns.std() * np.sqrt(252) + sharpe_ratio = annual_return / volatility if volatility > 0 else 0 + + # Calculate drawdown + cumulative_returns = (1 + returns).cumprod() + rolling_max = cumulative_returns.expanding().max() + drawdowns = (cumulative_returns - rolling_max) / rolling_max + max_drawdown = drawdowns.min() + + return { + 'total_trades': total_trades, + 'winning_trades': winning_trades, + 'losing_trades': losing_trades, + 'win_rate': win_rate, + 'total_return': total_return, + 'annual_return': annual_return, + 'volatility': volatility, + 'sharpe_ratio': sharpe_ratio, + 'max_drawdown': max_drawdown + } + + def save_results(self, filepath: str): + """Save backtest results to file""" + try: + with open(filepath, 'w') as f: + json.dump(self.results, f, default=str) + logger.info(f"Backtest results saved to {filepath}") + return True + except Exception as e: + logger.error(f"Error saving results: {str(e)}") + return False + + def load_results(self, filepath: str): + """Load backtest results from file""" + try: + with open(filepath, 'r') as f: + self.results = json.load(f) + logger.info(f"Backtest results loaded from {filepath}") + return True + except Exception as e: + logger.error(f"Error loading results: {str(e)}") + return False + +def main(): + # Load configuration + config_path = os.path.join(os.path.dirname(__file__), '..', 'config', 'config.json') + + # Create backtester + backtester = Backtester(config_path) + + # Define backtest parameters + symbol = "BTC/USDT" + timeframe = "1h" + start_date = datetime.now() - timedelta(days=30) + end_date = datetime.now() + initial_capital = 50.0 + + # Run backtest + results = backtester.run_backtest( + symbol=symbol, + timeframe=timeframe, + start_date=start_date, + end_date=end_date, + initial_capital=initial_capital + ) + + if results: + # Print performance metrics + metrics = results['performance_metrics'] + print("\nBacktest Results:") + print(f"Total Trades: {metrics['total_trades']}") + print(f"Win Rate: {metrics['win_rate']:.2%}") + print(f"Total Return: {metrics['total_return']:.2%}") + print(f"Annual Return: {metrics['annual_return']:.2%}") + print(f"Sharpe Ratio: {metrics['sharpe_ratio']:.2f}") + print(f"Max Drawdown: {metrics['max_drawdown']:.2%}") + + # Save results + backtester.save_results('backtest_results.json') + else: + print("Backtest failed to complete") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..8c77250 --- /dev/null +++ b/config.json @@ -0,0 +1,105 @@ +{ + "exchange": { + "name": "binance", + "api_key": "YOUR_API_KEY", + "api_secret": "YOUR_API_SECRET", + "testnet": true + }, + "trading": { + "symbols": ["BTC/USDT", "ETH/USDT"], + "timeframes": ["1m", "5m", "15m", "1h", "4h", "1d"], + "lookback_periods": 100, + "state_file": "trading_state.json", + "initial_capital": 50.0, + "max_positions": 3, + "leverage": 1 + }, + "risk_management": { + "risk_per_trade": 0.02, + "max_daily_loss": 0.05, + "max_drawdown": 0.15, + "stop_loss_pct": 0.02, + "take_profit_pct": 0.04, + "min_win_rate": 0.45, + "min_profit_factor": 1.2 + }, + "technical_analysis": { + "indicators": { + "sma": [20, 50, 200], + "ema": [20], + "rsi": [14], + "macd": { + "fast": 12, + "slow": 26, + "signal": 9 + }, + "bollinger_bands": { + "period": 20, + "std_dev": 2 + } + }, + "patterns": { + "candlestick": true, + "chart": true + } + }, + "machine_learning": { + "model_type": "lstm", + "features": [ + "open", "high", "low", "close", "volume", + "sma_20", "sma_50", "sma_200", + "rsi_14", "macd", "macd_signal", "macd_hist", + "bb_upper", "bb_middle", "bb_lower" + ], + "train_test_split": 0.2, + "validation_split": 0.1, + "rl": { + "learning_rate": 0.0003, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "gamma": 0.99, + "gae_lambda": 0.95, + "clip_range": 0.2, + "ent_coef": 0.01, + "vf_coef": 0.5 + } + }, + "sentiment_analysis": { + "news_api_key": "YOUR_NEWS_API_KEY", + "twitter_api_key": "YOUR_TWITTER_API_KEY", + "twitter_api_secret": "YOUR_TWITTER_API_SECRET", + "twitter_access_token": "YOUR_TWITTER_ACCESS_TOKEN", + "twitter_access_token_secret": "YOUR_TWITTER_ACCESS_TOKEN_SECRET", + "sentiment_weights": { + "news": 0.4, + "twitter": 0.3, + "market": 0.3 + }, + "sentiment_thresholds": { + "positive": 0.3, + "negative": -0.3 + } + }, + "signal_weights": { + "technical": 0.4, + "ml": 0.3, + "sentiment": 0.2, + "risk": 0.1 + }, + "signal_thresholds": { + "buy": 0.6, + "sell": -0.6 + }, + "logging": { + "level": "INFO", + "file": "trading_bot.log", + "max_size": 10485760, + "backup_count": 5 + }, + "web_dashboard": { + "host": "localhost", + "port": 5000, + "debug": false + } +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..5d56fb1 --- /dev/null +++ b/config.py @@ -0,0 +1,134 @@ +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Exchange Configuration +EXCHANGE_CONFIG = { + 'name': 'binance', + 'api_key': os.getenv('BINANCE_API_KEY', ''), + 'api_secret': os.getenv('BINANCE_API_SECRET', ''), + 'testnet': True # Use testnet for development +} + +# Trading Parameters +TRADING_CONFIG = { + 'symbols': ['BTC/USDT', 'ETH/USDT', 'EUR/USD'], + 'timeframes': ['1h', '4h', '1d'], + 'initial_capital': 10000, + 'risk_per_trade': 0.02, # 2% risk per trade + 'max_positions': 3, + 'position_sizing': { + 'method': 'fixed_fractional', + 'fraction': 0.02 # 2% of capital per trade + } +} + +# Technical Analysis Parameters +TECHNICAL_CONFIG = { + 'indicators': { + 'bollinger_bands': { + 'period': 20, + 'std_dev': 2 + }, + 'rsi': { + 'period': 14, + 'overbought': 70, + 'oversold': 30 + }, + 'macd': { + 'fast_period': 12, + 'slow_period': 26, + 'signal_period': 9 + }, + 'atr': { + 'period': 14 + } + }, + 'patterns': { + 'head_and_shoulders': True, + 'double_top_bottom': True, + 'triangles': True + } +} + +# Machine Learning Configuration +ML_CONFIG = { + 'model_type': 'lstm', # Options: 'lstm', 'rf', 'xgboost' + 'features': [ + 'open', 'high', 'low', 'close', 'volume', + 'sma_20', 'sma_50', 'rsi', 'macd', 'atr', + 'bb_upper', 'bb_lower', 'bb_width' + ], + 'sequence_length': 10, + 'prediction_horizon': 1, + 'train_test_split': 0.8, + 'validation_split': 0.1 +} + +# Reinforcement Learning Configuration +RL_CONFIG = { + 'algorithm': 'PPO', + 'learning_rate': 0.0003, + 'n_steps': 2048, + 'batch_size': 64, + 'n_epochs': 10, + 'gamma': 0.99, + 'gae_lambda': 0.95, + 'clip_range': 0.2, + 'ent_coef': 0.01, + 'vf_coef': 0.5 +} + +# Risk Management Configuration +RISK_CONFIG = { + 'stop_loss': { + 'method': 'atr', + 'atr_multiplier': 2 + }, + 'take_profit': { + 'method': 'risk_reward', + 'risk_reward_ratio': 2 + }, + 'trailing_stop': { + 'enabled': True, + 'activation_percentage': 0.02, + 'trail_percentage': 0.01 + } +} + +# Sentiment Analysis Configuration +SENTIMENT_CONFIG = { + 'sources': [ + 'reuters', + 'bloomberg', + 'forexfactory' + ], + 'update_interval': 3600, # 1 hour + 'weight': 0.2 # Weight in final decision +} + +# Backtesting Configuration +BACKTEST_CONFIG = { + 'start_date': '2020-01-01', + 'end_date': '2023-12-31', + 'initial_capital': 10000, + 'commission': 0.001, # 0.1% + 'slippage': 0.0001 # 0.01% +} + +# Logging Configuration +LOGGING_CONFIG = { + 'level': 'INFO', + 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + 'file': 'logs/trading_bot.log' +} + +# Web Dashboard Configuration +DASHBOARD_CONFIG = { + 'host': '0.0.0.0', + 'port': 5000, + 'debug': False, + 'update_interval': 5 # seconds +} \ No newline at end of file diff --git a/machine_learning.py b/machine_learning.py new file mode 100644 index 0000000..688ec63 --- /dev/null +++ b/machine_learning.py @@ -0,0 +1,271 @@ +import numpy as np +import pandas as pd +from sklearn.preprocessing import StandardScaler +from sklearn.model_selection import train_test_split +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import LSTM, Dense, Dropout +from tensorflow.keras.optimizers import Adam +from stable_baselines3 import PPO +from stable_baselines3.common.vec_env import DummyVecEnv +import gym +import logging +from typing import Dict, List, Tuple, Optional +import joblib +import os + +logger = logging.getLogger(__name__) + +class MachineLearningModel: + def __init__(self, config: Dict): + self.config = config + self.model_type = config['model_type'] + self.features = config['features'] + self.scaler = StandardScaler() + self.model = None + self.env = None + + def prepare_data(self, df: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]: + """ + Prepare data for machine learning + """ + # Select features + X = df[self.features].copy() + + # Create target variable (price movement direction) + y = np.where(df['close'].shift(-1) > df['close'], 1, 0) + + # Drop NaN values + valid_idx = ~np.isnan(y) + X = X[valid_idx] + y = y[valid_idx] + + # Scale features + X_scaled = self.scaler.fit_transform(X) + + return X_scaled, y + + def train_lstm(self, X: np.ndarray, y: np.ndarray): + """ + Train LSTM model + """ + # Reshape data for LSTM [samples, time steps, features] + X_reshaped = np.reshape(X, (X.shape[0], 1, X.shape[1])) + + # Split data + X_train, X_test, y_train, y_test = train_test_split( + X_reshaped, y, + test_size=self.config['train_test_split'], + random_state=42 + ) + + # Create model + model = Sequential([ + LSTM(50, activation='relu', input_shape=(1, X.shape[1]), return_sequences=True), + Dropout(0.2), + LSTM(50, activation='relu'), + Dropout(0.2), + Dense(1, activation='sigmoid') + ]) + + # Compile model + model.compile( + optimizer=Adam(learning_rate=0.001), + loss='binary_crossentropy', + metrics=['accuracy'] + ) + + # Train model + model.fit( + X_train, y_train, + epochs=50, + batch_size=32, + validation_split=self.config['validation_split'], + verbose=0 + ) + + # Evaluate model + _, accuracy = model.evaluate(X_test, y_test, verbose=0) + logger.info(f"LSTM model accuracy: {accuracy:.4f}") + + self.model = model + return model + + def train_reinforcement_learning(self, df: pd.DataFrame): + """ + Train reinforcement learning model + """ + # Create custom trading environment + self.env = TradingEnvironment(df) + self.env = DummyVecEnv([lambda: self.env]) + + # Create and train PPO model + model = PPO( + "MlpPolicy", + self.env, + learning_rate=self.config['rl']['learning_rate'], + n_steps=self.config['rl']['n_steps'], + batch_size=self.config['rl']['batch_size'], + n_epochs=self.config['rl']['n_epochs'], + gamma=self.config['rl']['gamma'], + gae_lambda=self.config['rl']['gae_lambda'], + clip_range=self.config['rl']['clip_range'], + ent_coef=self.config['rl']['ent_coef'], + vf_coef=self.config['rl']['vf_coef'], + verbose=1 + ) + + # Train model + model.learn(total_timesteps=10000) + + self.model = model + return model + + def predict(self, X: np.ndarray) -> np.ndarray: + """ + Make predictions using the trained model + """ + if self.model is None: + logger.error("Model not trained. Call train() first.") + return None + + if self.model_type == 'lstm': + X_reshaped = np.reshape(X, (X.shape[0], 1, X.shape[1])) + predictions = self.model.predict(X_reshaped) + else: # reinforcement learning + predictions = self.model.predict(X) + + return predictions + + def save_model(self, filepath: str): + """ + Save the trained model + """ + if self.model is None: + logger.error("Model not trained. Call train() first.") + return None + + try: + if self.model_type == 'lstm': + self.model.save(filepath) + else: + self.model.save(f"{filepath}_rl") + + # Save scaler separately + joblib.dump(self.scaler, f"{filepath}_scaler") + + logger.info(f"Model saved to {filepath}") + return True + except Exception as e: + logger.error(f"Error saving model: {str(e)}") + return False + + def load_model(self, filepath: str): + """ + Load a trained model + """ + try: + if self.model_type == 'lstm': + self.model = tf.keras.models.load_model(filepath) + else: + self.model = PPO.load(f"{filepath}_rl") + + # Load scaler + self.scaler = joblib.load(f"{filepath}_scaler") + + logger.info(f"Model loaded from {filepath}") + return True + except Exception as e: + logger.error(f"Error loading model: {str(e)}") + return False + + +class TradingEnvironment(gym.Env): + """ + Custom trading environment for reinforcement learning + """ + def __init__(self, df: pd.DataFrame): + super(TradingEnvironment, self).__init__() + + self.df = df + self.current_step = 0 + self.initial_balance = 10000 + self.balance = self.initial_balance + self.position = 0 + self.position_size = 0 + + # Define action and observation space + self.action_space = gym.spaces.Discrete(3) # Buy, Sell, Hold + self.observation_space = gym.spaces.Box( + low=-np.inf, + high=np.inf, + shape=(len(df.columns),), + dtype=np.float32 + ) + + def reset(self): + """ + Reset the environment + """ + self.current_step = 0 + self.balance = self.initial_balance + self.position = 0 + self.position_size = 0 + + return self._get_observation() + + def step(self, action): + """ + Execute one step in the environment + """ + # Get current price + current_price = self.df['close'].iloc[self.current_step] + + # Execute action + reward = 0 + done = False + + if action == 0: # Buy + if self.position <= 0: + self.position = 1 + self.position_size = self.balance / current_price + elif action == 1: # Sell + if self.position >= 0: + self.position = -1 + self.position_size = self.balance / current_price + + # Calculate reward + next_price = self.df['close'].iloc[self.current_step + 1] + price_change = (next_price - current_price) / current_price + + if self.position != 0: + reward = self.position * price_change * 100 + + # Update balance + self.balance *= (1 + reward/100) + + # Move to next step + self.current_step += 1 + + # Check if episode is done + if self.current_step >= len(self.df) - 1: + done = True + + return self._get_observation(), reward, done, {} + + def _get_observation(self): + """ + Get current observation + """ + return self.df.iloc[self.current_step].values.astype(np.float32) + + def render(self): + """ + Render the environment + """ + pass + + def close(self): + """ + Clean up resources + """ + pass \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..94b06db --- /dev/null +++ b/main.py @@ -0,0 +1,272 @@ +import os +import sys +import logging +import json +import time +from datetime import datetime +from typing import Dict, List +import pandas as pd +import ccxt +from concurrent.futures import ThreadPoolExecutor +import schedule +import threading +import signal +import queue + +from .trading_strategy import TradingStrategy +from .config import load_config + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('trading_bot.log'), + logging.StreamHandler(sys.stdout) + ] +) + +logger = logging.getLogger(__name__) + +class TradingBot: + def __init__(self, config_path: str): + # Load configuration + self.config = load_config(config_path) + + # Initialize exchange + self.exchange = self._initialize_exchange() + + # Initialize trading strategy + self.strategy = TradingStrategy(self.config) + + # Initialize state + self.is_running = False + self.symbols = self.config['trading']['symbols'] + self.timeframes = self.config['trading']['timeframes'] + self.data_queue = queue.Queue() + self.signal_queue = queue.Queue() + + # Load previous state if exists + self._load_state() + + def _initialize_exchange(self) -> ccxt.Exchange: + """ + Initialize the cryptocurrency exchange + """ + try: + exchange_class = getattr(ccxt, self.config['exchange']['name']) + exchange = exchange_class({ + 'apiKey': self.config['exchange']['api_key'], + 'secret': self.config['exchange']['api_secret'], + 'enableRateLimit': True + }) + + # Test connection + exchange.load_markets() + logger.info(f"Successfully connected to {self.config['exchange']['name']}") + + return exchange + + except Exception as e: + logger.error(f"Error initializing exchange: {str(e)}") + raise + + def _load_state(self): + """ + Load previous trading state + """ + state_file = self.config['trading']['state_file'] + if os.path.exists(state_file): + try: + self.strategy.load_state(state_file) + logger.info("Successfully loaded previous trading state") + except Exception as e: + logger.error(f"Error loading state: {str(e)}") + + def _save_state(self): + """ + Save current trading state + """ + try: + self.strategy.save_state(self.config['trading']['state_file']) + logger.info("Successfully saved trading state") + except Exception as e: + logger.error(f"Error saving state: {str(e)}") + + def fetch_market_data(self, symbol: str, timeframe: str) -> pd.DataFrame: + """ + Fetch market data from exchange + """ + try: + # Get OHLCV data + ohlcv = self.exchange.fetch_ohlcv( + symbol, + timeframe=timeframe, + limit=self.config['trading']['lookback_periods'] + ) + + # Convert to DataFrame + df = pd.DataFrame( + ohlcv, + columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'] + ) + + # Convert timestamp to datetime + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + df.set_index('timestamp', inplace=True) + + return df + + except Exception as e: + logger.error(f"Error fetching market data for {symbol}: {str(e)}") + return None + + def process_market_data(self): + """ + Process market data from queue + """ + while self.is_running: + try: + # Get data from queue + data = self.data_queue.get(timeout=1) + symbol, timeframe, df = data + + # Generate trading signals + signal = self.strategy.analyze_market(symbol, df) + + if signal: + # Add signal to queue + self.signal_queue.put(signal) + + except queue.Empty: + continue + except Exception as e: + logger.error(f"Error processing market data: {str(e)}") + + def execute_signals(self): + """ + Execute trading signals from queue + """ + while self.is_running: + try: + # Get signal from queue + signal = self.signal_queue.get(timeout=1) + + # Execute trade + if self.strategy.execute_trade(signal): + logger.info(f"Executed {signal.action} trade for {signal.symbol}") + + # Update positions with current prices + current_prices = { + symbol: self.exchange.fetch_ticker(symbol)['last'] + for symbol in self.symbols + } + self.strategy.update_positions(current_prices) + + # Save state after trade + self._save_state() + + except queue.Empty: + continue + except Exception as e: + logger.error(f"Error executing signals: {str(e)}") + + def update_market_data(self): + """ + Update market data for all symbols + """ + with ThreadPoolExecutor(max_workers=len(self.symbols)) as executor: + futures = [] + + for symbol in self.symbols: + for timeframe in self.timeframes: + future = executor.submit(self.fetch_market_data, symbol, timeframe) + futures.append((symbol, timeframe, future)) + + for symbol, timeframe, future in futures: + try: + df = future.result() + if df is not None: + self.data_queue.put((symbol, timeframe, df)) + except Exception as e: + logger.error(f"Error updating market data for {symbol}: {str(e)}") + + def schedule_tasks(self): + """ + Schedule periodic tasks + """ + # Schedule market data updates + for timeframe in self.timeframes: + schedule.every().minute.at(":00").do(self.update_market_data) + + # Schedule state saving + schedule.every().hour.at(":00").do(self._save_state) + + # Run the scheduler + while self.is_running: + schedule.run_pending() + time.sleep(1) + + def start(self): + """ + Start the trading bot + """ + try: + self.is_running = True + + # Start threads + threads = [ + threading.Thread(target=self.process_market_data), + threading.Thread(target=self.execute_signals), + threading.Thread(target=self.schedule_tasks) + ] + + for thread in threads: + thread.daemon = True + thread.start() + + # Initial market data update + self.update_market_data() + + logger.info("Trading bot started successfully") + + # Wait for shutdown signal + while self.is_running: + time.sleep(1) + + except Exception as e: + logger.error(f"Error starting trading bot: {str(e)}") + self.stop() + + def stop(self): + """ + Stop the trading bot + """ + try: + self.is_running = False + self._save_state() + logger.info("Trading bot stopped successfully") + except Exception as e: + logger.error(f"Error stopping trading bot: {str(e)}") + +def main(): + # Load configuration + config_path = os.path.join(os.path.dirname(__file__), '..', 'config', 'config.json') + + # Create trading bot + bot = TradingBot(config_path) + + # Handle shutdown signals + def signal_handler(signum, frame): + logger.info("Received shutdown signal") + bot.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Start trading bot + bot.start() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bbbb949 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +numpy>=1.21.0 +pandas>=1.3.0 +ccxt>=2.0.0 +scikit-learn>=0.24.2 +tensorflow>=2.6.0 +stable-baselines3>=1.5.0 +gym>=0.21.0 +ta-lib>=0.4.24 +textblob>=0.15.3 +tweepy>=4.10.0 +newsapi-python>=0.2.6 +yfinance>=0.1.63 +schedule>=1.1.0 +joblib>=1.0.2 +python-dotenv>=0.19.0 +flask>=2.0.1 +plotly>=5.3.1 +dash>=2.0.0 \ No newline at end of file diff --git a/risk_management.py b/risk_management.py new file mode 100644 index 0000000..6260dd3 --- /dev/null +++ b/risk_management.py @@ -0,0 +1,249 @@ +import numpy as np +import pandas as pd +from typing import Dict, Tuple, Optional +import logging +from dataclasses import dataclass +from datetime import datetime + +logger = logging.getLogger(__name__) + +@dataclass +class RiskMetrics: + """Data class to store risk metrics""" + daily_drawdown: float + max_drawdown: float + sharpe_ratio: float + sortino_ratio: float + win_rate: float + profit_factor: float + avg_win: float + avg_loss: float + max_consecutive_losses: int + max_consecutive_wins: int + +class RiskManager: + def __init__(self, config: Dict): + self.config = config + self.initial_capital = config['initial_capital'] + self.current_capital = self.initial_capital + self.risk_per_trade = config['risk_per_trade'] + self.max_daily_loss = config['max_daily_loss'] + self.max_drawdown = config['max_drawdown'] + self.max_positions = config['max_positions'] + self.leverage = config['leverage'] + self.stop_loss_pct = config['stop_loss_pct'] + self.take_profit_pct = config['take_profit_pct'] + + # Performance tracking + self.trades_history = [] + self.daily_pnl = [] + self.positions = {} + self.risk_metrics = None + + def calculate_position_size(self, + price: float, + stop_loss: float, + account_balance: float) -> Tuple[float, float]: + """ + Calculate position size based on risk parameters + """ + # Calculate risk amount in base currency + risk_amount = account_balance * self.risk_per_trade + + # Calculate position size based on stop loss + price_distance = abs(price - stop_loss) + position_size = risk_amount / price_distance + + # Apply leverage + position_size *= self.leverage + + # Calculate required margin + required_margin = (position_size * price) / self.leverage + + # Ensure we don't exceed maximum position size + max_position = account_balance * self.leverage / price + position_size = min(position_size, max_position) + + return position_size, required_margin + + def validate_trade(self, + symbol: str, + position_size: float, + price: float, + stop_loss: float, + take_profit: float) -> bool: + """ + Validate if a trade meets risk management criteria + """ + # Check if we have too many open positions + if len(self.positions) >= self.max_positions: + logger.warning("Maximum number of positions reached") + return False + + # Check if symbol is already in a position + if symbol in self.positions: + logger.warning(f"Position already exists for {symbol}") + return False + + # Calculate potential loss + potential_loss = position_size * abs(price - stop_loss) + + # Check if potential loss exceeds daily limit + if potential_loss > self.max_daily_loss: + logger.warning("Trade exceeds maximum daily loss limit") + return False + + # Check if stop loss is too far + stop_loss_distance = abs(price - stop_loss) / price + if stop_loss_distance > self.stop_loss_pct: + logger.warning("Stop loss distance exceeds maximum allowed") + return False + + # Check if take profit is reasonable + take_profit_distance = abs(take_profit - price) / price + if take_profit_distance > self.take_profit_pct: + logger.warning("Take profit distance exceeds maximum allowed") + return False + + return True + + def update_position(self, + symbol: str, + entry_price: float, + current_price: float, + position_size: float, + position_type: str) -> Optional[float]: + """ + Update position and check for stop loss or take profit + """ + if symbol not in self.positions: + self.positions[symbol] = { + 'entry_price': entry_price, + 'current_price': current_price, + 'position_size': position_size, + 'position_type': position_type, + 'entry_time': datetime.now() + } + return None + + position = self.positions[symbol] + pnl = 0 + + # Calculate unrealized PnL + if position_type == 'long': + pnl = (current_price - entry_price) * position_size + else: + pnl = (entry_price - current_price) * position_size + + # Check stop loss + if position_type == 'long': + if current_price <= entry_price * (1 - self.stop_loss_pct): + pnl = -position_size * entry_price * self.stop_loss_pct + del self.positions[symbol] + return pnl + else: + if current_price >= entry_price * (1 + self.stop_loss_pct): + pnl = -position_size * entry_price * self.stop_loss_pct + del self.positions[symbol] + return pnl + + # Check take profit + if position_type == 'long': + if current_price >= entry_price * (1 + self.take_profit_pct): + pnl = position_size * entry_price * self.take_profit_pct + del self.positions[symbol] + return pnl + else: + if current_price <= entry_price * (1 - self.take_profit_pct): + pnl = position_size * entry_price * self.take_profit_pct + del self.positions[symbol] + return pnl + + # Update current price + position['current_price'] = current_price + return None + + def calculate_risk_metrics(self) -> RiskMetrics: + """ + Calculate various risk metrics + """ + if not self.trades_history: + return None + + # Convert trades history to DataFrame + df = pd.DataFrame(self.trades_history) + + # Calculate daily returns + daily_returns = df.groupby(df['exit_time'].dt.date)['pnl'].sum() + + # Calculate drawdown + cumulative_returns = (1 + daily_returns).cumprod() + rolling_max = cumulative_returns.expanding().max() + drawdowns = (cumulative_returns - rolling_max) / rolling_max + + # Calculate metrics + daily_drawdown = drawdowns.min() + max_drawdown = drawdowns.min() + + # Calculate Sharpe ratio + risk_free_rate = 0.02 # 2% annual risk-free rate + excess_returns = daily_returns - risk_free_rate/252 + sharpe_ratio = np.sqrt(252) * excess_returns.mean() / excess_returns.std() + + # Calculate Sortino ratio + downside_returns = excess_returns[excess_returns < 0] + sortino_ratio = np.sqrt(252) * excess_returns.mean() / downside_returns.std() + + # Calculate win rate and profit factor + winning_trades = df[df['pnl'] > 0] + losing_trades = df[df['pnl'] < 0] + win_rate = len(winning_trades) / len(df) + profit_factor = abs(winning_trades['pnl'].sum() / losing_trades['pnl'].sum()) + + # Calculate average win and loss + avg_win = winning_trades['pnl'].mean() + avg_loss = losing_trades['pnl'].mean() + + # Calculate consecutive wins and losses + df['consecutive'] = (df['pnl'] > 0).astype(int) + max_consecutive_wins = df['consecutive'].groupby((df['consecutive'] != df['consecutive'].shift()).cumsum()).sum().max() + max_consecutive_losses = df['consecutive'].groupby((df['consecutive'] != df['consecutive'].shift()).cumsum()).sum().min() + + self.risk_metrics = RiskMetrics( + daily_drawdown=daily_drawdown, + max_drawdown=max_drawdown, + sharpe_ratio=sharpe_ratio, + sortino_ratio=sortino_ratio, + win_rate=win_rate, + profit_factor=profit_factor, + avg_win=avg_win, + avg_loss=avg_loss, + max_consecutive_losses=max_consecutive_losses, + max_consecutive_wins=max_consecutive_wins + ) + + return self.risk_metrics + + def should_stop_trading(self) -> bool: + """ + Check if trading should be stopped based on risk metrics + """ + if not self.risk_metrics: + return False + + # Stop if maximum drawdown is exceeded + if self.risk_metrics.max_drawdown < -self.max_drawdown: + logger.warning("Maximum drawdown exceeded. Stopping trading.") + return True + + # Stop if win rate is too low + if self.risk_metrics.win_rate < self.config['min_win_rate']: + logger.warning("Win rate below minimum threshold. Stopping trading.") + return True + + # Stop if profit factor is too low + if self.risk_metrics.profit_factor < self.config['min_profit_factor']: + logger.warning("Profit factor below minimum threshold. Stopping trading.") + return True + + return False \ No newline at end of file diff --git a/run_backtest.py b/run_backtest.py new file mode 100644 index 0000000..e17e073 --- /dev/null +++ b/run_backtest.py @@ -0,0 +1,84 @@ +import os +import sys +from datetime import datetime, timedelta +import logging +from src.backtesting import Backtester + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('backtest.log'), + logging.StreamHandler(sys.stdout) + ] +) + +logger = logging.getLogger(__name__) + +def run_backtest_scenarios(): + # Load configuration + config_path = os.path.join(os.path.dirname(__file__), 'config', 'config.json') + + # Create backtester + backtester = Backtester(config_path) + + # Define test scenarios + scenarios = [ + { + 'name': 'BTC/USDT 1h - Last 30 days', + 'symbol': 'BTC/USDT', + 'timeframe': '1h', + 'days': 30 + }, + { + 'name': 'ETH/USDT 4h - Last 60 days', + 'symbol': 'ETH/USDT', + 'timeframe': '4h', + 'days': 60 + }, + { + 'name': 'BTC/USDT 1d - Last 90 days', + 'symbol': 'BTC/USDT', + 'timeframe': '1d', + 'days': 90 + } + ] + + # Run each scenario + for scenario in scenarios: + logger.info(f"\nRunning scenario: {scenario['name']}") + + # Calculate date range + end_date = datetime.now() + start_date = end_date - timedelta(days=scenario['days']) + + # Run backtest + results = backtester.run_backtest( + symbol=scenario['symbol'], + timeframe=scenario['timeframe'], + start_date=start_date, + end_date=end_date, + initial_capital=50.0 + ) + + if results: + # Print performance metrics + metrics = results['performance_metrics'] + print(f"\nResults for {scenario['name']}:") + print(f"Total Trades: {metrics['total_trades']}") + print(f"Win Rate: {metrics['win_rate']:.2%}") + print(f"Total Return: {metrics['total_return']:.2%}") + print(f"Annual Return: {metrics['annual_return']:.2%}") + print(f"Sharpe Ratio: {metrics['sharpe_ratio']:.2f}") + print(f"Max Drawdown: {metrics['max_drawdown']:.2%}") + + # Save results + filename = f"backtest_results_{scenario['symbol'].replace('/', '_')}_{scenario['timeframe']}.json" + backtester.save_results(filename) + logger.info(f"Results saved to {filename}") + else: + logger.error(f"Backtest failed for scenario: {scenario['name']}") + +if __name__ == "__main__": + run_backtest_scenarios() \ No newline at end of file diff --git a/sentiment_analysis.py b/sentiment_analysis.py new file mode 100644 index 0000000..7327580 --- /dev/null +++ b/sentiment_analysis.py @@ -0,0 +1,267 @@ +import numpy as np +import pandas as pd +from typing import Dict, List, Optional +import logging +from datetime import datetime, timedelta +import requests +from textblob import TextBlob +import tweepy +from newsapi import NewsApiClient +import yfinance as yf +from concurrent.futures import ThreadPoolExecutor +import json +import os + +logger = logging.getLogger(__name__) + +class SentimentAnalyzer: + def __init__(self, config: Dict): + self.config = config + self.news_api = NewsApiClient(api_key=config['news_api_key']) + self.twitter_auth = tweepy.OAuthHandler( + config['twitter_api_key'], + config['twitter_api_secret'] + ) + self.twitter_auth.set_access_token( + config['twitter_access_token'], + config['twitter_access_token_secret'] + ) + self.twitter_api = tweepy.API(self.twitter_auth) + + # Initialize sentiment cache + self.sentiment_cache = {} + self.cache_duration = timedelta(hours=1) + + def analyze_text(self, text: str) -> float: + """ + Analyze sentiment of a single text using TextBlob + """ + try: + analysis = TextBlob(text) + # Normalize sentiment score to [-1, 1] + return analysis.sentiment.polarity + except Exception as e: + logger.error(f"Error analyzing text: {str(e)}") + return 0.0 + + def get_news_sentiment(self, symbol: str) -> Optional[float]: + """ + Get sentiment from news articles + """ + try: + # Check cache first + cache_key = f"news_{symbol}" + if cache_key in self.sentiment_cache: + cached_data = self.sentiment_cache[cache_key] + if datetime.now() - cached_data['timestamp'] < self.cache_duration: + return cached_data['sentiment'] + + # Get news articles + news = self.news_api.get_everything( + q=symbol, + language='en', + from_param=(datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d'), + sort_by='relevancy' + ) + + if not news['articles']: + return None + + # Analyze sentiment of each article + sentiments = [] + for article in news['articles']: + title_sentiment = self.analyze_text(article['title']) + if article['description']: + desc_sentiment = self.analyze_text(article['description']) + sentiments.append((title_sentiment + desc_sentiment) / 2) + else: + sentiments.append(title_sentiment) + + # Calculate weighted average sentiment + avg_sentiment = np.mean(sentiments) + + # Cache the result + self.sentiment_cache[cache_key] = { + 'sentiment': avg_sentiment, + 'timestamp': datetime.now() + } + + return avg_sentiment + + except Exception as e: + logger.error(f"Error getting news sentiment: {str(e)}") + return None + + def get_twitter_sentiment(self, symbol: str) -> Optional[float]: + """ + Get sentiment from Twitter + """ + try: + # Check cache first + cache_key = f"twitter_{symbol}" + if cache_key in self.sentiment_cache: + cached_data = self.sentiment_cache[cache_key] + if datetime.now() - cached_data['timestamp'] < self.cache_duration: + return cached_data['sentiment'] + + # Get tweets + tweets = self.twitter_api.search_tweets( + q=f"${symbol}", + lang="en", + count=100 + ) + + if not tweets: + return None + + # Analyze sentiment of each tweet + sentiments = [] + for tweet in tweets: + sentiment = self.analyze_text(tweet.text) + sentiments.append(sentiment) + + # Calculate weighted average sentiment + avg_sentiment = np.mean(sentiments) + + # Cache the result + self.sentiment_cache[cache_key] = { + 'sentiment': avg_sentiment, + 'timestamp': datetime.now() + } + + return avg_sentiment + + except Exception as e: + logger.error(f"Error getting Twitter sentiment: {str(e)}") + return None + + def get_market_sentiment(self, symbol: str) -> Optional[float]: + """ + Get market sentiment indicators + """ + try: + # Check cache first + cache_key = f"market_{symbol}" + if cache_key in self.sentiment_cache: + cached_data = self.sentiment_cache[cache_key] + if datetime.now() - cached_data['timestamp'] < self.cache_duration: + return cached_data['sentiment'] + + # Get market data + ticker = yf.Ticker(symbol) + info = ticker.info + + # Calculate various sentiment indicators + sentiment_indicators = [] + + # RSI sentiment + if 'RSI' in info: + rsi = info['RSI'] + rsi_sentiment = (rsi - 50) / 50 # Normalize to [-1, 1] + sentiment_indicators.append(rsi_sentiment) + + # Volume sentiment + if 'volume' in info and 'averageVolume' in info: + volume_ratio = info['volume'] / info['averageVolume'] + volume_sentiment = (volume_ratio - 1) / volume_ratio # Normalize to [-1, 1] + sentiment_indicators.append(volume_sentiment) + + # Price momentum sentiment + if 'regularMarketChangePercent' in info: + momentum_sentiment = info['regularMarketChangePercent'] / 100 + sentiment_indicators.append(momentum_sentiment) + + if not sentiment_indicators: + return None + + # Calculate weighted average sentiment + avg_sentiment = np.mean(sentiment_indicators) + + # Cache the result + self.sentiment_cache[cache_key] = { + 'sentiment': avg_sentiment, + 'timestamp': datetime.now() + } + + return avg_sentiment + + except Exception as e: + logger.error(f"Error getting market sentiment: {str(e)}") + return None + + def get_combined_sentiment(self, symbol: str) -> Optional[float]: + """ + Get combined sentiment from all sources + """ + try: + # Check cache first + cache_key = f"combined_{symbol}" + if cache_key in self.sentiment_cache: + cached_data = self.sentiment_cache[cache_key] + if datetime.now() - cached_data['timestamp'] < self.cache_duration: + return cached_data['sentiment'] + + # Get sentiment from all sources + sentiments = [] + weights = [] + + # News sentiment + news_sentiment = self.get_news_sentiment(symbol) + if news_sentiment is not None: + sentiments.append(news_sentiment) + weights.append(self.config['sentiment_weights']['news']) + + # Twitter sentiment + twitter_sentiment = self.get_twitter_sentiment(symbol) + if twitter_sentiment is not None: + sentiments.append(twitter_sentiment) + weights.append(self.config['sentiment_weights']['twitter']) + + # Market sentiment + market_sentiment = self.get_market_sentiment(symbol) + if market_sentiment is not None: + sentiments.append(market_sentiment) + weights.append(self.config['sentiment_weights']['market']) + + if not sentiments: + return None + + # Calculate weighted average sentiment + avg_sentiment = np.average(sentiments, weights=weights) + + # Cache the result + self.sentiment_cache[cache_key] = { + 'sentiment': avg_sentiment, + 'timestamp': datetime.now() + } + + return avg_sentiment + + except Exception as e: + logger.error(f"Error getting combined sentiment: {str(e)}") + return None + + def clear_cache(self): + """ + Clear the sentiment cache + """ + self.sentiment_cache.clear() + + def get_sentiment_signal(self, symbol: str) -> int: + """ + Convert sentiment to trading signal + """ + sentiment = self.get_combined_sentiment(symbol) + if sentiment is None: + return 0 + + # Define sentiment thresholds + positive_threshold = self.config['sentiment_thresholds']['positive'] + negative_threshold = self.config['sentiment_thresholds']['negative'] + + if sentiment > positive_threshold: + return 1 # Buy signal + elif sentiment < negative_threshold: + return -1 # Sell signal + else: + return 0 # Neutral signal \ No newline at end of file diff --git a/technical_analysis.py b/technical_analysis.py new file mode 100644 index 0000000..a8b5f99 --- /dev/null +++ b/technical_analysis.py @@ -0,0 +1,293 @@ +import numpy as np +import pandas as pd +import talib +from typing import Dict, List, Tuple, Optional +import logging + +logger = logging.getLogger(__name__) + +class TechnicalAnalyzer: + def __init__(self, config: Dict): + self.config = config + self.indicators = {} + self.patterns = {} + + def add_indicators(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Add technical indicators to the dataframe + """ + df = df.copy() + + # Trend Indicators + df['sma_20'] = talib.SMA(df['close'], timeperiod=20) + df['sma_50'] = talib.SMA(df['close'], timeperiod=50) + df['sma_200'] = talib.SMA(df['close'], timeperiod=200) + df['ema_20'] = talib.EMA(df['close'], timeperiod=20) + + # Volatility Indicators + df['upperband'], df['middleband'], df['lowerband'] = talib.BBANDS( + df['close'], + timeperiod=self.config['indicators']['bollinger_bands']['period'], + nbdevup=self.config['indicators']['bollinger_bands']['std_dev'], + nbdevdn=self.config['indicators']['bollinger_bands']['std_dev'] + ) + df['atr'] = talib.ATR( + df['high'], + df['low'], + df['close'], + timeperiod=self.config['indicators']['atr']['period'] + ) + + # Momentum Indicators + df['rsi'] = talib.RSI( + df['close'], + timeperiod=self.config['indicators']['rsi']['period'] + ) + df['macd'], df['macdsignal'], df['macdhist'] = talib.MACD( + df['close'], + fastperiod=self.config['indicators']['macd']['fast_period'], + slowperiod=self.config['indicators']['macd']['slow_period'], + signalperiod=self.config['indicators']['macd']['signal_period'] + ) + + # Volume Indicators + df['obv'] = talib.OBV(df['close'], df['volume']) + + # Additional Indicators + df['adx'] = talib.ADX(df['high'], df['low'], df['close'], timeperiod=14) + df['cci'] = talib.CCI(df['high'], df['low'], df['close'], timeperiod=14) + df['stoch_k'], df['stoch_d'] = talib.STOCH( + df['high'], + df['low'], + df['close'], + fastk_period=14, + slowk_period=3, + slowd_period=3 + ) + + # Store indicators for reference + self.indicators = { + 'trend': ['sma_20', 'sma_50', 'sma_200', 'ema_20'], + 'volatility': ['upperband', 'middleband', 'lowerband', 'atr'], + 'momentum': ['rsi', 'macd', 'macdsignal', 'macdhist'], + 'volume': ['obv'], + 'additional': ['adx', 'cci', 'stoch_k', 'stoch_d'] + } + + return df + + def detect_patterns(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Detect candlestick patterns and chart patterns + """ + df = df.copy() + + # Candlestick Patterns + if self.config['patterns']['candlestick']: + df['doji'] = talib.CDLDOJI(df['open'], df['high'], df['low'], df['close']) + df['hammer'] = talib.CDLHAMMER(df['open'], df['high'], df['low'], df['close']) + df['engulfing'] = talib.CDLENGULFING(df['open'], df['high'], df['low'], df['close']) + df['morning_star'] = talib.CDLMORNINGSTAR(df['open'], df['high'], df['low'], df['close']) + df['evening_star'] = talib.CDLEVENINGSTAR(df['open'], df['high'], df['low'], df['close']) + + # Chart Patterns + if self.config['patterns']['chart']: + df['head_and_shoulders'] = self._detect_head_and_shoulders(df) + df['double_top'] = self._detect_double_top(df) + df['double_bottom'] = self._detect_double_bottom(df) + df['triangle'] = self._detect_triangle(df) + + return df + + def _detect_head_and_shoulders(self, df: pd.DataFrame) -> pd.Series: + """ + Detect head and shoulders pattern + """ + pattern = pd.Series(0, index=df.index) + + for i in range(20, len(df) - 20): + # Find potential left shoulder + left_shoulder = df['high'].iloc[i-20:i].max() + left_shoulder_idx = df['high'].iloc[i-20:i].idxmax() + + # Find potential head + head = df['high'].iloc[i-10:i+10].max() + head_idx = df['high'].iloc[i-10:i+10].idxmax() + + # Find potential right shoulder + right_shoulder = df['high'].iloc[i:i+20].max() + right_shoulder_idx = df['high'].iloc[i:i+20].idxmax() + + # Find neckline + neckline = min(df['low'].iloc[left_shoulder_idx:right_shoulder_idx]) + + # Check pattern conditions + if (left_shoulder < head and right_shoulder < head and + abs(left_shoulder - right_shoulder) / head < 0.1): + pattern.iloc[i] = 1 + + return pattern + + def _detect_double_top(self, df: pd.DataFrame) -> pd.Series: + """ + Detect double top pattern + """ + pattern = pd.Series(0, index=df.index) + + for i in range(20, len(df) - 20): + # Find potential first peak + first_peak = df['high'].iloc[i-20:i].max() + first_peak_idx = df['high'].iloc[i-20:i].idxmax() + + # Find potential second peak + second_peak = df['high'].iloc[i:i+20].max() + second_peak_idx = df['high'].iloc[i:i+20].idxmax() + + # Find valley between peaks + valley = df['low'].iloc[first_peak_idx:second_peak_idx].min() + + # Check pattern conditions + if (abs(first_peak - second_peak) / first_peak < 0.02 and + (first_peak - valley) / first_peak > 0.02): + pattern.iloc[i] = 1 + + return pattern + + def _detect_double_bottom(self, df: pd.DataFrame) -> pd.Series: + """ + Detect double bottom pattern + """ + pattern = pd.Series(0, index=df.index) + + for i in range(20, len(df) - 20): + # Find potential first bottom + first_bottom = df['low'].iloc[i-20:i].min() + first_bottom_idx = df['low'].iloc[i-20:i].idxmin() + + # Find potential second bottom + second_bottom = df['low'].iloc[i:i+20].min() + second_bottom_idx = df['low'].iloc[i:i+20].idxmin() + + # Find peak between bottoms + peak = df['high'].iloc[first_bottom_idx:second_bottom_idx].max() + + # Check pattern conditions + if (abs(first_bottom - second_bottom) / first_bottom < 0.02 and + (peak - first_bottom) / first_bottom > 0.02): + pattern.iloc[i] = 1 + + return pattern + + def _detect_triangle(self, df: pd.DataFrame) -> pd.Series: + """ + Detect triangle patterns (ascending, descending, symmetrical) + """ + pattern = pd.Series(0, index=df.index) + + for i in range(20, len(df) - 20): + # Get highs and lows for the period + highs = df['high'].iloc[i-20:i] + lows = df['low'].iloc[i-20:i] + + # Calculate trend lines + high_slope = np.polyfit(range(len(highs)), highs, 1)[0] + low_slope = np.polyfit(range(len(lows)), lows, 1)[0] + + # Classify triangle type + if abs(high_slope) < 0.001 and low_slope > 0.001: + pattern.iloc[i] = 1 # Ascending triangle + elif high_slope < -0.001 and abs(low_slope) < 0.001: + pattern.iloc[i] = 2 # Descending triangle + elif abs(high_slope + low_slope) < 0.001: + pattern.iloc[i] = 3 # Symmetrical triangle + + return pattern + + def generate_signals(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Generate trading signals based on technical indicators and patterns + """ + df = df.copy() + + # Initialize signal column + df['signal'] = 0 + + # RSI signals + df.loc[df['rsi'] < self.config['indicators']['rsi']['oversold'], 'signal'] += 1 + df.loc[df['rsi'] > self.config['indicators']['rsi']['overbought'], 'signal'] -= 1 + + # MACD signals + df.loc[df['macd'] > df['macdsignal'], 'signal'] += 1 + df.loc[df['macd'] < df['macdsignal'], 'signal'] -= 1 + + # Bollinger Bands signals + df.loc[df['close'] < df['lowerband'], 'signal'] += 1 + df.loc[df['close'] > df['upperband'], 'signal'] -= 1 + + # Trend signals + df.loc[df['close'] > df['sma_20'], 'signal'] += 1 + df.loc[df['close'] < df['sma_20'], 'signal'] -= 1 + + # Pattern signals + if 'head_and_shoulders' in df.columns: + df.loc[df['head_and_shoulders'] == 1, 'signal'] -= 1 + if 'double_top' in df.columns: + df.loc[df['double_top'] == 1, 'signal'] -= 1 + if 'double_bottom' in df.columns: + df.loc[df['double_bottom'] == 1, 'signal'] += 1 + if 'triangle' in df.columns: + df.loc[df['triangle'] == 1, 'signal'] += 1 # Ascending + df.loc[df['triangle'] == 2, 'signal'] -= 1 # Descending + + # Normalize signals to -1, 0, 1 + df['signal'] = df['signal'].apply(lambda x: 1 if x > 2 else (-1 if x < -2 else 0)) + + return df + + def calculate_support_resistance(self, df: pd.DataFrame, window: int = 20) -> Tuple[pd.Series, pd.Series]: + """ + Calculate support and resistance levels + """ + support = df['low'].rolling(window=window).min() + resistance = df['high'].rolling(window=window).max() + + return support, resistance + + def calculate_volatility(self, df: pd.DataFrame) -> pd.Series: + """ + Calculate various volatility measures + """ + # ATR-based volatility + atr_volatility = df['atr'] / df['close'] + + # Bollinger Band width + bb_width = (df['upperband'] - df['lowerband']) / df['middleband'] + + # Historical volatility + returns = df['close'].pct_change() + hist_volatility = returns.rolling(window=20).std() + + return pd.DataFrame({ + 'atr_volatility': atr_volatility, + 'bb_width': bb_width, + 'hist_volatility': hist_volatility + }) + + def get_market_regime(self, df: pd.DataFrame) -> pd.Series: + """ + Determine market regime (trending, ranging, volatile) + """ + regime = pd.Series('unknown', index=df.index) + + # Calculate ADX for trend strength + adx = df['adx'] + + # Calculate volatility + volatility = self.calculate_volatility(df)['atr_volatility'] + + # Determine regime + regime.loc[adx > 25] = 'trending' + regime.loc[(adx <= 25) & (volatility > volatility.rolling(window=20).mean())] = 'volatile' + regime.loc[(adx <= 25) & (volatility <= volatility.rolling(window=20).mean())] = 'ranging' + + return regime \ No newline at end of file diff --git a/trading_strategy.py b/trading_strategy.py new file mode 100644 index 0000000..7fbbc9b --- /dev/null +++ b/trading_strategy.py @@ -0,0 +1,303 @@ +import numpy as np +import pandas as pd +from typing import Dict, List, Optional, Tuple +import logging +from datetime import datetime +from dataclasses import dataclass +import json +import os + +from .technical_analysis import TechnicalAnalyzer +from .machine_learning import MachineLearningModel +from .risk_management import RiskManager +from .sentiment_analysis import SentimentAnalyzer + +logger = logging.getLogger(__name__) + +@dataclass +class TradeSignal: + """Data class to store trade signals""" + symbol: str + action: str # 'buy', 'sell', or 'hold' + confidence: float + price: float + stop_loss: float + take_profit: float + position_size: float + timestamp: datetime + technical_score: float + ml_score: float + sentiment_score: float + risk_score: float + +class TradingStrategy: + def __init__(self, config: Dict): + self.config = config + self.technical_analyzer = TechnicalAnalyzer(config) + self.ml_model = MachineLearningModel(config) + self.risk_manager = RiskManager(config) + self.sentiment_analyzer = SentimentAnalyzer(config) + + # Initialize state + self.active_trades = {} + self.trade_history = [] + self.performance_metrics = {} + + def analyze_market(self, symbol: str, data: pd.DataFrame) -> TradeSignal: + """ + Analyze market conditions and generate trading signals + """ + try: + # Technical analysis + technical_signals = self.technical_analyzer.generate_signals(data) + technical_score = technical_signals['signal'] + + # Machine learning prediction + ml_predictions = self.ml_model.predict(data) + ml_score = ml_predictions[-1] if ml_predictions is not None else 0 + + # Sentiment analysis + sentiment_score = self.sentiment_analyzer.get_combined_sentiment(symbol) + if sentiment_score is None: + sentiment_score = 0 + + # Risk assessment + risk_score = self.risk_manager.calculate_risk_metrics() + + # Combine signals + combined_score = ( + technical_score * self.config['signal_weights']['technical'] + + ml_score * self.config['signal_weights']['ml'] + + sentiment_score * self.config['signal_weights']['sentiment'] + + risk_score * self.config['signal_weights']['risk'] + ) + + # Generate trading signal + current_price = data['close'].iloc[-1] + + # Calculate stop loss and take profit levels + stop_loss = self._calculate_stop_loss(current_price, combined_score) + take_profit = self._calculate_take_profit(current_price, combined_score) + + # Calculate position size + position_size, required_margin = self.risk_manager.calculate_position_size( + current_price, + stop_loss, + self.risk_manager.current_capital + ) + + # Determine action + if combined_score > self.config['signal_thresholds']['buy']: + action = 'buy' + elif combined_score < self.config['signal_thresholds']['sell']: + action = 'sell' + else: + action = 'hold' + + # Create trade signal + signal = TradeSignal( + symbol=symbol, + action=action, + confidence=abs(combined_score), + price=current_price, + stop_loss=stop_loss, + take_profit=take_profit, + position_size=position_size, + timestamp=datetime.now(), + technical_score=technical_score, + ml_score=ml_score, + sentiment_score=sentiment_score, + risk_score=risk_score + ) + + return signal + + except Exception as e: + logger.error(f"Error analyzing market: {str(e)}") + return None + + def execute_trade(self, signal: TradeSignal) -> bool: + """ + Execute a trade based on the signal + """ + try: + # Validate trade + if not self.risk_manager.validate_trade( + signal.symbol, + signal.position_size, + signal.price, + signal.stop_loss, + signal.take_profit + ): + return False + + # Execute trade + if signal.action in ['buy', 'sell']: + # Update position + pnl = self.risk_manager.update_position( + signal.symbol, + signal.price, + signal.price, + signal.position_size, + signal.action + ) + + if pnl is not None: + # Record trade + self.trade_history.append({ + 'symbol': signal.symbol, + 'action': signal.action, + 'entry_price': signal.price, + 'exit_price': signal.price, + 'position_size': signal.position_size, + 'pnl': pnl, + 'entry_time': signal.timestamp, + 'exit_time': datetime.now() + }) + + # Update performance metrics + self._update_performance_metrics() + + return True + + return False + + except Exception as e: + logger.error(f"Error executing trade: {str(e)}") + return False + + def update_positions(self, current_prices: Dict[str, float]): + """ + Update all open positions with current prices + """ + try: + for symbol, price in current_prices.items(): + if symbol in self.risk_manager.positions: + position = self.risk_manager.positions[symbol] + pnl = self.risk_manager.update_position( + symbol, + position['entry_price'], + price, + position['position_size'], + position['position_type'] + ) + + if pnl is not None: + # Record trade + self.trade_history.append({ + 'symbol': symbol, + 'action': 'close', + 'entry_price': position['entry_price'], + 'exit_price': price, + 'position_size': position['position_size'], + 'pnl': pnl, + 'entry_time': position['entry_time'], + 'exit_time': datetime.now() + }) + + # Update performance metrics + self._update_performance_metrics() + + except Exception as e: + logger.error(f"Error updating positions: {str(e)}") + + def _calculate_stop_loss(self, price: float, signal: float) -> float: + """ + Calculate stop loss level based on signal strength + """ + # Adjust stop loss distance based on signal strength + base_stop_loss = self.config['stop_loss_pct'] + signal_factor = abs(signal) + stop_loss_distance = base_stop_loss * (1 + signal_factor) + + if signal > 0: # Buy signal + return price * (1 - stop_loss_distance) + else: # Sell signal + return price * (1 + stop_loss_distance) + + def _calculate_take_profit(self, price: float, signal: float) -> float: + """ + Calculate take profit level based on signal strength + """ + # Adjust take profit distance based on signal strength + base_take_profit = self.config['take_profit_pct'] + signal_factor = abs(signal) + take_profit_distance = base_take_profit * (1 + signal_factor) + + if signal > 0: # Buy signal + return price * (1 + take_profit_distance) + else: # Sell signal + return price * (1 - take_profit_distance) + + def _update_performance_metrics(self): + """ + Update performance metrics based on trade history + """ + if not self.trade_history: + return + + # Convert trade history to DataFrame + df = pd.DataFrame(self.trade_history) + + # Calculate metrics + self.performance_metrics = { + 'total_trades': len(df), + 'winning_trades': len(df[df['pnl'] > 0]), + 'losing_trades': len(df[df['pnl'] < 0]), + 'win_rate': len(df[df['pnl'] > 0]) / len(df), + 'total_pnl': df['pnl'].sum(), + 'avg_pnl': df['pnl'].mean(), + 'max_drawdown': self.risk_manager.risk_metrics.max_drawdown if self.risk_manager.risk_metrics else 0, + 'sharpe_ratio': self.risk_manager.risk_metrics.sharpe_ratio if self.risk_manager.risk_metrics else 0, + 'profit_factor': self.risk_manager.risk_metrics.profit_factor if self.risk_manager.risk_metrics else 0 + } + + def should_stop_trading(self) -> bool: + """ + Check if trading should be stopped based on risk metrics + """ + return self.risk_manager.should_stop_trading() + + def save_state(self, filepath: str): + """ + Save trading strategy state + """ + try: + state = { + 'active_trades': self.active_trades, + 'trade_history': self.trade_history, + 'performance_metrics': self.performance_metrics, + 'risk_metrics': self.risk_manager.risk_metrics.__dict__ if self.risk_manager.risk_metrics else None + } + + with open(filepath, 'w') as f: + json.dump(state, f, default=str) + + logger.info(f"Trading strategy state saved to {filepath}") + return True + + except Exception as e: + logger.error(f"Error saving state: {str(e)}") + return False + + def load_state(self, filepath: str): + """ + Load trading strategy state + """ + try: + with open(filepath, 'r') as f: + state = json.load(f) + + self.active_trades = state['active_trades'] + self.trade_history = state['trade_history'] + self.performance_metrics = state['performance_metrics'] + + if state['risk_metrics']: + self.risk_manager.risk_metrics = RiskMetrics(**state['risk_metrics']) + + logger.info(f"Trading strategy state loaded from {filepath}") + return True + + except Exception as e: + logger.error(f"Error loading state: {str(e)}") + return False \ No newline at end of file diff --git a/trading_system.py b/trading_system.py new file mode 100644 index 0000000..5a68e1b --- /dev/null +++ b/trading_system.py @@ -0,0 +1,318 @@ +import numpy as np +import pandas as pd +from datetime import datetime, timedelta +import logging +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass +import json +import os + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('logs/trading_system.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +@dataclass +class Trade: + symbol: str + direction: str + entry_price: float + stop_loss: float + take_profit: float + position_size: float + entry_time: datetime + exit_price: Optional[float] = None + exit_time: Optional[datetime] = None + pnl: Optional[float] = None + status: str = 'open' + +class TradingSystem: + def __init__(self, initial_capital: float = 50.0): + self.initial_capital = initial_capital + self.current_capital = initial_capital + self.performance_history: List[Dict] = [] + self.active_trades: Dict[str, Trade] = {} + self.trade_history: List[Trade] = [] + self.strategy_parameters = self._get_initial_parameters() + self.risk_metrics = self._initialize_risk_metrics() + self.last_recalibration = datetime.now() + + # Load configuration + self._load_config() + + def _load_config(self): + """Load configuration from config file""" + try: + with open('config/config.json', 'r') as f: + self.config = json.load(f) + except FileNotFoundError: + logger.warning("Config file not found. Using default parameters.") + self.config = self._get_default_config() + + def _get_default_config(self) -> Dict: + """Get default configuration parameters""" + return { + 'risk_per_trade': 0.01, # 1% risk per trade + 'max_positions': 2, + 'min_win_rate': 0.4, + 'recalibration_window': 20, + 'max_drawdown': 0.1, # 10% maximum drawdown + 'leverage': 1, # No leverage initially + 'position_sizing': { + 'method': 'fixed_fractional', + 'fraction': 0.01 # 1% of capital per trade + } + } + + def _get_initial_parameters(self) -> Dict: + """Get initial strategy parameters""" + return { + 'rsi_period': 14, + 'rsi_overbought': 70, + 'rsi_oversold': 30, + 'bb_period': 20, + 'bb_std': 2, + 'macd_fast': 12, + 'macd_slow': 26, + 'macd_signal': 9, + 'atr_period': 14, + 'atr_multiplier': 2 + } + + def _initialize_risk_metrics(self) -> Dict: + """Initialize risk metrics tracking""" + return { + 'total_trades': 0, + 'winning_trades': 0, + 'losing_trades': 0, + 'win_rate': 0.0, + 'profit_factor': 0.0, + 'max_drawdown': 0.0, + 'current_drawdown': 0.0, + 'avg_trade': 0.0, + 'largest_win': 0.0, + 'largest_loss': 0.0 + } + + def monitor_performance(self, window_size: int = 20) -> bool: + """ + Monitor recent performance and determine if recalibration is needed + Returns True if recalibration is needed + """ + if len(self.performance_history) < window_size: + return False + + recent_performance = self.performance_history[-window_size:] + win_rate = sum(1 for trade in recent_performance if trade['pnl'] > 0) / window_size + + # Check various performance metrics + needs_recalibration = False + + # Win rate check + if win_rate < self.config['min_win_rate']: + logger.warning(f"Win rate {win_rate:.2%} below threshold {self.config['min_win_rate']:.2%}") + needs_recalibration = True + + # Drawdown check + current_drawdown = self._calculate_drawdown() + if current_drawdown > self.config['max_drawdown']: + logger.warning(f"Current drawdown {current_drawdown:.2%} exceeds maximum {self.config['max_drawdown']:.2%}") + needs_recalibration = True + + # Profit factor check + profit_factor = self._calculate_profit_factor(recent_performance) + if profit_factor < 1.0: + logger.warning(f"Profit factor {profit_factor:.2f} below 1.0") + needs_recalibration = True + + return needs_recalibration + + def recalibrate_strategy(self, market_data: pd.DataFrame): + """ + Adjust strategy parameters based on recent market conditions + """ + logger.info("Starting strategy recalibration") + + # Analyze market conditions + volatility = self._calculate_volatility(market_data) + trend_strength = self._calculate_trend_strength(market_data) + + # Adjust parameters based on market conditions + new_parameters = self.strategy_parameters.copy() + + # Adjust RSI levels based on volatility + if volatility > 0.02: # High volatility + new_parameters['rsi_overbought'] = 75 + new_parameters['rsi_oversold'] = 25 + else: # Low volatility + new_parameters['rsi_overbought'] = 70 + new_parameters['rsi_oversold'] = 30 + + # Adjust ATR multiplier based on trend strength + if trend_strength > 0.7: # Strong trend + new_parameters['atr_multiplier'] = 2.5 + else: # Weak trend + new_parameters['atr_multiplier'] = 2.0 + + # Update parameters + self.strategy_parameters = new_parameters + self.last_recalibration = datetime.now() + + logger.info("Strategy recalibration completed") + logger.info(f"New parameters: {new_parameters}") + + def calculate_position_size(self, entry_price: float, stop_loss: float) -> float: + """ + Calculate position size based on risk management rules + """ + risk_amount = self.current_capital * self.config['risk_per_trade'] + risk_per_unit = abs(entry_price - stop_loss) + + if risk_per_unit == 0: + logger.warning("Risk per unit is zero. Cannot calculate position size.") + return 0 + + position_size = risk_amount / risk_per_unit + + # Apply leverage if configured + if self.config['leverage'] > 1: + position_size *= self.config['leverage'] + + # Ensure position size doesn't exceed maximum allowed + max_position = self.current_capital * self.config['position_sizing']['fraction'] + position_size = min(position_size, max_position) + + return position_size + + def _calculate_volatility(self, data: pd.DataFrame) -> float: + """Calculate market volatility""" + returns = data['close'].pct_change() + return returns.std() + + def _calculate_trend_strength(self, data: pd.DataFrame) -> float: + """Calculate trend strength using ADX""" + # Implementation would go here + return 0.5 # Placeholder + + def _calculate_drawdown(self) -> float: + """Calculate current drawdown""" + if not self.performance_history: + return 0.0 + + peak = max(self.performance_history, key=lambda x: x['equity'])['equity'] + current = self.performance_history[-1]['equity'] + return (peak - current) / peak + + def _calculate_profit_factor(self, trades: List[Dict]) -> float: + """Calculate profit factor from recent trades""" + gross_profit = sum(t['pnl'] for t in trades if t['pnl'] > 0) + gross_loss = abs(sum(t['pnl'] for t in trades if t['pnl'] < 0)) + + if gross_loss == 0: + return float('inf') + + return gross_profit / gross_loss + + def update_risk_metrics(self, trade: Trade): + """Update risk metrics after a trade""" + self.risk_metrics['total_trades'] += 1 + + if trade.pnl and trade.pnl > 0: + self.risk_metrics['winning_trades'] += 1 + self.risk_metrics['largest_win'] = max( + self.risk_metrics['largest_win'], + trade.pnl + ) + elif trade.pnl and trade.pnl < 0: + self.risk_metrics['losing_trades'] += 1 + self.risk_metrics['largest_loss'] = min( + self.risk_metrics['largest_loss'], + trade.pnl + ) + + # Update win rate + if self.risk_metrics['total_trades'] > 0: + self.risk_metrics['win_rate'] = ( + self.risk_metrics['winning_trades'] / + self.risk_metrics['total_trades'] + ) + + # Update average trade + if trade.pnl: + self.risk_metrics['avg_trade'] = ( + (self.risk_metrics['avg_trade'] * (self.risk_metrics['total_trades'] - 1) + + trade.pnl) / self.risk_metrics['total_trades'] + ) + + def save_state(self): + """Save current system state""" + state = { + 'current_capital': self.current_capital, + 'strategy_parameters': self.strategy_parameters, + 'risk_metrics': self.risk_metrics, + 'last_recalibration': self.last_recalibration.isoformat(), + 'active_trades': { + symbol: { + 'direction': trade.direction, + 'entry_price': trade.entry_price, + 'stop_loss': trade.stop_loss, + 'take_profit': trade.take_profit, + 'position_size': trade.position_size, + 'entry_time': trade.entry_time.isoformat() + } + for symbol, trade in self.active_trades.items() + } + } + + try: + with open('data/system_state.json', 'w') as f: + json.dump(state, f, indent=4) + except Exception as e: + logger.error(f"Error saving system state: {str(e)}") + + def load_state(self): + """Load system state from file""" + try: + with open('data/system_state.json', 'r') as f: + state = json.load(f) + + self.current_capital = state['current_capital'] + self.strategy_parameters = state['strategy_parameters'] + self.risk_metrics = state['risk_metrics'] + self.last_recalibration = datetime.fromisoformat(state['last_recalibration']) + + # Reconstruct active trades + self.active_trades = {} + for symbol, trade_data in state['active_trades'].items(): + self.active_trades[symbol] = Trade( + symbol=symbol, + direction=trade_data['direction'], + entry_price=trade_data['entry_price'], + stop_loss=trade_data['stop_loss'], + take_profit=trade_data['take_profit'], + position_size=trade_data['position_size'], + entry_time=datetime.fromisoformat(trade_data['entry_time']) + ) + except FileNotFoundError: + logger.info("No saved state found. Starting fresh.") + except Exception as e: + logger.error(f"Error loading system state: {str(e)}") + + def get_system_status(self) -> Dict: + """Get current system status""" + return { + 'current_capital': self.current_capital, + 'total_trades': self.risk_metrics['total_trades'], + 'win_rate': self.risk_metrics['win_rate'], + 'profit_factor': self.risk_metrics['profit_factor'], + 'current_drawdown': self.risk_metrics['current_drawdown'], + 'active_trades': len(self.active_trades), + 'last_recalibration': self.last_recalibration.isoformat(), + 'strategy_parameters': self.strategy_parameters + } \ No newline at end of file diff --git a/visualization.py b/visualization.py new file mode 100644 index 0000000..df97be0 --- /dev/null +++ b/visualization.py @@ -0,0 +1,169 @@ +import pandas as pd +import plotly.graph_objects as go +from plotly.subplots import make_subplots +from typing import Dict, List +import json +import os + +def plot_backtest_results(results_file: str, save_path: str = None): + """ + Create interactive plots for backtest results + """ + # Load results + with open(results_file, 'r') as f: + results = json.load(f) + + # Convert data to DataFrames + trades_df = pd.DataFrame(results['trades']) + equity_df = pd.DataFrame(results['equity_curve']) + + # Create figure with secondary y-axis + fig = make_subplots( + rows=2, cols=1, + shared_xaxes=True, + vertical_spacing=0.03, + subplot_titles=('Price and Trades', 'Equity Curve'), + row_heights=[0.7, 0.3] + ) + + # Add price line + fig.add_trace( + go.Scatter( + x=trades_df['timestamp'], + y=trades_df['price'], + name='Price', + line=dict(color='blue') + ), + row=1, col=1 + ) + + # Add buy trades + buy_trades = trades_df[trades_df['action'] == 'buy'] + fig.add_trace( + go.Scatter( + x=buy_trades['timestamp'], + y=buy_trades['price'], + mode='markers', + name='Buy', + marker=dict(color='green', size=10) + ), + row=1, col=1 + ) + + # Add sell trades + sell_trades = trades_df[trades_df['action'] == 'sell'] + fig.add_trace( + go.Scatter( + x=sell_trades['timestamp'], + y=sell_trades['price'], + mode='markers', + name='Sell', + marker=dict(color='red', size=10) + ), + row=1, col=1 + ) + + # Add equity curve + fig.add_trace( + go.Scatter( + x=equity_df['timestamp'], + y=equity_df['equity'], + name='Equity', + line=dict(color='purple') + ), + row=2, col=1 + ) + + # Update layout + fig.update_layout( + title='Backtest Results', + xaxis_title='Date', + yaxis_title='Price', + yaxis2_title='Equity', + showlegend=True, + height=800 + ) + + # Save plot if path is provided + if save_path: + fig.write_html(save_path) + print(f"Plot saved to {save_path}") + + return fig + +def plot_performance_metrics(results_files: List[str], save_path: str = None): + """ + Create comparison plot of performance metrics across different scenarios + """ + metrics_data = [] + + for file in results_files: + with open(file, 'r') as f: + results = json.load(f) + metrics = results['performance_metrics'] + + # Extract scenario name from filename + scenario_name = os.path.basename(file).replace('backtest_results_', '').replace('.json', '') + + metrics_data.append({ + 'Scenario': scenario_name, + 'Total Return': metrics['total_return'], + 'Annual Return': metrics['annual_return'], + 'Sharpe Ratio': metrics['sharpe_ratio'], + 'Max Drawdown': metrics['max_drawdown'], + 'Win Rate': metrics['win_rate'] + }) + + # Create DataFrame + df = pd.DataFrame(metrics_data) + + # Create figure + fig = go.Figure() + + # Add bars for each metric + metrics = ['Total Return', 'Annual Return', 'Sharpe Ratio', 'Max Drawdown', 'Win Rate'] + for metric in metrics: + fig.add_trace( + go.Bar( + name=metric, + x=df['Scenario'], + y=df[metric] + ) + ) + + # Update layout + fig.update_layout( + title='Performance Metrics Comparison', + xaxis_title='Scenario', + yaxis_title='Value', + barmode='group', + height=600 + ) + + # Save plot if path is provided + if save_path: + fig.write_html(save_path) + print(f"Plot saved to {save_path}") + + return fig + +def main(): + # Example usage + results_dir = 'backtest_results' + results_files = [ + os.path.join(results_dir, f) for f in os.listdir(results_dir) + if f.startswith('backtest_results_') and f.endswith('.json') + ] + + # Create plots for each scenario + for file in results_files: + scenario_name = os.path.basename(file).replace('.json', '') + plot_path = os.path.join(results_dir, f'{scenario_name}_plot.html') + plot_backtest_results(file, plot_path) + + # Create comparison plot + comparison_path = os.path.join(results_dir, 'performance_comparison.html') + plot_performance_metrics(results_files, comparison_path) + +if __name__ == "__main__": + main() \ No newline at end of file