first
This commit is contained in:
commit
69462cf3e0
18
.env.template
Normal file
18
.env.template
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Alpaca API Configuration
|
||||||
|
# Get these from your Alpaca paper trading account at https://app.alpaca.markets/paper/dashboard/overview
|
||||||
|
|
||||||
|
# Your Alpaca API Key (Paper Trading)
|
||||||
|
ALPACA_API_KEY=PKXR08ET6CSGV5QFS89E
|
||||||
|
|
||||||
|
# Your Alpaca Secret Key (Paper Trading)
|
||||||
|
ALPACA_SECRET_KEY=5ILlNGVM7WfPwJk8kPRlHhQwDO022H19RWBOkwgd
|
||||||
|
|
||||||
|
# Trading Configuration
|
||||||
|
SYMBOL=ETH/USD
|
||||||
|
STRATEGY_TYPE=enhanced
|
||||||
|
MAX_POSITION_SIZE=0.95
|
||||||
|
RISK_PER_TRADE=0.02
|
||||||
|
|
||||||
|
# Risk Management
|
||||||
|
MAX_DRAWDOWN_LIMIT=0.15
|
||||||
|
DAILY_LOSS_LIMIT=0.05
|
||||||
70
.gitignore
vendored
Normal file
70
.gitignore
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Environment variables and secrets
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.old
|
||||||
|
|
||||||
|
# Data files (optional - uncomment if you don't want to track data)
|
||||||
|
# *.csv
|
||||||
|
# *.json
|
||||||
|
# *.parquet
|
||||||
|
|
||||||
|
# Trading specific
|
||||||
|
/trading_data/
|
||||||
|
/backtest_results/
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.pdf
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
.tmp/
|
||||||
141
PROJECT_OVERVIEW.md
Normal file
141
PROJECT_OVERVIEW.md
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# Project Setup Complete! 🎉
|
||||||
|
|
||||||
|
## What You Now Have
|
||||||
|
|
||||||
|
✅ **Professional Trading System Structure**
|
||||||
|
- Modular, maintainable, and scalable architecture
|
||||||
|
- Clear separation of concerns (data, strategy, risk, execution)
|
||||||
|
- Comprehensive logging and monitoring
|
||||||
|
- Full configuration management
|
||||||
|
|
||||||
|
✅ **Safety Features**
|
||||||
|
- Paper trading by default (no real money at risk)
|
||||||
|
- Multiple layers of risk management
|
||||||
|
- Emergency stops and circuit breakers
|
||||||
|
- Data validation and error handling
|
||||||
|
|
||||||
|
✅ **Easy to Use**
|
||||||
|
- Simple command-line interface
|
||||||
|
- Quick start script (`run.py`)
|
||||||
|
- Comprehensive documentation
|
||||||
|
- System testing capabilities
|
||||||
|
|
||||||
|
## Quick Start Guide
|
||||||
|
|
||||||
|
### 1. Install Dependencies
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure API Keys
|
||||||
|
Copy `.env.template` to `.env` and add your Alpaca paper trading API keys:
|
||||||
|
```bash
|
||||||
|
cp .env.template .env
|
||||||
|
# Edit .env with your API credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test the System
|
||||||
|
```bash
|
||||||
|
python test_system.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Run Backtesting
|
||||||
|
```bash
|
||||||
|
python main.py --mode backtest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Start Live Paper Trading (when ready)
|
||||||
|
```bash
|
||||||
|
python main.py --mode live
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the interactive menu:
|
||||||
|
```bash
|
||||||
|
python run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
trading/
|
||||||
|
├── main.py # 🚀 Main application entry point
|
||||||
|
├── run.py # 🎯 Interactive quick start menu
|
||||||
|
├── test_system.py # 🧪 System verification tests
|
||||||
|
├── requirements.txt # 📦 Python dependencies
|
||||||
|
├── .env.template # 🔐 Environment variables template
|
||||||
|
├── README.md # 📖 Comprehensive documentation
|
||||||
|
├──
|
||||||
|
├── config/ # ⚙️ Configuration Management
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── trading_config.py
|
||||||
|
├──
|
||||||
|
├── src/ # 🏗️ Core System Components
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── data_handler.py # 📊 Market data operations
|
||||||
|
│ ├── strategy.py # 🎯 Trading strategy logic
|
||||||
|
│ ├── risk_manager.py # 🛡️ Risk management system
|
||||||
|
│ ├── trading_engine.py # ⚡ Core trading engine
|
||||||
|
│ └── logger_setup.py # 📝 Logging configuration
|
||||||
|
├──
|
||||||
|
└── logs/ # 📋 Log files (created automatically)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Improvements from Original Code
|
||||||
|
|
||||||
|
### 🏗️ **Architecture**
|
||||||
|
- **Before**: Single monolithic files
|
||||||
|
- **After**: Modular, object-oriented design with clear responsibilities
|
||||||
|
|
||||||
|
### 🛡️ **Safety**
|
||||||
|
- **Before**: Hardcoded credentials, no risk management
|
||||||
|
- **After**: Environment variables, comprehensive risk controls, paper trading default
|
||||||
|
|
||||||
|
### 📊 **Maintainability**
|
||||||
|
- **Before**: Mixed concerns, hard to modify
|
||||||
|
- **After**: Clean interfaces, easy to customize and extend
|
||||||
|
|
||||||
|
### 🔧 **Configuration**
|
||||||
|
- **Before**: Hardcoded values throughout code
|
||||||
|
- **After**: Centralized configuration, environment-based settings
|
||||||
|
|
||||||
|
### 📝 **Monitoring**
|
||||||
|
- **Before**: Basic print statements
|
||||||
|
- **After**: Professional logging system with multiple log files
|
||||||
|
|
||||||
|
### 🚀 **Usability**
|
||||||
|
- **Before**: Required code modification to run
|
||||||
|
- **After**: Command-line interface, interactive menu, easy deployment
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Review Configuration**: Check `config/trading_config.py` for strategy parameters
|
||||||
|
2. **Test Thoroughly**: Run extensive backtests before live trading
|
||||||
|
3. **Monitor Closely**: Watch logs and performance metrics
|
||||||
|
4. **Start Small**: Use paper trading and small position sizes initially
|
||||||
|
5. **Iterate**: Adjust parameters based on performance
|
||||||
|
|
||||||
|
## Safety Reminders
|
||||||
|
|
||||||
|
- ⚠️ **Always use paper trading first**
|
||||||
|
- ⚠️ **Never disable risk management**
|
||||||
|
- ⚠️ **Monitor the system regularly**
|
||||||
|
- ⚠️ **Start with small position sizes**
|
||||||
|
- ⚠️ **Understand the risks involved**
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Check `README.md` for detailed documentation
|
||||||
|
- Review log files in `logs/` for troubleshooting
|
||||||
|
- Test with `test_system.py` to verify setup
|
||||||
|
- Use `--help` flag for command-line options
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Congratulations!** You now have a professional-grade automated trading system that's:
|
||||||
|
- ✅ Safe (paper trading default)
|
||||||
|
- ✅ Modular (easy to modify)
|
||||||
|
- ✅ Monitored (comprehensive logging)
|
||||||
|
- ✅ Tested (backtesting capabilities)
|
||||||
|
- ✅ Production-ready (proper architecture)
|
||||||
|
|
||||||
|
Happy trading! 📈
|
||||||
180
README.md
Normal file
180
README.md
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
# Trading Algorithm Project
|
||||||
|
|
||||||
|
A production-ready modular trading system that supports multiple strategies for paper and live trading on Alpaca Markets.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multiple Strategy Support**: Conservative and Enhanced trend-following strategies
|
||||||
|
- **Paper & Live Trading**: Safe testing with paper trading, production-ready for live trading
|
||||||
|
- **Modular Architecture**: Clean separation of concerns for easy maintenance
|
||||||
|
- **Risk Management**: Position sizing, drawdown limits, and trailing stops
|
||||||
|
- **Professional Logging**: Comprehensive trade and performance logging
|
||||||
|
- **Configuration Management**: Environment-based and file-based configuration
|
||||||
|
- **Strategy Comparison**: Built-in tools to compare strategy performance
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
trading/
|
||||||
|
├── src/
|
||||||
|
│ ├── data_handler.py # Market data retrieval and processing
|
||||||
|
│ ├── strategy_conservative.py # Conservative trend-following strategy
|
||||||
|
│ ├── strategy_enhanced.py # Enhanced trend-following with tighter risk controls
|
||||||
|
│ ├── strategy_factory.py # Strategy selection and instantiation
|
||||||
|
│ ├── risk_manager.py # Position sizing and risk controls
|
||||||
|
│ ├── trading_engine.py # Main trading execution engine
|
||||||
|
│ └── logger_setup.py # Logging configuration
|
||||||
|
├── config/
|
||||||
|
│ └── trading_config.py # Configuration settings
|
||||||
|
├── logs/ # Generated log files
|
||||||
|
├── main.py # Application entry point
|
||||||
|
├── compare_strategies.py # Strategy comparison tool
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Strategy Types
|
||||||
|
|
||||||
|
### Conservative Strategy
|
||||||
|
- **Philosophy**: Let big winners run, relaxed exit conditions
|
||||||
|
- **Trailing Stops**: 18-40% (wider range for bigger moves)
|
||||||
|
- **Emergency Exits**: 28-day maximum hold time
|
||||||
|
- **Best For**: Trending markets, patient traders
|
||||||
|
|
||||||
|
### Enhanced Strategy
|
||||||
|
- **Philosophy**: Tighter risk control, early loss protection
|
||||||
|
- **Trailing Stops**: 12-40% (tighter range for quicker exits)
|
||||||
|
- **Emergency Exits**: 7-14 day maximum hold time
|
||||||
|
- **Entry Conditions**: More selective (2% above trend line)
|
||||||
|
- **Best For**: Volatile markets, risk-conscious traders
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Setup Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone and navigate to project
|
||||||
|
cd /path/to/trading
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install alpaca-trade-api pandas numpy python-dotenv
|
||||||
|
|
||||||
|
# Setup environment variables
|
||||||
|
echo "ALPACA_API_KEY=your_api_key" > .env
|
||||||
|
echo "ALPACA_SECRET_KEY=your_secret_key" >> .env
|
||||||
|
echo "ALPACA_BASE_URL=https://paper-api.alpaca.markets" >> .env
|
||||||
|
echo "STRATEGY_TYPE=enhanced" >> .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Run Backtesting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test conservative strategy
|
||||||
|
python3 main.py --mode backtest --strategy conservative
|
||||||
|
|
||||||
|
# Test enhanced strategy
|
||||||
|
python3 main.py --mode backtest --strategy enhanced
|
||||||
|
|
||||||
|
# Compare both strategies
|
||||||
|
python3 compare_strategies.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Paper Trading
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start paper trading with enhanced strategy
|
||||||
|
python3 main.py --mode paper --strategy enhanced
|
||||||
|
|
||||||
|
# Start paper trading with conservative strategy
|
||||||
|
python3 main.py --mode paper --strategy conservative
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
- `ALPACA_API_KEY`: Your Alpaca API key
|
||||||
|
- `ALPACA_SECRET_KEY`: Your Alpaca secret key
|
||||||
|
- `ALPACA_BASE_URL`: API endpoint (paper or live)
|
||||||
|
- `STRATEGY_TYPE`: Default strategy ("conservative" or "enhanced")
|
||||||
|
|
||||||
|
### Strategy Selection
|
||||||
|
You can select strategies in multiple ways:
|
||||||
|
|
||||||
|
1. **Command Line**: `--strategy enhanced`
|
||||||
|
2. **Environment Variable**: `STRATEGY_TYPE=conservative`
|
||||||
|
3. **Config File**: Modify `trading_config.py`
|
||||||
|
|
||||||
|
Priority: Command line > Environment variable > Config file
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backtest enhanced strategy for 90 days
|
||||||
|
python3 main.py --mode backtest --strategy enhanced --days 90
|
||||||
|
|
||||||
|
# Paper trade with conservative strategy
|
||||||
|
python3 main.py --mode paper --strategy conservative
|
||||||
|
|
||||||
|
# Compare strategies side-by-side
|
||||||
|
python3 compare_strategies.py
|
||||||
|
|
||||||
|
# Check strategy configuration
|
||||||
|
python3 -c "from config.trading_config import TradingConfig; print(TradingConfig())"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Strategy Comparison
|
||||||
|
|
||||||
|
The `compare_strategies.py` script provides detailed performance comparison:
|
||||||
|
|
||||||
|
- **Returns**: Total return vs market benchmark
|
||||||
|
- **Risk Metrics**: Max drawdown, Sharpe ratio
|
||||||
|
- **Trade Analytics**: Win rate, profit factor, average wins/losses
|
||||||
|
- **Timing**: Time in market, trade frequency
|
||||||
|
|
||||||
|
## Risk Management
|
||||||
|
|
||||||
|
Both strategies include comprehensive risk controls:
|
||||||
|
|
||||||
|
- **Position Sizing**: Configurable risk per trade
|
||||||
|
- **Trailing Stops**: Dynamic stop-loss adjustment
|
||||||
|
- **Emergency Exits**: Maximum hold time limits
|
||||||
|
- **Drawdown Limits**: Account protection thresholds
|
||||||
|
- **Market Hours**: Trading only during market hours
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
The system generates detailed logs in the `logs/` directory:
|
||||||
|
|
||||||
|
- `trading.log`: General trading activity
|
||||||
|
- `trades.log`: Trade execution details
|
||||||
|
- `risk.log`: Risk management events
|
||||||
|
- `performance.log`: Performance metrics
|
||||||
|
|
||||||
|
## Safety Features
|
||||||
|
|
||||||
|
- **Paper Trading First**: Always test with paper money
|
||||||
|
- **Fail-Safe Defaults**: Conservative settings by default
|
||||||
|
- **Error Handling**: Graceful error recovery
|
||||||
|
- **Position Limits**: Maximum position size controls
|
||||||
|
- **Market Data Validation**: Data quality checks
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
To add a new strategy:
|
||||||
|
|
||||||
|
1. Create new strategy file in `src/strategy_your_name.py`
|
||||||
|
2. Inherit from base strategy pattern (see existing strategies)
|
||||||
|
3. Register in `src/strategy_factory.py`
|
||||||
|
4. Update configuration options
|
||||||
|
5. Test with backtesting before live usage
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check logs in `logs/` directory
|
||||||
|
2. Verify API keys and permissions
|
||||||
|
3. Test with paper trading first
|
||||||
|
4. Review strategy configuration
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This software is for educational and testing purposes. Always test thoroughly with paper trading before using real money. Past performance does not guarantee future results.
|
||||||
318
backtesting_long.py
Normal file
318
backtesting_long.py
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
import alpaca_trade_api as tradeapi
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Use past dates for backtesting
|
||||||
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
start_date = (datetime.now() - timedelta(days=180)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
api = tradeapi.REST('PKXR08ET6CSGV5QFS89E', '5ILlNGVM7WfPwJk8kPRlHhQwDO022H19RWBOkwgd', base_url='https://data.alpaca.markets')
|
||||||
|
|
||||||
|
# Get historical data
|
||||||
|
bars = api.get_crypto_bars('ETH/USD', tradeapi.TimeFrame.Hour, start_date, end_date).df
|
||||||
|
bars.index = pd.to_datetime(bars.index)
|
||||||
|
|
||||||
|
# SIMPLIFIED TREND-FOLLOWING INDICATORS - Focus on what works
|
||||||
|
bars['ema_fast'] = bars['close'].ewm(span=12).mean() # 12 hours - responsive but not noisy
|
||||||
|
bars['ema_slow'] = bars['close'].ewm(span=30).mean() # 30 hours - trend filter
|
||||||
|
bars['ema_trend'] = bars['close'].ewm(span=80).mean() # 80 hours - major trend
|
||||||
|
|
||||||
|
# RSI for momentum filtering
|
||||||
|
def calculate_rsi(prices, window=14):
|
||||||
|
delta = prices.diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
|
||||||
|
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
|
||||||
|
rs = gain / loss
|
||||||
|
rsi = 100 - (100 / (1 + rs))
|
||||||
|
return rsi
|
||||||
|
|
||||||
|
bars['rsi'] = calculate_rsi(bars['close'], 14)
|
||||||
|
|
||||||
|
# MACD for momentum confirmation
|
||||||
|
bars['ema_12'] = bars['close'].ewm(span=12).mean()
|
||||||
|
bars['ema_26'] = bars['close'].ewm(span=26).mean()
|
||||||
|
bars['macd'] = bars['ema_12'] - bars['ema_26']
|
||||||
|
bars['macd_signal'] = bars['macd'].ewm(span=9).mean()
|
||||||
|
|
||||||
|
# Price momentum
|
||||||
|
bars['price_momentum_6h'] = bars['close'].pct_change(periods=6)
|
||||||
|
|
||||||
|
# Volume confirmation (relaxed)
|
||||||
|
bars['volume_ma'] = bars['volume'].rolling(window=20).mean()
|
||||||
|
bars['volume_ok'] = bars['volume'] > bars['volume_ma'] * 0.7 # Very relaxed
|
||||||
|
|
||||||
|
# CONSERVATIVE BUY CONDITIONS - Back to what worked!
|
||||||
|
buy_conditions = (
|
||||||
|
# BASIC TREND REQUIREMENT - More strict
|
||||||
|
(bars['close'] > bars['ema_trend']) & # Must be above major trend (no tolerance)
|
||||||
|
|
||||||
|
# ENTRY TRIGGERS - More selective
|
||||||
|
(
|
||||||
|
# EMA bullish alignment with momentum
|
||||||
|
((bars['ema_fast'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['rsi'] > 45) & (bars['rsi'] < 75)) | # Balanced RSI
|
||||||
|
|
||||||
|
# MACD bullish with strong momentum
|
||||||
|
((bars['macd'] > bars['macd_signal']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.02) & # 2% momentum required
|
||||||
|
(bars['rsi'] > 40) & (bars['rsi'] < 70)) | # Not overbought
|
||||||
|
|
||||||
|
# Strong breakout with volume
|
||||||
|
((bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['close'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_trend']) &
|
||||||
|
(bars['volume_ok']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.015)) # Strong momentum
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ULTRA-CONSERVATIVE SELL CONDITIONS - Let winners run much longer!
|
||||||
|
sell_conditions = (
|
||||||
|
# ONLY MAJOR TREND BREAKS - Much more restrictive
|
||||||
|
(
|
||||||
|
# Confirmed EMA death cross AND major breakdown
|
||||||
|
((bars['ema_fast'] < bars['ema_slow']) &
|
||||||
|
(bars['close'] < bars['ema_trend'] * 0.90) & # 10% below trend (not 8%)
|
||||||
|
(bars['rsi'] < 35)) | # Very weak momentum (not 40)
|
||||||
|
|
||||||
|
# Sustained breakdown below major trend - more severe
|
||||||
|
((bars['close'] < bars['ema_trend'] * 0.88) & # 12% below major trend (not 8%)
|
||||||
|
(bars['price_momentum_6h'] < -0.04)) | # Stronger decline (not -0.03)
|
||||||
|
|
||||||
|
# Extreme overbought with strong reversal signs
|
||||||
|
((bars['rsi'] > 85) & # Higher threshold (not 80)
|
||||||
|
(bars['rsi'].shift(1) > bars['rsi']) & # RSI declining
|
||||||
|
(bars['rsi'].shift(2) > bars['rsi'].shift(1)) & # For 2 periods
|
||||||
|
(bars['rsi'].shift(3) > bars['rsi'].shift(2)) & # For 3 periods
|
||||||
|
(bars['macd'] < bars['macd_signal']) & # MACD bearish
|
||||||
|
(bars['price_momentum_6h'] < -0.02)) # Plus price weakness
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply signals with LONGER cooldown - Back to original
|
||||||
|
bars['buy_signal_raw'] = buy_conditions
|
||||||
|
bars['sell_signal_raw'] = sell_conditions
|
||||||
|
bars['buy_signal'] = False
|
||||||
|
bars['sell_signal'] = False
|
||||||
|
|
||||||
|
last_signal_idx = -50
|
||||||
|
COOLDOWN_HOURS = 24 # Back to 24-hour cooldown for quality trades
|
||||||
|
|
||||||
|
for i in range(len(bars)):
|
||||||
|
if i - last_signal_idx >= COOLDOWN_HOURS:
|
||||||
|
if bars['buy_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('buy_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
elif bars['sell_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('sell_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
|
||||||
|
# POSITION MANAGEMENT - Conservative entry + WIDER trailing stops
|
||||||
|
bars['position'] = 0
|
||||||
|
position = 0
|
||||||
|
entry_price = None
|
||||||
|
highest_price_since_entry = None
|
||||||
|
bars_since_entry = 0
|
||||||
|
|
||||||
|
for i in range(1, len(bars)):
|
||||||
|
current_price = bars['close'].iloc[i]
|
||||||
|
|
||||||
|
if bars['buy_signal'].iloc[i] and position == 0:
|
||||||
|
# CONSERVATIVE ENTRY - Back to original
|
||||||
|
if bars['close'].iloc[i] > bars['ema_trend'].iloc[i]: # Must be above trend
|
||||||
|
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
|
||||||
|
|
||||||
|
profit_pct = (current_price / entry_price - 1)
|
||||||
|
|
||||||
|
# CRYPTO-SCALE TRAILING STOPS - Let big winners truly run!
|
||||||
|
if profit_pct > 2.00: # 200%+ profit: 40% trailing stop
|
||||||
|
trailing_stop_pct = 0.60
|
||||||
|
elif profit_pct > 1.50: # 150%+ profit: 35% trailing stop
|
||||||
|
trailing_stop_pct = 0.65
|
||||||
|
elif profit_pct > 1.00: # 100%+ profit: 30% trailing stop
|
||||||
|
trailing_stop_pct = 0.70
|
||||||
|
elif profit_pct > 0.75: # 75%+ profit: 25% trailing stop
|
||||||
|
trailing_stop_pct = 0.75
|
||||||
|
elif profit_pct > 0.50: # 50%+ profit: 22% trailing stop
|
||||||
|
trailing_stop_pct = 0.78
|
||||||
|
elif profit_pct > 0.25: # 25%+ profit: 20% trailing stop
|
||||||
|
trailing_stop_pct = 0.80
|
||||||
|
else: # < 25% profit: 18% trailing stop
|
||||||
|
trailing_stop_pct = 0.82
|
||||||
|
|
||||||
|
trailing_stop_price = highest_price_since_entry * trailing_stop_pct
|
||||||
|
|
||||||
|
# MINIMAL EXIT CONDITIONS - Only major trend breaks
|
||||||
|
exit_conditions = (
|
||||||
|
bars['sell_signal'].iloc[i] or
|
||||||
|
current_price <= trailing_stop_price or
|
||||||
|
# Emergency stops - very relaxed
|
||||||
|
(bars_since_entry >= 672 and profit_pct < -0.30) or # 28 days & -30%
|
||||||
|
(bars['close'].iloc[i] < bars['ema_trend'].iloc[i] * 0.75) # 25% below major trend
|
||||||
|
)
|
||||||
|
|
||||||
|
if exit_conditions:
|
||||||
|
position = 0
|
||||||
|
entry_price = None
|
||||||
|
highest_price_since_entry = None
|
||||||
|
bars_since_entry = 0
|
||||||
|
|
||||||
|
bars.iloc[i, bars.columns.get_loc('position')] = position
|
||||||
|
|
||||||
|
# ENHANCED POSITION MANAGEMENT - Better risk control for longer periods
|
||||||
|
bars['position'] = 0
|
||||||
|
position = 0
|
||||||
|
entry_price = None
|
||||||
|
highest_price_since_entry = None
|
||||||
|
bars_since_entry = 0
|
||||||
|
|
||||||
|
for i in range(1, len(bars)):
|
||||||
|
current_price = bars['close'].iloc[i]
|
||||||
|
|
||||||
|
if bars['buy_signal'].iloc[i] and position == 0:
|
||||||
|
# MORE STRICT ENTRY - Only in very strong trends
|
||||||
|
if (bars['close'].iloc[i] > bars['ema_trend'].iloc[i] * 1.02 and # 2% above major trend
|
||||||
|
bars['ema_fast'].iloc[i] > bars['ema_slow'].iloc[i] * 1.01): # Fast EMA clearly above slow
|
||||||
|
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
|
||||||
|
|
||||||
|
profit_pct = (current_price / entry_price - 1)
|
||||||
|
|
||||||
|
# CRYPTO-SCALE TRAILING STOPS with better risk management
|
||||||
|
if profit_pct > 2.00: # 200%+ profit: 40% trailing stop
|
||||||
|
trailing_stop_pct = 0.60
|
||||||
|
elif profit_pct > 1.50: # 150%+ profit: 35% trailing stop
|
||||||
|
trailing_stop_pct = 0.65
|
||||||
|
elif profit_pct > 1.00: # 100%+ profit: 30% trailing stop
|
||||||
|
trailing_stop_pct = 0.70
|
||||||
|
elif profit_pct > 0.75: # 75%+ profit: 25% trailing stop
|
||||||
|
trailing_stop_pct = 0.75
|
||||||
|
elif profit_pct > 0.50: # 50%+ profit: 22% trailing stop
|
||||||
|
trailing_stop_pct = 0.78
|
||||||
|
elif profit_pct > 0.25: # 25%+ profit: 20% trailing stop
|
||||||
|
trailing_stop_pct = 0.80
|
||||||
|
elif profit_pct > 0.10: # 10%+ profit: 15% trailing stop
|
||||||
|
trailing_stop_pct = 0.85
|
||||||
|
else: # < 10% profit: 12% trailing stop (tighter for early losses)
|
||||||
|
trailing_stop_pct = 0.88
|
||||||
|
|
||||||
|
trailing_stop_price = highest_price_since_entry * trailing_stop_pct
|
||||||
|
|
||||||
|
# IMPROVED EXIT CONDITIONS - Better loss control
|
||||||
|
exit_conditions = (
|
||||||
|
bars['sell_signal'].iloc[i] or
|
||||||
|
current_price <= trailing_stop_price or
|
||||||
|
# Tighter time-based stops to prevent long drawdowns
|
||||||
|
(bars_since_entry >= 168 and profit_pct < -0.10) or # 7 days & -10% (not 28 days & -30%)
|
||||||
|
(bars_since_entry >= 336 and profit_pct < -0.05) or # 14 days & -5%
|
||||||
|
(bars['close'].iloc[i] < bars['ema_trend'].iloc[i] * 0.85) # 15% below major trend (not 25%)
|
||||||
|
)
|
||||||
|
|
||||||
|
if exit_conditions:
|
||||||
|
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 with lower costs (fewer trades)
|
||||||
|
bars['market_return'] = bars['close'].pct_change()
|
||||||
|
bars['strategy_return'] = bars['position'].shift(1) * bars['market_return']
|
||||||
|
|
||||||
|
trade_cost = 0.0005 # Slightly lower for fewer, larger trades
|
||||||
|
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
|
||||||
|
|
||||||
|
# Performance metrics
|
||||||
|
total_return = bars['cum_strategy'].iloc[-1] * 100
|
||||||
|
market_return_pct = bars['cum_market'].iloc[-1] * 100
|
||||||
|
|
||||||
|
# Count trades
|
||||||
|
position_changes = bars['position'].diff()
|
||||||
|
buys = (position_changes == 1).sum()
|
||||||
|
sells = (position_changes == -1).sum()
|
||||||
|
|
||||||
|
# Calculate 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 * trade_cost)
|
||||||
|
trade_returns.append(trade_return)
|
||||||
|
entry_price = None
|
||||||
|
|
||||||
|
if len(trade_returns) > 0:
|
||||||
|
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
|
||||||
|
|
||||||
|
# Sharpe ratio
|
||||||
|
if bars['strategy_return'].std() > 0:
|
||||||
|
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
|
||||||
|
|
||||||
|
print("===== TREND-FOLLOWING STRATEGY =====")
|
||||||
|
print(f"Strategy Return: {total_return:.2f}%")
|
||||||
|
print(f"Market Return: {market_return_pct:.2f}%")
|
||||||
|
print(f"Outperformance: {total_return - market_return_pct:.2f}%")
|
||||||
|
print(f"Sharpe Ratio: {sharpe:.2f}")
|
||||||
|
print(f"Max Drawdown: {max_drawdown:.2f}%")
|
||||||
|
print(f"Total Trades: {len(trade_returns)}")
|
||||||
|
print(f"Buy Signals: {buys}")
|
||||||
|
print(f"Sell Signals: {sells}")
|
||||||
|
print(f"Win Rate: {win_rate:.2f}%")
|
||||||
|
print(f"Profit Factor: {profit_factor:.2f}")
|
||||||
|
print(f"Avg Win: {avg_win:.2f}%")
|
||||||
|
print(f"Avg Loss: {avg_loss:.2f}%")
|
||||||
|
print(f"Max Win: {max_win:.2f}%")
|
||||||
|
print(f"Max Loss: {max_loss:.2f}%")
|
||||||
|
|
||||||
|
# Calculate time in market
|
||||||
|
time_in_market = (bars['position'] > 0).mean() * 100
|
||||||
|
print(f"Time in Market: {time_in_market:.1f}%")
|
||||||
252
backtesting_short.py
Normal file
252
backtesting_short.py
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import alpaca_trade_api as tradeapi
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Use past dates for backtesting
|
||||||
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
start_date = (datetime.now() - timedelta(days=90)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
api = tradeapi.REST('PKXR08ET6CSGV5QFS89E', '5ILlNGVM7WfPwJk8kPRlHhQwDO022H19RWBOkwgd', base_url='https://data.alpaca.markets')
|
||||||
|
|
||||||
|
# Get historical data
|
||||||
|
bars = api.get_crypto_bars('ETH/USD', tradeapi.TimeFrame.Hour, start_date, end_date).df
|
||||||
|
bars.index = pd.to_datetime(bars.index)
|
||||||
|
|
||||||
|
# SIMPLIFIED TREND-FOLLOWING INDICATORS - Focus on what works
|
||||||
|
bars['ema_fast'] = bars['close'].ewm(span=12).mean() # 12 hours - responsive but not noisy
|
||||||
|
bars['ema_slow'] = bars['close'].ewm(span=30).mean() # 30 hours - trend filter
|
||||||
|
bars['ema_trend'] = bars['close'].ewm(span=80).mean() # 80 hours - major trend
|
||||||
|
|
||||||
|
# RSI for momentum filtering
|
||||||
|
def calculate_rsi(prices, window=14):
|
||||||
|
delta = prices.diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
|
||||||
|
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
|
||||||
|
rs = gain / loss
|
||||||
|
rsi = 100 - (100 / (1 + rs))
|
||||||
|
return rsi
|
||||||
|
|
||||||
|
bars['rsi'] = calculate_rsi(bars['close'], 14)
|
||||||
|
|
||||||
|
# MACD for momentum confirmation
|
||||||
|
bars['ema_12'] = bars['close'].ewm(span=12).mean()
|
||||||
|
bars['ema_26'] = bars['close'].ewm(span=26).mean()
|
||||||
|
bars['macd'] = bars['ema_12'] - bars['ema_26']
|
||||||
|
bars['macd_signal'] = bars['macd'].ewm(span=9).mean()
|
||||||
|
|
||||||
|
# Price momentum
|
||||||
|
bars['price_momentum_6h'] = bars['close'].pct_change(periods=6)
|
||||||
|
|
||||||
|
# Volume confirmation (relaxed)
|
||||||
|
bars['volume_ma'] = bars['volume'].rolling(window=20).mean()
|
||||||
|
bars['volume_ok'] = bars['volume'] > bars['volume_ma'] * 0.7 # Very relaxed
|
||||||
|
|
||||||
|
# CONSERVATIVE BUY CONDITIONS - Back to what worked!
|
||||||
|
buy_conditions = (
|
||||||
|
# BASIC TREND REQUIREMENT - More strict
|
||||||
|
(bars['close'] > bars['ema_trend']) & # Must be above major trend (no tolerance)
|
||||||
|
|
||||||
|
# ENTRY TRIGGERS - More selective
|
||||||
|
(
|
||||||
|
# EMA bullish alignment with momentum
|
||||||
|
((bars['ema_fast'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['rsi'] > 45) & (bars['rsi'] < 75)) | # Balanced RSI
|
||||||
|
|
||||||
|
# MACD bullish with strong momentum
|
||||||
|
((bars['macd'] > bars['macd_signal']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.02) & # 2% momentum required
|
||||||
|
(bars['rsi'] > 40) & (bars['rsi'] < 70)) | # Not overbought
|
||||||
|
|
||||||
|
# Strong breakout with volume
|
||||||
|
((bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['close'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_trend']) &
|
||||||
|
(bars['volume_ok']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.015)) # Strong momentum
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ULTRA-CONSERVATIVE SELL CONDITIONS - Let winners run much longer!
|
||||||
|
sell_conditions = (
|
||||||
|
# ONLY MAJOR TREND BREAKS - Much more restrictive
|
||||||
|
(
|
||||||
|
# Confirmed EMA death cross AND major breakdown
|
||||||
|
((bars['ema_fast'] < bars['ema_slow']) &
|
||||||
|
(bars['close'] < bars['ema_trend'] * 0.90) & # 10% below trend (not 8%)
|
||||||
|
(bars['rsi'] < 35)) | # Very weak momentum (not 40)
|
||||||
|
|
||||||
|
# Sustained breakdown below major trend - more severe
|
||||||
|
((bars['close'] < bars['ema_trend'] * 0.88) & # 12% below major trend (not 8%)
|
||||||
|
(bars['price_momentum_6h'] < -0.04)) | # Stronger decline (not -0.03)
|
||||||
|
|
||||||
|
# Extreme overbought with strong reversal signs
|
||||||
|
((bars['rsi'] > 85) & # Higher threshold (not 80)
|
||||||
|
(bars['rsi'].shift(1) > bars['rsi']) & # RSI declining
|
||||||
|
(bars['rsi'].shift(2) > bars['rsi'].shift(1)) & # For 2 periods
|
||||||
|
(bars['rsi'].shift(3) > bars['rsi'].shift(2)) & # For 3 periods
|
||||||
|
(bars['macd'] < bars['macd_signal']) & # MACD bearish
|
||||||
|
(bars['price_momentum_6h'] < -0.02)) # Plus price weakness
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply signals with LONGER cooldown - Back to original
|
||||||
|
bars['buy_signal_raw'] = buy_conditions
|
||||||
|
bars['sell_signal_raw'] = sell_conditions
|
||||||
|
bars['buy_signal'] = False
|
||||||
|
bars['sell_signal'] = False
|
||||||
|
|
||||||
|
last_signal_idx = -50
|
||||||
|
COOLDOWN_HOURS = 24 # Back to 24-hour cooldown for quality trades
|
||||||
|
|
||||||
|
for i in range(len(bars)):
|
||||||
|
if i - last_signal_idx >= COOLDOWN_HOURS:
|
||||||
|
if bars['buy_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('buy_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
elif bars['sell_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('sell_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
|
||||||
|
# POSITION MANAGEMENT - Conservative entry + WIDER trailing stops
|
||||||
|
bars['position'] = 0
|
||||||
|
position = 0
|
||||||
|
entry_price = None
|
||||||
|
highest_price_since_entry = None
|
||||||
|
bars_since_entry = 0
|
||||||
|
|
||||||
|
for i in range(1, len(bars)):
|
||||||
|
current_price = bars['close'].iloc[i]
|
||||||
|
|
||||||
|
if bars['buy_signal'].iloc[i] and position == 0:
|
||||||
|
# CONSERVATIVE ENTRY - Back to original
|
||||||
|
if bars['close'].iloc[i] > bars['ema_trend'].iloc[i]: # Must be above trend
|
||||||
|
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
|
||||||
|
|
||||||
|
profit_pct = (current_price / entry_price - 1)
|
||||||
|
|
||||||
|
# CRYPTO-SCALE TRAILING STOPS - Let big winners truly run!
|
||||||
|
if profit_pct > 2.00: # 200%+ profit: 40% trailing stop
|
||||||
|
trailing_stop_pct = 0.60
|
||||||
|
elif profit_pct > 1.50: # 150%+ profit: 35% trailing stop
|
||||||
|
trailing_stop_pct = 0.65
|
||||||
|
elif profit_pct > 1.00: # 100%+ profit: 30% trailing stop
|
||||||
|
trailing_stop_pct = 0.70
|
||||||
|
elif profit_pct > 0.75: # 75%+ profit: 25% trailing stop
|
||||||
|
trailing_stop_pct = 0.75
|
||||||
|
elif profit_pct > 0.50: # 50%+ profit: 22% trailing stop
|
||||||
|
trailing_stop_pct = 0.78
|
||||||
|
elif profit_pct > 0.25: # 25%+ profit: 20% trailing stop
|
||||||
|
trailing_stop_pct = 0.80
|
||||||
|
else: # < 25% profit: 18% trailing stop
|
||||||
|
trailing_stop_pct = 0.82
|
||||||
|
|
||||||
|
trailing_stop_price = highest_price_since_entry * trailing_stop_pct
|
||||||
|
|
||||||
|
# MINIMAL EXIT CONDITIONS - Only major trend breaks
|
||||||
|
exit_conditions = (
|
||||||
|
bars['sell_signal'].iloc[i] or
|
||||||
|
current_price <= trailing_stop_price or
|
||||||
|
# Emergency stops - very relaxed
|
||||||
|
(bars_since_entry >= 672 and profit_pct < -0.30) or # 28 days & -30%
|
||||||
|
(bars['close'].iloc[i] < bars['ema_trend'].iloc[i] * 0.75) # 25% below major trend
|
||||||
|
)
|
||||||
|
|
||||||
|
if exit_conditions:
|
||||||
|
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 with lower costs (fewer trades)
|
||||||
|
bars['market_return'] = bars['close'].pct_change()
|
||||||
|
bars['strategy_return'] = bars['position'].shift(1) * bars['market_return']
|
||||||
|
|
||||||
|
trade_cost = 0.0005 # Slightly lower for fewer, larger trades
|
||||||
|
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
|
||||||
|
|
||||||
|
# Performance metrics
|
||||||
|
total_return = bars['cum_strategy'].iloc[-1] * 100
|
||||||
|
market_return_pct = bars['cum_market'].iloc[-1] * 100
|
||||||
|
|
||||||
|
# Count trades
|
||||||
|
position_changes = bars['position'].diff()
|
||||||
|
buys = (position_changes == 1).sum()
|
||||||
|
sells = (position_changes == -1).sum()
|
||||||
|
|
||||||
|
# Calculate 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 * trade_cost)
|
||||||
|
trade_returns.append(trade_return)
|
||||||
|
entry_price = None
|
||||||
|
|
||||||
|
if len(trade_returns) > 0:
|
||||||
|
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
|
||||||
|
|
||||||
|
# Sharpe ratio
|
||||||
|
if bars['strategy_return'].std() > 0:
|
||||||
|
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
|
||||||
|
|
||||||
|
print("===== TREND-FOLLOWING STRATEGY =====")
|
||||||
|
print(f"Strategy Return: {total_return:.2f}%")
|
||||||
|
print(f"Market Return: {market_return_pct:.2f}%")
|
||||||
|
print(f"Outperformance: {total_return - market_return_pct:.2f}%")
|
||||||
|
print(f"Sharpe Ratio: {sharpe:.2f}")
|
||||||
|
print(f"Max Drawdown: {max_drawdown:.2f}%")
|
||||||
|
print(f"Total Trades: {len(trade_returns)}")
|
||||||
|
print(f"Buy Signals: {buys}")
|
||||||
|
print(f"Sell Signals: {sells}")
|
||||||
|
print(f"Win Rate: {win_rate:.2f}%")
|
||||||
|
print(f"Profit Factor: {profit_factor:.2f}")
|
||||||
|
print(f"Avg Win: {avg_win:.2f}%")
|
||||||
|
print(f"Avg Loss: {avg_loss:.2f}%")
|
||||||
|
print(f"Max Win: {max_win:.2f}%")
|
||||||
|
print(f"Max Loss: {max_loss:.2f}%")
|
||||||
|
|
||||||
|
# Calculate time in market
|
||||||
|
time_in_market = (bars['position'] > 0).mean() * 100
|
||||||
|
print(f"Time in Market: {time_in_market:.1f}%")
|
||||||
269
compare_strategies.py
Normal file
269
compare_strategies.py
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Strategy Comparison Script
|
||||||
|
Runs both strategies and compares their performance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add src directory to path for imports
|
||||||
|
sys.path.append('src')
|
||||||
|
sys.path.append('config')
|
||||||
|
|
||||||
|
from src.logger_setup import setup_logging
|
||||||
|
from src.data_handler import DataHandler
|
||||||
|
from src.strategy_factory import get_strategy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def run_strategy_comparison():
|
||||||
|
"""Compare both strategies side by side"""
|
||||||
|
setup_logging()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("STRATEGY COMPARISON - CONSERVATIVE vs ENHANCED")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get data once for both strategies
|
||||||
|
data_handler = DataHandler()
|
||||||
|
bars = data_handler.get_historical_data(days=180)
|
||||||
|
|
||||||
|
if bars.empty:
|
||||||
|
print("❌ No historical data available")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate technical indicators
|
||||||
|
bars = data_handler.calculate_technical_indicators(bars)
|
||||||
|
|
||||||
|
strategies = ['conservative', 'enhanced']
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for strategy_type in strategies:
|
||||||
|
print(f"\n📊 Running {strategy_type.upper()} strategy...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create strategy
|
||||||
|
strategy = get_strategy(strategy_type)
|
||||||
|
|
||||||
|
# Generate signals
|
||||||
|
strategy_bars = bars.copy()
|
||||||
|
strategy_bars = strategy.generate_signals(strategy_bars)
|
||||||
|
|
||||||
|
# Run simulation
|
||||||
|
results[strategy_type] = run_strategy_simulation(strategy_bars, strategy)
|
||||||
|
|
||||||
|
print(f"✅ {strategy.name} completed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error running {strategy_type} strategy: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Display comparison
|
||||||
|
display_strategy_comparison(results)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in strategy comparison: {e}")
|
||||||
|
print(f"❌ Comparison failed: {e}")
|
||||||
|
|
||||||
|
def run_strategy_simulation(bars, strategy):
|
||||||
|
"""Run simulation for a strategy"""
|
||||||
|
|
||||||
|
# Position tracking
|
||||||
|
position = 0
|
||||||
|
entry_price = None
|
||||||
|
highest_price_since_entry = None
|
||||||
|
bars_since_entry = 0
|
||||||
|
trades = []
|
||||||
|
|
||||||
|
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:
|
||||||
|
# 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
|
||||||
|
|
||||||
|
if current_price > highest_price_since_entry:
|
||||||
|
highest_price_since_entry = current_price
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
# Record trade
|
||||||
|
profit_pct = (current_price / entry_price - 1)
|
||||||
|
trades.append({
|
||||||
|
'entry_price': entry_price,
|
||||||
|
'exit_price': current_price,
|
||||||
|
'profit_pct': profit_pct,
|
||||||
|
'bars_held': bars_since_entry,
|
||||||
|
'exit_reason': exit_reason
|
||||||
|
})
|
||||||
|
|
||||||
|
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 costs
|
||||||
|
trade_cost = 0.0005
|
||||||
|
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_strategy'] = (1 + bars['strategy_return']).cumprod() - 1
|
||||||
|
bars['cum_market'] = (1 + bars['market_return']).cumprod() - 1
|
||||||
|
|
||||||
|
# Calculate metrics
|
||||||
|
total_return = bars['cum_strategy'].iloc[-1] * 100
|
||||||
|
market_return = bars['cum_market'].iloc[-1] * 100
|
||||||
|
|
||||||
|
if len(trades) > 0:
|
||||||
|
import numpy as np
|
||||||
|
trade_returns = [t['profit_pct'] for t in trades]
|
||||||
|
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
|
||||||
|
|
||||||
|
# Max drawdown
|
||||||
|
running_max = bars['cum_strategy'].cummax()
|
||||||
|
drawdown = (bars['cum_strategy'] - running_max)
|
||||||
|
max_drawdown = drawdown.min() * 100
|
||||||
|
|
||||||
|
# Sharpe ratio
|
||||||
|
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
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_return': total_return,
|
||||||
|
'market_return': market_return,
|
||||||
|
'outperformance': total_return - market_return,
|
||||||
|
'sharpe_ratio': sharpe,
|
||||||
|
'max_drawdown': max_drawdown,
|
||||||
|
'total_trades': len(trades),
|
||||||
|
'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': (bars['position'] > 0).mean() * 100,
|
||||||
|
'trades': trades
|
||||||
|
}
|
||||||
|
|
||||||
|
def display_strategy_comparison(results):
|
||||||
|
"""Display side-by-side comparison"""
|
||||||
|
|
||||||
|
if len(results) < 2:
|
||||||
|
print("❌ Need at least 2 strategies to compare")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("STRATEGY COMPARISON RESULTS")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
print(f"{'Metric':<25} {'Conservative':<20} {'Enhanced':<20} {'Difference':<15}")
|
||||||
|
print("-" * 100)
|
||||||
|
|
||||||
|
conservative = results.get('conservative', {})
|
||||||
|
enhanced = results.get('enhanced', {})
|
||||||
|
|
||||||
|
metrics = [
|
||||||
|
('Total Return', 'total_return', '%'),
|
||||||
|
('Market Return', 'market_return', '%'),
|
||||||
|
('Outperformance', 'outperformance', '%'),
|
||||||
|
('Sharpe Ratio', 'sharpe_ratio', ''),
|
||||||
|
('Max Drawdown', 'max_drawdown', '%'),
|
||||||
|
('Total Trades', 'total_trades', ''),
|
||||||
|
('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', '%'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for display_name, key, unit in metrics:
|
||||||
|
cons_val = conservative.get(key, 0)
|
||||||
|
enh_val = enhanced.get(key, 0)
|
||||||
|
diff = enh_val - cons_val
|
||||||
|
|
||||||
|
if unit == '%':
|
||||||
|
cons_str = f"{cons_val:>8.2f}%"
|
||||||
|
enh_str = f"{enh_val:>8.2f}%"
|
||||||
|
diff_str = f"{diff:>+8.2f}%"
|
||||||
|
else:
|
||||||
|
cons_str = f"{cons_val:>8.2f}"
|
||||||
|
enh_str = f"{enh_val:>8.2f}"
|
||||||
|
diff_str = f"{diff:>+8.2f}"
|
||||||
|
|
||||||
|
print(f"{display_name:<25} {cons_str:<20} {enh_str:<20} {diff_str:<15}")
|
||||||
|
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n📋 SUMMARY:")
|
||||||
|
|
||||||
|
if enhanced.get('total_return', 0) > conservative.get('total_return', 0):
|
||||||
|
winner = "Enhanced"
|
||||||
|
return_diff = enhanced.get('total_return', 0) - conservative.get('total_return', 0)
|
||||||
|
else:
|
||||||
|
winner = "Conservative"
|
||||||
|
return_diff = conservative.get('total_return', 0) - enhanced.get('total_return', 0)
|
||||||
|
|
||||||
|
print(f"🏆 Best performing strategy: {winner} (+{return_diff:.2f}% return)")
|
||||||
|
|
||||||
|
# Risk comparison
|
||||||
|
cons_risk = abs(conservative.get('max_drawdown', 0))
|
||||||
|
enh_risk = abs(enhanced.get('max_drawdown', 0))
|
||||||
|
|
||||||
|
if cons_risk < enh_risk:
|
||||||
|
print(f"🛡️ Lower risk strategy: Conservative ({cons_risk:.2f}% max drawdown)")
|
||||||
|
else:
|
||||||
|
print(f"🛡️ Lower risk strategy: Enhanced ({enh_risk:.2f}% max drawdown)")
|
||||||
|
|
||||||
|
print("\n💡 RECOMMENDATIONS:")
|
||||||
|
print("• Conservative: Better for letting big winners run, more relaxed exits")
|
||||||
|
print("• Enhanced: Better risk control, tighter stops for early losses")
|
||||||
|
print("• Choose based on your risk tolerance and market conditions")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_strategy_comparison()
|
||||||
4
config/__init__.py
Normal file
4
config/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
Configuration Package
|
||||||
|
Contains all configuration settings for the trading system.
|
||||||
|
"""
|
||||||
101
config/trading_config.py
Normal file
101
config/trading_config.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Trading Configuration Module
|
||||||
|
Contains all configuration settings for the trading system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Try to load environment variables if python-dotenv is available
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
except ImportError:
|
||||||
|
# python-dotenv not installed, will use os.getenv with defaults
|
||||||
|
pass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AlpacaConfig:
|
||||||
|
"""Alpaca API configuration"""
|
||||||
|
api_key: str = os.getenv('ALPACA_API_KEY', 'PKXR08ET6CSGV5QFS89E')
|
||||||
|
secret_key: str = os.getenv('ALPACA_SECRET_KEY', '5ILlNGVM7WfPwJk8kPRlHhQwDO022H19RWBOkwgd')
|
||||||
|
base_url: str = 'https://paper-api.alpaca.markets' # Paper trading URL
|
||||||
|
data_url: str = 'https://data.alpaca.markets'
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TradingConfig:
|
||||||
|
"""Main trading configuration"""
|
||||||
|
# Strategy Selection
|
||||||
|
strategy_type: str = os.getenv('STRATEGY_TYPE', 'enhanced') # 'conservative' or 'enhanced'
|
||||||
|
|
||||||
|
# Symbol to trade
|
||||||
|
symbol: str = os.getenv('SYMBOL', 'ETH/USD')
|
||||||
|
|
||||||
|
# Position sizing
|
||||||
|
max_position_size: float = float(os.getenv('MAX_POSITION_SIZE', '0.95')) # Maximum 95% of portfolio
|
||||||
|
risk_per_trade: float = float(os.getenv('RISK_PER_TRADE', '0.02')) # Risk 2% per trade
|
||||||
|
|
||||||
|
# Trading parameters
|
||||||
|
trade_cost: float = 0.0005 # 0.05% trading cost
|
||||||
|
cooldown_hours: int = 24 # Hours between signals
|
||||||
|
|
||||||
|
# Risk management
|
||||||
|
max_drawdown_limit: float = float(os.getenv('MAX_DRAWDOWN_LIMIT', '0.15')) # Stop trading if 15% drawdown
|
||||||
|
daily_loss_limit: float = float(os.getenv('DAILY_LOSS_LIMIT', '0.05')) # Stop trading if 5% daily loss
|
||||||
|
|
||||||
|
# Timeframe
|
||||||
|
timeframe: str = 'Hour' # Trading timeframe
|
||||||
|
|
||||||
|
# Strategy parameters
|
||||||
|
ema_fast: int = 12
|
||||||
|
ema_slow: int = 30
|
||||||
|
ema_trend: int = 80
|
||||||
|
rsi_period: int = 14
|
||||||
|
macd_fast: int = 12
|
||||||
|
macd_slow: int = 26
|
||||||
|
macd_signal: int = 9
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StrategyConfig:
|
||||||
|
"""Strategy-specific configuration"""
|
||||||
|
# Entry conditions
|
||||||
|
min_trend_threshold: float = 1.02 # Must be 2% above major trend
|
||||||
|
min_momentum_threshold: float = 0.02 # 2% momentum required
|
||||||
|
rsi_oversold: float = 45
|
||||||
|
rsi_overbought: float = 75
|
||||||
|
volume_threshold: float = 0.7
|
||||||
|
|
||||||
|
# Exit conditions
|
||||||
|
trailing_stops: Dict[float, float] = None
|
||||||
|
emergency_stop_days: int = 7
|
||||||
|
emergency_stop_loss: float = -0.10
|
||||||
|
trend_break_threshold: float = 0.85
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.trailing_stops is None:
|
||||||
|
self.trailing_stops = {
|
||||||
|
2.00: 0.60, # 200%+ profit: 40% trailing stop
|
||||||
|
1.50: 0.65, # 150%+ profit: 35% trailing stop
|
||||||
|
1.00: 0.70, # 100%+ profit: 30% trailing stop
|
||||||
|
0.75: 0.75, # 75%+ profit: 25% trailing stop
|
||||||
|
0.50: 0.78, # 50%+ profit: 22% trailing stop
|
||||||
|
0.25: 0.80, # 25%+ profit: 20% trailing stop
|
||||||
|
0.10: 0.85, # 10%+ profit: 15% trailing stop
|
||||||
|
0.00: 0.88 # < 10% profit: 12% trailing stop
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoggingConfig:
|
||||||
|
"""Logging configuration"""
|
||||||
|
log_level: str = 'INFO'
|
||||||
|
log_file: str = 'logs/trading.log'
|
||||||
|
log_format: str = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
max_file_size: int = 10 * 1024 * 1024 # 10MB
|
||||||
|
backup_count: int = 5
|
||||||
|
|
||||||
|
# Global configuration instances
|
||||||
|
alpaca_config = AlpacaConfig()
|
||||||
|
trading_config = TradingConfig()
|
||||||
|
strategy_config = StrategyConfig()
|
||||||
|
logging_config = LoggingConfig()
|
||||||
345
main.py
Normal file
345
main.py
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Main Trading Application
|
||||||
|
Entry point for the automated trading system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 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"""
|
||||||
|
global trading_engine, running
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.info("Starting live trading mode")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initialize trading engine
|
||||||
|
trading_engine = TradingEngine(paper_trading=True)
|
||||||
|
|
||||||
|
# Main trading loop
|
||||||
|
cycle_count = 0
|
||||||
|
last_summary_time = datetime.now()
|
||||||
|
|
||||||
|
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
|
||||||
|
if datetime.now() - last_summary_time > timedelta(hours=1):
|
||||||
|
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']
|
||||||
|
})
|
||||||
|
last_summary_time = datetime.now()
|
||||||
|
else:
|
||||||
|
logger.error(f"Cycle {cycle_count} failed")
|
||||||
|
|
||||||
|
# Wait before next cycle (3600 seconds = 1 hour for hourly timeframe)
|
||||||
|
sleep_duration = 3600 # 1 hour
|
||||||
|
|
||||||
|
for _ in range(sleep_duration):
|
||||||
|
if not running:
|
||||||
|
break
|
||||||
|
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', 'backtest'], default='backtest',
|
||||||
|
help='Trading mode: live 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)')
|
||||||
|
|
||||||
|
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 == 'live':
|
||||||
|
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)
|
||||||
20
requirements.txt
Normal file
20
requirements.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Core trading and data analysis libraries
|
||||||
|
alpaca-trade-api>=3.1.1
|
||||||
|
pandas>=1.5.0
|
||||||
|
numpy>=1.21.0
|
||||||
|
|
||||||
|
# Configuration and environment management
|
||||||
|
python-dotenv>=0.19.0
|
||||||
|
|
||||||
|
# Optional: For enhanced data analysis and visualization (uncomment if needed)
|
||||||
|
# matplotlib>=3.5.0
|
||||||
|
# seaborn>=0.11.0
|
||||||
|
# plotly>=5.0.0
|
||||||
|
|
||||||
|
# Optional: For advanced analytics (uncomment if needed)
|
||||||
|
# scikit-learn>=1.0.0
|
||||||
|
# scipy>=1.7.0
|
||||||
|
|
||||||
|
# Optional: For web interface (uncomment if needed)
|
||||||
|
# flask>=2.0.0
|
||||||
|
# dash>=2.0.0
|
||||||
101
run.py
Executable file
101
run.py
Executable file
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Quick Start Script
|
||||||
|
Easy way to run the trading system with common configurations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def install_requirements():
|
||||||
|
"""Install required packages"""
|
||||||
|
print("Installing requirements...")
|
||||||
|
try:
|
||||||
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
|
||||||
|
print("✓ Requirements installed successfully")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"✗ Failed to install requirements: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_test():
|
||||||
|
"""Run system test"""
|
||||||
|
print("Running system test...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run([sys.executable, "test_system.py"], capture_output=True, text=True)
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print("Errors:", result.stderr)
|
||||||
|
return result.returncode == 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Test failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_backtest():
|
||||||
|
"""Run backtesting"""
|
||||||
|
print("Running backtest...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run([sys.executable, "main.py", "--mode", "backtest"],
|
||||||
|
capture_output=False, text=True)
|
||||||
|
return result.returncode == 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Backtest failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_live():
|
||||||
|
"""Run live trading"""
|
||||||
|
print("Starting live trading (Paper Trading)...")
|
||||||
|
print("Press Ctrl+C to stop")
|
||||||
|
try:
|
||||||
|
result = subprocess.run([sys.executable, "main.py", "--mode", "live"],
|
||||||
|
capture_output=False, text=True)
|
||||||
|
return result.returncode == 0
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nLive trading stopped by user")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Live trading failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main menu"""
|
||||||
|
print("=" * 50)
|
||||||
|
print("AUTOMATED TRADING SYSTEM - QUICK START")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\nSelect an option:")
|
||||||
|
print("1. Install requirements")
|
||||||
|
print("2. Run system test")
|
||||||
|
print("3. Run backtesting")
|
||||||
|
print("4. Start live trading (Paper)")
|
||||||
|
print("5. Exit")
|
||||||
|
|
||||||
|
choice = input("\nEnter your choice (1-5): ").strip()
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
install_requirements()
|
||||||
|
elif choice == "2":
|
||||||
|
run_test()
|
||||||
|
elif choice == "3":
|
||||||
|
run_backtest()
|
||||||
|
elif choice == "4":
|
||||||
|
print("\n⚠️ WARNING: This will start live paper trading!")
|
||||||
|
print("Make sure you have:")
|
||||||
|
print("- Valid Alpaca paper trading API keys")
|
||||||
|
print("- Reviewed the configuration settings")
|
||||||
|
print("- Tested with backtesting first")
|
||||||
|
|
||||||
|
confirm = input("\nContinue? (yes/no): ").strip().lower()
|
||||||
|
if confirm in ['yes', 'y']:
|
||||||
|
run_live()
|
||||||
|
else:
|
||||||
|
print("Live trading cancelled")
|
||||||
|
elif choice == "5":
|
||||||
|
print("Goodbye!")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Invalid choice. Please enter 1-5.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
8
src/__init__.py
Normal file
8
src/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Trading System Package
|
||||||
|
A professional automated trading system for cryptocurrency trading.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__author__ = "Trading System"
|
||||||
|
__description__ = "Automated cryptocurrency trading system with risk management"
|
||||||
178
src/data_handler.py
Normal file
178
src/data_handler.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
"""
|
||||||
|
Data Handler Module
|
||||||
|
Handles all market data fetching and processing operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
import alpaca_trade_api as tradeapi
|
||||||
|
from config.trading_config import alpaca_config, trading_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DataHandler:
|
||||||
|
"""Handles market data operations"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the data handler with Alpaca API"""
|
||||||
|
self.api = tradeapi.REST(
|
||||||
|
alpaca_config.api_key,
|
||||||
|
alpaca_config.secret_key,
|
||||||
|
base_url=alpaca_config.data_url
|
||||||
|
)
|
||||||
|
logger.info("DataHandler initialized successfully")
|
||||||
|
|
||||||
|
def get_historical_data(self,
|
||||||
|
symbol: str = None,
|
||||||
|
days: int = 180,
|
||||||
|
timeframe: str = None) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Fetch historical market data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: Trading symbol (default from config)
|
||||||
|
days: Number of days of historical data
|
||||||
|
timeframe: Data timeframe (default from config)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame with OHLCV data and datetime index
|
||||||
|
"""
|
||||||
|
symbol = symbol or trading_config.symbol
|
||||||
|
timeframe = timeframe or trading_config.timeframe
|
||||||
|
|
||||||
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Fetching {days} days of {symbol} data from {start_date} to {end_date}")
|
||||||
|
|
||||||
|
# Convert timeframe string to Alpaca TimeFrame
|
||||||
|
tf_map = {
|
||||||
|
'Minute': tradeapi.TimeFrame.Minute,
|
||||||
|
'Hour': tradeapi.TimeFrame.Hour,
|
||||||
|
'Day': tradeapi.TimeFrame.Day
|
||||||
|
}
|
||||||
|
tf = tf_map.get(timeframe, tradeapi.TimeFrame.Hour)
|
||||||
|
|
||||||
|
bars = self.api.get_crypto_bars(symbol, tf, start_date, end_date).df
|
||||||
|
bars.index = pd.to_datetime(bars.index)
|
||||||
|
|
||||||
|
logger.info(f"Successfully fetched {len(bars)} bars of data")
|
||||||
|
return bars
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching historical data: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def calculate_technical_indicators(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Calculate all technical indicators for the strategy
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bars: OHLCV DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame with technical indicators added
|
||||||
|
"""
|
||||||
|
logger.info("Calculating technical indicators")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# EMAs
|
||||||
|
bars['ema_fast'] = bars['close'].ewm(span=trading_config.ema_fast).mean()
|
||||||
|
bars['ema_slow'] = bars['close'].ewm(span=trading_config.ema_slow).mean()
|
||||||
|
bars['ema_trend'] = bars['close'].ewm(span=trading_config.ema_trend).mean()
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
bars['rsi'] = self._calculate_rsi(bars['close'], trading_config.rsi_period)
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
ema_12 = bars['close'].ewm(span=trading_config.macd_fast).mean()
|
||||||
|
ema_26 = bars['close'].ewm(span=trading_config.macd_slow).mean()
|
||||||
|
bars['macd'] = ema_12 - ema_26
|
||||||
|
bars['macd_signal'] = bars['macd'].ewm(span=trading_config.macd_signal).mean()
|
||||||
|
|
||||||
|
# Price momentum
|
||||||
|
bars['price_momentum_6h'] = bars['close'].pct_change(periods=6)
|
||||||
|
|
||||||
|
# Volume indicators
|
||||||
|
bars['volume_ma'] = bars['volume'].rolling(window=20).mean()
|
||||||
|
bars['volume_ok'] = bars['volume'] > bars['volume_ma'] * 0.7
|
||||||
|
|
||||||
|
logger.info("Technical indicators calculated successfully")
|
||||||
|
return bars
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating technical indicators: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calculate_rsi(prices: pd.Series, window: int = 14) -> pd.Series:
|
||||||
|
"""Calculate RSI indicator"""
|
||||||
|
delta = prices.diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
|
||||||
|
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
|
||||||
|
rs = gain / loss
|
||||||
|
rsi = 100 - (100 / (1 + rs))
|
||||||
|
return rsi
|
||||||
|
|
||||||
|
def get_latest_price(self, symbol: str = None) -> float:
|
||||||
|
"""
|
||||||
|
Get the latest price for a symbol
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: Trading symbol (default from config)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Latest price as float
|
||||||
|
"""
|
||||||
|
symbol = symbol or trading_config.symbol
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get latest bar
|
||||||
|
bars = self.api.get_crypto_bars(
|
||||||
|
symbol,
|
||||||
|
tradeapi.TimeFrame.Minute,
|
||||||
|
datetime.now() - timedelta(minutes=5),
|
||||||
|
datetime.now()
|
||||||
|
).df
|
||||||
|
|
||||||
|
if not bars.empty:
|
||||||
|
return float(bars['close'].iloc[-1])
|
||||||
|
else:
|
||||||
|
logger.warning(f"No recent data available for {symbol}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching latest price for {symbol}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_data_quality(self, bars: pd.DataFrame) -> bool:
|
||||||
|
"""
|
||||||
|
Validate the quality of market data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bars: Market data DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if data quality is acceptable
|
||||||
|
"""
|
||||||
|
if bars.empty:
|
||||||
|
logger.error("Data is empty")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for excessive missing data
|
||||||
|
missing_pct = bars.isnull().sum().sum() / (len(bars) * len(bars.columns))
|
||||||
|
if missing_pct > 0.05: # More than 5% missing
|
||||||
|
logger.warning(f"High percentage of missing data: {missing_pct:.2%}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for reasonable price ranges
|
||||||
|
price_change = bars['close'].pct_change().abs()
|
||||||
|
extreme_moves = (price_change > 0.20).sum() # 20% moves
|
||||||
|
if extreme_moves > len(bars) * 0.01: # More than 1% of data points
|
||||||
|
logger.warning(f"Excessive extreme price moves detected: {extreme_moves}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("Data quality validation passed")
|
||||||
|
return True
|
||||||
114
src/logger_setup.py
Normal file
114
src/logger_setup.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
"""
|
||||||
|
Logging Setup Module
|
||||||
|
Configures comprehensive logging for the trading system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from config.trading_config import logging_config
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""Setup comprehensive logging configuration"""
|
||||||
|
|
||||||
|
# Create logs directory if it doesn't exist
|
||||||
|
log_dir = os.path.dirname(logging_config.log_file)
|
||||||
|
if not os.path.exists(log_dir):
|
||||||
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
|
# Create root logger
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(getattr(logging, logging_config.log_level))
|
||||||
|
|
||||||
|
# Remove any existing handlers
|
||||||
|
for handler in root_logger.handlers[:]:
|
||||||
|
root_logger.removeHandler(handler)
|
||||||
|
|
||||||
|
# Create formatter
|
||||||
|
formatter = logging.Formatter(logging_config.log_format)
|
||||||
|
|
||||||
|
# File handler with rotation
|
||||||
|
file_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
logging_config.log_file,
|
||||||
|
maxBytes=logging_config.max_file_size,
|
||||||
|
backupCount=logging_config.backup_count
|
||||||
|
)
|
||||||
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Console handler
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(getattr(logging, logging_config.log_level))
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# Create separate handlers for different types of logs
|
||||||
|
|
||||||
|
# Trading actions log
|
||||||
|
trading_logger = logging.getLogger('trading')
|
||||||
|
trading_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
'logs/trading_actions.log',
|
||||||
|
maxBytes=logging_config.max_file_size,
|
||||||
|
backupCount=logging_config.backup_count
|
||||||
|
)
|
||||||
|
trading_handler.setFormatter(formatter)
|
||||||
|
trading_logger.addHandler(trading_handler)
|
||||||
|
|
||||||
|
# Risk management log
|
||||||
|
risk_logger = logging.getLogger('risk')
|
||||||
|
risk_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
'logs/risk_management.log',
|
||||||
|
maxBytes=logging_config.max_file_size,
|
||||||
|
backupCount=logging_config.backup_count
|
||||||
|
)
|
||||||
|
risk_handler.setFormatter(formatter)
|
||||||
|
risk_logger.addHandler(risk_handler)
|
||||||
|
|
||||||
|
# Performance log
|
||||||
|
performance_logger = logging.getLogger('performance')
|
||||||
|
performance_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
'logs/performance.log',
|
||||||
|
maxBytes=logging_config.max_file_size,
|
||||||
|
backupCount=logging_config.backup_count
|
||||||
|
)
|
||||||
|
performance_handler.setFormatter(formatter)
|
||||||
|
performance_logger.addHandler(performance_handler)
|
||||||
|
|
||||||
|
# Log startup message
|
||||||
|
root_logger.info("="*50)
|
||||||
|
root_logger.info("Trading System Started")
|
||||||
|
root_logger.info(f"Timestamp: {datetime.now()}")
|
||||||
|
root_logger.info("="*50)
|
||||||
|
|
||||||
|
def log_trading_action(action: str, details: dict):
|
||||||
|
"""Log trading actions with details"""
|
||||||
|
trading_logger = logging.getLogger('trading')
|
||||||
|
|
||||||
|
log_msg = f"ACTION: {action}"
|
||||||
|
for key, value in details.items():
|
||||||
|
log_msg += f" | {key}: {value}"
|
||||||
|
|
||||||
|
trading_logger.info(log_msg)
|
||||||
|
|
||||||
|
def log_risk_event(event: str, details: dict):
|
||||||
|
"""Log risk management events"""
|
||||||
|
risk_logger = logging.getLogger('risk')
|
||||||
|
|
||||||
|
log_msg = f"RISK: {event}"
|
||||||
|
for key, value in details.items():
|
||||||
|
log_msg += f" | {key}: {value}"
|
||||||
|
|
||||||
|
risk_logger.warning(log_msg)
|
||||||
|
|
||||||
|
def log_performance_metric(metric: str, value: float, additional_info: dict = None):
|
||||||
|
"""Log performance metrics"""
|
||||||
|
performance_logger = logging.getLogger('performance')
|
||||||
|
|
||||||
|
log_msg = f"METRIC: {metric} = {value}"
|
||||||
|
if additional_info:
|
||||||
|
for key, val in additional_info.items():
|
||||||
|
log_msg += f" | {key}: {val}"
|
||||||
|
|
||||||
|
performance_logger.info(log_msg)
|
||||||
270
src/risk_manager.py
Normal file
270
src/risk_manager.py
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
Risk Management Module
|
||||||
|
Handles all risk management operations including position sizing,
|
||||||
|
drawdown monitoring, and emergency stops.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from config.trading_config import trading_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class RiskManager:
|
||||||
|
"""
|
||||||
|
Risk management system to protect capital and enforce trading rules
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize risk manager"""
|
||||||
|
self.daily_pnl = 0.0
|
||||||
|
self.session_start_value = None
|
||||||
|
self.max_drawdown_reached = False
|
||||||
|
self.trading_halted = False
|
||||||
|
self.last_reset_date = datetime.now().date()
|
||||||
|
logger.info("RiskManager initialized")
|
||||||
|
|
||||||
|
def check_risk_limits(self,
|
||||||
|
current_portfolio_value: float,
|
||||||
|
peak_portfolio_value: float) -> dict:
|
||||||
|
"""
|
||||||
|
Check all risk limits and return status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_portfolio_value: Current portfolio value
|
||||||
|
peak_portfolio_value: Historical peak portfolio value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with risk status information
|
||||||
|
"""
|
||||||
|
risk_status = {
|
||||||
|
'trading_allowed': True,
|
||||||
|
'warnings': [],
|
||||||
|
'violations': [],
|
||||||
|
'current_drawdown': 0.0,
|
||||||
|
'daily_pnl_pct': 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Calculate current drawdown
|
||||||
|
if peak_portfolio_value > 0:
|
||||||
|
current_drawdown = (peak_portfolio_value - current_portfolio_value) / peak_portfolio_value
|
||||||
|
risk_status['current_drawdown'] = current_drawdown
|
||||||
|
|
||||||
|
# Check maximum drawdown limit
|
||||||
|
if current_drawdown > trading_config.max_drawdown_limit:
|
||||||
|
risk_status['violations'].append(
|
||||||
|
f"Maximum drawdown exceeded: {current_drawdown:.2%} > {trading_config.max_drawdown_limit:.2%}"
|
||||||
|
)
|
||||||
|
risk_status['trading_allowed'] = False
|
||||||
|
self.max_drawdown_reached = True
|
||||||
|
elif current_drawdown > trading_config.max_drawdown_limit * 0.8:
|
||||||
|
risk_status['warnings'].append(
|
||||||
|
f"Approaching maximum drawdown: {current_drawdown:.2%}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check daily loss limit
|
||||||
|
if self.session_start_value is not None:
|
||||||
|
daily_pnl_pct = (current_portfolio_value - self.session_start_value) / self.session_start_value
|
||||||
|
risk_status['daily_pnl_pct'] = daily_pnl_pct
|
||||||
|
|
||||||
|
if daily_pnl_pct < -trading_config.daily_loss_limit:
|
||||||
|
risk_status['violations'].append(
|
||||||
|
f"Daily loss limit exceeded: {daily_pnl_pct:.2%} < -{trading_config.daily_loss_limit:.2%}"
|
||||||
|
)
|
||||||
|
risk_status['trading_allowed'] = False
|
||||||
|
elif daily_pnl_pct < -trading_config.daily_loss_limit * 0.8:
|
||||||
|
risk_status['warnings'].append(
|
||||||
|
f"Approaching daily loss limit: {daily_pnl_pct:.2%}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update trading status
|
||||||
|
if risk_status['violations']:
|
||||||
|
self.trading_halted = True
|
||||||
|
logger.warning(f"Trading halted due to risk violations: {risk_status['violations']}")
|
||||||
|
|
||||||
|
if risk_status['warnings']:
|
||||||
|
logger.warning(f"Risk warnings: {risk_status['warnings']}")
|
||||||
|
|
||||||
|
return risk_status
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in risk limit check: {e}")
|
||||||
|
# Fail safe - halt trading on error
|
||||||
|
risk_status['trading_allowed'] = False
|
||||||
|
risk_status['violations'].append(f"Risk check error: {e}")
|
||||||
|
return risk_status
|
||||||
|
|
||||||
|
def calculate_position_size(self,
|
||||||
|
account_value: float,
|
||||||
|
asset_price: float,
|
||||||
|
volatility: float = None,
|
||||||
|
risk_override: float = None) -> dict:
|
||||||
|
"""
|
||||||
|
Calculate appropriate position size based on risk parameters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account_value: Current account value
|
||||||
|
asset_price: Current asset price
|
||||||
|
volatility: Asset volatility (optional)
|
||||||
|
risk_override: Risk amount override (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with position sizing information
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Base risk amount
|
||||||
|
if risk_override:
|
||||||
|
risk_amount = risk_override
|
||||||
|
else:
|
||||||
|
risk_amount = account_value * trading_config.risk_per_trade
|
||||||
|
|
||||||
|
# Maximum position value
|
||||||
|
max_position_value = account_value * trading_config.max_position_size
|
||||||
|
|
||||||
|
# Calculate position size based on stop loss
|
||||||
|
# Using conservative 8% stop loss for position sizing
|
||||||
|
stop_loss_pct = 0.08
|
||||||
|
if volatility:
|
||||||
|
# Adjust stop loss based on volatility
|
||||||
|
stop_loss_pct = max(0.05, min(0.15, volatility * 2))
|
||||||
|
|
||||||
|
position_value = risk_amount / stop_loss_pct
|
||||||
|
|
||||||
|
# Apply maximum position limit
|
||||||
|
position_value = min(position_value, max_position_value)
|
||||||
|
|
||||||
|
# Calculate number of shares/units
|
||||||
|
position_size = position_value / asset_price
|
||||||
|
|
||||||
|
position_info = {
|
||||||
|
'position_value': position_value,
|
||||||
|
'position_size': position_size,
|
||||||
|
'risk_amount': risk_amount,
|
||||||
|
'stop_loss_pct': stop_loss_pct,
|
||||||
|
'risk_per_trade_pct': risk_amount / account_value,
|
||||||
|
'position_pct_of_account': position_value / account_value
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Position sizing: ${position_value:.2f} "
|
||||||
|
f"({position_info['position_pct_of_account']:.1%} of account, "
|
||||||
|
f"{position_info['risk_per_trade_pct']:.1%} risk)")
|
||||||
|
|
||||||
|
return position_info
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating position size: {e}")
|
||||||
|
return {
|
||||||
|
'position_value': 0,
|
||||||
|
'position_size': 0,
|
||||||
|
'risk_amount': 0,
|
||||||
|
'stop_loss_pct': 0,
|
||||||
|
'risk_per_trade_pct': 0,
|
||||||
|
'position_pct_of_account': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_daily_tracking(self, current_portfolio_value: float):
|
||||||
|
"""
|
||||||
|
Update daily tracking metrics
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_portfolio_value: Current portfolio value
|
||||||
|
"""
|
||||||
|
current_date = datetime.now().date()
|
||||||
|
|
||||||
|
# Reset daily tracking if new day
|
||||||
|
if current_date != self.last_reset_date:
|
||||||
|
self.session_start_value = current_portfolio_value
|
||||||
|
self.daily_pnl = 0.0
|
||||||
|
self.last_reset_date = current_date
|
||||||
|
logger.info(f"Daily tracking reset for {current_date}")
|
||||||
|
|
||||||
|
# Set session start value if not set
|
||||||
|
if self.session_start_value is None:
|
||||||
|
self.session_start_value = current_portfolio_value
|
||||||
|
|
||||||
|
def should_reduce_position(self,
|
||||||
|
current_drawdown: float,
|
||||||
|
consecutive_losses: int) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if position size should be reduced
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_drawdown: Current portfolio drawdown
|
||||||
|
consecutive_losses: Number of consecutive losing trades
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if position should be reduced
|
||||||
|
"""
|
||||||
|
# Reduce position if significant drawdown
|
||||||
|
if current_drawdown > trading_config.max_drawdown_limit * 0.5:
|
||||||
|
logger.info("Recommending position size reduction due to drawdown")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Reduce position after multiple consecutive losses
|
||||||
|
if consecutive_losses >= 3:
|
||||||
|
logger.info("Recommending position size reduction due to consecutive losses")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_emergency_exit_signal(self,
|
||||||
|
bars: pd.DataFrame,
|
||||||
|
current_position_pct: float) -> bool:
|
||||||
|
"""
|
||||||
|
Check for emergency exit conditions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bars: Market data DataFrame
|
||||||
|
current_position_pct: Current position as percentage of portfolio
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if emergency exit is recommended
|
||||||
|
"""
|
||||||
|
if bars.empty or len(bars) < 20:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check for extreme volatility
|
||||||
|
recent_returns = bars['close'].pct_change().tail(20)
|
||||||
|
volatility = recent_returns.std()
|
||||||
|
|
||||||
|
if volatility > 0.05: # 5% hourly volatility
|
||||||
|
logger.warning(f"High volatility detected: {volatility:.3f}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for flash crash conditions
|
||||||
|
max_decline = recent_returns.min()
|
||||||
|
if max_decline < -0.15: # 15% decline in one period
|
||||||
|
logger.warning(f"Flash crash condition detected: {max_decline:.2%}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for sustained negative momentum
|
||||||
|
momentum_periods = bars['price_momentum_6h'].tail(10)
|
||||||
|
if (momentum_periods < -0.02).sum() >= 7: # 7 out of 10 periods negative
|
||||||
|
logger.warning("Sustained negative momentum detected")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in emergency exit check: {e}")
|
||||||
|
return True # Fail safe - recommend exit on error
|
||||||
|
|
||||||
|
def reset_risk_state(self):
|
||||||
|
"""Reset risk management state (use carefully)"""
|
||||||
|
self.max_drawdown_reached = False
|
||||||
|
self.trading_halted = False
|
||||||
|
self.daily_pnl = 0.0
|
||||||
|
logger.warning("Risk management state has been reset")
|
||||||
|
|
||||||
|
def get_risk_summary(self) -> dict:
|
||||||
|
"""Get current risk management summary"""
|
||||||
|
return {
|
||||||
|
'trading_halted': self.trading_halted,
|
||||||
|
'max_drawdown_reached': self.max_drawdown_reached,
|
||||||
|
'daily_pnl': self.daily_pnl,
|
||||||
|
'session_start_value': self.session_start_value,
|
||||||
|
'last_reset_date': self.last_reset_date.strftime('%Y-%m-%d')
|
||||||
|
}
|
||||||
256
src/strategy.py
Normal file
256
src/strategy.py
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
"""
|
||||||
|
Trading Strategy Module
|
||||||
|
Contains the core trading strategy logic and signal generation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import logging
|
||||||
|
from config.trading_config import strategy_config, trading_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TrendFollowingStrategy:
|
||||||
|
"""
|
||||||
|
Trend-following strategy implementation
|
||||||
|
Generates buy/sell signals based on EMA, RSI, MACD, and momentum indicators
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the strategy"""
|
||||||
|
self.last_signal_idx = -50
|
||||||
|
self.position = 0
|
||||||
|
self.entry_price = None
|
||||||
|
self.highest_price_since_entry = None
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
logger.info("TrendFollowingStrategy initialized")
|
||||||
|
|
||||||
|
def generate_signals(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Generate buy and sell signals based on strategy rules
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bars: DataFrame with OHLCV data and technical indicators
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame with buy_signal and sell_signal columns added
|
||||||
|
"""
|
||||||
|
logger.info("Generating trading signals")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Generate raw signals
|
||||||
|
bars = self._generate_buy_conditions(bars)
|
||||||
|
bars = self._generate_sell_conditions(bars)
|
||||||
|
|
||||||
|
# Apply cooldown period
|
||||||
|
bars = self._apply_signal_cooldown(bars)
|
||||||
|
|
||||||
|
logger.info("Trading signals generated successfully")
|
||||||
|
return bars
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating signals: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _generate_buy_conditions(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Generate buy signal conditions"""
|
||||||
|
|
||||||
|
# Basic trend requirement - must be above major trend
|
||||||
|
trend_condition = bars['close'] > bars['ema_trend']
|
||||||
|
|
||||||
|
# Entry trigger conditions
|
||||||
|
ema_bullish = (
|
||||||
|
(bars['ema_fast'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['rsi'] > strategy_config.rsi_oversold) &
|
||||||
|
(bars['rsi'] < strategy_config.rsi_overbought)
|
||||||
|
)
|
||||||
|
|
||||||
|
macd_bullish = (
|
||||||
|
(bars['macd'] > bars['macd_signal']) &
|
||||||
|
(bars['price_momentum_6h'] > strategy_config.min_momentum_threshold) &
|
||||||
|
(bars['rsi'] > 40) & (bars['rsi'] < 70)
|
||||||
|
)
|
||||||
|
|
||||||
|
breakout_condition = (
|
||||||
|
(bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['close'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_trend']) &
|
||||||
|
(bars['volume_ok']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.015)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine all conditions
|
||||||
|
buy_conditions = trend_condition & (ema_bullish | macd_bullish | breakout_condition)
|
||||||
|
bars['buy_signal_raw'] = buy_conditions
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def _generate_sell_conditions(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Generate sell signal conditions"""
|
||||||
|
|
||||||
|
# EMA death cross with major breakdown
|
||||||
|
ema_bearish = (
|
||||||
|
(bars['ema_fast'] < bars['ema_slow']) &
|
||||||
|
(bars['close'] < bars['ema_trend'] * 0.90) &
|
||||||
|
(bars['rsi'] < 35)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sustained breakdown below major trend
|
||||||
|
trend_breakdown = (
|
||||||
|
(bars['close'] < bars['ema_trend'] * 0.88) &
|
||||||
|
(bars['price_momentum_6h'] < -0.04)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extreme overbought with reversal signs
|
||||||
|
overbought_reversal = (
|
||||||
|
(bars['rsi'] > 85) &
|
||||||
|
(bars['rsi'].shift(1) > bars['rsi']) &
|
||||||
|
(bars['rsi'].shift(2) > bars['rsi'].shift(1)) &
|
||||||
|
(bars['rsi'].shift(3) > bars['rsi'].shift(2)) &
|
||||||
|
(bars['macd'] < bars['macd_signal']) &
|
||||||
|
(bars['price_momentum_6h'] < -0.02)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine all sell conditions
|
||||||
|
sell_conditions = ema_bearish | trend_breakdown | overbought_reversal
|
||||||
|
bars['sell_signal_raw'] = sell_conditions
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def _apply_signal_cooldown(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Apply cooldown period between signals"""
|
||||||
|
|
||||||
|
bars['buy_signal'] = False
|
||||||
|
bars['sell_signal'] = False
|
||||||
|
|
||||||
|
last_signal_idx = -50
|
||||||
|
cooldown_hours = trading_config.cooldown_hours
|
||||||
|
|
||||||
|
for i in range(len(bars)):
|
||||||
|
if i - last_signal_idx >= cooldown_hours:
|
||||||
|
if bars['buy_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('buy_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
elif bars['sell_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('sell_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def calculate_position_size(self,
|
||||||
|
current_price: float,
|
||||||
|
account_value: float,
|
||||||
|
risk_amount: float = None) -> float:
|
||||||
|
"""
|
||||||
|
Calculate position size based on risk management rules
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_price: Current asset price
|
||||||
|
account_value: Total account value
|
||||||
|
risk_amount: Amount to risk (optional, uses config default)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Position size in dollars
|
||||||
|
"""
|
||||||
|
if risk_amount is None:
|
||||||
|
risk_amount = account_value * trading_config.risk_per_trade
|
||||||
|
|
||||||
|
# Maximum position size
|
||||||
|
max_position = account_value * trading_config.max_position_size
|
||||||
|
|
||||||
|
# Risk-based position size (simplified for trend following)
|
||||||
|
# Using 10% stop loss for position sizing
|
||||||
|
stop_loss_pct = 0.10
|
||||||
|
position_size = risk_amount / stop_loss_pct
|
||||||
|
|
||||||
|
# Apply maximum position limit
|
||||||
|
position_size = min(position_size, max_position)
|
||||||
|
|
||||||
|
logger.info(f"Calculated position size: ${position_size:.2f}")
|
||||||
|
return position_size
|
||||||
|
|
||||||
|
def get_trailing_stop_price(self,
|
||||||
|
entry_price: float,
|
||||||
|
highest_price: float,
|
||||||
|
current_profit_pct: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate trailing stop price based on profit level
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry_price: Original entry price
|
||||||
|
highest_price: Highest price since entry
|
||||||
|
current_profit_pct: Current profit percentage
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Trailing stop price
|
||||||
|
"""
|
||||||
|
# Get appropriate trailing stop percentage
|
||||||
|
trailing_stop_pct = 0.88 # Default
|
||||||
|
|
||||||
|
for profit_threshold in sorted(strategy_config.trailing_stops.keys(), reverse=True):
|
||||||
|
if current_profit_pct >= profit_threshold:
|
||||||
|
trailing_stop_pct = strategy_config.trailing_stops[profit_threshold]
|
||||||
|
break
|
||||||
|
|
||||||
|
trailing_stop_price = highest_price * trailing_stop_pct
|
||||||
|
|
||||||
|
logger.debug(f"Trailing stop: {trailing_stop_price:.2f} "
|
||||||
|
f"(profit: {current_profit_pct:.2%}, "
|
||||||
|
f"stop %: {(1-trailing_stop_pct):.1%})")
|
||||||
|
|
||||||
|
return trailing_stop_price
|
||||||
|
|
||||||
|
def should_exit_position(self,
|
||||||
|
current_price: float,
|
||||||
|
entry_price: float,
|
||||||
|
highest_price: float,
|
||||||
|
bars_since_entry: int,
|
||||||
|
sell_signal: bool,
|
||||||
|
ema_trend: float) -> tuple:
|
||||||
|
"""
|
||||||
|
Determine if position should be exited
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_price: Current asset price
|
||||||
|
entry_price: Entry price
|
||||||
|
highest_price: Highest price since entry
|
||||||
|
bars_since_entry: Number of bars since entry
|
||||||
|
sell_signal: Whether sell signal is active
|
||||||
|
ema_trend: Current EMA trend value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (should_exit: bool, exit_reason: str)
|
||||||
|
"""
|
||||||
|
profit_pct = (current_price / entry_price - 1)
|
||||||
|
|
||||||
|
# Get trailing stop price
|
||||||
|
trailing_stop_price = self.get_trailing_stop_price(
|
||||||
|
entry_price, highest_price, profit_pct
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check exit conditions
|
||||||
|
if sell_signal:
|
||||||
|
return True, "sell_signal"
|
||||||
|
|
||||||
|
if current_price <= trailing_stop_price:
|
||||||
|
return True, f"trailing_stop (profit: {profit_pct:.2%})"
|
||||||
|
|
||||||
|
# Emergency time-based stops
|
||||||
|
emergency_hours = strategy_config.emergency_stop_days * 24
|
||||||
|
if (bars_since_entry >= emergency_hours and
|
||||||
|
profit_pct < strategy_config.emergency_stop_loss):
|
||||||
|
return True, f"emergency_stop ({bars_since_entry}h, {profit_pct:.2%})"
|
||||||
|
|
||||||
|
# Major trend break
|
||||||
|
if current_price < ema_trend * strategy_config.trend_break_threshold:
|
||||||
|
return True, f"trend_break ({profit_pct:.2%})"
|
||||||
|
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
def reset_position_state(self):
|
||||||
|
"""Reset position tracking state"""
|
||||||
|
self.position = 0
|
||||||
|
self.entry_price = None
|
||||||
|
self.highest_price_since_entry = None
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
logger.info("Position state reset")
|
||||||
199
src/strategy_conservative.py
Normal file
199
src/strategy_conservative.py
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
"""
|
||||||
|
Conservative Trading Strategy
|
||||||
|
Based on backtesting_short.py - Simple but effective approach with relaxed exit conditions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ConservativeTrendStrategy:
|
||||||
|
"""
|
||||||
|
Conservative trend-following strategy
|
||||||
|
- Simple entry conditions
|
||||||
|
- Relaxed trailing stops (18% minimum)
|
||||||
|
- Very relaxed emergency stops (28 days, -30%)
|
||||||
|
- Focuses on letting winners run
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the conservative strategy"""
|
||||||
|
self.name = "Conservative Trend Strategy"
|
||||||
|
self.last_signal_idx = -50
|
||||||
|
self.position = 0
|
||||||
|
self.entry_price = None
|
||||||
|
self.highest_price_since_entry = None
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
logger.info(f"{self.name} initialized")
|
||||||
|
|
||||||
|
def generate_signals(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Generate buy and sell signals"""
|
||||||
|
logger.info("Generating conservative strategy signals")
|
||||||
|
|
||||||
|
try:
|
||||||
|
bars = self._generate_buy_conditions(bars)
|
||||||
|
bars = self._generate_sell_conditions(bars)
|
||||||
|
bars = self._apply_signal_cooldown(bars)
|
||||||
|
|
||||||
|
logger.info("Conservative strategy signals generated successfully")
|
||||||
|
return bars
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating conservative signals: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _generate_buy_conditions(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Generate conservative buy conditions - same as original"""
|
||||||
|
|
||||||
|
# Basic trend requirement - must be above major trend (no tolerance)
|
||||||
|
trend_condition = bars['close'] > bars['ema_trend']
|
||||||
|
|
||||||
|
# Entry trigger conditions (same as original backtesting_short.py)
|
||||||
|
ema_bullish = (
|
||||||
|
(bars['ema_fast'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['rsi'] > 45) & (bars['rsi'] < 75) # Balanced RSI
|
||||||
|
)
|
||||||
|
|
||||||
|
macd_bullish = (
|
||||||
|
(bars['macd'] > bars['macd_signal']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.02) & # 2% momentum required
|
||||||
|
(bars['rsi'] > 40) & (bars['rsi'] < 70) # Not overbought
|
||||||
|
)
|
||||||
|
|
||||||
|
breakout_condition = (
|
||||||
|
(bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['close'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_trend']) &
|
||||||
|
(bars['volume_ok']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.015) # Strong momentum
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine all conditions
|
||||||
|
buy_conditions = trend_condition & (ema_bullish | macd_bullish | breakout_condition)
|
||||||
|
bars['buy_signal_raw'] = buy_conditions
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def _generate_sell_conditions(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Generate conservative sell conditions - ultra-conservative"""
|
||||||
|
|
||||||
|
# EMA death cross with major breakdown
|
||||||
|
ema_bearish = (
|
||||||
|
(bars['ema_fast'] < bars['ema_slow']) &
|
||||||
|
(bars['close'] < bars['ema_trend'] * 0.90) & # 10% below trend
|
||||||
|
(bars['rsi'] < 35) # Very weak momentum
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sustained breakdown below major trend
|
||||||
|
trend_breakdown = (
|
||||||
|
(bars['close'] < bars['ema_trend'] * 0.88) & # 12% below major trend
|
||||||
|
(bars['price_momentum_6h'] < -0.04) # Stronger decline
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extreme overbought with strong reversal signs
|
||||||
|
overbought_reversal = (
|
||||||
|
(bars['rsi'] > 85) & # Higher threshold
|
||||||
|
(bars['rsi'].shift(1) > bars['rsi']) & # RSI declining
|
||||||
|
(bars['rsi'].shift(2) > bars['rsi'].shift(1)) & # For 2 periods
|
||||||
|
(bars['rsi'].shift(3) > bars['rsi'].shift(2)) & # For 3 periods
|
||||||
|
(bars['macd'] < bars['macd_signal']) & # MACD bearish
|
||||||
|
(bars['price_momentum_6h'] < -0.02) # Plus price weakness
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine all sell conditions
|
||||||
|
sell_conditions = ema_bearish | trend_breakdown | overbought_reversal
|
||||||
|
bars['sell_signal_raw'] = sell_conditions
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def _apply_signal_cooldown(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Apply 24-hour cooldown between signals"""
|
||||||
|
|
||||||
|
bars['buy_signal'] = False
|
||||||
|
bars['sell_signal'] = False
|
||||||
|
|
||||||
|
last_signal_idx = -50
|
||||||
|
cooldown_hours = 24 # Conservative: 24-hour cooldown
|
||||||
|
|
||||||
|
for i in range(len(bars)):
|
||||||
|
if i - last_signal_idx >= cooldown_hours:
|
||||||
|
if bars['buy_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('buy_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
elif bars['sell_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('sell_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def get_entry_conditions(self, bars: pd.DataFrame, i: int) -> bool:
|
||||||
|
"""Conservative entry - must be above trend"""
|
||||||
|
return bars['close'].iloc[i] > bars['ema_trend'].iloc[i]
|
||||||
|
|
||||||
|
def get_trailing_stop_price(self,
|
||||||
|
entry_price: float,
|
||||||
|
highest_price: float,
|
||||||
|
current_profit_pct: float) -> float:
|
||||||
|
"""Conservative trailing stops - let big winners run!"""
|
||||||
|
|
||||||
|
# Conservative trailing stops (wider stops to let winners run)
|
||||||
|
if current_profit_pct > 2.00: # 200%+ profit: 40% trailing stop
|
||||||
|
trailing_stop_pct = 0.60
|
||||||
|
elif current_profit_pct > 1.50: # 150%+ profit: 35% trailing stop
|
||||||
|
trailing_stop_pct = 0.65
|
||||||
|
elif current_profit_pct > 1.00: # 100%+ profit: 30% trailing stop
|
||||||
|
trailing_stop_pct = 0.70
|
||||||
|
elif current_profit_pct > 0.75: # 75%+ profit: 25% trailing stop
|
||||||
|
trailing_stop_pct = 0.75
|
||||||
|
elif current_profit_pct > 0.50: # 50%+ profit: 22% trailing stop
|
||||||
|
trailing_stop_pct = 0.78
|
||||||
|
elif current_profit_pct > 0.25: # 25%+ profit: 20% trailing stop
|
||||||
|
trailing_stop_pct = 0.80
|
||||||
|
else: # < 25% profit: 18% trailing stop
|
||||||
|
trailing_stop_pct = 0.82
|
||||||
|
|
||||||
|
trailing_stop_price = highest_price * trailing_stop_pct
|
||||||
|
|
||||||
|
logger.debug(f"Conservative trailing stop: {trailing_stop_price:.2f} "
|
||||||
|
f"(profit: {current_profit_pct:.2%}, stop: {(1-trailing_stop_pct):.1%})")
|
||||||
|
|
||||||
|
return trailing_stop_price
|
||||||
|
|
||||||
|
def should_exit_position(self,
|
||||||
|
current_price: float,
|
||||||
|
entry_price: float,
|
||||||
|
highest_price: float,
|
||||||
|
bars_since_entry: int,
|
||||||
|
sell_signal: bool,
|
||||||
|
ema_trend: float) -> tuple:
|
||||||
|
"""Conservative exit conditions - very relaxed"""
|
||||||
|
|
||||||
|
profit_pct = (current_price / entry_price - 1)
|
||||||
|
trailing_stop_price = self.get_trailing_stop_price(entry_price, highest_price, profit_pct)
|
||||||
|
|
||||||
|
# Check exit conditions
|
||||||
|
if sell_signal:
|
||||||
|
return True, "sell_signal"
|
||||||
|
|
||||||
|
if current_price <= trailing_stop_price:
|
||||||
|
return True, f"trailing_stop (profit: {profit_pct:.2%})"
|
||||||
|
|
||||||
|
# Very relaxed emergency stops
|
||||||
|
if bars_since_entry >= 672 and profit_pct < -0.30: # 28 days & -30%
|
||||||
|
return True, f"emergency_stop_28d ({profit_pct:.2%})"
|
||||||
|
|
||||||
|
# Major trend break (very relaxed)
|
||||||
|
if current_price < ema_trend * 0.75: # 25% below major trend
|
||||||
|
return True, f"major_trend_break ({profit_pct:.2%})"
|
||||||
|
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
def reset_position_state(self):
|
||||||
|
"""Reset position tracking state"""
|
||||||
|
self.position = 0
|
||||||
|
self.entry_price = None
|
||||||
|
self.highest_price_since_entry = None
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
logger.info(f"{self.name} position state reset")
|
||||||
205
src/strategy_enhanced.py
Normal file
205
src/strategy_enhanced.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
Enhanced Trading Strategy
|
||||||
|
Based on backtesting_long.py - More sophisticated approach with better risk management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class EnhancedTrendStrategy:
|
||||||
|
"""
|
||||||
|
Enhanced trend-following strategy
|
||||||
|
- Stricter entry conditions (2% above major trend)
|
||||||
|
- Tighter trailing stops with better risk management
|
||||||
|
- More aggressive time-based stops (7-14 days)
|
||||||
|
- Better loss control for early positions
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the enhanced strategy"""
|
||||||
|
self.name = "Enhanced Trend Strategy"
|
||||||
|
self.last_signal_idx = -50
|
||||||
|
self.position = 0
|
||||||
|
self.entry_price = None
|
||||||
|
self.highest_price_since_entry = None
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
logger.info(f"{self.name} initialized")
|
||||||
|
|
||||||
|
def generate_signals(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Generate buy and sell signals"""
|
||||||
|
logger.info("Generating enhanced strategy signals")
|
||||||
|
|
||||||
|
try:
|
||||||
|
bars = self._generate_buy_conditions(bars)
|
||||||
|
bars = self._generate_sell_conditions(bars)
|
||||||
|
bars = self._apply_signal_cooldown(bars)
|
||||||
|
|
||||||
|
logger.info("Enhanced strategy signals generated successfully")
|
||||||
|
return bars
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating enhanced signals: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _generate_buy_conditions(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Generate enhanced buy conditions - same as original"""
|
||||||
|
|
||||||
|
# Basic trend requirement - must be above major trend (no tolerance)
|
||||||
|
trend_condition = bars['close'] > bars['ema_trend']
|
||||||
|
|
||||||
|
# Entry trigger conditions (same as original)
|
||||||
|
ema_bullish = (
|
||||||
|
(bars['ema_fast'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['rsi'] > 45) & (bars['rsi'] < 75) # Balanced RSI
|
||||||
|
)
|
||||||
|
|
||||||
|
macd_bullish = (
|
||||||
|
(bars['macd'] > bars['macd_signal']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.02) & # 2% momentum required
|
||||||
|
(bars['rsi'] > 40) & (bars['rsi'] < 70) # Not overbought
|
||||||
|
)
|
||||||
|
|
||||||
|
breakout_condition = (
|
||||||
|
(bars['close'] > bars['ema_fast']) &
|
||||||
|
(bars['close'] > bars['ema_slow']) &
|
||||||
|
(bars['close'] > bars['ema_trend']) &
|
||||||
|
(bars['volume_ok']) &
|
||||||
|
(bars['price_momentum_6h'] > 0.015) # Strong momentum
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine all conditions
|
||||||
|
buy_conditions = trend_condition & (ema_bullish | macd_bullish | breakout_condition)
|
||||||
|
bars['buy_signal_raw'] = buy_conditions
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def _generate_sell_conditions(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Generate enhanced sell conditions - same as conservative"""
|
||||||
|
|
||||||
|
# EMA death cross with major breakdown
|
||||||
|
ema_bearish = (
|
||||||
|
(bars['ema_fast'] < bars['ema_slow']) &
|
||||||
|
(bars['close'] < bars['ema_trend'] * 0.90) & # 10% below trend
|
||||||
|
(bars['rsi'] < 35) # Very weak momentum
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sustained breakdown below major trend
|
||||||
|
trend_breakdown = (
|
||||||
|
(bars['close'] < bars['ema_trend'] * 0.88) & # 12% below major trend
|
||||||
|
(bars['price_momentum_6h'] < -0.04) # Stronger decline
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extreme overbought with strong reversal signs
|
||||||
|
overbought_reversal = (
|
||||||
|
(bars['rsi'] > 85) & # Higher threshold
|
||||||
|
(bars['rsi'].shift(1) > bars['rsi']) & # RSI declining
|
||||||
|
(bars['rsi'].shift(2) > bars['rsi'].shift(1)) & # For 2 periods
|
||||||
|
(bars['rsi'].shift(3) > bars['rsi'].shift(2)) & # For 3 periods
|
||||||
|
(bars['macd'] < bars['macd_signal']) & # MACD bearish
|
||||||
|
(bars['price_momentum_6h'] < -0.02) # Plus price weakness
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine all sell conditions
|
||||||
|
sell_conditions = ema_bearish | trend_breakdown | overbought_reversal
|
||||||
|
bars['sell_signal_raw'] = sell_conditions
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def _apply_signal_cooldown(self, bars: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Apply 24-hour cooldown between signals"""
|
||||||
|
|
||||||
|
bars['buy_signal'] = False
|
||||||
|
bars['sell_signal'] = False
|
||||||
|
|
||||||
|
last_signal_idx = -50
|
||||||
|
cooldown_hours = 24 # Same 24-hour cooldown
|
||||||
|
|
||||||
|
for i in range(len(bars)):
|
||||||
|
if i - last_signal_idx >= cooldown_hours:
|
||||||
|
if bars['buy_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('buy_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
elif bars['sell_signal_raw'].iloc[i]:
|
||||||
|
bars.iloc[i, bars.columns.get_loc('sell_signal')] = True
|
||||||
|
last_signal_idx = i
|
||||||
|
|
||||||
|
return bars
|
||||||
|
|
||||||
|
def get_entry_conditions(self, bars: pd.DataFrame, i: int) -> bool:
|
||||||
|
"""Enhanced entry - MORE STRICT (2% above trend + fast EMA above slow)"""
|
||||||
|
return (bars['close'].iloc[i] > bars['ema_trend'].iloc[i] * 1.02 and # 2% above major trend
|
||||||
|
bars['ema_fast'].iloc[i] > bars['ema_slow'].iloc[i] * 1.01) # Fast EMA clearly above slow
|
||||||
|
|
||||||
|
def get_trailing_stop_price(self,
|
||||||
|
entry_price: float,
|
||||||
|
highest_price: float,
|
||||||
|
current_profit_pct: float) -> float:
|
||||||
|
"""Enhanced trailing stops with better risk management"""
|
||||||
|
|
||||||
|
# Enhanced trailing stops with better risk management
|
||||||
|
if current_profit_pct > 2.00: # 200%+ profit: 40% trailing stop
|
||||||
|
trailing_stop_pct = 0.60
|
||||||
|
elif current_profit_pct > 1.50: # 150%+ profit: 35% trailing stop
|
||||||
|
trailing_stop_pct = 0.65
|
||||||
|
elif current_profit_pct > 1.00: # 100%+ profit: 30% trailing stop
|
||||||
|
trailing_stop_pct = 0.70
|
||||||
|
elif current_profit_pct > 0.75: # 75%+ profit: 25% trailing stop
|
||||||
|
trailing_stop_pct = 0.75
|
||||||
|
elif current_profit_pct > 0.50: # 50%+ profit: 22% trailing stop
|
||||||
|
trailing_stop_pct = 0.78
|
||||||
|
elif current_profit_pct > 0.25: # 25%+ profit: 20% trailing stop
|
||||||
|
trailing_stop_pct = 0.80
|
||||||
|
elif current_profit_pct > 0.10: # 10%+ profit: 15% trailing stop
|
||||||
|
trailing_stop_pct = 0.85
|
||||||
|
else: # < 10% profit: 12% trailing stop (tighter for early losses)
|
||||||
|
trailing_stop_pct = 0.88
|
||||||
|
|
||||||
|
trailing_stop_price = highest_price * trailing_stop_pct
|
||||||
|
|
||||||
|
logger.debug(f"Enhanced trailing stop: {trailing_stop_price:.2f} "
|
||||||
|
f"(profit: {current_profit_pct:.2%}, stop: {(1-trailing_stop_pct):.1%})")
|
||||||
|
|
||||||
|
return trailing_stop_price
|
||||||
|
|
||||||
|
def should_exit_position(self,
|
||||||
|
current_price: float,
|
||||||
|
entry_price: float,
|
||||||
|
highest_price: float,
|
||||||
|
bars_since_entry: int,
|
||||||
|
sell_signal: bool,
|
||||||
|
ema_trend: float) -> tuple:
|
||||||
|
"""Enhanced exit conditions - better loss control"""
|
||||||
|
|
||||||
|
profit_pct = (current_price / entry_price - 1)
|
||||||
|
trailing_stop_price = self.get_trailing_stop_price(entry_price, highest_price, profit_pct)
|
||||||
|
|
||||||
|
# Check exit conditions
|
||||||
|
if sell_signal:
|
||||||
|
return True, "sell_signal"
|
||||||
|
|
||||||
|
if current_price <= trailing_stop_price:
|
||||||
|
return True, f"trailing_stop (profit: {profit_pct:.2%})"
|
||||||
|
|
||||||
|
# Tighter time-based stops to prevent long drawdowns
|
||||||
|
if bars_since_entry >= 168 and profit_pct < -0.10: # 7 days & -10%
|
||||||
|
return True, f"emergency_stop_7d ({profit_pct:.2%})"
|
||||||
|
|
||||||
|
if bars_since_entry >= 336 and profit_pct < -0.05: # 14 days & -5%
|
||||||
|
return True, f"emergency_stop_14d ({profit_pct:.2%})"
|
||||||
|
|
||||||
|
# Major trend break (tighter)
|
||||||
|
if current_price < ema_trend * 0.85: # 15% below major trend
|
||||||
|
return True, f"trend_break_15pct ({profit_pct:.2%})"
|
||||||
|
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
def reset_position_state(self):
|
||||||
|
"""Reset position tracking state"""
|
||||||
|
self.position = 0
|
||||||
|
self.entry_price = None
|
||||||
|
self.highest_price_since_entry = None
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
logger.info(f"{self.name} position state reset")
|
||||||
68
src/strategy_factory.py
Normal file
68
src/strategy_factory.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""
|
||||||
|
Strategy Factory
|
||||||
|
Handles creation and selection of different trading strategies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from config.trading_config import trading_config
|
||||||
|
from src.strategy_conservative import ConservativeTrendStrategy
|
||||||
|
from src.strategy_enhanced import EnhancedTrendStrategy
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class StrategyFactory:
|
||||||
|
"""Factory class for creating trading strategies"""
|
||||||
|
|
||||||
|
AVAILABLE_STRATEGIES = {
|
||||||
|
'conservative': ConservativeTrendStrategy,
|
||||||
|
'enhanced': EnhancedTrendStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_strategy(cls, strategy_type: str = None):
|
||||||
|
"""
|
||||||
|
Create a strategy instance based on configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strategy_type: Strategy type override (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Strategy instance
|
||||||
|
"""
|
||||||
|
strategy_type = strategy_type or trading_config.strategy_type
|
||||||
|
|
||||||
|
if strategy_type not in cls.AVAILABLE_STRATEGIES:
|
||||||
|
available = ', '.join(cls.AVAILABLE_STRATEGIES.keys())
|
||||||
|
raise ValueError(f"Unknown strategy type: {strategy_type}. Available: {available}")
|
||||||
|
|
||||||
|
strategy_class = cls.AVAILABLE_STRATEGIES[strategy_type]
|
||||||
|
strategy = strategy_class()
|
||||||
|
|
||||||
|
logger.info(f"Created strategy: {strategy.name}")
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_available_strategies(cls) -> list:
|
||||||
|
"""Get list of available strategy types"""
|
||||||
|
return list(cls.AVAILABLE_STRATEGIES.keys())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_strategy_description(cls, strategy_type: str) -> str:
|
||||||
|
"""Get description of a strategy type"""
|
||||||
|
descriptions = {
|
||||||
|
'conservative': "Conservative approach with relaxed exits, lets big winners run (18-40% trailing stops, 28-day emergency stops)",
|
||||||
|
'enhanced': "Enhanced approach with better risk control, tighter stops for early losses (12-40% trailing stops, 7-14 day emergency stops)"
|
||||||
|
}
|
||||||
|
return descriptions.get(strategy_type, "No description available")
|
||||||
|
|
||||||
|
def get_strategy(strategy_type: str = None):
|
||||||
|
"""
|
||||||
|
Convenience function to get a strategy instance
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strategy_type: Strategy type ('conservative' or 'enhanced')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Strategy instance
|
||||||
|
"""
|
||||||
|
return StrategyFactory.create_strategy(strategy_type)
|
||||||
398
src/trading_engine.py
Normal file
398
src/trading_engine.py
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
"""
|
||||||
|
Trading Engine Module
|
||||||
|
Core trading engine that executes trades and manages positions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
import alpaca_trade_api as tradeapi
|
||||||
|
from config.trading_config import alpaca_config, trading_config
|
||||||
|
from src.data_handler import DataHandler
|
||||||
|
from src.strategy_factory import get_strategy
|
||||||
|
from src.risk_manager import RiskManager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TradingEngine:
|
||||||
|
"""
|
||||||
|
Main trading engine that coordinates all trading operations
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, paper_trading: bool = True):
|
||||||
|
"""
|
||||||
|
Initialize the trading engine
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paper_trading: Whether to use paper trading (default: True)
|
||||||
|
"""
|
||||||
|
self.paper_trading = paper_trading
|
||||||
|
|
||||||
|
# Initialize components
|
||||||
|
self.data_handler = DataHandler()
|
||||||
|
self.strategy = get_strategy() # Use strategy factory
|
||||||
|
self.risk_manager = RiskManager()
|
||||||
|
|
||||||
|
# Initialize Alpaca trading API
|
||||||
|
base_url = alpaca_config.base_url if paper_trading else 'https://api.alpaca.markets'
|
||||||
|
self.trading_api = tradeapi.REST(
|
||||||
|
alpaca_config.api_key,
|
||||||
|
alpaca_config.secret_key,
|
||||||
|
base_url=base_url
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trading state
|
||||||
|
self.current_position = 0
|
||||||
|
self.entry_price = None
|
||||||
|
self.highest_price_since_entry = None
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
self.consecutive_losses = 0
|
||||||
|
self.peak_portfolio_value = None
|
||||||
|
|
||||||
|
logger.info(f"TradingEngine initialized (paper_trading: {paper_trading})")
|
||||||
|
|
||||||
|
def get_account_info(self) -> dict:
|
||||||
|
"""Get current account information"""
|
||||||
|
try:
|
||||||
|
account = self.trading_api.get_account()
|
||||||
|
|
||||||
|
account_info = {
|
||||||
|
'equity': float(account.equity),
|
||||||
|
'cash': float(account.cash),
|
||||||
|
'buying_power': float(account.buying_power),
|
||||||
|
'day_trade_count': int(account.day_trade_count),
|
||||||
|
'pattern_day_trader': account.pattern_day_trader
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update peak portfolio value
|
||||||
|
if self.peak_portfolio_value is None or account_info['equity'] > self.peak_portfolio_value:
|
||||||
|
self.peak_portfolio_value = account_info['equity']
|
||||||
|
|
||||||
|
# Update risk manager daily tracking
|
||||||
|
self.risk_manager.update_daily_tracking(account_info['equity'])
|
||||||
|
|
||||||
|
return account_info
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting account info: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_current_position(self, symbol: str = None) -> dict:
|
||||||
|
"""Get current position information"""
|
||||||
|
symbol = symbol or trading_config.symbol
|
||||||
|
|
||||||
|
try:
|
||||||
|
positions = self.trading_api.list_positions()
|
||||||
|
|
||||||
|
for position in positions:
|
||||||
|
if position.symbol == symbol:
|
||||||
|
return {
|
||||||
|
'symbol': position.symbol,
|
||||||
|
'qty': float(position.qty),
|
||||||
|
'market_value': float(position.market_value),
|
||||||
|
'avg_entry_price': float(position.avg_entry_price),
|
||||||
|
'unrealized_pl': float(position.unrealized_pl),
|
||||||
|
'unrealized_plpc': float(position.unrealized_plpc)
|
||||||
|
}
|
||||||
|
|
||||||
|
# No position found
|
||||||
|
return {
|
||||||
|
'symbol': symbol,
|
||||||
|
'qty': 0,
|
||||||
|
'market_value': 0,
|
||||||
|
'avg_entry_price': 0,
|
||||||
|
'unrealized_pl': 0,
|
||||||
|
'unrealized_plpc': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting position info: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def place_order(self,
|
||||||
|
symbol: str,
|
||||||
|
qty: float,
|
||||||
|
side: str,
|
||||||
|
order_type: str = 'market',
|
||||||
|
time_in_force: str = 'gtc') -> dict:
|
||||||
|
"""
|
||||||
|
Place a trading order
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: Trading symbol
|
||||||
|
qty: Quantity to trade
|
||||||
|
side: 'buy' or 'sell'
|
||||||
|
order_type: Order type (default: 'market')
|
||||||
|
time_in_force: Time in force (default: 'gtc')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Order information dictionary
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Placing {side} order: {qty:.6f} {symbol} at {order_type}")
|
||||||
|
|
||||||
|
# Convert quantity to string with appropriate precision
|
||||||
|
qty_str = f"{qty:.6f}"
|
||||||
|
|
||||||
|
order = self.trading_api.submit_order(
|
||||||
|
symbol=symbol,
|
||||||
|
qty=qty_str,
|
||||||
|
side=side,
|
||||||
|
type=order_type,
|
||||||
|
time_in_force=time_in_force
|
||||||
|
)
|
||||||
|
|
||||||
|
order_info = {
|
||||||
|
'id': order.id,
|
||||||
|
'symbol': order.symbol,
|
||||||
|
'qty': float(order.qty),
|
||||||
|
'side': order.side,
|
||||||
|
'order_type': order.order_type,
|
||||||
|
'status': order.status,
|
||||||
|
'submitted_at': order.submitted_at
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Order placed successfully: {order_info}")
|
||||||
|
return order_info
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error placing order: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def execute_buy_signal(self, current_price: float, account_value: float) -> bool:
|
||||||
|
"""
|
||||||
|
Execute a buy signal
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_price: Current asset price
|
||||||
|
account_value: Current account value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if order was placed successfully
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check if we already have a position
|
||||||
|
if self.current_position > 0:
|
||||||
|
logger.info("Buy signal ignored - already have position")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Calculate position size
|
||||||
|
position_info = self.risk_manager.calculate_position_size(
|
||||||
|
account_value, current_price
|
||||||
|
)
|
||||||
|
|
||||||
|
position_size = position_info['position_size']
|
||||||
|
|
||||||
|
if position_size <= 0:
|
||||||
|
logger.warning("Position size is 0 or negative - skipping buy")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Place buy order
|
||||||
|
order_info = self.place_order(
|
||||||
|
symbol=trading_config.symbol,
|
||||||
|
qty=position_size,
|
||||||
|
side='buy'
|
||||||
|
)
|
||||||
|
|
||||||
|
if order_info:
|
||||||
|
# Update position tracking
|
||||||
|
self.current_position = position_size
|
||||||
|
self.entry_price = current_price
|
||||||
|
self.highest_price_since_entry = current_price
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
|
||||||
|
logger.info(f"Buy executed: {position_size:.6f} at ${current_price:.2f}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing buy signal: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute_sell_signal(self, current_price: float, reason: str = "") -> bool:
|
||||||
|
"""
|
||||||
|
Execute a sell signal
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_price: Current asset price
|
||||||
|
reason: Reason for selling
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if order was placed successfully
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check if we have a position to sell
|
||||||
|
if self.current_position <= 0:
|
||||||
|
logger.info("Sell signal ignored - no position to sell")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Place sell order
|
||||||
|
order_info = self.place_order(
|
||||||
|
symbol=trading_config.symbol,
|
||||||
|
qty=self.current_position,
|
||||||
|
side='sell'
|
||||||
|
)
|
||||||
|
|
||||||
|
if order_info:
|
||||||
|
# Calculate trade performance
|
||||||
|
if self.entry_price:
|
||||||
|
profit_pct = (current_price / self.entry_price - 1)
|
||||||
|
profit_amount = (current_price - self.entry_price) * self.current_position
|
||||||
|
|
||||||
|
logger.info(f"Sell executed: {self.current_position:.6f} at ${current_price:.2f}")
|
||||||
|
logger.info(f"Trade result: {profit_pct:.2%} (${profit_amount:.2f}) - {reason}")
|
||||||
|
|
||||||
|
# Update consecutive losses counter
|
||||||
|
if profit_pct < 0:
|
||||||
|
self.consecutive_losses += 1
|
||||||
|
else:
|
||||||
|
self.consecutive_losses = 0
|
||||||
|
|
||||||
|
# Reset position tracking
|
||||||
|
self.strategy.reset_position_state()
|
||||||
|
self.current_position = 0
|
||||||
|
self.entry_price = None
|
||||||
|
self.highest_price_since_entry = None
|
||||||
|
self.bars_since_entry = 0
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing sell signal: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_trading_cycle(self) -> dict:
|
||||||
|
"""
|
||||||
|
Run one complete trading cycle
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with cycle results
|
||||||
|
"""
|
||||||
|
cycle_results = {
|
||||||
|
'timestamp': datetime.now(),
|
||||||
|
'success': False,
|
||||||
|
'account_info': None,
|
||||||
|
'position_info': None,
|
||||||
|
'signals': {'buy': False, 'sell': False},
|
||||||
|
'actions_taken': [],
|
||||||
|
'risk_status': None,
|
||||||
|
'current_price': None
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("Starting trading cycle")
|
||||||
|
|
||||||
|
# Get account information
|
||||||
|
account_info = self.get_account_info()
|
||||||
|
if not account_info:
|
||||||
|
logger.error("Failed to get account information")
|
||||||
|
return cycle_results
|
||||||
|
|
||||||
|
cycle_results['account_info'] = account_info
|
||||||
|
|
||||||
|
# Check risk limits
|
||||||
|
risk_status = self.risk_manager.check_risk_limits(
|
||||||
|
account_info['equity'],
|
||||||
|
self.peak_portfolio_value or account_info['equity']
|
||||||
|
)
|
||||||
|
cycle_results['risk_status'] = risk_status
|
||||||
|
|
||||||
|
if not risk_status['trading_allowed']:
|
||||||
|
logger.warning("Trading not allowed due to risk limits")
|
||||||
|
cycle_results['actions_taken'].append("trading_halted")
|
||||||
|
return cycle_results
|
||||||
|
|
||||||
|
# Get current position
|
||||||
|
position_info = self.get_current_position()
|
||||||
|
if not position_info:
|
||||||
|
logger.error("Failed to get position information")
|
||||||
|
return cycle_results
|
||||||
|
|
||||||
|
cycle_results['position_info'] = position_info
|
||||||
|
|
||||||
|
# Sync position tracking with actual position
|
||||||
|
self.current_position = position_info['qty']
|
||||||
|
if self.current_position > 0 and not self.entry_price:
|
||||||
|
self.entry_price = position_info['avg_entry_price']
|
||||||
|
self.highest_price_since_entry = position_info['avg_entry_price']
|
||||||
|
|
||||||
|
# Get market data and generate signals
|
||||||
|
bars = self.data_handler.get_historical_data(days=30)
|
||||||
|
if bars.empty:
|
||||||
|
logger.error("No market data available")
|
||||||
|
return cycle_results
|
||||||
|
|
||||||
|
# Validate data quality
|
||||||
|
if not self.data_handler.validate_data_quality(bars):
|
||||||
|
logger.error("Data quality check failed")
|
||||||
|
return cycle_results
|
||||||
|
|
||||||
|
# Calculate technical indicators
|
||||||
|
bars = self.data_handler.calculate_technical_indicators(bars)
|
||||||
|
|
||||||
|
# Generate trading signals
|
||||||
|
bars = self.strategy.generate_signals(bars)
|
||||||
|
|
||||||
|
# Get current price and latest signals
|
||||||
|
current_price = bars['close'].iloc[-1]
|
||||||
|
buy_signal = bars['buy_signal'].iloc[-1]
|
||||||
|
sell_signal = bars['sell_signal'].iloc[-1]
|
||||||
|
|
||||||
|
cycle_results['current_price'] = current_price
|
||||||
|
cycle_results['signals'] = {'buy': buy_signal, 'sell': sell_signal}
|
||||||
|
|
||||||
|
# Update tracking for existing position
|
||||||
|
if self.current_position > 0 and self.entry_price:
|
||||||
|
self.bars_since_entry += 1
|
||||||
|
if current_price > self.highest_price_since_entry:
|
||||||
|
self.highest_price_since_entry = current_price
|
||||||
|
|
||||||
|
# Check exit conditions using strategy-specific method
|
||||||
|
should_exit, exit_reason = self.strategy.should_exit_position(
|
||||||
|
current_price=current_price,
|
||||||
|
entry_price=self.entry_price,
|
||||||
|
highest_price=self.highest_price_since_entry,
|
||||||
|
bars_since_entry=self.bars_since_entry,
|
||||||
|
sell_signal=sell_signal,
|
||||||
|
ema_trend=bars['ema_trend'].iloc[-1]
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_exit:
|
||||||
|
if self.execute_sell_signal(current_price, exit_reason):
|
||||||
|
cycle_results['actions_taken'].append(f"sell_{exit_reason}")
|
||||||
|
|
||||||
|
# Check for buy signal (only if no position)
|
||||||
|
elif buy_signal and self.current_position == 0:
|
||||||
|
# Use strategy-specific entry validation
|
||||||
|
if self.strategy.get_entry_conditions(bars, len(bars)-1):
|
||||||
|
if self.execute_buy_signal(current_price, account_info['equity']):
|
||||||
|
cycle_results['actions_taken'].append("buy_signal")
|
||||||
|
|
||||||
|
# Check emergency exit conditions
|
||||||
|
if (self.current_position > 0 and
|
||||||
|
self.risk_manager.get_emergency_exit_signal(bars, self.current_position)):
|
||||||
|
if self.execute_sell_signal(current_price, "emergency_exit"):
|
||||||
|
cycle_results['actions_taken'].append("emergency_exit")
|
||||||
|
|
||||||
|
cycle_results['success'] = True
|
||||||
|
logger.info("Trading cycle completed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in trading cycle: {e}")
|
||||||
|
cycle_results['actions_taken'].append(f"error: {str(e)}")
|
||||||
|
|
||||||
|
return cycle_results
|
||||||
|
|
||||||
|
def get_trading_summary(self) -> dict:
|
||||||
|
"""Get current trading status summary"""
|
||||||
|
return {
|
||||||
|
'current_position': self.current_position,
|
||||||
|
'entry_price': self.entry_price,
|
||||||
|
'highest_price_since_entry': self.highest_price_since_entry,
|
||||||
|
'bars_since_entry': self.bars_since_entry,
|
||||||
|
'consecutive_losses': self.consecutive_losses,
|
||||||
|
'peak_portfolio_value': self.peak_portfolio_value,
|
||||||
|
'risk_summary': self.risk_manager.get_risk_summary()
|
||||||
|
}
|
||||||
183
test_system.py
Normal file
183
test_system.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
"""
|
||||||
|
System Test Script
|
||||||
|
Verifies that the trading system is properly installed and configured.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def test_imports():
|
||||||
|
"""Test that all required modules can be imported"""
|
||||||
|
print("Testing imports...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pandas as pd
|
||||||
|
print("✓ pandas imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ pandas import failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import alpaca_trade_api as tradeapi
|
||||||
|
print("✓ alpaca_trade_api imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ alpaca_trade_api import failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Test local imports
|
||||||
|
sys.path.append('src')
|
||||||
|
sys.path.append('config')
|
||||||
|
|
||||||
|
try:
|
||||||
|
from config.trading_config import trading_config, alpaca_config
|
||||||
|
print("✓ trading configuration imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ trading configuration import failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.data_handler import DataHandler
|
||||||
|
print("✓ DataHandler imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ DataHandler import failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.strategy import TrendFollowingStrategy
|
||||||
|
print("✓ TrendFollowingStrategy imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ TrendFollowingStrategy import failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.risk_manager import RiskManager
|
||||||
|
print("✓ RiskManager imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ RiskManager import failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.trading_engine import TradingEngine
|
||||||
|
print("✓ TradingEngine imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ TradingEngine import failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_configuration():
|
||||||
|
"""Test configuration settings"""
|
||||||
|
print("\nTesting configuration...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from config.trading_config import trading_config, alpaca_config
|
||||||
|
|
||||||
|
print(f"✓ Trading symbol: {trading_config.symbol}")
|
||||||
|
print(f"✓ Risk per trade: {trading_config.risk_per_trade}")
|
||||||
|
print(f"✓ Max position size: {trading_config.max_position_size}")
|
||||||
|
print(f"✓ Alpaca base URL: {alpaca_config.base_url}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Configuration test failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_data_connection():
|
||||||
|
"""Test connection to data source"""
|
||||||
|
print("\nTesting data connection...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.data_handler import DataHandler
|
||||||
|
|
||||||
|
data_handler = DataHandler()
|
||||||
|
print("✓ DataHandler initialized")
|
||||||
|
|
||||||
|
# Test getting a small amount of recent data
|
||||||
|
try:
|
||||||
|
bars = data_handler.get_historical_data(days=1)
|
||||||
|
if not bars.empty:
|
||||||
|
print(f"✓ Data connection successful - got {len(bars)} data points")
|
||||||
|
print(f"✓ Latest price: ${bars['close'].iloc[-1]:.2f}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("✗ No data received")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Data fetch failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ DataHandler initialization failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_directory_structure():
|
||||||
|
"""Test that required directories exist"""
|
||||||
|
print("\nTesting directory structure...")
|
||||||
|
|
||||||
|
required_dirs = ['src', 'config', 'logs']
|
||||||
|
|
||||||
|
for dir_name in required_dirs:
|
||||||
|
if os.path.exists(dir_name):
|
||||||
|
print(f"✓ {dir_name}/ directory exists")
|
||||||
|
else:
|
||||||
|
print(f"✗ {dir_name}/ directory missing")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run all tests"""
|
||||||
|
print("=" * 50)
|
||||||
|
print("TRADING SYSTEM INSTALLATION TEST")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("Directory Structure", test_directory_structure),
|
||||||
|
("Module Imports", test_imports),
|
||||||
|
("Configuration", test_configuration),
|
||||||
|
("Data Connection", test_data_connection)
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for test_name, test_func in tests:
|
||||||
|
print(f"\n--- {test_name} ---")
|
||||||
|
try:
|
||||||
|
result = test_func()
|
||||||
|
results.append((test_name, result))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ {test_name} failed with exception: {e}")
|
||||||
|
results.append((test_name, False))
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("TEST SUMMARY")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
for test_name, result in results:
|
||||||
|
status = "PASS" if result else "FAIL"
|
||||||
|
symbol = "✓" if result else "✗"
|
||||||
|
print(f"{symbol} {test_name}: {status}")
|
||||||
|
if not result:
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
if all_passed:
|
||||||
|
print("🎉 All tests passed! The trading system is ready to use.")
|
||||||
|
print("\nNext steps:")
|
||||||
|
print("1. Review configuration in config/trading_config.py")
|
||||||
|
print("2. Run backtesting: python main.py --mode backtest")
|
||||||
|
print("3. For live trading: python main.py --mode live")
|
||||||
|
else:
|
||||||
|
print("❌ Some tests failed. Please fix the issues before proceeding.")
|
||||||
|
print("\nCommon solutions:")
|
||||||
|
print("1. Install dependencies: pip install -r requirements.txt")
|
||||||
|
print("2. Check API credentials in .env file")
|
||||||
|
print("3. Verify internet connection")
|
||||||
|
|
||||||
|
return 0 if all_passed else 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
exit_code = main()
|
||||||
|
sys.exit(exit_code)
|
||||||
Loading…
x
Reference in New Issue
Block a user